- Merge devel-spellcheck branch:

- Added spellchecker exceptions dictionary (shared or per-user)
  - Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options)
release-0.7
alecpl 13 years ago
parent eb2365c478
commit 66df084203

@ -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)

@ -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

@ -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

@ -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 */;

@ -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 */;

@ -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")
);

@ -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")
);

@ -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");

@ -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");

@ -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;

@ -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');
}

@ -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 = '<?xml version="1.0" encoding="utf-8" ?>'
$gtext = '<?xml version="1.0" encoding="utf-8" ?>'
.'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
.'<text>' . $text . '</text>'
.'<text>' . $gtext . '</text>'
.'</spellrequest>';
$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 o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/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;
}
}

@ -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'
});

@ -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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
};
this.createXMLReq = function (text) {
this.createXMLReq = function (text)
{
return '<?xml version="1.0" encoding="utf-8" ?>'
+ '<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
+ '<text>' + text + '</text></spellrequest>';
};
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 = '<?xml version="1.0" encoding="utf-8" ?><learnword><text>' + word + '</text></learnword>';
$.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 = $('<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

@ -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';

@ -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'],

@ -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));

@ -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;
}

@ -23,6 +23,8 @@
$lang = get_input_value('lang', RCUBE_INPUT_GET);
$data = file_get_contents('php://input');
$learn_word = strpos($data, '<learnword>');
// Get data string
$left = strpos($data, '<text>');
$right = strrpos($data, '</text>');
@ -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 = '<?xml version="1.0" encoding="'.RCMAIL_CHARSET.'"?><learnwordresult></learnwordresult>';
}
else {
$spellchecker->check($data);
$result = $spellchecker->get_xml();
}
// set response length
header("Content-Length: " . strlen($result));

@ -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"}}';

Loading…
Cancel
Save