# HG changeset patch # User Charlie Root # Date 1527454436 14400 # Node ID 50ac5484d514d0ad8a1778d884a03cb2c196b151 # Parent d41c01c5c933b59c3d5f150a1aea763561aa2cbd one fix to distro diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/.gitignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/.gitignore Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,2 @@ +*.swp +config.inc.php diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/README.md Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,77 @@ + +Advanced Search Plugin for Roundcube +==================================== + +## Getting It + +You can download direct from GitHub or consider using +the [plugin repository for Roundcube](http://plugins.roundcube.net/) + +## Usage + +After install, 'Advanced search' will show up under the 'more' menu. + +Please use the _'stable'_ brach for deployment. + +Advantages: + +* This version should be tested and bug-free +* It uses minified versions of the JavaScript + +## Requirements +Version 2.0.0 requires Roundcube 0.9.4 or later + +## License + +This plugin is released under the GNU General Public License Version 3 +or later (http://www.gnu.org/licenses/gpl.html). + +Even if skins might contain some programming work, they are not considered +as a linked part of the plugin and therefore skins DO NOT fall under the +provisions of the GPL license. See the README file located in the core skins +folder for details on the skin license. + +## Download + +### GIT : +* Clone the GitHub repository to 'advanced_search': + + > git clone git://github.com/GMS-SA/roundcube-advanced-search.git advanced_search + +* Change to the 'stable' branch: + + > cd advanced_search + > git checkout -b stable origin/stable + +### ZIP : +* Swap branches to 'stable' +* Click on the 'ZIP' download icon +* Rename the unziped directory 'advanced_search' + +## Install + +* Place the 'advanced_search' plugin folder into the plugins directory of Roundcube. +* If using git and not wanting all the '.git' repository data in your live webmail: + + > cd advanced_search + > git archive --format=tar --prefix=advanced_search/ stable | tar -x -C /path/to/roundcube/plugins/ + + This will give you a git-free copy of the stable branch. +* Add advanced_search to $rcmail_config['plugins'] in your Roundcube config + +* To override defaults, copy the config-default.inc.php file to config.inc.php and modify + +## Upgrade +If upgrading from 1.2.0 or lower, you *must* review the config file. + +## Configuration + +* Available search criterias +* Targeted roundcube menu for the advanced search + +## Credits + +* Wilwert Claude +* Ludovicy Steve +* Moules Chris +* [Global Media Systems](http://www.gms.lu) diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/advanced_search.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/advanced_search.js Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,726 @@ +(function($) { + /** + * The fontend scripts for an advanced search. + * + * @version 2.1.6 + * @licence GNU GPLv3+ + * @author Wilwert Claude + * @author Ludovicy Steve + * @author Chris Moules + * @website http://www.gms.lu + */ + + $.stack = { + /** + * This object is used to buffer all the server side information which doesn't change. So the script + * doesn't have to send an ajax-request for every new added row. + * + * @name stack + * @namespace + */ + date_criteria: {}, + flag_criteria: {}, + email_criteria: {}, + row: null, + messages: null + }; + + var search_loading = ''; + + $(document).on("change", "#button_display_option", function(e) { + var img = $('img', $(this).closest('p')); + var src = img.attr('src'); + if(this.value == 'messagemenu') { + src = src.replace('menu_location_b.jpg', 'menu_location_a.jpg'); + } else { + src = src.replace('menu_location_a.jpg', 'menu_location_b.jpg'); + } + img.attr('src', src); + }); + + $(document).on("change", "#_show_message_mbox_info, #_show_message_label_header", function(e) { + var img = $('img', $(this).closest('p')); + if($(this).is(':checked')) { + img.removeClass('disabled'); + } else { + img.addClass('disabled'); + } + }); + + /** + * The callback function of the initial dialog call. It creates the dialog and buffers the serverside + * informations into an object. + * + * @param {object} r The serverside informations + */ + rcmail.addEventListener('plugin.show', function(r) { + $.stack.date_criteria = r.date_criteria; + $.stack.flag_criteria = r.flag_criteria; + $.stack.email_criteria = r.email_criteria; + $.stack.row = r.row; + $.stack.html = r.html; + + var $html = $(r.html); + var saved_searches_label = rcmail.gettext('saved_searches', 'advanced_search'); + var saved_searches = ' '; + title = $('
" + search_name + "
" ).dialog({ + resizable: true, + height:180, + modal: true, + title: rcmail.gettext('advanced_search.deletesearch'), + buttons: [ + { + text: txt['delete'], + click: function() { + rcmail.http_request('plugin.delete_search', {search_name: search_name}); + $("[value=" + search_name + "]", "[name=select_saved_search]").remove(); + $("[name=select_saved_search]").val("").trigger("change"); + $( this ).dialog( "close" ); + }, + }, + { + text: txt['cancel'], + click: function() { + $( this ).dialog( "close" ); + } + }] + + }); + }); + + $(document).on("click", "#save_the_search", function(e) { + e.stopPropagation(); + e.preventDefault(); + var labelName = rcmail.gettext('name', 'advanced_search'); + var labelSave = rcmail.gettext('save', 'advanced_search'); + var labelCancel = rcmail.gettext('cancel', 'advanced_search'); + var save_search = '' + labelName + ' | |
"+search_name+"
").dialog({resizable:true,height:180,modal:true,title:rcmail.gettext("advanced_search.deletesearch"),buttons:[{text:txt["delete"],click:function(){rcmail.http_request("plugin.delete_search",{search_name:search_name});$("[value="+search_name+"]","[name=select_saved_search]").remove();$("[name=select_saved_search]").val("").trigger("change");$(this).dialog("close")}},{text:txt["cancel"],click:function(){$(this).dialog("close")}}]})});$(document).on("click","#save_the_search",function(e){e.stopPropagation();e.preventDefault();var labelName=rcmail.gettext("name","advanced_search");var labelSave=rcmail.gettext("save","advanced_search");var labelCancel=rcmail.gettext("cancel","advanced_search");var save_search=""+labelName+' | |
' . $check1 . ' ' . $label1 . ' ' . $img1 . '
'); + $args['blocks']['label_display_options']['options'][1] = array('title' => '', 'content' => '' . $check2 . ' ' . $label2 . ' ' . $img2 . '
'); + $args['blocks']['label_display_options']['options'][2] = array('title' => '', 'content' => '' . $label3 . ' ' . $select . ' ' . $img3 . '
'); + } + + return($args); + } + + private function perform_search($search_string, $folders, $page = 1) + { + // Search all folders and build a final set + if ($folders[0] == 'all' || empty($folders)) { + $folders_search = $this->rc->imap->list_folders_subscribed(); + } else { + $folders_search = $folders; + } + $count = 0; + $folder_count = array(); + foreach ($folders_search as $mbox) { + $this->rc->storage->set_folder($mbox); + $this->rc->storage->search($mbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $result = array(); + $fcount = $this->rc->storage->count($mbox, 'ALL', !empty($_REQUEST['_refresh'])); + $count += $fcount; + $folder_count[$mbox] = $fcount; + } + foreach ($folder_count as $k => $v) { + if ($v == 0) { + unset($folder_count[$k]); + } + } + $fetch = $this->do_pagination($folder_count, $page); + $mails = array(); + $currentMailbox = ""; + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $showMboxColumn = isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] ? true : false; + $uid_mboxes = array(); + foreach ($fetch as $mailbox => $data) { + if ($currentMailbox != $mailbox) { + $currentMailbox = $mailbox; + if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) { + $this->rc->output->command('advanced_search_add_mbox', $mailbox, $folder_count[$mailbox], $showMboxColumn); + } + } + $uid_mboxes = array_merge($uid_mboxes, $this->getMails($mailbox, $data, $search_string, $showMboxColumn)); + } + + return array('result' => array(), 'count' => $count, 'uid_mboxes' => $uid_mboxes); + } + + private function getMails($mailbox, $data, $search_string, $showMboxColumn) + { + $pageSize = $this->rc->storage->get_pagesize(); + $msgNum = $data['from']; + $startPage = ceil($msgNum/$pageSize); + $msgMod = $msgNum % $pageSize; + $multiPage = "false"; + $firstArrayElement = $msgMod == 0 ? ($pageSize-1) : ($msgMod-1); + $quantity = $data['to'] - $data['from']; + if ($data['from'] + $quantity > $pageSize) { + $multiPage = "true"; + } + $this->rc->storage->set_folder($mailbox); + $this->rc->storage->search($mailbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $messages = $this->rc->storage->list_messages('', $startPage); + if ($multiPage) { + $messages = array_merge($messages, $this->rc->storage->list_messages('', $startPage+1)); + } + //FIRST: 0 QUANTITY: 2 + $sliceTo = $quantity + 1; + $mslice = array_slice($messages, $firstArrayElement, $sliceTo, true); + $messages = $mslice; + $avbox = array(); + $showAvmbox = false; + foreach ($messages as $set_flag) { + $set_flag->flags['skip_mbox_check'] = true; + if ($showMboxColumn === true) { + $set_flag->avmbox = $mailbox; + $avbox[] = 'avmbox'; + $showAvmbox = true; + } + } + $uid_mboxes = $this->rcmail_js_message_list($messages, false, null, $showAvmbox, $avbox, $showMboxColumn); + + return $uid_mboxes; + } + + public function save_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + if ($search_name) { + $search = array(); + $search['search'] = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC); + $search['search_name'] = $search_name; + $search['folder'] = rcube_utils::get_input_value('folder', rcube_utils::INPUT_GPC); + $search['sub_folders'] = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $prefs['advanced_search'][$search_name] = $search; + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_saved'], 'confirmation'); + } + } + + public function delete_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + if ($search_name) { + $prefs = (array)$this->rc->user->get_prefs(); + unset($prefs['advanced_search'][$search_name]); + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_deleted'], 'notice'); + } + } + + public function get_saved_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + + $search = isset($prefs['advanced_search'][$search_name]) ? $prefs['advanced_search'][$search_name] : false; + $this->rc->output->command('plugin.load_saved_search', $search); + $this->rc->output->send(); + } + + private function get_saved_search_names() + { + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $names = array(); + foreach($prefs['advanced_search'] as $name => $search) { + $names[] = $name; + } + + return $names; + } +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/advanced_search.php.orig --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/advanced_search.php.orig Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,1083 @@ +rc = rcmail::get_instance(); + $this->load_config("config-default.inc.php"); + $this->load_config(); + $this->config = $this->rc->config->get('advanced_search_plugin'); + $this->register_action('plugin.display_advanced_search', array($this, 'display_advanced_search')); + $this->register_action('plugin.trigger_search', array($this, 'trigger_search')); + $this->register_action('plugin.trigger_search_pagination', array($this, 'trigger_search_pagination')); + $this->register_action('plugin.save_search', array($this, 'save_search')); + $this->register_action('plugin.delete_search', array($this, 'delete_search')); + $this->register_action('plugin.get_saved_search', array($this, 'get_saved_search')); + + $this->skin = $this->rc->config->get('skin'); + $this->add_texts('localization', true); + $this->populate_i18n(); + if(isset($this->config['criteria'])) { + foreach($this->config['criteria'] as $key => $translation) { + $this->config['criteria'][$key] = $this->gettext($key); + } + } + $this->include_script('advanced_search.min.js'); + + if ($this->rc->task == 'mail') { + $file = 'skins/' . $this->skin . '/advanced_search.css'; + + if (file_exists($this->home . '/' . $file)) { + $this->include_stylesheet($file); + } else { + $this->include_stylesheet('skins/default/advanced_search.css'); + } + + if (empty($this->rc->action)) { + $this->add_menu_entry(); + } + } elseif ($this->rc->task == 'settings') { + $this->add_hook('preferences_list', array($this, 'preferences_list')); + $this->add_hook('preferences_save', array($this, 'preferences_save')); + $this->add_hook('preferences_sections_list', array($this, 'preferences_section')); + $file = 'skins/' . $this->skin . '/advanced_search.css'; + if (file_exists($this->home . '/' . $file)) { + $this->include_stylesheet($file); + } else { + $this->include_stylesheet('skins/default/advanced_search.css'); + } + } + + $this->add_hook('startup', array($this, 'startup')); + } + + + public function startup($args) + { + $search = get_input_value('_search', RCUBE_INPUT_GET); + if (!isset($search)) { + $search = get_input_value('_search', RCUBE_INPUT_POST); + } + $rsearch = $search == 'advanced_search_active'; + $uid = get_input_value('_uid', RCUBE_INPUT_GET); + $draft_uid = get_input_value('_draft_uid', RCUBE_INPUT_GET); + $mbox = get_input_value('_mbox', RCUBE_INPUT_GET); + $page = get_input_value('_page', RCUBE_INPUT_GET); + $sort = get_input_value('_sort', RCUBE_INPUT_GET); + + if (!empty($uid)) { + $parts = explode('__MB__', $uid); + if (count($parts) == 2) { + $search = 'advanced_search_active'; + } + } + if (!empty($draft_uid)) { + $parts = explode('__MB__', $draft_uid); + if (count($parts) == 2) { + $search = 'advanced_search_active'; + } + } + + if ($search == 'advanced_search_active') { + if ($args['action'] == 'show' && !empty($uid)) { + $parts = explode('__MB__', $uid); + $uid = $parts[0]; + $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_uid' => $uid)); + } + if ($args['action'] == 'compose') { + $draft_uid = get_input_value('_draft_uid', RCUBE_INPUT_GET); + $parts = explode('__MB__', $draft_uid); + $draft_uid = $parts[0]; + if (!empty($draft_uid)) { + $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_draft_uid' => $draft_uid)); + } + } + if ($args['action'] == 'list' && $rsearch) { + $this->rc->output->command('advanced_search_active', '_page=' . $page . '&_sort=' . $sort); + $this->rc->output->send(); + $args['abort'] = true; + } + if ($args['action'] == 'mark') { + $flag = get_input_value('_flag', RCUBE_INPUT_POST); + $uid = get_input_value('_uid', RCUBE_INPUT_POST); + + $post_str = '_flag=' . $flag . '&_uid=' . $uid; + if ($quiet = get_input_value('_quiet', RCUBE_INPUT_POST)) { + $post_str .= '&_quiet=' . $quiet; + } + if ($from = get_input_value('_from', RCUBE_INPUT_POST)) { + $post_str .= '&_from=' . $from; + } + if ($count = get_input_value('_count', RCUBE_INPUT_POST)) { + $post_str .= '&_count=' . $count; + } + if ($ruid = get_input_value('_ruid', RCUBE_INPUT_POST)) { + $post_str .= '&_ruid=' . $ruid; + } + $this->rc->output->command('label_mark', $post_str); + $this->rc->output->send(); + $args['abort'] = true; + } + + } else { + if ($args['action'] != 'plugin.get_saved_search' && $args['action'] != 'plugin.save_search' && $args['action'] != 'plugin.delete_search') { + $this->rc->output->command('plugin.advanced_search_del_header', array()); + } + } + } + + /** + * This function populates an array with localization texts. + * This is needed as ew are using a lot of localizations from core. + * The core localizations are not avalable directly in JS + * + * @access private + * @return null + */ + private function populate_i18n() + { + $core = array('advsearch', 'search', 'resetsearch', 'addfield', 'delete', 'cancel'); + + foreach ($core as $label) { + $this->i18n_strings[$label] = $this->rc->gettext($label); + } + + $local = array('in', 'and', 'or', 'not', 'where', 'exclude', 'andsubfolders', 'allfolders', 'save_the_search', 'has_been_saved', 'deletesearch', 'has_been_deleted'); + + foreach ($local as $label) { + $this->i18n_strings[$label] = $this->gettext($label); + } + } + + /** + * This adds a button into the configured menu to use the advanced search + * + * @access public + * @return null + */ + public function add_menu_entry() + { + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu']; + if ($target_menu != 'toolbar') { + $this->api->add_content( + html::tag( + 'li', + null, + $this->api->output->button( + array( + 'command' => 'plugin.advanced_search', + 'label' => 'advsearch', + 'type' => 'link', + 'classact' => 'icon advanced-search active', + 'class' => 'icon advanced-search', + 'innerclass' => 'icon advanced-search' + ) + ) + ), + $target_menu + ); + } else { + $this->api->add_content( + $this->api->output->button( + array( + 'command' => 'plugin.advanced_search', + 'title' => 'advsearch', + 'label' => 'search', + 'type' => 'link', + 'classact' => 'button advanced-search active', + 'class' => 'button advanced-search', + 'innerclass' => 'button advanced-search', + ) + ), + $target_menu + ); + } + } + + /** + * This function quotes some specific values based on their data type + * + * @param mixed $input The value to get quoted + * @access public + * @return The quoted value + */ + public function quote($value) + { + if (getType($value) == 'string') { + if (!preg_match('/"/', $value)) { + $value = preg_replace('/^"/', '', $value); + $value = preg_replace('/"$/', '', $value); + $value = preg_replace('/"/', '\\"', $value); + } + + $value = '"' . $value . '"'; + } + + return $value; + } + + /** + * This function generates the IMAP compatible search query based on the request data (by javascript) + * + * @param array $input The raw criteria data sent by javascript + * @access private + * @return string or int + */ + private function process_search_part($search_part) + { + $command_str = ''; + $flag = false; + + // Check for valid input + if (!array_key_exists($search_part['filter'], $this->config['criteria'])) { + $this->rc->output->show_message($this->gettext('internalerror'), 'error'); + + return 0; + } + if (in_array($search_part['filter'], $this->config['flag_criteria'])) { + $flag = true; + } + if (!$flag && !(isset($search_part['filter-val']) && $search_part['filter-val'] != '')) { + return 1; + } + + // Negate part + if ($search_part['not'] == 'true') { + $command_str .= 'NOT '; + } + + $command_str .= $search_part['filter']; + + if (!$flag) { + if (in_array($search_part['filter'], $this->config['date_criteria'])) { + // Take date format from user environment + $date_format = $this->rc->config->get('date_format', 'Y-m-d'); + // Try to use PHP5.2+ DateTime but fallback to ugly old method + if (class_exists('DateTime')) { + $date = DateTime::createFromFormat($date_format, $search_part['filter-val']); + $command_str .= ' ' . $this->quote($date->format("d-M-Y")); + } else { + $date_format = preg_replace('/(\w)/', '%$1', $date_format); + $date_array = strptime($search_part['filter-val'], $date_format); + $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); + $command_str .= ' ' . $this->quote(date("d-M-Y", $unix_ts)); + } + } elseif (in_array($search_part['filter'], $this->config['email_criteria'])) { + // Strip possible ',' added by autocomplete + $command_str .= ' ' . $this->quote(trim($search_part['filter-val'], " \t,")); + } else { + // Don't try to use a value for a binary flag object + $command_str .= ' ' . $this->quote($search_part['filter-val']); + } + } + + return $command_str; + } + + /** + * This function generates the IMAP compatible search query based on the request data (by javascript) + * + * @param array $input The raw criteria data sent by javascript + * @access public + * @return The final search command + */ + public function get_search_query($input) + { + $command = array(); + + foreach ($input as $search_part) { + // Skip excluded parts + if ($search_part['excluded'] == 'true') { + continue; + } + if (! $part_command = $this->process_search_part($search_part)) { + return 0; + } + // Skip invalid parts + if ($part_command === 1) { + continue; + } + + $command[] = array('method' => isset($search_part['method']) ? $search_part['method'] : 'and', + 'command' => $part_command); + } + + $command_string = $this->build_search_string($command); + + return $command_string; + } + + /** + * This function converts the preconfigured query parts (as array) into an IMAP compatible string + * + * @param array $command_array An array containing the advanced search criteria + * @access public + * @return The command string + */ + private function build_search_string($command_array) + { + $command = array(); + $paranthesis = 0; + $prev_method = null; + $next_method = null; + $cnt = count($command_array); + + foreach ($command_array as $k => $v) { + $part = ''; + $next_method = 'unknown'; + + // Lookup next method + if ($k < $cnt-1) { + $next_method = $command_array[$k+1]['method']; + } + + // If previous option was OR, close any open brakets + if ($paranthesis > 0 && $prev_method == 'or' && $v['method'] != 'or') { + for (; $paranthesis > 0; $paranthesis--) { + $part .= ')'; + } + } + + // If there are two consecutive ORs, add brakets + // If the next option is a new OR, add the prefix here + // If the next option is _not_ an OR, and the current option is AND, prefix ALL + if ($next_method == 'or') { + if ($v['method'] == 'or') { + $part .= ' ('; + $paranthesis++; + } + $part .= 'OR '; + } elseif ($v['method'] == 'and') { + $part .= 'ALL '; + } + + $part .= $v['command']; + + // If this is the end of the query, and we have open brakets, close them + if ($k == $cnt-1 && $paranthesis > 0) { + for (; $paranthesis > 0; $paranthesis--) { + $part .= ')'; + } + } + + $prev_method = $v['method']; + $command[] = $part; + } + + $command = implode(' ', $command); + + return $command; + } + + /** + * This functions sends the initial data to the client side where a form (in dialog) is built for the advanced search + * + * @access public + * @return null + */ + public function display_advanced_search() + { + $ret = array('html' => $this->generate_searchbox(), + 'row' => $this->add_row(), + 'saved_searches' => $this->get_saved_search_names(), + 'title' => $this->i18n_strings['advsearch'], + 'date_criteria' => $this->config['date_criteria'], + 'flag_criteria' => $this->config['flag_criteria'], + 'email_criteria' => $this->config['email_criteria']); + + $this->rc->output->command('plugin.show', $ret); + } + + public function generate_searchbox() + { + $search_button = new html_inputfield(array('type' => 'submit', 'name' => 'search', 'class' => 'button mainaction', 'value' => $this->i18n_strings['search'])); + $reset_button = new html_inputfield(array('type' => 'reset', 'name' => 'reset', 'class' => 'button reset', 'value' => $this->i18n_strings['resetsearch'])); + $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'])); + $delete_button = new html_inputfield(array('type' => 'submit', 'name' => 'delete', 'style' => 'display: none;', 'class' => 'button delete_search', 'value' => $this->i18n_strings['deletesearch'])); + + $layout_table = new html_table(); + $layout_table->add(null, $search_button->show()); + $folderConfig = array('name' => 'folder'); + $layout_table->add( + null, + $this->i18n_strings['in'] . ': ' . + $this->folder_selector($folderConfig)->show($this->rc->storage->get_folder()) . + html::span( + array('class' => 'sub-folders'), + $this->i18n_strings['andsubfolders'] . ': ' . + html::tag( + 'input', + array('type' => 'checkbox', 'name' => 'subfolder'), + null + ) + ) . + $this->i19n_strings['where'] + ); + $first_row = $this->add_row(true); + $layout_table->add_row(); + $layout_table->add(array('class' => 'adv-search-and-or'), null); + $layout_table->add(null, $first_row); + $layout_table->add_row(); + $layout_table->add(null, $search_button->show()); + $layout_table->add(null, $save_button . ' ' . $reset_button->show() . ' ' . $delete_button->show()); + + return html::tag( + 'div', + array('id' => 'adsearch-popup'), + html::tag( + 'form', + array('method' => 'post', 'action' => '#'), + $layout_table->show() + ) + ); + } + + /** + * This function is used to render the html of the advanced search form and also + * the later following rows are created by this function + * + * @param array $folders Array of folders + * @param boolean $first True if form gets created, False to create a new row + * @access public + * @return string The final html + */ + public function add_row($first = false) + { + $row_html = ''; + $optgroups = ''; + + $criteria = $this->config['criteria']; + $all_criteria = array( + $this->gettext('Common') => $this->config['prefered_criteria'], + $this->gettext('Addresses') => $this->config['email_criteria'], + $this->gettext('Dates') => $this->config['date_criteria'], + $this->gettext('Flags') => $this->config['flag_criteria'], + $this->gettext('Other') => $this->config['other_criteria'], + ); + + foreach ($all_criteria as $label => $specific_criteria) { + $options = ''; + + foreach ($specific_criteria as $value) { + $options .= html::tag('option', array('value' => $value), $criteria[$value]); + } + + $optgroups .= html::tag('optgroup', array('label' => $label), $options); + } + + $tmp = html::tag('select', array('name' => 'filter'), $optgroups); + $tmp .= $this->i18n_strings['not'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'not'), null); + $tmp .= html::tag('input', array('type' => 'text', 'name' => 'filter-val')); + $tmp .= $this->i18n_strings['exclude'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'filter-exclude'), null); + $tmp .= html::tag('button', array('name' => 'add', 'class' => 'add'), $this->i18n_strings['addfield']); + + if ($first) { + $row_html = $tmp; + } else { + $and_or_select = new html_select(array('name' => 'method')); + $and_or_select->add($this->i18n_strings['and'], 'and'); + $and_or_select->add($this->i18n_strings['or'], 'or'); + $tmp .= html::tag('button', array('name' => 'delete', 'class' => 'delete'), $this->i18n_strings['delete']); + $row_html = html::tag( + 'tr', + null, + html::tag( + 'td', + array('class' => 'adv-search-and-or'), + $and_or_select->show() + ) . + html::tag( + 'td', + null, + $tmp + ) + ); + } + + return $row_html; + } + + /** + * Return folders list as html_select object + * + * This is a copy of the core function and adapted to fit + * the needs of the advanced_search function + * + * @param array $p Named parameters + * + * @return html_select HTML drop-down object + */ + public function folder_selector($p = array()) + { + $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true); + $a_mailboxes = array(); + $storage = $this->rc->get_storage(); + + $list = $storage->list_folders_subscribed(); + $delimiter = $storage->get_hierarchy_delimiter(); + + foreach ($list as $folder) { + $this->rc->build_folder_tree($a_mailboxes, $folder, $delimiter); + } + + $select = new html_select($p); + $select->add($this->i18n_strings['allfolders'], "all"); + + $this->rc->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p); + + return $select; + } + + public function trigger_search_pagination($param) + { + $_GET['search'] = $_SESSION['av_search']; + $_GET['folder'] = $_SESSION['av_folder']; + $_GET['sub_folders'] = $_SESSION['av_sub_folders']; + $this->trigger_search(true); + } + + /** + * Here is where the actual query is fired to the imap server and the result is evaluated and sent back to the client side + * + * @access public + * @return null + */ + public function trigger_search($inPagination = false) + { + $search = get_input_value('search', RCUBE_INPUT_GPC); + // reset list_page and old search results + $this->rc->storage->set_page(1); + $this->rc->storage->set_search_set(null); + $page = get_input_value('_page', RCUBE_INPUT_GPC); + $page = $page ? $page : 1; + $pagesize = $this->rc->storage->get_pagesize(); + + if (!empty($search)) { + $mbox = get_input_value('folder', RCUBE_INPUT_GPC); + $imap_charset = RCMAIL_CHARSET; + $sort_column = rcmail_sort_column(); + $search_str = $this->get_search_query($search); + $sub_folders = get_input_value('sub_folders', RCUBE_INPUT_GPC) == 'true'; + $folders = array(); + $result_h = array(); + $count = 0; + $new_id = 1; + $current_mbox = $this->rc->storage->get_folder(); + $uid_list = array(); + //Store information in session for pagination + $_SESSION['av_search'] = $search; + $_SESSION['av_folder'] = $mbox; + $_SESSION['av_sub_folders'] = get_input_value('sub_folders', RCUBE_INPUT_GPC); + $nosub = $sub_folders; + $folders = $this->rc->get_storage()->list_folders_subscribed(); + if (empty($folders) || ($sub_folders === false && $mbox !== 'all')) { + $folders = array($mbox); + } elseif ($mbox !== 'all') { + if ($sub_folders === false) { + $folders = array($mbox); + } else { + $folders = $this->rc->get_storage()->list_folders_subscribed_direct($mbox); + } + } + $md5folders = array(); + foreach ($folders as $folder) { + $md5folders[md5($folder)] = $folder; + } + $this->rc->output->set_env('as_md5_folders', $md5folders); + + if ($search_str) { + $res = $this->perform_search($search_str, $folders, $page); + $count = $res['count']; + } + + + if ($count > 0) { + $_SESSION['advanced_search']['uid_list'] = $uid_list; + if ($search_str && $inPagination == false) { + $this->rc->output->show_message('searchsuccessful', 'confirmation', array('nr' => $count)); + } + } elseif ($err_code = $this->rc->storage->get_error_code()) { + rcmail_display_server_error(); + } else { + $this->rc->output->show_message('searchnomatch', 'notice'); + } + + $current_folder = get_input_value('current_folder', RCUBE_INPUT_GPC); + + $this->rc->output->set_env('search_request', 'advanced_search_active'); + $this->rc->output->set_env('messagecount', $count); + $this->rc->output->set_env('pagecount', ceil($count / $pagesize)); + $this->rc->output->set_env('exists', $this->rc->storage->count($current_folder, 'EXISTS')); + $this->rc->output->command('set_rowcount', rcmail_get_messagecount_text($count, $page)); + $this->rc->output->command('plugin.search_complete'); + $this->rc->output->send(); + } + } + + /** + * return javascript commands to add rows to the message list + */ + public function rcmail_js_message_list($a_headers, $insert_top = false, $a_show_cols = null, $avmbox = false, $avcols = array(), $showMboxColumn = false) + { + global $CONFIG, $RCMAIL, $OUTPUT; + $uid_mboxes = array(); + + if (empty($a_show_cols)) { + if (!empty($_SESSION['list_attrib']['columns'])) { + $a_show_cols = $_SESSION['list_attrib']['columns']; + } else { + $a_show_cols = is_array($CONFIG['list_cols']) ? $CONFIG['list_cols'] : array('subject'); + } + } else { + if (!is_array($a_show_cols)) { + $a_show_cols = preg_split('/[\s,;]+/', strip_quotes($a_show_cols)); + } + $head_replace = true; + } + + $mbox = $RCMAIL->storage->get_folder(); + + // make sure 'threads' and 'subject' columns are present + if (!in_array('subject', $a_show_cols)) { + array_unshift($a_show_cols, 'subject'); + } + if (!in_array('threads', $a_show_cols)) { + array_unshift($a_show_cols, 'threads'); + } + $_SESSION['list_attrib']['columns'] = $a_show_cols; + + // Make sure there are no duplicated columns (#1486999) + $a_show_cols = array_merge($a_show_cols, $avcols); + $a_show_cols = array_unique($a_show_cols); + + // Plugins may set header's list_cols/list_flags and other rcube_message_header variables + // and list columns + $plugin = $RCMAIL->plugins->exec_hook( + 'messages_list', + array('messages' => $a_headers, 'cols' => $a_show_cols) + ); + $a_show_cols = $plugin['cols']; + $a_headers = $plugin['messages']; + $thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : null; + + // get name of smart From/To column in folder context + if (($f = array_search('fromto', $a_show_cols)) !== false) { + $smart_col = rcmail_message_list_smart_column_name(); + } + if ($this->coltypesSet == false) { + $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col); + if ($showMboxColumn === true) { + $OUTPUT->command('plugin.advanced_search_add_header', array()); + } + $this->coltypesSet = true; + } + + if (empty($a_headers)) { + return; + } + + // remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here + foreach (array('threads', 'attachment', 'flag', 'status', 'priority') as $col) { + if (($key = array_search($col, $a_show_cols)) !== false) { + unset($a_show_cols[$key]); + } + } + + // loop through message headers + foreach ($a_headers as $n => $header) { + if (empty($header)) { + continue; + } + $a_msg_cols = array(); + $a_msg_flags = array(); + // format each col; similar as in rcmail_message_list() + foreach ($a_show_cols as $col) { + $col_name = $col == 'fromto' ? $smart_col : $col; + + if (in_array($col_name, array('from', 'to', 'cc', 'replyto'))) { + $cont = rcmail_address_string($header->$col_name, 3, false, null, $header->charset); + } elseif ($col == 'subject') { + $cont = trim(rcube_mime::decode_header($header->$col, $header->charset)); + if (!$cont) { + $cont = rcube_label('nosubject'); + } + $cont = Q($cont); + } elseif ($col == 'size') { + $cont = show_bytes($header->$col); + } elseif ($col == 'date') { + $cont = format_date($header->date); + } else { + $cont = Q($header->$col); + } + $a_msg_cols[$col] = $cont; + } + + $a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags)); + if ($header->depth) { + $a_msg_flags['depth'] = $header->depth; + } elseif ($header->has_children) { + $roots[] = $header->uid; + } + if ($header->parent_uid) { + $a_msg_flags['parent_uid'] = $header->parent_uid; + } + if ($header->has_children) { + $a_msg_flags['has_children'] = $header->has_children; + } + if ($header->unread_children) { + $a_msg_flags['unread_children'] = $header->unread_children; + } + if ($header->others['list-post']) { + $a_msg_flags['ml'] = 1; + } + if ($header->priority) { + $a_msg_flags['prio'] = (int) $header->priority; + } + $a_msg_flags['ctype'] = Q($header->ctype); + $a_msg_flags['mbox'] = $mbox; + if (!empty($header->list_flags) && is_array($header->list_flags)) { + $a_msg_flags = array_merge($a_msg_flags, $header->list_flags); + } + if (!empty($header->list_cols) && is_array($header->list_cols)) { + $a_msg_cols = array_merge($a_msg_cols, $header->list_cols); + } + if ($showMboxColumn === true) { + $a_msg_flags['avmbox'] = $avmbox; + } + + $OUTPUT->command( + 'add_message_row', + $header->uid . '__MB__' . md5($mbox), + $a_msg_cols, + $a_msg_flags, + $insert_top + ); + $id = $header->uid . '__MB__' . md5($mbox); + $uid_mboxes[$id] = array('uid' => $header->uid, 'mbox' => $mbox, 'md5mbox' => md5($mbox)); + } + + if ($RCMAIL->storage->get_threading()) { + $OUTPUT->command('init_threads', (array) $roots, $mbox); + } + + return $uid_mboxes; + } + + + private function do_pagination($folders, $onPage) + { + $perPage = $this->rc->storage->get_pagesize(); + $from = $perPage * $onPage - $perPage + 1; + $to = $from + $perPage - 1; + $got = 0; + $pos = 0; + $cbox = ""; + $boxStart = 0; + $boxStop = 0; + $fetch = array(); + foreach ($folders as $box => $num) { + $i = $num; + if ($box != $cbox) { + $boxStart = 0; + $boxStop = 0; + $cbox = $box; + } + while ($i--) { + $pos++; + $boxStart++; + if ($pos >= $from && $pos <= $to) { + if (!isset($fetch[$box])) { + $fetch[$box] = array("from" => $boxStart); + } + $fetch[$box]['to'] = $boxStart; + $got++; + } + } + if ($got >= $perPage) { + break; + } + } + + return $fetch; + } + + /** + * Save advanced search preferences + * + * @access public + */ + public function preferences_save($args) + { + if ($args['section'] != 'advancedsearch') { + return; + } + $rcmail = rcmail::get_instance(); + + $displayOptions = array(); + $displayOptions['_show_message_label_header'] = get_input_value('_show_message_label_header', RCUBE_INPUT_POST) == 1 ? true : false; + $displayOptions['_show_message_mbox_info'] = get_input_value('_show_message_mbox_info', RCUBE_INPUT_POST) == 1 ? true : false; + $displayOptions['target_menu'] = get_input_value('button_display_option', RCUBE_INPUT_POST); + + $args['prefs']['advanced_search_display_options'] = $displayOptions; + + return($args); + } + + /** + * Add a section advanced search to the preferences section list + * + * @access public + */ + public function preferences_section($args) + { + $args['list']['advancedsearch'] = array( + 'id' => 'advancedsearch', + 'section' => Q($this->gettext('advancedsearch')) + ); + + return($args); + } + + /** + * Display advanced search configuration in user preferences tab + * + * @access public + */ + public function preferences_list($args) + { + if ($args['section'] == 'advancedsearch') { + + $this->rc = rcmail::get_instance(); + $args['blocks']['label_display_options'] = array('options' => array(), 'name' => Q($this->gettext('label_display_options'))); + + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu']; + $options = ''; + $optarg = array('value' => 'messagemenu'); + if ($target_menu == 'messagemenu') { + $optarg['selected'] = 'selected'; + $target_image = 'menu_location_a.jpg'; + } + $options .= html::tag('option', $optarg, Q($this->gettext('display_in_messagemenu'))); + $optarg = array('value' => 'toolbar'); + if ($target_menu == 'toolbar') { + $optarg['selected'] = 'selected'; + $target_image = 'menu_location_b.jpg'; + } + $options .= html::tag('option', $optarg, Q($this->gettext('display_in_toolbar'))); + $select = html::tag('select', array('name' => 'button_display_option', 'id' => 'button_display_option'), $options); + + $label1 = html::label('_show_message_label_header', Q($this->gettext('mailbox_headers_in_results'))); + $label2 = html::label('_show_message_mbox_info', Q($this->gettext('mailbox_info_in_results'))); + $label3 = html::label('button_display_option', Q($this->gettext('show_advanced_search'))); + + $arg1 = array('name' => '_show_message_label_header', 'id' => '_show_message_label_header', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1); + if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) { + $arg1['checked'] = 'checked'; + $img1class = 'enabled'; + } else { + $img1class = 'disabled'; + } + $check1 = html::tag('input', $arg1); + $arg2 = array('name' => '_show_message_mbox_info', 'id' => '_show_message_mbox_info', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1); + if (isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] === true) { + $arg2['checked'] = 'checked'; + $img2class = 'enabled'; + } else { + $img2class = 'disabled'; + } + + $img1 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_row.jpg'), 'class' => $img1class)); + $img2 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_col.jpg'), 'class' => $img2class)); + $img3 = html::img(array('src' => $this->url('skins/larry/images/' . $target_image))); + + $check2 = html::tag('input', $arg2); + $args['blocks']['label_display_options']['options'][0] = array('title' => '', 'content' => '' . $check1 . ' ' . $label1 . ' ' . $img1 . '
'); + $args['blocks']['label_display_options']['options'][1] = array('title' => '', 'content' => '' . $check2 . ' ' . $label2 . ' ' . $img2 . '
'); + $args['blocks']['label_display_options']['options'][2] = array('title' => '', 'content' => '' . $label3 . ' ' . $select . ' ' . $img3 . '
'); + } + + return($args); + } + + private function perform_search($search_string, $folders, $page = 1) + { + // Search all folders and build a final set + if ($folders[0] == 'all' || empty($folders)) { + $folders_search = $this->rc->imap->list_folders_subscribed(); + } else { + $folders_search = $folders; + } + $count = 0; + $folder_count = array(); + foreach ($folders_search as $mbox) { + $this->rc->storage->set_folder($mbox); + $this->rc->storage->search($mbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $result = array(); + $fcount = $this->rc->storage->count($mbox, 'ALL', !empty($_REQUEST['_refresh'])); + $count += $fcount; + $folder_count[$mbox] = $fcount; + } + foreach ($folder_count as $k => $v) { + if ($v == 0) { + unset($folder_count[$k]); + } + } + $fetch = $this->do_pagination($folder_count, $page); + $mails = array(); + $currentMailbox = ""; + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $showMboxColumn = isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] ? true : false; + $uid_mboxes = array(); + foreach ($fetch as $mailbox => $data) { + if ($currentMailbox != $mailbox) { + $currentMailbox = $mailbox; + if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) { + $this->rc->output->command('advanced_search_add_mbox', $mailbox, $folder_count[$mailbox], $showMboxColumn); + } + } + $uid_mboxes = array_merge($uid_mboxes, $this->getMails($mailbox, $data, $search_string, $showMboxColumn)); + } + + return array('result' => array(), 'count' => $count, 'uid_mboxes' => $uid_mboxes); + } + + private function getMails($mailbox, $data, $search_string, $showMboxColumn) + { + $pageSize = $this->rc->storage->get_pagesize(); + $msgNum = $data['from']; + $startPage = ceil($msgNum/$pageSize); + $msgMod = $msgNum % $pageSize; + $multiPage = "false"; + $firstArrayElement = $msgMod == 0 ? ($pageSize-1) : ($msgMod-1); + $quantity = $data['to'] - $data['from']; + if ($data['from'] + $quantity > $pageSize) { + $multiPage = "true"; + } + $this->rc->storage->set_folder($mailbox); + $this->rc->storage->search($mailbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $messages = $this->rc->storage->list_messages('', $startPage); + if ($multiPage) { + $messages = array_merge($messages, $this->rc->storage->list_messages('', $startPage+1)); + } + //FIRST: 0 QUANTITY: 2 + $sliceTo = $quantity + 1; + $mslice = array_slice($messages, $firstArrayElement, $sliceTo, true); + $messages = $mslice; + $avbox = array(); + $showAvmbox = false; + foreach ($messages as $set_flag) { + $set_flag->flags['skip_mbox_check'] = true; + if ($showMboxColumn === true) { + $set_flag->avmbox = $mailbox; + $avbox[] = 'avmbox'; + $showAvmbox = true; + } + } + $uid_mboxes = $this->rcmail_js_message_list($messages, false, null, $showAvmbox, $avbox, $showMboxColumn); + + return $uid_mboxes; + } + + public function save_search() + { + $search_name = get_input_value('search_name', RCUBE_INPUT_GPC); + if ($search_name) { + $search = array(); + $search['search'] = get_input_value('search', RCUBE_INPUT_GPC); + $search['search_name'] = $search_name; + $search['folder'] = get_input_value('folder', RCUBE_INPUT_GPC); + $search['sub_folders'] = get_input_value('sub_folders', RCUBE_INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $prefs['advanced_search'][$search_name] = $search; + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_saved'], 'confirmation'); + } + } + + public function delete_search() + { + $search_name = get_input_value('search_name', RCUBE_INPUT_GPC); + if ($search_name) { + $prefs = (array)$this->rc->user->get_prefs(); + unset($prefs['advanced_search'][$search_name]); + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_deleted'], 'notice'); + } + } + + public function get_saved_search() + { + $search_name = get_input_value('search_name', RCUBE_INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + + $search = isset($prefs['advanced_search'][$search_name]) ? $prefs['advanced_search'][$search_name] : false; + $this->rc->output->command('plugin.load_saved_search', $search); + $this->rc->output->send(); + } + + private function get_saved_search_names() + { + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $names = array(); + foreach($prefs['advanced_search'] as $name => $search) { + $names[] = $name; + } + + return $names; + } +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/advanced_search.php~ --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/advanced_search.php~ Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,1088 @@ +rc = rcmail::get_instance(); + $this->load_config("config-default.inc.php"); + $this->load_config(); + $this->config = $this->rc->config->get('advanced_search_plugin'); + $this->register_action('plugin.display_advanced_search', array($this, 'display_advanced_search')); + $this->register_action('plugin.trigger_search', array($this, 'trigger_search')); + $this->register_action('plugin.trigger_search_pagination', array($this, 'trigger_search_pagination')); + $this->register_action('plugin.save_search', array($this, 'save_search')); + $this->register_action('plugin.delete_search', array($this, 'delete_search')); + $this->register_action('plugin.get_saved_search', array($this, 'get_saved_search')); + + $this->skin = $this->rc->config->get('skin'); + $this->add_texts('localization', true); + $this->populate_i18n(); + if(isset($this->config['criteria'])) { + foreach($this->config['criteria'] as $key => $translation) { + $this->config['criteria'][$key] = $this->gettext($key); + } + } + $this->include_script('advanced_search.js'); + + if ($this->rc->task == 'mail') { + $file = 'skins/' . $this->skin . '/advanced_search.css'; + + if (file_exists($this->home . '/' . $file)) { + $this->include_stylesheet($file); + } else { + $this->include_stylesheet('skins/default/advanced_search.css'); + } + + if (empty($this->rc->action)) { + $this->add_menu_entry(); + } + } elseif ($this->rc->task == 'settings') { + $this->add_hook('preferences_list', array($this, 'preferences_list')); + $this->add_hook('preferences_save', array($this, 'preferences_save')); + $this->add_hook('preferences_sections_list', array($this, 'preferences_section')); + $file = 'skins/' . $this->skin . '/advanced_search.css'; + if (file_exists($this->home . '/' . $file)) { + $this->include_stylesheet($file); + } else { + $this->include_stylesheet('skins/default/advanced_search.css'); + } + } + + $this->add_hook('startup', array($this, 'startup')); + } + + + public function startup($args) + { + $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GET); + if (!isset($search)) { + $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_POST); + } + $rsearch = $search == 'advanced_search_active'; + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET); + $draft_uid = rcube_utils::get_input_value('_draft_uid', rcube_utils::INPUT_GET); + $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET); + $page = rcube_utils::get_input_value('_page', rcube_utils::INPUT_GET); + $sort = rcube_utils::get_input_value('_sort', rcube_utils::INPUT_GET); + + if (!empty($uid)) { + $parts = explode('__MB__', $uid); + if (count($parts) == 2) { + $search = 'advanced_search_active'; + } + } + if (!empty($draft_uid)) { + $parts = explode('__MB__', $draft_uid); + if (count($parts) == 2) { + $search = 'advanced_search_active'; + } + } + + if ($search == 'advanced_search_active') { + if ($args['action'] == 'show' && !empty($uid)) { + $parts = explode('__MB__', $uid); + $uid = $parts[0]; + $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_uid' => $uid)); + } + if ($args['action'] == 'compose') { + $draft_uid = rcube_utils::get_input_value('_draft_uid', rcube_utils::INPUT_GET); + $parts = explode('__MB__', $draft_uid); + $draft_uid = $parts[0]; + if (!empty($draft_uid)) { + $this->rc->output->redirect(array('_task' => 'mail', '_action' => $args['action'], '_mbox' => $mbox, '_draft_uid' => $draft_uid)); + } + } + if ($args['action'] == 'list' && $rsearch) { + $this->rc->output->command('advanced_search_active', '_page=' . $page . '&_sort=' . $sort); + $this->rc->output->send(); + $args['abort'] = true; + } + if ($args['action'] == 'mark') { + $flag = rcube_utils::get_input_value('_flag', rcube_utils::INPUT_POST); + $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST); + + $post_str = '_flag=' . $flag . '&_uid=' . $uid; + if ($quiet = rcube_utils::get_input_value('_quiet', rcube_utils::INPUT_POST)) { + $post_str .= '&_quiet=' . $quiet; + } + if ($from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST)) { + $post_str .= '&_from=' . $from; + } + if ($count = rcube_utils::get_input_value('_count', rcube_utils::INPUT_POST)) { + $post_str .= '&_count=' . $count; + } + if ($ruid = rcube_utils::get_input_value('_ruid', rcube_utils::INPUT_POST)) { + $post_str .= '&_ruid=' . $ruid; + } + $this->rc->output->command('label_mark', $post_str); + $this->rc->output->send(); + $args['abort'] = true; + } + + } else { + if ($args['action'] != 'plugin.get_saved_search' && $args['action'] != 'plugin.save_search' && $args['action'] != 'plugin.delete_search') { + $this->rc->output->command('plugin.advanced_search_del_header', array()); + } + } + } + + /** + * This function populates an array with localization texts. + * This is needed as ew are using a lot of localizations from core. + * The core localizations are not avalable directly in JS + * + * @access private + * @return null + */ + private function populate_i18n() + { + $core = array('advsearch', 'search', 'resetsearch', 'addfield', 'delete', 'cancel'); + + foreach ($core as $label) { + $this->i18n_strings[$label] = $this->rc->gettext($label); + } + + $local = array('in', 'and', 'or', 'not', 'where', 'exclude', 'andsubfolders', 'allfolders', 'save_the_search', 'has_been_saved', 'deletesearch', 'has_been_deleted'); + + foreach ($local as $label) { + $this->i18n_strings[$label] = $this->gettext($label); + } + } + + /** + * This adds a button into the configured menu to use the advanced search + * + * @access public + * @return null + */ + public function add_menu_entry() + { + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu']; + if ($target_menu != 'toolbar') { + $this->api->add_content( + html::tag( + 'li', + null, + $this->api->output->button( + array( + 'command' => 'plugin.advanced_search', + 'label' => 'advsearch', + 'type' => 'link', + 'classact' => 'icon advanced-search active', + 'class' => 'icon advanced-search', + 'innerclass' => 'icon advanced-search' + ) + ) + ), + $target_menu + ); + } else { + $this->api->add_content( + $this->api->output->button( + array( + 'command' => 'plugin.advanced_search', + 'title' => 'advsearch', + 'label' => 'search', + 'type' => 'link', + 'classact' => 'button advanced-search active', + 'class' => 'button advanced-search', + 'innerclass' => 'button advanced-search', + ) + ), + $target_menu + ); + } + } + + /** + * This function quotes some specific values based on their data type + * + * @param mixed $input The value to get quoted + * @access public + * @return The quoted value + */ + public function quote($value) + { + if (getType($value) == 'string') { + if (!preg_match('/"/', $value)) { + $value = preg_replace('/^"/', '', $value); + $value = preg_replace('/"$/', '', $value); + $value = preg_replace('/"/', '\\"', $value); + } + + $value = '"' . $value . '"'; + } + + return $value; + } + + /** + * This function generates the IMAP compatible search query based on the request data (by javascript) + * + * @param array $input The raw criteria data sent by javascript + * @access private + * @return string or int + */ + private function process_search_part($search_part) + { + $command_str = ''; + $flag = false; + + // Check for valid input + if (!array_key_exists($search_part['filter'], $this->config['criteria'])) { + $this->rc->output->show_message($this->gettext('internalerror'), 'error'); + + return 0; + } + if (in_array($search_part['filter'], $this->config['flag_criteria'])) { + $flag = true; + } + if (!$flag && !(isset($search_part['filter-val']) && $search_part['filter-val'] != '')) { + return 1; + } + + // Negate part + if ($search_part['not'] == 'true') { + $command_str .= 'NOT '; + } + + $command_str .= $search_part['filter']; + + if (!$flag) { + if (in_array($search_part['filter'], $this->config['date_criteria'])) { + // Take date format from user environment + $date_format = $this->rc->config->get('date_format', 'Y-m-d'); + // Try to use PHP5.2+ DateTime but fallback to ugly old method + if (class_exists('DateTime')) { + $date = DateTime::createFromFormat($date_format, $search_part['filter-val']); + $command_str .= ' ' . $this->quote($date->format("d-M-Y")); + } else { + $date_format = preg_replace('/(\w)/', '%$1', $date_format); + $date_array = strptime($search_part['filter-val'], $date_format); + $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); + $command_str .= ' ' . $this->quote(date("d-M-Y", $unix_ts)); + } + } elseif (in_array($search_part['filter'], $this->config['email_criteria'])) { + // Strip possible ',' added by autocomplete + $command_str .= ' ' . $this->quote(trim($search_part['filter-val'], " \t,")); + } else { + // Don't try to use a value for a binary flag object + $command_str .= ' ' . $this->quote($search_part['filter-val']); + } + } + + return $command_str; + } + + /** + * This function generates the IMAP compatible search query based on the request data (by javascript) + * + * @param array $input The raw criteria data sent by javascript + * @access public + * @return The final search command + */ + public function get_search_query($input) + { + $command = array(); + + foreach ($input as $search_part) { + // Skip excluded parts + if ($search_part['excluded'] == 'true') { + continue; + } + if (! $part_command = $this->process_search_part($search_part)) { + return 0; + } + // Skip invalid parts + if ($part_command === 1) { + continue; + } + + $command[] = array('method' => isset($search_part['method']) ? $search_part['method'] : 'and', + 'command' => $part_command); + } + + $command_string = $this->build_search_string($command); + + return $command_string; + } + + /** + * This function converts the preconfigured query parts (as array) into an IMAP compatible string + * + * @param array $command_array An array containing the advanced search criteria + * @access public + * @return The command string + */ + private function build_search_string($command_array) + { + $command = array(); + $paranthesis = 0; + $prev_method = null; + $next_method = null; + $cnt = count($command_array); + + foreach ($command_array as $k => $v) { + $part = ''; + $next_method = 'unknown'; + + // Lookup next method + if ($k < $cnt-1) { + $next_method = $command_array[$k+1]['method']; + } + + // If previous option was OR, close any open brakets + if ($paranthesis > 0 && $prev_method == 'or' && $v['method'] != 'or') { + for (; $paranthesis > 0; $paranthesis--) { + $part .= ')'; + } + } + + // If there are two consecutive ORs, add brakets + // If the next option is a new OR, add the prefix here + // If the next option is _not_ an OR, and the current option is AND, prefix ALL + if ($next_method == 'or') { + if ($v['method'] == 'or') { + $part .= ' ('; + $paranthesis++; + } + $part .= 'OR '; + } elseif ($v['method'] == 'and') { + $part .= 'ALL '; + } + + $part .= $v['command']; + + // If this is the end of the query, and we have open brakets, close them + if ($k == $cnt-1 && $paranthesis > 0) { + for (; $paranthesis > 0; $paranthesis--) { + $part .= ')'; + } + } + + $prev_method = $v['method']; + $command[] = $part; + } + + $command = implode(' ', $command); + + return $command; + } + + /** + * This functions sends the initial data to the client side where a form (in dialog) is built for the advanced search + * + * @access public + * @return null + */ + public function display_advanced_search() + { + $ret = array('html' => $this->generate_searchbox(), + 'row' => $this->add_row(), + 'saved_searches' => $this->get_saved_search_names(), + 'title' => $this->i18n_strings['advsearch'], + 'date_criteria' => $this->config['date_criteria'], + 'flag_criteria' => $this->config['flag_criteria'], + 'email_criteria' => $this->config['email_criteria']); + + $this->rc->output->command('plugin.show', $ret); + } + + public function generate_searchbox() + { + $search_button = new html_inputfield(array('type' => 'submit', 'name' => 'search', 'class' => 'button mainaction', 'value' => $this->i18n_strings['search'])); + $reset_button = new html_inputfield(array('type' => 'reset', 'name' => 'reset', 'class' => 'button reset', 'value' => $this->i18n_strings['resetsearch'])); + $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'])); + $delete_button = new html_inputfield(array('type' => 'submit', 'name' => 'delete', 'style' => 'display: none;', 'class' => 'button delete_search', 'value' => $this->i18n_strings['deletesearch'])); + + $layout_table = new html_table(); + $layout_table->add(null, $search_button->show()); + $folderConfig = array('name' => 'folder'); + $layout_table->add( + null, + $this->i18n_strings['in'] . ': ' . + $this->folder_selector($folderConfig)->show($this->rc->storage->get_folder()) . + html::span( + array('class' => 'sub-folders'), + $this->i18n_strings['andsubfolders'] . ': ' . + html::tag( + 'input', + array('type' => 'checkbox', 'name' => 'subfolder'), + null + ) + ) . + $this->i19n_strings['where'] + ); + $first_row = $this->add_row(true); + $layout_table->add_row(); + $layout_table->add(array('class' => 'adv-search-and-or'), null); + $layout_table->add(null, $first_row); + $layout_table->add_row(); + $layout_table->add(null, $search_button->show()); + $layout_table->add(null, $save_button . ' ' . $reset_button->show() . ' ' . $delete_button->show()); + + return html::tag( + 'div', + array('id' => 'adsearch-popup'), + html::tag( + 'form', + array('method' => 'post', 'action' => '#'), + $layout_table->show() + ) + ); + } + + /** + * This function is used to render the html of the advanced search form and also + * the later following rows are created by this function + * + * @param array $folders Array of folders + * @param boolean $first True if form gets created, False to create a new row + * @access public + * @return string The final html + */ + public function add_row($first = false) + { + $row_html = ''; + $optgroups = ''; + + $criteria = $this->config['criteria']; + $all_criteria = array( + $this->gettext('Common') => $this->config['prefered_criteria'], + $this->gettext('Addresses') => $this->config['email_criteria'], + $this->gettext('Dates') => $this->config['date_criteria'], + $this->gettext('Flags') => $this->config['flag_criteria'], + $this->gettext('Other') => $this->config['other_criteria'], + ); + + foreach ($all_criteria as $label => $specific_criteria) { + $options = ''; + + foreach ($specific_criteria as $value) { + $options .= html::tag('option', array('value' => $value), $criteria[$value]); + } + + $optgroups .= html::tag('optgroup', array('label' => $label), $options); + } + + $tmp = html::tag('select', array('name' => 'filter'), $optgroups); + $tmp .= $this->i18n_strings['not'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'not'), null); + $tmp .= html::tag('input', array('type' => 'text', 'name' => 'filter-val')); + $tmp .= $this->i18n_strings['exclude'] . ':' . html::tag('input', array('type' => 'checkbox', 'name' => 'filter-exclude'), null); + $tmp .= html::tag('button', array('name' => 'add', 'class' => 'add'), $this->i18n_strings['addfield']); + + if ($first) { + $row_html = $tmp; + } else { + $and_or_select = new html_select(array('name' => 'method')); + $and_or_select->add($this->i18n_strings['and'], 'and'); + $and_or_select->add($this->i18n_strings['or'], 'or'); + $tmp .= html::tag('button', array('name' => 'delete', 'class' => 'delete'), $this->i18n_strings['delete']); + $row_html = html::tag( + 'tr', + null, + html::tag( + 'td', + array('class' => 'adv-search-and-or'), + $and_or_select->show() + ) . + html::tag( + 'td', + null, + $tmp + ) + ); + } + + return $row_html; + } + + /** + * Return folders list as html_select object + * + * This is a copy of the core function and adapted to fit + * the needs of the advanced_search function + * + * @param array $p Named parameters + * + * @return html_select HTML drop-down object + */ + public function folder_selector($p = array()) + { + $p += array('maxlength' => 100, 'realnames' => false, 'is_escaped' => true); + $a_mailboxes = array(); + $storage = $this->rc->get_storage(); + + $list = $storage->list_folders_subscribed(); + $delimiter = $storage->get_hierarchy_delimiter(); + + foreach ($list as $folder) { + $this->rc->build_folder_tree($a_mailboxes, $folder, $delimiter); + } + + $select = new html_select($p); + $select->add($this->i18n_strings['allfolders'], "all"); + + $this->rc->render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p); + + return $select; + } + + public function trigger_search_pagination($param) + { + $_GET['search'] = $_SESSION['av_search']; + $_GET['folder'] = $_SESSION['av_folder']; + $_GET['sub_folders'] = $_SESSION['av_sub_folders']; + $this->trigger_search(true); + } + + /** + * Here is where the actual query is fired to the imap server and the result is evaluated and sent back to the client side + * + * @access public + * @return null + */ + public function trigger_search($inPagination = false) + { + $search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC); + // reset list_page and old search results + $this->rc->storage->set_page(1); + $this->rc->storage->set_search_set(null); + $page = rcube_utils::get_input_value('_page', rcube_utils::INPUT_GPC); + $page = $page ? $page : 1; + $pagesize = $this->rc->storage->get_pagesize(); + + if (!empty($search)) { + $mbox = rcube_utils::get_input_value('folder', rcube_utils::INPUT_GPC); + $imap_charset = RCMAIL_CHARSET; + $sort_column = rcmail_sort_column(); + $search_str = $this->get_search_query($search); + $sub_folders = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC) == 'true'; + $folders = array(); + $result_h = array(); + $count = 0; + $new_id = 1; + $current_mbox = $this->rc->storage->get_folder(); + $uid_list = array(); + //Store information in session for pagination + $_SESSION['av_search'] = $search; + $_SESSION['av_folder'] = $mbox; + $_SESSION['av_sub_folders'] = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC); + $nosub = $sub_folders; + $folders = $this->rc->get_storage()->list_folders_subscribed(); + if (empty($folders) || ($sub_folders === false && $mbox !== 'all')) { + $folders = array($mbox); + } elseif ($mbox !== 'all') { + if ($sub_folders === false) { + $folders = array($mbox); + } else { + $folders = $this->rc->get_storage()->list_folders_subscribed_direct($mbox); + } + } + $md5folders = array(); + foreach ($folders as $folder) { + $md5folders[md5($folder)] = $folder; + } + $this->rc->output->set_env('as_md5_folders', $md5folders); + + if ($search_str) { + $res = $this->perform_search($search_str, $folders, $page); + $count = $res['count']; + } + + + if ($count > 0) { + $_SESSION['advanced_search']['uid_list'] = $uid_list; + if ($search_str && $inPagination == false) { + $this->rc->output->show_message('searchsuccessful', 'confirmation', array('nr' => $count)); + } + } elseif ($err_code = $this->rc->storage->get_error_code()) { + rcmail_display_server_error(); + } else { + $this->rc->output->show_message('searchnomatch', 'notice'); + } + + $current_folder = rcube_utils::get_input_value('current_folder', rcube_utils::INPUT_GPC); + + $this->rc->output->set_env('search_request', 'advanced_search_active'); + $this->rc->output->set_env('messagecount', $count); + $this->rc->output->set_env('pagecount', ceil($count / $pagesize)); + $this->rc->output->set_env('exists', $this->rc->storage->count($current_folder, 'EXISTS')); + $this->rc->output->command('set_rowcount', rcmail_get_messagecount_text($count, $page)); + $this->rc->output->command('plugin.search_complete'); + $this->rc->output->send(); + } + } + + /** + * return javascript commands to add rows to the message list + */ + public function rcmail_js_message_list($a_headers, $insert_top = false, $a_show_cols = null, $avmbox = false, $avcols = array(), $showMboxColumn = false) + { + global $CONFIG, $RCMAIL, $OUTPUT; + $uid_mboxes = array(); + + if (empty($a_show_cols)) { + if (!empty($_SESSION['list_attrib']['columns'])) { + $a_show_cols = $_SESSION['list_attrib']['columns']; + } else { + $a_show_cols = is_array($CONFIG['list_cols']) ? $CONFIG['list_cols'] : array('subject'); + } + } else { + if (!is_array($a_show_cols)) { + $a_show_cols = preg_split('/[\s,;]+/', strip_quotes($a_show_cols)); + } + $head_replace = true; + } + + $mbox = $RCMAIL->storage->get_folder(); + + // make sure 'threads' and 'subject' columns are present + if (!in_array('subject', $a_show_cols)) { + array_unshift($a_show_cols, 'subject'); + } + if (!in_array('threads', $a_show_cols)) { + array_unshift($a_show_cols, 'threads'); + } + $_SESSION['list_attrib']['columns'] = $a_show_cols; + + // Make sure there are no duplicated columns (#1486999) + $a_show_cols = array_merge($a_show_cols, $avcols); + $a_show_cols = array_unique($a_show_cols); + + // Plugins may set header's list_cols/list_flags and other rcube_message_header variables + // and list columns + $plugin = $RCMAIL->plugins->exec_hook( + 'messages_list', + array('messages' => $a_headers, 'cols' => $a_show_cols) + ); + $a_show_cols = $plugin['cols']; + $a_headers = $plugin['messages']; + $thead = $head_replace ? rcmail_message_list_head($_SESSION['list_attrib'], $a_show_cols) : null; + + // get name of smart From/To column in folder context + if (($f = array_search('fromto', $a_show_cols)) !== false) { + $smart_col = rcmail_message_list_smart_column_name(); + } + if ($this->coltypesSet == false) { + $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col); + if ($showMboxColumn === true) { + $OUTPUT->command('plugin.advanced_search_add_header', array()); + } + $this->coltypesSet = true; + } + + if (empty($a_headers)) { + return; + } + + // remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here + foreach (array('threads', 'attachment', 'flag', 'status', 'priority') as $col) { + if (($key = array_search($col, $a_show_cols)) !== false) { + unset($a_show_cols[$key]); + } + } + + // loop through message headers + foreach ($a_headers as $n => $header) { + if (empty($header)) { + continue; + } + $a_msg_cols = array(); + $a_msg_flags = array(); + // format each col; similar as in rcmail_message_list() + foreach ($a_show_cols as $col) { + $col_name = $col == 'fromto' ? $smart_col : $col; + + if (in_array($col_name, array('from', 'to', 'cc', 'replyto'))) { + $cont = rcmail_address_string($header->$col_name, 3, false, null, $header->charset); + } elseif ($col == 'subject') { + $cont = trim(rcube_mime::decode_header($header->$col, $header->charset)); + if (!$cont) { + $cont = rcube_label('nosubject'); + } + $cont = rcube::Q($cont); + } elseif ($col == 'size') { + $cont = $this->rc->show_bytes($header->$col); + } elseif ($col == 'date') { + $cont = rcube_utils::format_datestr($header->date,$date_format); + } else { + $cont = rcube::Q($header->$col); + } + $a_msg_cols[$col] = $cont; + } + + $a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags)); + if ($header->depth) { + $a_msg_flags['depth'] = $header->depth; + } elseif ($header->has_children) { + $roots[] = $header->uid; + } + if ($header->parent_uid) { + $a_msg_flags['parent_uid'] = $header->parent_uid; + } + if ($header->has_children) { + $a_msg_flags['has_children'] = $header->has_children; + } + if ($header->unread_children) { + $a_msg_flags['unread_children'] = $header->unread_children; + } + if ($header->others['list-post']) { + $a_msg_flags['ml'] = 1; + } + if ($header->priority) { + $a_msg_flags['prio'] = (int) $header->priority; + } + $a_msg_flags['ctype'] = rcube::Q($header->ctype); + $a_msg_flags['mbox'] = $mbox; + if (!empty($header->list_flags) && is_array($header->list_flags)) { + $a_msg_flags = array_merge($a_msg_flags, $header->list_flags); + } + if (!empty($header->list_cols) && is_array($header->list_cols)) { + $a_msg_cols = array_merge($a_msg_cols, $header->list_cols); + } + if ($showMboxColumn === true) { + $a_msg_flags['avmbox'] = $avmbox; + } + + $OUTPUT->command( + 'add_message_row', + $header->uid . '__MB__' . md5($mbox), + $a_msg_cols, + $a_msg_flags, + $insert_top + ); + $id = $header->uid . '__MB__' . md5($mbox); + $uid_mboxes[$id] = array('uid' => $header->uid, 'mbox' => $mbox, 'md5mbox' => md5($mbox)); + } + + if ($RCMAIL->storage->get_threading()) { + $OUTPUT->command('init_threads', (array) $roots, $mbox); + } + + return $uid_mboxes; + } + + + private function do_pagination($folders, $onPage) + { + $perPage = $this->rc->storage->get_pagesize(); + $from = $perPage * $onPage - $perPage + 1; + $to = $from + $perPage - 1; + $got = 0; + $pos = 0; + $cbox = ""; + $boxStart = 0; + $boxStop = 0; + $fetch = array(); + foreach ($folders as $box => $num) { + $i = $num; + if ($box != $cbox) { + $boxStart = 0; + $boxStop = 0; + $cbox = $box; + } + while ($i--) { + $pos++; + $boxStart++; + if ($pos >= $from && $pos <= $to) { + if (!isset($fetch[$box])) { + $fetch[$box] = array("from" => $boxStart); + } + $fetch[$box]['to'] = $boxStart; + $got++; + } + } + if ($got >= $perPage) { + break; + } + } + + return $fetch; + } + + /** + * Save advanced search preferences + * + * @access public + */ + public function preferences_save($args) + { + if ($args['section'] != 'advancedsearch') { + return; + } + $rcmail = rcmail::get_instance(); + + $displayOptions = array(); + $displayOptions['_show_message_label_header'] = rcube_utils::get_input_value('_show_message_label_header', rcube_utils::INPUT_POST) == 1 ? true : false; + $displayOptions['_show_message_mbox_info'] = rcube_utils::get_input_value('_show_message_mbox_info', rcube_utils::INPUT_POST) == 1 ? true : false; + $displayOptions['target_menu'] = rcube_utils::get_input_value('button_display_option', rcube_utils::INPUT_POST); + + $args['prefs']['advanced_search_display_options'] = $displayOptions; + + return($args); + } + + /** + * Add a section advanced search to the preferences section list + * + * @access public + */ + public function preferences_section($args) + { + $args['list']['advancedsearch'] = array( + 'id' => 'advancedsearch', + 'section' => rcube::Q($this->gettext('advancedsearch')) + ); + + return($args); + } + + /** + * Display advanced search configuration in user preferences tab + * + * @access public + */ + public function preferences_list($args) + { + if ($args['section'] == 'advancedsearch') { + + $this->rc = rcmail::get_instance(); + $args['blocks']['label_display_options'] = array('options' => array(), 'name' => rcube::Q($this->gettext('label_display_options'))); + + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $target_menu = (isset($displayOptions['target_menu']) && !empty($displayOptions['target_menu'])) ? $displayOptions['target_menu'] : $this->config['target_menu']; + $options = ''; + $optarg = array('value' => 'messagemenu'); + if ($target_menu == 'messagemenu') { + $optarg['selected'] = 'selected'; + $target_image = 'menu_location_a.jpg'; + } + $options .= html::tag('option', $optarg, rcube::Q($this->gettext('display_in_messagemenu'))); + $optarg = array('value' => 'toolbar'); + if ($target_menu == 'toolbar') { + $optarg['selected'] = 'selected'; + $target_image = 'menu_location_b.jpg'; + } + $options .= html::tag('option', $optarg,rcube::Q($this->gettext('display_in_toolbar'))); + $select = html::tag('select', array('name' => 'button_display_option', 'id' => 'button_display_option'), $options); + + $label1 = html::label('_show_message_label_header', rcube::Q($this->gettext('mailbox_headers_in_results'))); + $label2 = html::label('_show_message_mbox_info', rcube::Q($this->gettext('mailbox_info_in_results'))); + $label3 = html::label('button_display_option', rcube::Q($this->gettext('show_advanced_search'))); + + $arg1 = array('name' => '_show_message_label_header', 'id' => '_show_message_label_header', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1); + if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) { + $arg1['checked'] = 'checked'; + $img1class = 'enabled'; + } else { + $img1class = 'disabled'; + } + $check1 = html::tag('input', $arg1); + $arg2 = array('name' => '_show_message_mbox_info', 'id' => '_show_message_mbox_info', 'type' => 'checkbox', 'title' => "", 'class' => 'watermark linput', 'value' => 1); + if (isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] === true) { + $arg2['checked'] = 'checked'; + $img2class = 'enabled'; + } else { + $img2class = 'disabled'; + } + + $img1 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_row.jpg'), 'class' => $img1class)); + $img2 = html::img(array('src' => $this->url('skins/larry/images/show_mbox_col.jpg'), 'class' => $img2class)); + $img3 = html::img(array('src' => $this->url('skins/larry/images/' . $target_image))); + + $check2 = html::tag('input', $arg2); + $args['blocks']['label_display_options']['options'][0] = array('title' => '', 'content' => '' . $check1 . ' ' . $label1 . ' ' . $img1 . '
'); + $args['blocks']['label_display_options']['options'][1] = array('title' => '', 'content' => '' . $check2 . ' ' . $label2 . ' ' . $img2 . '
'); + $args['blocks']['label_display_options']['options'][2] = array('title' => '', 'content' => '' . $label3 . ' ' . $select . ' ' . $img3 . '
'); + } + + return($args); + } + + private function perform_search($search_string, $folders, $page = 1) + { + // Search all folders and build a final set + if ($folders[0] == 'all' || empty($folders)) { + $folders_search = $this->rc->imap->list_folders_subscribed(); + } else { + $folders_search = $folders; + } + $count = 0; + $folder_count = array(); + foreach ($folders_search as $mbox) { + $this->rc->storage->set_folder($mbox); + $this->rc->storage->search($mbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $result = array(); + $fcount = $this->rc->storage->count($mbox, 'ALL', !empty($_REQUEST['_refresh'])); + $count += $fcount; + $folder_count[$mbox] = $fcount; + } + foreach ($folder_count as $k => $v) { + if ($v == 0) { + unset($folder_count[$k]); + } + } + $fetch = $this->do_pagination($folder_count, $page); + $mails = array(); + $currentMailbox = ""; + $displayOptions = $this->rc->config->get('advanced_search_display_options', array()); + $showMboxColumn = isset($displayOptions['_show_message_mbox_info']) && $displayOptions['_show_message_mbox_info'] ? true : false; + $uid_mboxes = array(); + foreach ($fetch as $mailbox => $data) { + if ($currentMailbox != $mailbox) { + $currentMailbox = $mailbox; + if (isset($displayOptions['_show_message_label_header']) && $displayOptions['_show_message_label_header'] === true) { + $this->rc->output->command('advanced_search_add_mbox', $mailbox, $folder_count[$mailbox], $showMboxColumn); + } + } + $uid_mboxes = array_merge($uid_mboxes, $this->getMails($mailbox, $data, $search_string, $showMboxColumn)); + } + + return array('result' => array(), 'count' => $count, 'uid_mboxes' => $uid_mboxes); + } + + private function getMails($mailbox, $data, $search_string, $showMboxColumn) + { + $pageSize = $this->rc->storage->get_pagesize(); + $msgNum = $data['from']; + $startPage = ceil($msgNum/$pageSize); + $msgMod = $msgNum % $pageSize; + $multiPage = "false"; + $firstArrayElement = $msgMod == 0 ? ($pageSize-1) : ($msgMod-1); + $quantity = $data['to'] - $data['from']; + if ($data['from'] + $quantity > $pageSize) { + $multiPage = "true"; + } + $this->rc->storage->set_folder($mailbox); + $this->rc->storage->search($mailbox, $search_string, RCMAIL_CHARSET, $_SESSION['sort_col']); + $messages = $this->rc->storage->list_messages('', $startPage); + if ($multiPage) { + $messages = array_merge($messages, $this->rc->storage->list_messages('', $startPage+1)); + } + //FIRST: 0 QUANTITY: 2 + $sliceTo = $quantity + 1; + $mslice = array_slice($messages, $firstArrayElement, $sliceTo, true); + $messages = $mslice; + $avbox = array(); + $showAvmbox = false; + foreach ($messages as $set_flag) { + $set_flag->flags['skip_mbox_check'] = true; + if ($showMboxColumn === true) { + $set_flag->avmbox = $mailbox; + $avbox[] = 'avmbox'; + $showAvmbox = true; + } + } + $uid_mboxes = $this->rcmail_js_message_list($messages, false, null, $showAvmbox, $avbox, $showMboxColumn); + + return $uid_mboxes; + } + + public function save_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + if ($search_name) { + $search = array(); + $search['search'] = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC); + $search['search_name'] = $search_name; + $search['folder'] = rcube_utils::get_input_value('folder', rcube_utils::INPUT_GPC); + $search['sub_folders'] = rcube_utils::get_input_value('sub_folders', rcube_utils::INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $prefs['advanced_search'][$search_name] = $search; + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_saved'], 'confirmation'); + } + } + + public function delete_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + if ($search_name) { + $prefs = (array)$this->rc->user->get_prefs(); + unset($prefs['advanced_search'][$search_name]); + $this->rc->user->save_prefs(array('advanced_search' => $prefs['advanced_search'])); + $this->rc->output->show_message('"' . $search_name . '" ' . $this->i18n_strings['has_been_deleted'], 'notice'); + } + } + + public function get_saved_search() + { + $search_name = rcube_utils::get_input_value('search_name', rcube_utils::INPUT_GPC); + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + + $search = isset($prefs['advanced_search'][$search_name]) ? $prefs['advanced_search'][$search_name] : false; + $this->rc->output->command('plugin.load_saved_search', $search); + $this->rc->output->send(); + } + + private function get_saved_search_names() + { + $prefs = (array)$this->rc->user->get_prefs(); + if (!isset($prefs['advanced_search'])) { + $prefs['advanced_search'] = array(); + } + $names = array(); + foreach($prefs['advanced_search'] as $name => $search) { + $names[] = $name; + } + + return $names; + } +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/composer.json --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/composer.json Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,23 @@ +{ + "name": "gms-sa/advanced-search", + "type": "roundcube-plugin", + "description": "Advanced search allowing multiple search criteria and folder recursion", + "keywords": ["mail","search","advanced"], + "homepage": "http://github.com/GMS-SA/roundcube-advanced-search/tree/stable", + "license": "GPL-3.0", + "repositories": [ + { + "type": "composer", + "url": "http://plugins.roundcube.net" + } + ], + "require": { + "roundcube/plugin-installer": ">=0.1.2" + }, + "extra": { + "roundcube": { + "min-version": "0.9.4", + "max-version": "1.0.9999" + } + } +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/config-default.inc.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/config-default.inc.php Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,45 @@ + 'messagemenu', + + // Every criteria which takes a email as argument + 'email_criteria' => array('HEADER FROM', 'HEADER TO', 'CC', 'BCC'), + + // Every criteria which takes a date as argument + 'date_criteria' => array('BEFORE', 'ON', 'SINCE', 'SENTBEFORE', 'SENTON', 'SENTSINCE'), + + // Every criteria which doesn't take an argument + 'flag_criteria' => array('ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'SEEN'), + + // Prefered criteria to show on the top of lists + 'prefered_criteria' => array('SUBJECT', 'BODY', 'HEADER FROM', 'HEADER TO', 'SENTSINCE', 'LARGER'), + + // Other criteria, anything not in the above lists, except 'prefered_criteria' + 'other_criteria' => array('SUBJECT', 'BODY', 'KEYWORD', 'LARGER', 'SMALLER'), + + // All filter criteria + 'criteria' => array( + 'ANSWERED' => 'Answered', + 'BCC' => 'Bcc', + 'BEFORE' => 'Before', + 'CC' => 'Cc', + 'DELETED' => 'Deleted', + 'DRAFT' => 'Draft', + 'FLAGGED' => 'Flagged', + 'KEYWORD' => 'Keyword', + 'LARGER' => 'Larger Than', + 'BODY' => 'Message Body', + 'ON' => 'On', + 'SEEN' => 'Read', + 'SENTBEFORE' => 'Sent Before', + 'HEADER FROM' => 'From', + 'SENTON' => 'Sent On', + 'SENTSINCE' => 'Sent Since', + 'HEADER TO' => 'To', + 'SINCE' => 'Since', + 'SMALLER' => 'Smaller Than', + 'SUBJECT' => 'Subject Contains' + ) +); diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/localization/de_DE.inc --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/localization/de_DE.inc Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,51 @@ + a.advanced-search span.advanced-search { + display: none; +} + +.advanced_search_dialog .ui-dialog-title { + width: 100%; +} + +.advanced_search_dialog .ui-dialog-title span.saved_searches { + border: 1px solid lightgray; + float: right; + display: block; + padding-right: 10px; + padding-left: 10px; + padding-bottom: 3px; + padding-top: 3px; + border-radius: 4px; + margin-right: 20px; +} + +#rcavbox1, #rcavbox2 { + width: 30%; +} + +#messagetoolbar a.advanced-search { + background: url(../../../../skins/classic/images/abook_toolbar.png) 0 0 no-repeat transparent; + background-position: -168px 0px; +} + +#messagemenu a.advanced-search { + color: #a0a0a0; + text-decoration: none; + background: url(../../../../skins/classic/images/icons/glass.png) no-repeat 7px 0; +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/advanced_search.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/plugins/advanced_search/skins/larry/advanced_search.css Sun May 27 16:53:56 2018 -0400 @@ -0,0 +1,218 @@ +a.icon.advanced-search span.advanced-search { + background-image: url(images/icon-loupe.png) !important; + background-position: 0 !important; +} + +a.button.advanced-search { + text-align: center; + font-size: 10px; + color: #555; + min-width: 50px; + max-width: 75px; + height: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 28px 2px 0 2px; + text-shadow: 0px 1px 1px #eee; + box-shadow: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + -o-box-shadow: none; + background: url(../../../../skins/larry/images/buttons.png?v=abf1.36693) -100px 0 no-repeat transparent; + border: 0; + border-radius: 0; + background-position: center -970px; +} + +.records-table tbody .aslabel_mbox td { + border-top: 1px solid #fff; + border-bottom: 1px solid #bbd3da; + cursor: default; + font-weight: normal; + background-color: #d9ecf4; + color: #376572; + text-shadow: 0px 1px 1px #fff; + text-decoration: none; + font-weight: bold; +} + +.records-table tbody .aslabel_mbox .aslabel_found { + top: 3px; + right: 6px; + min-width: 1.8em; + padding: 2px 4px; + background: #82acb5; + background: -moz-linear-gradient(top, #82acb5 0%, #6a939f 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#82acb5), color-stop(100%,#6a939f)); + background: -o-linear-gradient(top, #82acb5 0%, #6a939f 100%); + background: -ms-linear-gradient(top, #82acb5 0%, #6a939f 100%); + background: linear-gradient(top, #82acb5 0%, #6a939f 100%); + box-shadow: inset 0 1px 1px 0 #536d72; + -o-box-shadow: inset 0 1px 1px 0 #536d72; + -webkit-box-shadow: inset 0 1px 1px 0 #536d72; + -moz-box-shadow: inset 0 1px 1px 0 #536d72; + border-radius: 9px; + color: #fff; + text-align: center; + font-weight: bold; + text-shadow: none; + display: block; +} + +#adsearch-popup button.add, #adsearch-popup button.delete{ + display: inline-block; + margin: 0 2px; + padding: 2px 5px; + color: #525252; + text-shadow: 0px 1px 1px #fff; + border: 1px solid #c0c0c0; + border-radius: 4px; + background: #f7f7f7; + background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6)); + background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); + background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); + background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%); + box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3); + -o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3); + -webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3); + -moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3); + text-decoration: none; + outline: none; +} + +#adv-search tbody tr { + padding: 4px 10px; + background: #eee; + border-bottom: 2px solid #fff; +} + +#adv-search, #adv-search tr { + width: 100%; +} + +td.adv-search-and-or { + width: 80px; + text-align: center; +} + +#adsearch-popup .mainaction { + height: 24px; + padding-left: 10px; + padding-right: 10px; + margin-right: 10px; +} + +#rcavbox1, #rcavbox2 { + width: 30%; +} + +#rcmrowadvancedsearch td.section { + background-position: 6px -1650px !important; +} + +#rcmrowadvancedsearch.selected td.section { + background-position: 6px -1674px !important; +} + +p.avsearchpref span { + min-width: 280px; + display: block; + max-width: 280px; + float: left; + vertical-align: middle; +} + +p.avsearchpref img { + border-radius: 10px; +} + +p.avsearchpref img.disabled { + -webkit-filter: grayscale(100%); + -moz-filter: grayscale(100%); + filter: grayscale(100%); +} + +button.save_search { + color: #ffffff; + background-color: #2f96b4; + border-radius: 4px; + height: 20px; + padding-left: 10px; + padding-right: 10px; + margin-right: 10px; +} + +input.save_search { + padding: 0 10px 0 10px; + height: 20px; + background: #20b8fb; + border: none; + font-weight: bold; + color: #fff; + text-shadow: none; + background: #6193DF; + background: -moz-linear-gradient(top, #6193DF 0%, #2B84C0 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#6193DF), color-stop(100%,#2B84C0)); + background: -o-linear-gradient(top, #6193DF 0%, #2B84C0 100%); + background: -ms-linear-gradient(top, #6193DF 0%, #2B84C0 100%); + background: linear-gradient(top, #6193DF 0%, #2B84C0 100%); +} + +input.save_search:active { + background: #81C0EB; + background: -moz-linear-gradient(top, #6193DF 0%, #81C0EB 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#6193DF), color-stop(100%,#81C0EB)); + background: -o-linear-gradient(top, #6193DF 0%, #81C0EB 100%); + background: -ms-linear-gradient(top, #6193DF 0%, #81C0EB 100%); + background: linear-gradient(top, #6193DF 0%, #81C0EB 100%); +} + +input.delete_search { + padding: 0 10px 0 10px; + height: 19px; + background: #20b8fb; + border: none; + font-weight: bold; + color: #fff; + text-shadow: none; + background: #85190A; + background: -moz-linear-gradient(top, #85190A 0%, #CA361A 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#85190A), color-stop(100%,#CA361A)); + background: -o-linear-gradient(top, #85190A 0%, #CA361A 100%); + background: -ms-linear-gradient(top, #85190A 0%, #CA361A 100%); + background: linear-gradient(top, #85190A 0%, #CA361A 100%); +} + +input.delete_search:active { + background: #85190A; + background: -moz-linear-gradient(top, #85190A 0%, #F09F98 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#85190A), color-stop(100%,#F09F98)); + background: -o-linear-gradient(top, #85190A 0%, #F09F98 100%); + background: -ms-linear-gradient(top, #85190A 0%, #F09F98 100%); + background: linear-gradient(top, #85190A 0%, #F09F98 100%); +} + + +.advanced_search_dialog .ui-dialog-title { + width: 100%; +} + +.advanced_search_dialog .ui-dialog-title span.saved_searches { + border: 1px solid lightgray; + float: right; + display: block; + padding-right: 10px; + -moz-box-shadow: inset 0 0 5px #888; + -webkit-box-shadow: inset 0 0 5px#888; + box-shadow: inner 0 0 5px #888; + padding-left: 10px; + padding-bottom: 3px; + padding-top: 3px; + border-radius: 4px; +} + +#adsearch-popup input[name="filter-val"] { + margin-right: 4px; +} diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/images/icon-loupe.png Binary file plugins/advanced_search/skins/larry/images/icon-loupe.png has changed diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/images/menu_location_a.jpg Binary file plugins/advanced_search/skins/larry/images/menu_location_a.jpg has changed diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/images/menu_location_b.jpg Binary file plugins/advanced_search/skins/larry/images/menu_location_b.jpg has changed diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/images/show_mbox_col.jpg Binary file plugins/advanced_search/skins/larry/images/show_mbox_col.jpg has changed diff -r d41c01c5c933 -r 50ac5484d514 plugins/advanced_search/skins/larry/images/show_mbox_row.jpg Binary file plugins/advanced_search/skins/larry/images/show_mbox_row.jpg has changed