alecpl
2011-09-06 66df084203a217ab74a416064c459cc3420a648c
- Merge devel-spellcheck branch:
- Added spellchecker exceptions dictionary (shared or per-user)
- Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options)


20 files modified
675 ■■■■ changed files
CHANGELOG 3 ●●●●● patch | view | raw | blame | history
SQL/mssql.initial.sql 10 ●●●●● patch | view | raw | blame | history
SQL/mssql.upgrade.sql 11 ●●●●● patch | view | raw | blame | history
SQL/mysql.initial.sql 11 ●●●●● patch | view | raw | blame | history
SQL/mysql.update.sql 11 ●●●●● patch | view | raw | blame | history
SQL/postgres.initial.sql 13 ●●●●● patch | view | raw | blame | history
SQL/postgres.update.sql 10 ●●●●● patch | view | raw | blame | history
SQL/sqlite.initial.sql 15 ●●●●● patch | view | raw | blame | history
SQL/sqlite.update.sql 10 ●●●●● patch | view | raw | blame | history
config/main.inc.php.dist 13 ●●●●● patch | view | raw | blame | history
program/include/main.inc 13 ●●●●● patch | view | raw | blame | history
program/include/rcube_spellchecker.php 238 ●●●●● patch | view | raw | blame | history
program/js/editor.js 13 ●●●● patch | view | raw | blame | history
program/js/googiespell.js 245 ●●●● patch | view | raw | blame | history
program/localization/en_US/labels.inc 5 ●●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 11 ●●●●● patch | view | raw | blame | history
program/steps/settings/func.inc 21 ●●●● patch | view | raw | blame | history
program/steps/settings/save_prefs.inc 5 ●●●● patch | view | raw | blame | history
program/steps/utils/spell.inc 13 ●●●● patch | view | raw | blame | history
program/steps/utils/spell_html.inc 4 ●●●● patch | view | raw | blame | history
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)
SQL/mssql.initial.sql
@@ -93,6 +93,13 @@
) 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 @@
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
SQL/mssql.upgrade.sql
@@ -110,3 +110,14 @@
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
SQL/mysql.initial.sql
@@ -144,4 +144,15 @@
) /*!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 */;
SQL/mysql.update.sql
@@ -144,3 +144,14 @@
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 */;
SQL/postgres.initial.sql
@@ -225,3 +225,16 @@
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")
);
SQL/postgres.update.sql
@@ -100,3 +100,13 @@
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")
);
SQL/sqlite.initial.sql
@@ -146,3 +146,18 @@
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");
SQL/sqlite.update.sql
@@ -226,3 +226,13 @@
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");
config/main.inc.php.dist
@@ -427,6 +427,10 @@
// 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 @@
// 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;
program/include/main.inc
@@ -1595,7 +1595,7 @@
  $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
  if ($hook['abort'])
    return;
    return;
  $lang = strtolower($_SESSION['language']);
@@ -1607,9 +1607,14 @@
  $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');
}
program/include/rcube_spellchecker.php
@@ -34,8 +34,11 @@
    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 @@
     */
    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 @@
                '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 @@
     *
     * @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 @@
            return $this->_pspell_suggestions($word);
        }
        return $this->_googie_suggestions($word);
        return $this->_googie_suggestions($word);
    }
    /**
     * Returns mispelled words
@@ -179,7 +189,7 @@
            $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
        }
        return $out;
        return $result;
    }
@@ -211,15 +221,18 @@
        // 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 @@
     */
    private function _pspell_words($text = null, $is_html=false)
    {
        $result = array();
        if ($text) {
            // init spellchecker
            $this->_pspell_init();
@@ -257,15 +272,19 @@
            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;
                }
            }
            return $result;
        }
        $result = array();
        foreach ($this->matches as $m) {
            $result[] = $m[0];
@@ -330,21 +349,21 @@
        }
        // 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))
@@ -357,6 +376,19 @@
        }
        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 @@
        $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;
    }
}
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 @@
      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 @@
    });
  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'
    });
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 @@
    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 @@
    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 @@
    });
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 @@
//////
// 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 @@
    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 @@
//////
// 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 @@
//////
// 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);
@@ -230,6 +249,25 @@
                    ref.custom_no_spelling_error(ref);
            }
            ref.removeIndicator();
        }
    });
};
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) {
        }
    });
};
@@ -274,7 +312,8 @@
    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 @@
//////
// 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 @@
    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.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 @@
            break;
        }
    }
    if (!changed) {
        // Build up the result list
        var suggestions = this.results[id]['suggestions'],
@@ -421,6 +475,26 @@
            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 @@
            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 @@
            $(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 @@
            $(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 @@
        $(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 @@
                      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 @@
//////
// 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.resumeEditing = function() {
this.resumeEditing = function()
{
    this.setStateChanged('ready');
    if (this.edit_layer)
@@ -629,7 +703,8 @@
    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 @@
            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 @@
    return span;
};
this.showErrorsInIframe = function() {
this.showErrorsInIframe = function()
{
    var output = document.createElement('div'),
        pointer = 0,
        results = this.results;
@@ -717,7 +794,8 @@
//////
// 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.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.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.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 @@
    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 @@
//////
// 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.resumeEditingState = function() {
this.resumeEditingState = function()
{
    this.setStateChanged('resume_editing');
    //Change link text to resume
@@ -906,7 +993,8 @@
    catch (e) {};
};
this.checkSpellingState = function(fire) {
this.checkSpellingState = function(fire)
{
    if (fire)
        this.setStateChanged('ready');
@@ -939,12 +1027,14 @@
//////
// 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.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 @@
    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.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
program/localization/en_US/labels.inc
@@ -427,6 +427,11 @@
$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';
program/steps/mail/compose.inc
@@ -697,8 +697,8 @@
  // 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 @@
    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'],
program/steps/settings/func.inc
@@ -448,8 +448,9 @@
    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 @@
      $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));
program/steps/settings/save_prefs.inc
@@ -71,6 +71,9 @@
      '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 @@
          $a_user_prefs['default_imap_folders'][] = $a_user_prefs[$p];
      }
    }
  break;
}
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, '<learnword>');
// Get data string
$left = strpos($data, '<text>');
$right = strrpos($data, '</text>');
@@ -30,8 +32,15 @@
$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));
program/steps/utils/spell_html.inc
@@ -40,6 +40,10 @@
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"}}';