From 66df084203a217ab74a416064c459cc3420a648c Mon Sep 17 00:00:00 2001 From: alecpl Date: Tue, 6 Sep 2011 13:39:45 +0000 Subject: - Merge devel-spellcheck branch: - Added spellchecker exceptions dictionary (shared or per-user) - Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options) --- CHANGELOG | 3 + SQL/mssql.initial.sql | 10 ++ SQL/mssql.upgrade.sql | 11 ++ SQL/mysql.initial.sql | 11 ++ SQL/mysql.update.sql | 11 ++ SQL/postgres.initial.sql | 13 ++ SQL/postgres.update.sql | 10 ++ SQL/sqlite.initial.sql | 15 ++ SQL/sqlite.update.sql | 10 ++ config/main.inc.php.dist | 13 ++ program/include/main.inc | 13 +- program/include/rcube_spellchecker.php | 238 +++++++++++++++++++++++++++++--- program/js/editor.js | 13 +- program/js/googiespell.js | 245 +++++++++++++++++++++++---------- program/localization/en_US/labels.inc | 5 + program/steps/mail/compose.inc | 11 +- program/steps/settings/func.inc | 21 ++- program/steps/settings/save_prefs.inc | 5 +- program/steps/utils/spell.inc | 13 +- program/steps/utils/spell_html.inc | 4 + 20 files changed, 563 insertions(+), 112 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3c63d4281..ecc67ce0b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ CHANGELOG Roundcube Webmail =========================== +- Added spellchecker exceptions dictionary (shared or per-user) +- Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options) + RELEASE 0.6-rc ---------------- - Send X-Frame-Options headers to protect from clickjacking (#1487037) diff --git a/SQL/mssql.initial.sql b/SQL/mssql.initial.sql index 4aa6fc9f7..321da7cbb 100644 --- a/SQL/mssql.initial.sql +++ b/SQL/mssql.initial.sql @@ -93,6 +93,13 @@ CREATE TABLE [dbo].[users] ( ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO +CREATE TABLE [dbo].[dictionary] ( + [user_id] [int] , + [language] [varchar] (5) COLLATE Latin1_General_CI_AI NOT NULL , + [data] [text] COLLATE Latin1_General_CI_AI NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + ALTER TABLE [dbo].[cache] WITH NOCHECK ADD PRIMARY KEY CLUSTERED ( @@ -264,6 +271,9 @@ GO CREATE INDEX [IX_users_alias] ON [dbo].[users]([alias]) ON [PRIMARY] GO +CREATE UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY] +GO + ALTER TABLE [dbo].[identities] ADD CONSTRAINT [FK_identities_user_id] FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id]) ON DELETE CASCADE ON UPDATE CASCADE diff --git a/SQL/mssql.upgrade.sql b/SQL/mssql.upgrade.sql index 606db6046..a77362ac4 100644 --- a/SQL/mssql.upgrade.sql +++ b/SQL/mssql.upgrade.sql @@ -110,3 +110,14 @@ DELETE FROM [dbo].[messages] GO DELETE FROM [dbo].[cache] GO + +-- Updates from version 0.6-stable + +CREATE TABLE [dbo].[dictionary] ( + [user_id] [int] , + [language] [varchar] (5) COLLATE Latin1_General_CI_AI NOT NULL , + [data] [text] COLLATE Latin1_General_CI_AI NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO +CREATE UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY] +GO diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql index 14bbb968a..4cabb8132 100644 --- a/SQL/mysql.initial.sql +++ b/SQL/mysql.initial.sql @@ -144,4 +144,15 @@ CREATE TABLE `identities` ( ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; +-- Table structure for table `dictionary` + +CREATE TABLE `dictionary` ( + `user_id` int(10) UNSIGNED DEFAULT NULL, + `language` varchar(5) NOT NULL, + `data` longtext NOT NULL, + CONSTRAINT `user_id_fk_dictionary` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE `uniqueness` (`user_id`, `language`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + /*!40014 SET FOREIGN_KEY_CHECKS=1 */; diff --git a/SQL/mysql.update.sql b/SQL/mysql.update.sql index ed21bda2e..ee3929c4c 100644 --- a/SQL/mysql.update.sql +++ b/SQL/mysql.update.sql @@ -144,3 +144,14 @@ ALTER TABLE `contactgroupmembers` ADD INDEX `contactgroupmembers_contact_index` TRUNCATE TABLE `messages`; TRUNCATE TABLE `cache`; + +-- Updates from version 0.6-stable + +CREATE TABLE `dictionary` ( + `user_id` int(10) UNSIGNED DEFAULT NULL, + `language` varchar(5) NOT NULL, + `data` longtext NOT NULL, + CONSTRAINT `user_id_fk_dictionary` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE `uniqueness` (`user_id`, `language`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; diff --git a/SQL/postgres.initial.sql b/SQL/postgres.initial.sql index 5350e791f..c801a773c 100644 --- a/SQL/postgres.initial.sql +++ b/SQL/postgres.initial.sql @@ -225,3 +225,16 @@ CREATE TABLE messages ( CREATE INDEX messages_index_idx ON messages (user_id, cache_key, idx); CREATE INDEX messages_created_idx ON messages (created); + +-- +-- Table "dictionary" +-- Name: dictionary; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE dictionary ( + user_id integer DEFAULT NULL + REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + "language" varchar(5) NOT NULL, + data text NOT NULL, + CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language") +); diff --git a/SQL/postgres.update.sql b/SQL/postgres.update.sql index 94513c53f..0a2ed99dd 100644 --- a/SQL/postgres.update.sql +++ b/SQL/postgres.update.sql @@ -100,3 +100,13 @@ CREATE INDEX contactgroupmembers_contact_id_idx ON contactgroupmembers (contact_ TRUNCATE messages; TRUNCATE cache; + +-- Updates from version 0.6-stable + +CREATE TABLE dictionary ( + user_id integer DEFAULT NULL + REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + "language" varchar(5) NOT NULL, + data text NOT NULL, + CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language") +); diff --git a/SQL/sqlite.initial.sql b/SQL/sqlite.initial.sql index d2885e968..337dfbe8d 100644 --- a/SQL/sqlite.initial.sql +++ b/SQL/sqlite.initial.sql @@ -146,3 +146,18 @@ CREATE TABLE messages ( CREATE UNIQUE INDEX ix_messages_user_cache_uid ON messages (user_id,cache_key,uid); CREATE INDEX ix_messages_index ON messages (user_id,cache_key,idx); CREATE INDEX ix_messages_created ON messages (created); + +-- -------------------------------------------------------- + +-- +-- Table structure for table dictionary +-- + +CREATE TABLE dictionary ( + user_id integer DEFAULT NULL, + "language" varchar(5) NOT NULL, + data text NOT NULL +); + +CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language"); + diff --git a/SQL/sqlite.update.sql b/SQL/sqlite.update.sql index 30c3ae90b..8d5163f47 100644 --- a/SQL/sqlite.update.sql +++ b/SQL/sqlite.update.sql @@ -226,3 +226,13 @@ DROP TABLE contacts_tmp; DELETE FROM messages; DELETE FROM cache; CREATE INDEX ix_contactgroupmembers_contact_id ON contactgroupmembers (contact_id); + +-- Updates from version 0.6-stable + +CREATE TABLE dictionary ( + user_id integer DEFAULT NULL, + "language" varchar(5) NOT NULL, + data text NOT NULL +); + +CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language"); diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 8421d93f6..ad2e17efa 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -427,6 +427,10 @@ $rcmail_config['quota_zero_as_unlimited'] = false; // requires to be compiled with Open SSL support $rcmail_config['enable_spellcheck'] = true; +// Enables spellchecker exceptions dictionary. +// Setting it to 'shared' will make the dictionary shared by all users. +$rcmail_config['spellcheck_dictionary'] = false; + // Set the spell checking engine. 'googie' is the default. 'pspell' is also available, // but requires the Pspell extensions. When using Nox Spell Server, also set 'googie' here. $rcmail_config['spellcheck_engine'] = 'googie'; @@ -442,6 +446,15 @@ $rcmail_config['spellcheck_uri'] = ''; // Leave empty for default set of available language. $rcmail_config['spellcheck_languages'] = NULL; +// Makes that words with all letters capitalized will be ignored (e.g. GOOGLE) +$rcmail_config['spellcheck_ignore_caps'] = false; + +// Makes that words with numbers will be ignored (e.g. g00gle) +$rcmail_config['spellcheck_ignore_nums'] = false; + +// Makes that words with symbols will be ignored (e.g. g@@gle) +$rcmail_config['spellcheck_ignore_syms'] = false; + // don't let users set pagesize to more than this value if set $rcmail_config['max_pagesize'] = 200; diff --git a/program/include/main.inc b/program/include/main.inc index 100feb68b..4c24ce31e 100644 --- a/program/include/main.inc +++ b/program/include/main.inc @@ -1595,7 +1595,7 @@ function rcube_html_editor($mode='') $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode)); if ($hook['abort']) - return; + return; $lang = strtolower($_SESSION['language']); @@ -1607,9 +1607,14 @@ function rcube_html_editor($mode='') $RCMAIL->output->include_script('tiny_mce/tiny_mce.js'); $RCMAIL->output->include_script('editor.js'); - $RCMAIL->output->add_script(sprintf("rcmail_editor_init('\$__skin_path', '%s', %d, '%s');", - JQ($lang), intval($CONFIG['enable_spellcheck']), $mode), - 'foot'); + $RCMAIL->output->add_script(sprintf("rcmail_editor_init(%s)", + json_encode(array( + 'mode' => $mode, + 'skin_path' => '$__skin_path', + 'lang' => $lang, + 'spellcheck' => intval($CONFIG['enable_spellcheck']), + 'spelldict' => intval($CONFIG['spellcheck_dictionary']), + ))), 'foot'); } diff --git a/program/include/rcube_spellchecker.php b/program/include/rcube_spellchecker.php index 828240678..e6bd13194 100644 --- a/program/include/rcube_spellchecker.php +++ b/program/include/rcube_spellchecker.php @@ -34,8 +34,11 @@ class rcube_spellchecker private $lang; private $rc; private $error; - private $separator = '/[ !"#$%&()*+\\,\/\n:;<=>?@\[\]^_{|}-]+|\.[^\w]/'; - + private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.]([^\w]|$)/'; + private $options = array(); + private $dict; + private $have_dict; + // default settings const GOOGLE_HOST = 'ssl://www.google.com'; @@ -50,9 +53,9 @@ class rcube_spellchecker */ function __construct($lang = 'en') { - $this->rc = rcmail::get_instance(); + $this->rc = rcmail::get_instance(); $this->engine = $this->rc->config->get('spellcheck_engine', 'googie'); - $this->lang = $lang ? $lang : 'en'; + $this->lang = $lang ? $lang : 'en'; if ($this->engine == 'pspell' && !extension_loaded('pspell')) { raise_error(array( @@ -60,6 +63,13 @@ class rcube_spellchecker 'file' => __FILE__, 'line' => __LINE__, 'message' => "Pspell extension not available"), true, true); } + + $this->options = array( + 'ignore_syms' => $this->rc->config->get('spellcheck_ignore_syms'), + 'ignore_nums' => $this->rc->config->get('spellcheck_ignore_nums'), + 'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'), + 'dictionary' => $this->rc->config->get('spellcheck_dictionary'), + ); } @@ -71,7 +81,7 @@ class rcube_spellchecker * * @return bool True when no mispelling found, otherwise false */ - function check($text, $is_html=false) + function check($text, $is_html = false) { // convert to plain text if ($is_html) { @@ -116,9 +126,9 @@ class rcube_spellchecker return $this->_pspell_suggestions($word); } - return $this->_googie_suggestions($word); + return $this->_googie_suggestions($word); } - + /** * Returns mispelled words @@ -179,7 +189,7 @@ class rcube_spellchecker $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4]; } - return $out; + return $result; } @@ -211,15 +221,18 @@ class rcube_spellchecker // tokenize $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); - $diff = 0; - $matches = array(); + $diff = 0; + $matches = array(); foreach ($text as $w) { $word = trim($w[0]); $pos = $w[1] - $diff; $len = mb_strlen($word); - if ($word && preg_match('/[^0-9\.]/', $word) && !pspell_check($this->plink, $word)) { + // skip exceptions + if ($this->is_exception($word)) { + } + else if (!pspell_check($this->plink, $word)) { $suggestions = pspell_suggest($this->plink, $word); if (sizeof($suggestions) > self::MAX_SUGGESTIONS) @@ -240,6 +253,8 @@ class rcube_spellchecker */ private function _pspell_words($text = null, $is_html=false) { + $result = array(); + if ($text) { // init spellchecker $this->_pspell_init(); @@ -257,7 +272,13 @@ class rcube_spellchecker foreach ($text as $w) { $word = trim($w[0]); - if ($word && preg_match('/[^0-9\.]/', $word) && !pspell_check($this->plink, $word)) { + + // skip exceptions + if ($this->is_exception($word)) { + continue; + } + + if (!pspell_check($this->plink, $word)) { $result[] = $word; } } @@ -265,8 +286,6 @@ class rcube_spellchecker return $result; } - $result = array(); - foreach ($this->matches as $m) { $result[] = $m[0]; } @@ -330,21 +349,21 @@ class rcube_spellchecker } // Google has some problem with spaces, use \n instead - $text = str_replace(' ', "\n", $text); + $gtext = str_replace(' ', "\n", $text); - $text = '' + $gtext = '' .'' - .'' . $text . '' + .'' . $gtext . '' .''; $store = ''; if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) { $out = "POST $path HTTP/1.0\r\n"; $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n"; - $out .= "Content-Length: " . strlen($text) . "\r\n"; + $out .= "Content-Length: " . strlen($gtext) . "\r\n"; $out .= "Content-Type: application/x-www-form-urlencoded\r\n"; $out .= "Connection: Close\r\n\r\n"; - $out .= $text; + $out .= $gtext; fwrite($fp, $out); while (!feof($fp)) @@ -358,6 +377,19 @@ class rcube_spellchecker preg_match_all('/([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER); + // skip exceptions (if appropriate options are enabled) + if (!empty($this->options['ignore_syms']) || !empty($this->options['ignore_nums']) + || !empty($this->options['ignore_caps']) || !empty($this->options['dictionary']) + ) { + foreach ($matches as $idx => $m) { + $word = mb_substr($text, $m[1], $m[2], RCMAIL_CHARSET); + // skip exceptions + if ($this->is_exception($word)) { + unset($matches[$idx]); + } + } + } + return $matches; } @@ -413,4 +445,172 @@ class rcube_spellchecker $h2t = new html2text($text, false, true, 0); return $h2t->get_text(); } + + + /** + * Check if the specified word is an exception accoring to + * spellcheck options. + * + * @param string $word The word + * + * @return bool True if the word is an exception, False otherwise + */ + public function is_exception($word) + { + // Contain only symbols (e.g. "+9,0", "2:2") + if (!$word || preg_match('/^[0-9@#$%^&_+~*=:;?!,.-]+$/', $word)) + return true; + + // Contain symbols (e.g. "g@@gle"), all symbols excluding separators + if (!empty($this->options['ignore_syms']) && preg_match('/[@#$%^&_+~*=-]/', $word)) + return true; + + // Contain numbers (e.g. "g00g13") + if (!empty($this->options['ignore_nums']) && preg_match('/[0-9]/', $word)) + return true; + + // Blocked caps (e.g. "GOOGLE") + if (!empty($this->options['ignore_caps']) && $word == mb_strtoupper($word)) + return true; + + // Use exceptions from dictionary + if (!empty($this->options['dictionary'])) { + $this->load_dict(); + + // @TODO: should dictionary be case-insensitive? + if (!empty($this->dict) && in_array($word, $this->dict)) + return true; + } + + return false; + } + + + /** + * Add a word to dictionary + * + * @param string $word The word to add + */ + public function add_word($word) + { + $this->load_dict(); + + foreach (explode(' ', $word) as $word) { + // sanity check + if (strlen($word) < 512) { + $this->dict[] = $word; + $valid = true; + } + } + + if ($valid) { + $this->dict = array_unique($this->dict); + $this->update_dict(); + } + } + + + /** + * Remove a word from dictionary + * + * @param string $word The word to remove + */ + public function remove_word($word) + { + $this->load_dict(); + + if (($key = array_search($word, $this->dict)) !== false) { + unset($this->dict[$key]); + $this->update_dict(); + } + } + + + /** + * Update dictionary row in DB + */ + private function update_dict() + { + if (strcasecmp($this->options['dictionary'], 'shared') != 0) { + $userid = (int) $this->rc->user->ID; + } + + $plugin = $this->rc->plugins->exec_hook('spell_dictionary_save', array( + 'userid' => $userid, 'language' => $this->lang, 'dictionary' => $this->dict)); + + if (!empty($plugin['abort'])) { + return; + } + + if ($this->have_dict) { + if (!empty($this->dict)) { + $this->rc->db->query( + "UPDATE ".get_table_name('dictionary') + ." SET data = ?" + ." WHERE user_id " . ($plugin['userid'] ? "= ".$plugin['userid'] : "IS NULL") + ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + implode(' ', $plugin['dictionary']), $plugin['language']); + } + // don't store empty dict + else { + $this->rc->db->query( + "DELETE FROM " . get_table_name('dictionary') + ." WHERE user_id " . ($plugin['userid'] ? "= ".$plugin['userid'] : "IS NULL") + ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + $plugin['language']); + } + } + else if (!empty($this->dict)) { + $this->rc->db->query( + "INSERT INTO " .get_table_name('dictionary') + ." (user_id, " . $this->rc->db->quoteIdentifier('language') . ", data) VALUES (?, ?, ?)", + $plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary'])); + } + } + + + /** + * Get dictionary from DB + */ + private function load_dict() + { + if (is_array($this->dict)) { + return $this->dict; + } + + if (strcasecmp($this->options['dictionary'], 'shared') != 0) { + $userid = (int) $this->rc->user->ID; + } + + $plugin = $this->rc->plugins->exec_hook('spell_dictionary_get', array( + 'userid' => $userid, 'language' => $this->lang, 'dictionary' => array())); + + if (empty($plugin['abort'])) { + $dict = array(); + $this->rc->db->query( + "SELECT data FROM ".get_table_name('dictionary') + ." WHERE user_id ". ($plugin['userid'] ? "= ".$plugin['userid'] : "IS NULL") + ." AND " . $this->rc->db->quoteIdentifier('language') . " = ?", + $plugin['language']); + + if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) { + $this->have_dict = true; + if (!empty($sql_arr['data'])) { + $dict = explode(' ', $sql_arr['data']); + } + } + + $plugin['dictionary'] = array_merge((array)$plugin['dictionary'], $dict); + } + + if (!empty($plugin['dictionary']) && is_array($plugin['dictionary'])) { + $this->dict = $plugin['dictionary']; + } + else { + $this->dict = array(); + } + + return $this->dict; + } + } diff --git a/program/js/editor.js b/program/js/editor.js index a3aef72a2..906faece7 100644 --- a/program/js/editor.js +++ b/program/js/editor.js @@ -14,15 +14,15 @@ */ // Initialize HTML editor -function rcmail_editor_init(skin_path, editor_lang, spellcheck, mode) +function rcmail_editor_init(config) { var ret, conf = { mode: 'textareas', editor_selector: 'mce_editor', apply_source_formatting: true, theme: 'advanced', - language: editor_lang, - content_css: skin_path + '/editor_content.css', + language: config.lang, + content_css: config.skin_path + '/editor_content.css', theme_advanced_toolbar_location: 'top', theme_advanced_toolbar_align: 'left', theme_advanced_buttons3: '', @@ -35,7 +35,7 @@ function rcmail_editor_init(skin_path, editor_lang, spellcheck, mode) rc_client: rcmail }; - if (mode == 'identity') + if (config.mode == 'identity') $.extend(conf, { plugins: 'paste,tabfocus', theme_advanced_buttons1: 'bold,italic,underline,strikethrough,justifyleft,justifycenter,justifyright,justifyfull,separator,outdent,indent,charmap,hr,link,unlink,code,forecolor', @@ -43,11 +43,12 @@ function rcmail_editor_init(skin_path, editor_lang, spellcheck, mode) }); else // mail compose $.extend(conf, { - plugins: 'paste,emotions,media,nonbreaking,table,searchreplace,visualchars,directionality,tabfocus' + (spellcheck ? ',spellchecker' : ''), + plugins: 'paste,emotions,media,nonbreaking,table,searchreplace,visualchars,directionality,tabfocus' + (config.spellcheck ? ',spellchecker' : ''), theme_advanced_buttons1: 'bold,italic,underline,|,justifyleft,justifycenter,justifyright,justifyfull,|,bullist,numlist,outdent,indent,ltr,rtl,blockquote,|,forecolor,backcolor,fontselect,fontsizeselect', - theme_advanced_buttons2: 'link,unlink,table,|,emotions,charmap,image,media,|,code,search' + (spellcheck ? ',spellchecker' : '') + ',undo,redo', + theme_advanced_buttons2: 'link,unlink,table,|,emotions,charmap,image,media,|,code,search' + (config.spellcheck ? ',spellchecker' : '') + ',undo,redo', spellchecker_languages: (rcmail.env.spellcheck_langs ? rcmail.env.spellcheck_langs : 'Dansk=da,Deutsch=de,+English=en,Espanol=es,Francais=fr,Italiano=it,Nederlands=nl,Polski=pl,Portugues=pt,Suomi=fi,Svenska=sv'), spellchecker_rpc_url: '?_task=utils&_action=spell_html', + spellchecker_enable_learn_rpc: config.spelldict, accessibility_focus: false, oninit: 'rcmail_editor_callback' }); diff --git a/program/js/googiespell.js b/program/js/googiespell.js index c1b04cadd..6101fd2ea 100644 --- a/program/js/googiespell.js +++ b/program/js/googiespell.js @@ -1,8 +1,9 @@ /* SpellCheck jQuery'fied spell checker based on GoogieSpell 4.0 - Copyright Amir Salihefendic 2006 - Copyright Aleksander Machniak 2009 + Copyright (C) 2006 Amir Salihefendic + Copyright (C) 2009 Aleksander Machniak + Copyright (C) 2011 Kolab Systems AG LICENSE GPL AUTHORS @@ -13,7 +14,8 @@ var GOOGIE_CUR_LANG, GOOGIE_DEFAULT_LANG = 'en'; -function GoogieSpell(img_dir, server_url) { +function GoogieSpell(img_dir, server_url, has_dict) +{ var ref = this, cookie_value = getCookie('language'); @@ -49,6 +51,7 @@ function GoogieSpell(img_dir, server_url) { this.lang_rsm_edt = "Resume editing"; this.lang_no_error_found = "No spelling errors found"; this.lang_no_suggestions = "No suggestions"; + this.lang_learn_word = "Add to dictionary"; this.show_spell_img = false; // roundcube mod. this.decoration = true; @@ -64,6 +67,7 @@ function GoogieSpell(img_dir, server_url) { this.extra_menu_items = []; this.custom_spellcheck_starter = null; this.main_controller = true; + this.has_dictionary = has_dict; // Observers this.lang_state_observer = null; @@ -90,7 +94,8 @@ function GoogieSpell(img_dir, server_url) { }); -this.decorateTextarea = function(id) { +this.decorateTextarea = function(id) +{ this.text_area = typeof id === 'string' ? document.getElementById(id) : id; if (this.text_area) { @@ -119,16 +124,19 @@ this.decorateTextarea = function(id) { ////// // API Functions (the ones that you can call) ///// -this.setSpellContainer = function(id) { +this.setSpellContainer = function(id) +{ this.spell_container = typeof id === 'string' ? document.getElementById(id) : id; }; -this.setLanguages = function(lang_dict) { +this.setLanguages = function(lang_dict) +{ this.lang_to_word = lang_dict; this.langlist_codes = this.array_keys(lang_dict); }; -this.setCurrentLanguage = function(lan_code) { +this.setCurrentLanguage = function(lan_code) +{ GOOGIE_CUR_LANG = lan_code; //Set cookie @@ -137,29 +145,35 @@ this.setCurrentLanguage = function(lan_code) { setCookie('language', lan_code, now); }; -this.setForceWidthHeight = function(width, height) { +this.setForceWidthHeight = function(width, height) +{ // Set to null if you want to use one of them this.force_width = width; this.force_height = height; }; -this.setDecoration = function(bool) { +this.setDecoration = function(bool) +{ this.decoration = bool; }; -this.dontUseCloseButtons = function() { +this.dontUseCloseButtons = function() +{ this.use_close_btn = false; }; -this.appendNewMenuItem = function(name, call_back_fn, checker) { +this.appendNewMenuItem = function(name, call_back_fn, checker) +{ this.extra_menu_items.push([name, call_back_fn, checker]); }; -this.appendCustomMenuBuilder = function(eval, builder) { +this.appendCustomMenuBuilder = function(eval, builder) +{ this.custom_menu_builder.push([eval, builder]); }; -this.setFocus = function() { +this.setFocus = function() +{ try { this.focus_link_b.focus(); this.focus_link_t.focus(); @@ -174,13 +188,15 @@ this.setFocus = function() { ////// // Set functions (internal) ///// -this.setStateChanged = function(current_state) { +this.setStateChanged = function(current_state) +{ this.state = current_state; if (this.spelling_state_observer != null && this.report_state_change) this.spelling_state_observer(current_state, this); }; -this.setReportStateChange = function(bool) { +this.setReportStateChange = function(bool) +{ this.report_state_change = bool; }; @@ -188,28 +204,31 @@ this.setReportStateChange = function(bool) { ////// // Request functions ///// -this.getUrl = function() { +this.getUrl = function() +{ return this.server_url + GOOGIE_CUR_LANG; }; -this.escapeSpecial = function(val) { +this.escapeSpecial = function(val) +{ return val.replace(/&/g, "&").replace(//g, ">"); }; -this.createXMLReq = function (text) { +this.createXMLReq = function (text) +{ return '' + '' + '' + text + ''; }; -this.spellCheck = function(ignore) { +this.spellCheck = function(ignore) +{ this.prepare(ignore); var req_text = this.escapeSpecial(this.orginal_text), ref = this; - $.ajax({ type: 'POST', url: this.getUrl(), - data: this.createXMLReq(req_text), dataType: 'text', + $.ajax({ type: 'POST', url: this.getUrl(), data: this.createXMLReq(req_text), dataType: 'text', error: function(o) { if (ref.custom_ajax_error) ref.custom_ajax_error(ref); @@ -234,6 +253,25 @@ this.spellCheck = function(ignore) { }); }; +this.learnWord = function(word, id) +{ + word = this.escapeSpecial(word.innerHTML); + + var ref = this, + req_text = '' + word + ''; + + $.ajax({ type: 'POST', url: this.getUrl(), data: req_text, dataType: 'text', + error: function(o) { + if (ref.custom_ajax_error) + ref.custom_ajax_error(ref); + else + alert('An error was encountered on the server. Please try again later.'); + }, + success: function(data) { + } + }); +}; + ////// // Spell checking functions @@ -274,7 +312,8 @@ this.prepare = function(ignore, no_indicator) this.orginal_text = $(this.text_area).val(); }; -this.parseResult = function(r_text) { +this.parseResult = function(r_text) +{ // Returns an array: result[item] -> ['attrs'], ['suggestions'] var re_split_attr_c = /\w+="(\d+|true)"/g, re_split_text = /\t/g, @@ -324,21 +363,25 @@ this.processData = function(data) ////// // Error menu functions ///// -this.createErrorWindow = function() { +this.createErrorWindow = function() +{ this.error_window = document.createElement('div'); $(this.error_window).addClass('googie_window popupmenu').attr('googie_action_btn', '1'); }; -this.isErrorWindowShown = function() { +this.isErrorWindowShown = function() +{ return $(this.error_window).is(':visible'); }; -this.hideErrorWindow = function() { +this.hideErrorWindow = function() +{ $(this.error_window).hide(); $(this.error_window_iframe).hide(); }; -this.updateOrginalText = function(offset, old_value, new_value, id) { +this.updateOrginalText = function(offset, old_value, new_value, id) +{ var part_1 = this.orginal_text.substring(0, offset), part_2 = this.orginal_text.substring(offset+old_value.length), add_2_offset = new_value.length - old_value.length; @@ -357,18 +400,20 @@ this.saveOldValue = function(elm, old_value) { elm.old_value = old_value; }; -this.createListSeparator = function() { +this.createListSeparator = function() +{ var td = document.createElement('td'), tr = document.createElement('tr'); $(td).html(' ').attr('googie_action_btn', '1') - .css({'cursor': 'default', 'font-size': '3px', 'border-top': '1px solid #ccc', 'padding-top': '3px'}); + .css({'cursor': 'default', 'font-size': '3px', 'border-top': '1px solid #ccc', 'padding-top': '3px'}); tr.appendChild(td); return tr; }; -this.correctError = function(id, elm, l_elm, rm_pre_space) { +this.correctError = function(id, elm, l_elm, rm_pre_space) +{ var old_value = elm.innerHTML, new_value = l_elm.nodeType == 3 ? l_elm.nodeValue : l_elm.innerHTML, offset = this.results[id]['attrs']['o']; @@ -393,7 +438,15 @@ this.correctError = function(id, elm, l_elm, rm_pre_space) { this.errorFixed(); }; -this.showErrorWindow = function(elm, id) { +this.ignoreError = function(elm, id) +{ + // @TODO: ignore all same words + $(elm).removeAttr('class').css('color', '').unbind(); + this.hideErrorWindow(); +}; + +this.showErrorWindow = function(elm, id) +{ if (this.show_menu_observer) this.show_menu_observer(this); @@ -414,6 +467,7 @@ this.showErrorWindow = function(elm, id) { break; } } + if (!changed) { // Build up the result list var suggestions = this.results[id]['suggestions'], @@ -421,6 +475,26 @@ this.showErrorWindow = function(elm, id) { len = this.results[id]['attrs']['l'], row, item, dummy; + // [Add to dictionary] button + if (this.has_dictionary && !$(elm).attr('is_corrected')) { + row = document.createElement('tr'), + item = document.createElement('td'), + dummy = document.createElement('span'); + + $(dummy).text(this.lang_learn_word); + $(item).attr('googie_action_btn', '1').css('cursor', 'default') + .mouseover(ref.item_onmouseover) + .mouseout(ref.item_onmouseout) + .click(function(e) { + ref.learnWord(elm, id); + ref.ignoreError(elm, id); + }); + + item.appendChild(dummy); + row.appendChild(item); + list.appendChild(row); + } +/* if (suggestions.length == 0) { row = document.createElement('tr'), item = document.createElement('td'), @@ -433,7 +507,7 @@ this.showErrorWindow = function(elm, id) { row.appendChild(item); list.appendChild(row); } - +*/ for (var i=0, len=suggestions.length; i < len; i++) { row = document.createElement('tr'), item = document.createElement('td'), @@ -441,16 +515,15 @@ this.showErrorWindow = function(elm, id) { $(dummy).html(suggestions[i]); - $(item).bind('mouseover', this.item_onmouseover) - .bind('mouseout', this.item_onmouseout) - .bind('click', function(e) { ref.correctError(id, elm, e.target.firstChild) }); + $(item).mouseover(this.item_onmouseover).mouseout(this.item_onmouseout) + .click(function(e) { ref.correctError(id, elm, e.target.firstChild) }); item.appendChild(dummy); row.appendChild(item); list.appendChild(row); } - //The element is changed, append the revert + // The element is changed, append the revert if (elm.is_changed && elm.innerHTML != elm.old_value) { var old_value = elm.old_value, revert_row = document.createElement('tr'), @@ -459,11 +532,10 @@ this.showErrorWindow = function(elm, id) { $(rev_span).addClass('googie_list_revert').html(this.lang_revert + ' ' + old_value); - $(revert).bind('mouseover', this.item_onmouseover) - .bind('mouseout', this.item_onmouseout) - .bind('click', function(e) { + $(revert).mouseover(this.item_onmouseover).mouseout(this.item_onmouseout) + .click(function(e) { ref.updateOrginalText(offset, elm.innerHTML, old_value, id); - $(elm).attr('is_corrected', true).css('color', '#b91414').html(old_value); + $(elm).removeAttr('is_corrected').css('color', '#b91414').html(old_value); ref.hideErrorWindow(); }); @@ -498,11 +570,11 @@ this.showErrorWindow = function(elm, id) { $(ok_pic).attr('src', this.img_dir + 'ok.gif') .width(32).height(16) .css({'cursor': 'pointer', 'margin-left': '2px', 'margin-right': '2px'}) - .bind('click', onsub); + .click(onsub); $(edit_form).attr('googie_action_btn', '1') .css({'margin': 0, 'padding': 0, 'cursor': 'default', 'white-space': 'nowrap'}) - .bind('submit', onsub); + .submit(onsub); edit_form.appendChild(edit_input); edit_form.appendChild(ok_pic); @@ -523,9 +595,9 @@ this.showErrorWindow = function(elm, id) { e_col = document.createElement('td'); $(e_col).html(e_elm[0]) - .bind('mouseover', ref.item_onmouseover) - .bind('mouseout', ref.item_onmouseout) - .bind('click', function() { return e_elm[1](elm, ref) }); + .mouseover(ref.item_onmouseover) + .mouseout(ref.item_onmouseout) + .click(function() { return e_elm[1](elm, ref) }); e_row.appendChild(e_col); list.appendChild(e_row); @@ -575,7 +647,8 @@ this.showErrorWindow = function(elm, id) { ////// // Edit layer (the layer where the suggestions are stored) ////// -this.createEditLayer = function(width, height) { +this.createEditLayer = function(width, height) +{ this.edit_layer = document.createElement('div'); $(this.edit_layer).addClass('googie_edit_layer').attr('id', 'googie_edit_layer') .width('auto').height(height); @@ -603,7 +676,8 @@ this.createEditLayer = function(width, height) { } }; -this.resumeEditing = function() { +this.resumeEditing = function() +{ this.setStateChanged('ready'); if (this.edit_layer) @@ -629,7 +703,8 @@ this.resumeEditing = function() { this.checkSpellingState(false); }; -this.createErrorLink = function(text, id) { +this.createErrorLink = function(text, id) +{ var elm = document.createElement('span'), ref = this, d = function (e) { @@ -638,13 +713,14 @@ this.createErrorLink = function(text, id) { return false; }; - $(elm).html(text).addClass('googie_link').bind('click', d) - .attr({'googie_action_btn' : '1', 'g_id' : id, 'is_corrected' : false}); + $(elm).html(text).addClass('googie_link').click(d).removeAttr('is_corrected') + .attr({'googie_action_btn' : '1', 'g_id' : id}); return elm; }; -this.createPart = function(txt_part) { +this.createPart = function(txt_part) +{ if (txt_part == " ") return document.createTextNode(" "); @@ -659,7 +735,8 @@ this.createPart = function(txt_part) { return span; }; -this.showErrorsInIframe = function() { +this.showErrorsInIframe = function() +{ var output = document.createElement('div'), pointer = 0, results = this.results; @@ -717,7 +794,8 @@ this.showErrorsInIframe = function() { ////// // Choose language menu ////// -this.createLangWindow = function() { +this.createLangWindow = function() +{ this.language_window = document.createElement('div'); $(this.language_window).addClass('googie_window popupmenu') .width(100).attr('googie_action_btn', '1'); @@ -776,16 +854,19 @@ this.createLangWindow = function() { this.language_window.appendChild(table); }; -this.isLangWindowShown = function() { +this.isLangWindowShown = function() +{ return $(this.language_window).is(':visible'); }; -this.hideLangWindow = function() { +this.hideLangWindow = function() +{ $(this.language_window).hide(); $(this.switch_lan_pic).removeClass().addClass('googie_lang_3d_on'); }; -this.showLangWindow = function(elm) { +this.showLangWindow = function(elm) +{ if (this.show_menu_observer) this.show_menu_observer(this); @@ -806,11 +887,13 @@ this.showLangWindow = function(elm) { this.highlightCurSel(); }; -this.deHighlightCurSel = function() { +this.deHighlightCurSel = function() +{ $(this.lang_cur_elm).removeClass().addClass('googie_list_onout'); }; -this.highlightCurSel = function() { +this.highlightCurSel = function() +{ if (GOOGIE_CUR_LANG == null) GOOGIE_CUR_LANG = GOOGIE_DEFAULT_LANG; for (var i=0; i < this.lang_elms.length; i++) { @@ -824,7 +907,8 @@ this.highlightCurSel = function() { } }; -this.createChangeLangPic = function() { +this.createChangeLangPic = function() +{ var img = $('') .attr({src: this.img_dir + 'change_lang.gif', 'alt': 'Change language', 'googie_action_btn': '1'}), switch_lan = document.createElement('span'); @@ -847,7 +931,8 @@ this.createChangeLangPic = function() { return switch_lan; }; -this.createSpellDiv = function() { +this.createSpellDiv = function() +{ var span = document.createElement('span'); $(span).addClass('googie_check_spelling_link').text(this.lang_chck_spell); @@ -862,7 +947,8 @@ this.createSpellDiv = function() { ////// // State functions ///// -this.flashNoSpellingErrorState = function(on_finish) { +this.flashNoSpellingErrorState = function(on_finish) +{ this.setStateChanged('no_error_found'); var ref = this; @@ -888,7 +974,8 @@ this.flashNoSpellingErrorState = function(on_finish) { } }; -this.resumeEditingState = function() { +this.resumeEditingState = function() +{ this.setStateChanged('resume_editing'); //Change link text to resume @@ -906,7 +993,8 @@ this.resumeEditingState = function() { catch (e) {}; }; -this.checkSpellingState = function(fire) { +this.checkSpellingState = function(fire) +{ if (fire) this.setStateChanged('ready'); @@ -939,12 +1027,14 @@ this.checkSpellingState = function(fire) { ////// // Misc. functions ///// -this.isDefined = function(o) { +this.isDefined = function(o) +{ return (o !== undefined && o !== null) }; -this.errorFixed = function() { - this.cnt_errors_fixed++; +this.errorFixed = function() +{ + this.cnt_errors_fixed++; if (this.all_errors_fixed_observer) if (this.cnt_errors_fixed == this.cnt_errors) { this.hideErrorWindow(); @@ -952,15 +1042,18 @@ this.errorFixed = function() { } }; -this.errorFound = function() { +this.errorFound = function() +{ this.cnt_errors++; }; -this.createCloseButton = function(c_fn) { +this.createCloseButton = function(c_fn) +{ return this.createButton(this.lang_close, 'googie_list_close', c_fn); }; -this.createButton = function(name, css_class, c_fn) { +this.createButton = function(name, css_class, c_fn) +{ var btn_row = document.createElement('tr'), btn = document.createElement('td'), spn_btn; @@ -982,14 +1075,16 @@ this.createButton = function(name, css_class, c_fn) { return btn_row; }; -this.removeIndicator = function(elm) { +this.removeIndicator = function(elm) +{ //$(this.indicator).remove(); // roundcube mod. if (window.rcmail) rcmail.set_busy(false, null, this.rc_msg_id); }; -this.appendIndicator = function(elm) { +this.appendIndicator = function(elm) +{ // modified by roundcube if (window.rcmail) this.rc_msg_id = rcmail.set_busy(true, 'checking'); @@ -1005,19 +1100,23 @@ this.appendIndicator = function(elm) { */ } -this.createFocusLink = function(name) { +this.createFocusLink = function(name) +{ var link = document.createElement('a'); $(link).attr({'href': 'javascript:;', 'name': name}); return link; }; -this.item_onmouseover = function(e) { +this.item_onmouseover = function(e) +{ if (this.className != 'googie_list_revert' && this.className != 'googie_list_close') this.className = 'googie_list_onhover'; else this.parentNode.className = 'googie_list_onhover'; }; -this.item_onmouseout = function(e) { + +this.item_onmouseout = function(e) +{ if (this.className != 'googie_list_revert' && this.className != 'googie_list_close') this.className = 'googie_list_onout'; else diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index f37fe2520..7facc12df 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -427,6 +427,11 @@ $labels['reqdsn'] = 'Always request a delivery status notification'; $labels['replysamefolder'] = 'Place replies in the folder of the message being replied to'; $labels['defaultaddressbook'] = 'Add new contacts to the selected addressbook'; $labels['spellcheckbeforesend'] = 'Check spelling before sending a message'; +$labels['spellcheckoptions'] = 'Spellcheck Options'; +$labels['spellcheckignoresyms'] = 'Ignore words with symbols'; +$labels['spellcheckignorenums'] = 'Ignore words with numbers'; +$labels['spellcheckignorecaps'] = 'Ignore words with all letters capitalized'; +$labels['addtodict'] = 'Add to dictionary'; $labels['folder'] = 'Folder'; $labels['folders'] = 'Folders'; diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 38749675e..ddfd62772 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -697,8 +697,8 @@ function rcmail_compose_body($attrib) // include GoogieSpell if (!empty($CONFIG['enable_spellcheck'])) { - - $engine = $RCMAIL->config->get('spellcheck_engine','googie'); + $engine = $RCMAIL->config->get('spellcheck_engine','googie'); + $dictionary = (bool) $RCMAIL->config->get('spellcheck_dictionary'); $spellcheck_langs = (array) $RCMAIL->config->get('spellcheck_languages', array('da'=>'Dansk', 'de'=>'Deutsch', 'en' => 'English', 'es'=>'Español', 'fr'=>'Français', 'it'=>'Italiano', 'nl'=>'Nederlands', 'pl'=>'Polski', @@ -728,25 +728,28 @@ function rcmail_compose_body($attrib) foreach ($spellcheck_langs as $key => $name) { $editor_lang_set[] = ($key == $lang ? '+' : '') . JQ($name).'='.JQ($key); } - + $OUTPUT->include_script('googiespell.js'); $OUTPUT->add_script(sprintf( - "var googie = new GoogieSpell('\$__skin_path/images/googiespell/','?_task=utils&_action=spell&lang=');\n". + "var googie = new GoogieSpell('\$__skin_path/images/googiespell/','?_task=utils&_action=spell&lang=', %s);\n". "googie.lang_chck_spell = \"%s\";\n". "googie.lang_rsm_edt = \"%s\";\n". "googie.lang_close = \"%s\";\n". "googie.lang_revert = \"%s\";\n". "googie.lang_no_error_found = \"%s\";\n". + "googie.lang_learn_word = \"%s\";\n". "googie.setLanguages(%s);\n". "googie.setCurrentLanguage('%s');\n". "googie.setSpellContainer('spellcheck-control');\n". "googie.decorateTextarea('%s');\n". "%s.set_env('spellcheck', googie);", + !empty($dictionary) ? 'true' : 'false', JQ(Q(rcube_label('checkspelling'))), JQ(Q(rcube_label('resumeediting'))), JQ(Q(rcube_label('close'))), JQ(Q(rcube_label('revertto'))), JQ(Q(rcube_label('nospellerrors'))), + JQ(Q(rcube_label('addtodict'))), json_serialize($spellcheck_langs), $lang, $attrib['id'], diff --git a/program/steps/settings/func.inc b/program/steps/settings/func.inc index 54f955282..9e6b601a1 100644 --- a/program/steps/settings/func.inc +++ b/program/steps/settings/func.inc @@ -448,8 +448,9 @@ function rcmail_user_prefs($current=null) case 'compose': $blocks = array( - 'main' => array('name' => Q(rcube_label('mainoptions'))), - 'sig' => array('name' => Q(rcube_label('signatureoptions'))), + 'main' => array('name' => Q(rcube_label('mainoptions'))), + 'spellcheck' => array('name' => Q(rcube_label('spellcheckoptions'))), + 'sig' => array('name' => Q(rcube_label('signatureoptions'))), ); // Show checkbox for HTML Editor @@ -549,12 +550,26 @@ function rcmail_user_prefs($current=null) $field_id = 'rcmfd_spellcheck_before_send'; $input_spellcheck = new html_checkbox(array('name' => '_spellcheck_before_send', 'id' => $field_id, 'value' => 1)); - $blocks['main']['options']['spellcheck_before_send'] = array( + $blocks['spellcheck']['options']['spellcheck_before_send'] = array( 'title' => html::label($field_id, Q(rcube_label('spellcheckbeforesend'))), 'content' => $input_spellcheck->show($config['spellcheck_before_send']?1:0), ); } + if ($config['enable_spellcheck']) { + foreach (array('syms', 'nums', 'caps') as $key) { + $key = 'spellcheck_ignore_'.$key; + if (!isset($no_override[$key])) { + $input_spellcheck = new html_checkbox(array('name' => '_'.$key, 'id' => 'rcmfd_'.$key, 'value' => 1)); + + $blocks['spellcheck']['options'][$key] = array( + 'title' => html::label($field_id, Q(rcube_label(str_replace('_', '', $key)))), + 'content' => $input_spellcheck->show($config[$key]?1:0), + ); + } + } + } + if (!isset($no_override['show_sig'])) { $field_id = 'rcmfd_show_sig'; $select_show_sig = new html_select(array('name' => '_show_sig', 'id' => $field_id)); diff --git a/program/steps/settings/save_prefs.inc b/program/steps/settings/save_prefs.inc index 208874f5d..7155575fc 100644 --- a/program/steps/settings/save_prefs.inc +++ b/program/steps/settings/save_prefs.inc @@ -71,6 +71,9 @@ switch ($CURR_SECTION) 'dsn_default' => isset($_POST['_dsn_default']) ? TRUE : FALSE, 'reply_same_folder' => isset($_POST['_reply_same_folder']) ? TRUE : FALSE, 'spellcheck_before_send' => isset($_POST['_spellcheck_before_send']) ? TRUE : FALSE, + 'spellcheck_ignore_syms' => isset($_POST['_spellcheck_ignore_syms']) ? TRUE : FALSE, + 'spellcheck_ignore_nums' => isset($_POST['_spellcheck_ignore_nums']) ? TRUE : FALSE, + 'spellcheck_ignore_caps' => isset($_POST['_spellcheck_ignore_caps']) ? TRUE : FALSE, 'show_sig' => isset($_POST['_show_sig']) ? intval($_POST['_show_sig']) : 1, 'top_posting' => !empty($_POST['_top_posting']), 'strip_existing_sig' => isset($_POST['_strip_existing_sig']), @@ -167,7 +170,7 @@ switch ($CURR_SECTION) $a_user_prefs['default_imap_folders'][] = $a_user_prefs[$p]; } } - + break; } diff --git a/program/steps/utils/spell.inc b/program/steps/utils/spell.inc index 358576c7c..b485545be 100644 --- a/program/steps/utils/spell.inc +++ b/program/steps/utils/spell.inc @@ -23,6 +23,8 @@ $lang = get_input_value('lang', RCUBE_INPUT_GET); $data = file_get_contents('php://input'); +$learn_word = strpos($data, ''); + // Get data string $left = strpos($data, ''); $right = strrpos($data, ''); @@ -30,8 +32,15 @@ $data = substr($data, $left+6, $right-($left+6)); $data = html_entity_decode($data, ENT_QUOTES, RCMAIL_CHARSET); $spellchecker = new rcube_spellchecker($lang); -$spellchecker->check($data); -$result = $spellchecker->get_xml(); + +if ($learn_word) { + $spellchecker->add_word($data); + $result = ''; +} +else { + $spellchecker->check($data); + $result = $spellchecker->get_xml(); +} // set response length header("Content-Length: " . strlen($result)); diff --git a/program/steps/utils/spell_html.inc b/program/steps/utils/spell_html.inc index d69c73f37..2af30ba00 100644 --- a/program/steps/utils/spell_html.inc +++ b/program/steps/utils/spell_html.inc @@ -40,6 +40,10 @@ if ($request['method'] == 'checkWords') { else if ($request['method'] == 'getSuggestions') { $result['result'] = $spellchecker->get_suggestions($data); } +else if ($request['method'] == 'learnWord') { + $spellchecker->add_word($data); + $result['result'] = true; +} if ($error = $spellchecker->error()) { echo '{"error":{"errstr":"' . addslashes($error) . '","errfile":"","errline":null,"errcontext":"","level":"FATAL"}}'; -- cgit v1.2.3