Aleksander Machniak
2015-08-29 a63f14ec4045e82f47b237663bcf09939a0eadc5
Emoticons-related code refactoring

- Emoticons: All emoticons-related functionality is handled by the plugin now
- Emoticons: Added option to switch on/off emoticons in compose editor (#1485732)
- Emoticons: Added option to switch on/off emoticons in plain text messages
- Plugin API: Added disabled_plugins an disabled_buttons options in html_editor hook
- Plugin API: Added html2text hook
4 files added
12 files modified
689 ■■■■■ changed files
CHANGELOG 5 ●●●●● patch | view | raw | blame | history
plugins/emoticons/composer.json 4 ●●●● patch | view | raw | blame | history
plugins/emoticons/config.inc.php.dist 7 ●●●●● patch | view | raw | blame | history
plugins/emoticons/emoticons.php 208 ●●●● patch | view | raw | blame | history
plugins/emoticons/emoticons_engine.php 152 ●●●●● patch | view | raw | blame | history
plugins/emoticons/localization/en_US.inc 23 ●●●●● patch | view | raw | blame | history
plugins/emoticons/tests/Emoticons.php 41 ●●●●● patch | view | raw | blame | history
plugins/emoticons/tests/EmoticonsEngine.php 48 ●●●●● patch | view | raw | blame | history
program/include/bc.php 5 ●●●●● patch | view | raw | blame | history
program/include/rcmail.php 91 ●●●●● patch | view | raw | blame | history
program/js/editor.js 11 ●●●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 8 ●●●●● patch | view | raw | blame | history
program/steps/mail/func.inc 3 ●●●● patch | view | raw | blame | history
program/steps/mail/sendmail.inc 68 ●●●●● patch | view | raw | blame | history
program/steps/utils/html2text.inc 14 ●●●●● patch | view | raw | blame | history
tests/phpunit.xml 1 ●●●● patch | view | raw | blame | history
CHANGELOG
@@ -1,6 +1,9 @@
CHANGELOG Roundcube Webmail
===========================
- Emoticons: Added option to switch on/off emoticons in compose editor (#1485732)
- Emoticons: Added option to switch on/off emoticons in plain text messages
- Emoticons: All emoticons-related functionality is handled by the plugin now
- Installer: Add button to save generated config file in system temp directory (#1488149)
- Remove common subject prefixes Re:, Re[x]:, Re-x: on reply (#1490497)
- Added GSSAPI/Kerberos authentication plugin - krb_authentication
@@ -13,6 +16,8 @@
- Installer: Remove system() function use (#1490139)
- Password plugin: Added 'kpasswd' driver by Peter Allgeyer
- Add initdb.sh to create database from initial.sql script with prefix support (#1490188)
- Plugin API: Added disabled_plugins an disabled_buttons options in html_editor hook
- Plugin API: Added html2text hook
- Plugin API: Added message_part_body hook
- Plugin API: Added message_ready hook
- Plugin API: Add special onload() method to execute plugin actions before startup (session and GUI initialization)
plugins/emoticons/composer.json
@@ -1,9 +1,9 @@
{
    "name": "roundcube/emoticons",
    "type": "roundcube-plugin",
    "description": "Sample plugin to replace emoticons in plain text message body with real icons.",
    "description": "Plugin that adds emoticons support.",
    "license": "GPLv3+",
    "version": "1.4",
    "version": "2.0",
    "authors": [
        {
            "name": "Thomas Bruederli",
plugins/emoticons/config.inc.php.dist
New file
@@ -0,0 +1,7 @@
<?php
// Enable emoticons in plain text messages preview
$config['emoticons_display'] = false;
// Enable emoticons in compose editor (HTML)
$config['emoticons_compose'] = true;
plugins/emoticons/emoticons.php
@@ -1,9 +1,10 @@
<?php
/**
 * Display Emoticons
 * Emoticons
 *
 * Sample plugin to replace emoticons in plain text message body with real icons
 * Plugin to replace emoticons in plain text message body with real icons.
 * Also it enables emoticons in HTML compose editor. Both features are optional.
 *
 * @version @package_version@
 * @license GNU GPLv3+
@@ -13,66 +14,173 @@
 */
class emoticons extends rcube_plugin
{
    public $task = 'mail';
    public $task = 'mail|settings|utils';
    /**
     * Plugin initilization.
     */
    function init()
    {
        $this->add_hook('message_part_after', array($this, 'replace'));
        $rcube = rcube::get_instance();
        $this->add_hook('message_part_after', array($this, 'message_part_after'));
        $this->add_hook('message_outgoing_body', array($this, 'message_outgoing_body'));
        $this->add_hook('html2text', array($this, 'html2text'));
        $this->add_hook('html_editor', array($this, 'html_editor'));
        if ($rcube->task == 'settings') {
            $this->add_hook('preferences_list', array($this, 'preferences_list'));
            $this->add_hook('preferences_save', array($this, 'preferences_save'));
        }
    }
    function replace($args)
    /**
     * 'message_part_after' hook handler to replace common plain text emoticons
     * with emoticon images (<img>)
     */
    function message_part_after($args)
    {
        // This is a lookbehind assertion which will exclude html entities
        // E.g. situation when ";)" in "&quot;)" shouldn't be replaced by the icon
        // It's so long because of assertion format restrictions
        $entity = '(?<!&'
            . '[a-zA-Z0-9]{2}' . '|' . '#[0-9]{2}' . '|'
            . '[a-zA-Z0-9]{3}' . '|' . '#[0-9]{3}' . '|'
            . '[a-zA-Z0-9]{4}' . '|' . '#[0-9]{4}' . '|'
            . '[a-zA-Z0-9]{5}' . '|'
            . '[a-zA-Z0-9]{6}' . '|'
            . '[a-zA-Z0-9]{7}'
            . ')';
        // map of emoticon replacements
        $map = array(
            '/(?<!mailto):D/'   => $this->img_tag('smiley-laughing.gif',    ':D'    ),
            '/:-D/'             => $this->img_tag('smiley-laughing.gif',    ':-D'   ),
            '/:\(/'             => $this->img_tag('smiley-frown.gif',       ':('    ),
            '/:-\(/'            => $this->img_tag('smiley-frown.gif',       ':-('   ),
            '/'.$entity.';\)/'  => $this->img_tag('smiley-wink.gif',        ';)'    ),
            '/'.$entity.';-\)/' => $this->img_tag('smiley-wink.gif',        ';-)'   ),
            '/8\)/'             => $this->img_tag('smiley-cool.gif',        '8)'    ),
            '/8-\)/'            => $this->img_tag('smiley-cool.gif',        '8-)'   ),
            '/(?<!mailto):O/i'  => $this->img_tag('smiley-surprised.gif',   ':O'    ),
            '/(?<!mailto):-O/i' => $this->img_tag('smiley-surprised.gif',   ':-O'   ),
            '/(?<!mailto):P/i'  => $this->img_tag('smiley-tongue-out.gif',  ':P'    ),
            '/(?<!mailto):-P/i' => $this->img_tag('smiley-tongue-out.gif',  ':-P'   ),
            '/(?<!mailto):@/i'  => $this->img_tag('smiley-yell.gif',        ':@'    ),
            '/(?<!mailto):-@/i' => $this->img_tag('smiley-yell.gif',        ':-@'   ),
            '/O:\)/i'           => $this->img_tag('smiley-innocent.gif',    'O:)'   ),
            '/O:-\)/i'          => $this->img_tag('smiley-innocent.gif',    'O:-)'  ),
            '/(?<!O):\)/'       => $this->img_tag('smiley-smile.gif',       ':)'    ),
            '/(?<!O):-\)/'      => $this->img_tag('smiley-smile.gif',       ':-)'   ),
            '/(?<!mailto):\$/'  => $this->img_tag('smiley-embarassed.gif',  ':$'    ),
            '/(?<!mailto):-\$/' => $this->img_tag('smiley-embarassed.gif',  ':-$'   ),
            '/(?<!mailto):\*/i'  => $this->img_tag('smiley-kiss.gif',       ':*'    ),
            '/(?<!mailto):-\*/i' => $this->img_tag('smiley-kiss.gif',       ':-*'   ),
            '/(?<!mailto):S/i'  => $this->img_tag('smiley-undecided.gif',   ':S'    ),
            '/(?<!mailto):-S/i' => $this->img_tag('smiley-undecided.gif',   ':-S'   ),
        );
        if ($args['type'] == 'plain') {
            $args['body'] = preg_replace(
                array_keys($map), array_values($map), $args['body']);
            $this->load_config();
            $rcube = rcube::get_instance();
            if (!$rcube->config->get('emoticons_display', false)) {
                return $args;
            }
            require_once __DIR__ . '/emoticons_engine.php';
            $args['body'] = emoticons_engine::text2icons($args['body']);
        }
        return $args;
    }
    private function img_tag($ico, $title)
    /**
     * 'message_outgoing_body' hook handler to replace image emoticons from TinyMCE
     * editor with image attachments.
     */
    function message_outgoing_body($args)
    {
        $path = './program/js/tinymce/plugins/emoticons/img/';
        return html::img(array('src' => $path.$ico, 'title' => $title));
        if ($args['type'] == 'html') {
            $this->load_config();
            $rcube = rcube::get_instance();
            if (!$rcube->config->get('emoticons_compose', true)) {
                return $args;
            }
            require_once __DIR__ . '/emoticons_engine.php';
            // look for "emoticon" images from TinyMCE and change their src paths to
            // be file paths on the server instead of URL paths.
            $images = emoticons_engine::replace($args['body']);
            // add these images as attachments to the MIME message
            foreach ($images as $img_name => $img_file) {
                $args['message']->addHTMLImage($img_file, 'image/gif', '', true, $img_name);
            }
        }
        return $args;
    }
    /**
     * 'html2text' hook handler to replace image emoticons from TinyMCE
     * editor with plain text emoticons.
     *
     * This is executed on html2text action, i.e. when switching from HTML to text
     * in compose window (or similiar place). Also when generating alternative
     * text/plain part.
     */
    function html2text($args)
    {
        $rcube = rcube::get_instance();
        if ($rcube->action == 'html2text' || $rcube->action == 'send') {
            $this->load_config();
            if (!$rcube->config->get('emoticons_compose', true)) {
                return $args;
            }
            require_once __DIR__ . '/emoticons_engine.php';
            $args['body'] = emoticons_engine::icons2text($args['body']);
        }
        return $args;
    }
    /**
     * 'html_editor' hook handler, where we enable emoticons in TinyMCE
     */
    function html_editor($args)
    {
        $rcube = rcube::get_instance();
        $this->load_config();
        if (!$rcube->config->get('emoticons_compose', true)) {
            $args['disabled_plugins'][] = 'emoticons';
            $args['disabled_buttons'][] = 'emoticons';
        }
        return $args;
    }
    /**
     * 'preferences_list' hook handler
     */
    function preferences_list($args)
    {
        $rcube         = rcube::get_instance();
        $dont_override = $rcube->config->get('dont_override', array());
        if ($args['section'] == 'mailview' && !in_array('emoticons_display', $dont_override)) {
            $this->load_config();
            $this->add_texts('localization');
            $field_id = 'emoticons_display';
            $checkbox = new html_checkbox(array('name' => '_' . $field_id, 'id' => $field_id, 'value' => 1));
            $args['blocks']['main']['options']['emoticons_display'] = array(
                    'title'   => $this->gettext('emoticonsdisplay'),
                    'content' => $checkbox->show(intval($rcube->config->get('emoticons_display', false)))
            );
        }
        else if ($args['section'] == 'compose' && !in_array('emoticons_compose', $dont_override)) {
            $this->load_config();
            $this->add_texts('localization');
            $field_id = 'emoticons_compose';
            $checkbox = new html_checkbox(array('name' => '_' . $field_id, 'id' => $field_id, 'value' => 1));
            $args['blocks']['main']['options']['emoticons_compose'] = array(
                    'title'   => $this->gettext('emoticonscompose'),
                    'content' => $checkbox->show(intval($rcube->config->get('emoticons_compose', true)))
            );
        }
        return $args;
    }
    /**
     * 'preferences_save' hook handler
     */
    function preferences_save($args)
    {
        $rcube         = rcube::get_instance();
        $dont_override = $rcube->config->get('dont_override', array());
        if ($args['section'] == 'mailview' && !in_array('emoticons_display', $dont_override)) {
            $args['prefs']['emoticons_display'] = rcube_utils::get_input_value('_emoticons_display', rcube_utils::INPUT_POST) ? true : false;
        }
        else if ($args['section'] == 'compose' && !in_array('emoticons_compose', $dont_override)) {
            $args['prefs']['emoticons_compose'] = rcube_utils::get_input_value('_emoticons_compose', rcube_utils::INPUT_POST) ? true : false;
        }
        return $args;
    }
}
plugins/emoticons/emoticons_engine.php
New file
@@ -0,0 +1,152 @@
<?php
/**
 * @license GNU GPLv3+
 * @author Thomas Bruederli
 * @author Aleksander Machniak
 */
class emoticons_engine
{
    const IMG_PATH = 'program/js/tinymce/plugins/emoticons/img/';
    /**
     * Replaces TinyMCE's emoticon images with plain-text representation
     *
     * @param string $html HTML content
     *
     * @return string HTML content
     */
    public static function icons2text($html)
    {
        $emoticons = array(
            '8-)' => 'smiley-cool',
            ':-#' => 'smiley-foot-in-mouth',
            ':-*' => 'smiley-kiss',
            ':-X' => 'smiley-sealed',
            ':-P' => 'smiley-tongue-out',
            ':-@' => 'smiley-yell',
            ":'(" => 'smiley-cry',
            ':-(' => 'smiley-frown',
            ':-D' => 'smiley-laughing',
            ':-)' => 'smiley-smile',
            ':-S' => 'smiley-undecided',
            ':-$' => 'smiley-embarassed',
            'O:-)' => 'smiley-innocent',
            ':-|' => 'smiley-money-mouth',
            ':-O' => 'smiley-surprised',
            ';-)' => 'smiley-wink',
        );
        foreach ($emoticons as $idx => $file) {
            // <img title="Cry" src="http://.../program/js/tinymce/plugins/emoticons/img/smiley-cry.gif" border="0" alt="Cry" />
            $file      = preg_quote(self::IMG_PATH . $file . '.gif', '/');
            $search[]  = '/<img (title="[a-z ]+" )?src="[^"]+' . $file . '"[^>]+\/>/i';
            $replace[] = $idx;
        }
        return preg_replace($search, $replace, $html);
    }
    /**
     * Replace common plain text emoticons with empticon <img> tags
     *
     * @param string $text Text
     *
     * @return string Converted text
     */
    public static function text2icons($text)
    {
        // This is a lookbehind assertion which will exclude html entities
        // E.g. situation when ";)" in "&quot;)" shouldn't be replaced by the icon
        // It's so long because of assertion format restrictions
        $entity = '(?<!&'
            . '[a-zA-Z0-9]{2}' . '|' . '#[0-9]{2}' . '|'
            . '[a-zA-Z0-9]{3}' . '|' . '#[0-9]{3}' . '|'
            . '[a-zA-Z0-9]{4}' . '|' . '#[0-9]{4}' . '|'
            . '[a-zA-Z0-9]{5}' . '|'
            . '[a-zA-Z0-9]{6}' . '|'
            . '[a-zA-Z0-9]{7}'
            . ')';
        // map of emoticon replacements
        $map = array(
            '/(?<!mailto):D/'   => self::img_tag('smiley-laughing.gif',    ':D'    ),
            '/:-D/'             => self::img_tag('smiley-laughing.gif',    ':-D'   ),
            '/:\(/'             => self::img_tag('smiley-frown.gif',       ':('    ),
            '/:-\(/'            => self::img_tag('smiley-frown.gif',       ':-('   ),
            '/'.$entity.';\)/'  => self::img_tag('smiley-wink.gif',        ';)'    ),
            '/'.$entity.';-\)/' => self::img_tag('smiley-wink.gif',        ';-)'   ),
            '/8\)/'             => self::img_tag('smiley-cool.gif',        '8)'    ),
            '/8-\)/'            => self::img_tag('smiley-cool.gif',        '8-)'   ),
            '/(?<!mailto):O/i'  => self::img_tag('smiley-surprised.gif',   ':O'    ),
            '/(?<!mailto):-O/i' => self::img_tag('smiley-surprised.gif',   ':-O'   ),
            '/(?<!mailto):P/i'  => self::img_tag('smiley-tongue-out.gif',  ':P'    ),
            '/(?<!mailto):-P/i' => self::img_tag('smiley-tongue-out.gif',  ':-P'   ),
            '/(?<!mailto):@/i'  => self::img_tag('smiley-yell.gif',        ':@'    ),
            '/(?<!mailto):-@/i' => self::img_tag('smiley-yell.gif',        ':-@'   ),
            '/O:\)/i'           => self::img_tag('smiley-innocent.gif',    'O:)'   ),
            '/O:-\)/i'          => self::img_tag('smiley-innocent.gif',    'O:-)'  ),
            '/(?<!O):\)/'       => self::img_tag('smiley-smile.gif',       ':)'    ),
            '/(?<!O):-\)/'      => self::img_tag('smiley-smile.gif',       ':-)'   ),
            '/(?<!mailto):\$/'  => self::img_tag('smiley-embarassed.gif',  ':$'    ),
            '/(?<!mailto):-\$/' => self::img_tag('smiley-embarassed.gif',  ':-$'   ),
            '/(?<!mailto):\*/i'  => self::img_tag('smiley-kiss.gif',       ':*'    ),
            '/(?<!mailto):-\*/i' => self::img_tag('smiley-kiss.gif',       ':-*'   ),
            '/(?<!mailto):S/i'  => self::img_tag('smiley-undecided.gif',   ':S'    ),
            '/(?<!mailto):-S/i' => self::img_tag('smiley-undecided.gif',   ':-S'   ),
        );
        return preg_replace(array_keys($map), array_values($map), $text);
    }
    protected static function img_tag($ico, $title)
    {
        return html::img(array('src' => './' . self::IMG_PATH . $ico, 'title' => $title));
    }
    /**
     * Replace emoticon icons <img> 'src' attribute, so it can
     * be replaced with real file by Mail_Mime.
     *
     * @param string &$html HTML content
     *
     * @return array List of image files
     */
    public static function replace(&$html)
    {
        // Replace this:
        // <img src="http[s]://.../tinymce/plugins/emoticons/img/smiley-cool.gif" ... />
        // with this:
        // <img src="/path/on/server/.../tinymce/plugins/emoticons/img/smiley-cool.gif" ... />
        $rcube      = rcube::get_instance();
        $assets_dir = $rcube->config->get('assets_dir');
        $path       = unslashify($assets_dir ?: INSTALL_PATH) . '/' . self::IMG_PATH;
        $offset     = 0;
        $images     = array();
        // remove any null-byte characters before parsing
        $html = preg_replace('/\x00/', '', $html);
        if (preg_match_all('# src=[\'"]([^\'"]+)#', $html, $matches, PREG_OFFSET_CAPTURE)) {
            foreach ($matches[1] as $m) {
                // find emoticon image tags
                if (preg_match('#'. self::IMG_PATH . '(.*)$#', $m[0], $imatches)) {
                    $image_name = $imatches[1];
                    // sanitize image name so resulting attachment doesn't leave images dir
                    $image_name = preg_replace('/[^a-zA-Z0-9_\.\-]/i', '', $image_name);
                    $image_file = $path . $image_name;
                    // Add the same image only once
                    $images[$image_name] = $image_file;
                    $html    = substr_replace($html, $image_file, $m[1] + $offset, strlen($m[0]));
                    $offset += strlen($image_file) - strlen($m[0]);
                }
            }
        }
        return $images;
    }
}
plugins/emoticons/localization/en_US.inc
New file
@@ -0,0 +1,23 @@
<?php
/*
 +-----------------------------------------------------------------------+
 | plugins/emoticons/localization/<lang>.inc                             |
 |                                                                       |
 | Localization file of the Roundcube Webmail Emoticons plugin           |
 | Copyright (C) 2012-2015, The Roundcube Dev Team                       |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 +-----------------------------------------------------------------------+
 For translation see https://www.transifex.com/projects/p/roundcube-webmail/resource/plugin-emoticons/
*/
$labels = array();
$labels['emoticonsdisplay'] = 'Display emoticons in plain text messages';
$labels['emoticonscompose'] = 'Enable emoticons';
?>
plugins/emoticons/tests/Emoticons.php
@@ -19,45 +19,4 @@
        $this->assertInstanceOf('emoticons', $plugin);
        $this->assertInstanceOf('rcube_plugin', $plugin);
    }
    /**
     * replace() method tests
     */
    function test_replace()
    {
        $rcube  = rcube::get_instance();
        $plugin = new emoticons($rcube->api);
        $map = array(
            ':D'  => array('smiley-laughing.gif',    ':D'    ),
            ':-D' => array('smiley-laughing.gif',    ':-D'   ),
            ':('  => array('smiley-frown.gif',       ':('    ),
            ':-(' => array('smiley-frown.gif',       ':-('   ),
            '8)'  => array('smiley-cool.gif',        '8)'    ),
            '8-)' => array('smiley-cool.gif',        '8-)'   ),
            ':O'  => array('smiley-surprised.gif',   ':O'    ),
            ':-O' => array('smiley-surprised.gif',   ':-O'   ),
            ':P'  => array('smiley-tongue-out.gif',  ':P'    ),
            ':-P' => array('smiley-tongue-out.gif',  ':-P'   ),
            ':@'  => array('smiley-yell.gif',        ':@'    ),
            ':-@' => array('smiley-yell.gif',        ':-@'   ),
            'O:)' => array('smiley-innocent.gif',    'O:)'   ),
            'O:-)' => array('smiley-innocent.gif',    'O:-)' ),
            ':)'  => array('smiley-smile.gif',       ':)'    ),
            ':-)' => array('smiley-smile.gif',       ':-)'   ),
            ':$'  => array('smiley-embarassed.gif',  ':$'    ),
            ':-$' => array('smiley-embarassed.gif',  ':-$'   ),
            ':*'  => array('smiley-kiss.gif',       ':*'     ),
            ':-*' => array('smiley-kiss.gif',       ':-*'    ),
            ':S'  => array('smiley-undecided.gif',   ':S'    ),
            ':-S' => array('smiley-undecided.gif',   ':-S'   ),
        );
        foreach ($map as $body => $expected) {
            $args = array('type' => 'plain', 'body' => $body);
            $args = $plugin->replace($args);
            $this->assertRegExp('/' . preg_quote($expected[0], '/') . '/', $args['body']);
            $this->assertRegExp('/title="' . preg_quote($expected[1], '/') . '"/', $args['body']);
        }
    }
}
plugins/emoticons/tests/EmoticonsEngine.php
New file
@@ -0,0 +1,48 @@
<?php
class EmoticonsEngine extends PHPUnit_Framework_TestCase
{
    function setUp()
    {
        include_once __DIR__ . '/../emoticons_engine.php';
    }
    /**
     * text2icons() method tests
     */
    function test_text2icons()
    {
        $map = array(
            ':D'  => array('smiley-laughing.gif',    ':D'    ),
            ':-D' => array('smiley-laughing.gif',    ':-D'   ),
            ':('  => array('smiley-frown.gif',       ':('    ),
            ':-(' => array('smiley-frown.gif',       ':-('   ),
            '8)'  => array('smiley-cool.gif',        '8)'    ),
            '8-)' => array('smiley-cool.gif',        '8-)'   ),
            ':O'  => array('smiley-surprised.gif',   ':O'    ),
            ':-O' => array('smiley-surprised.gif',   ':-O'   ),
            ':P'  => array('smiley-tongue-out.gif',  ':P'    ),
            ':-P' => array('smiley-tongue-out.gif',  ':-P'   ),
            ':@'  => array('smiley-yell.gif',        ':@'    ),
            ':-@' => array('smiley-yell.gif',        ':-@'   ),
            'O:)' => array('smiley-innocent.gif',    'O:)'   ),
            'O:-)' => array('smiley-innocent.gif',    'O:-)' ),
            ':)'  => array('smiley-smile.gif',       ':)'    ),
            ':-)' => array('smiley-smile.gif',       ':-)'   ),
            ':$'  => array('smiley-embarassed.gif',  ':$'    ),
            ':-$' => array('smiley-embarassed.gif',  ':-$'   ),
            ':*'  => array('smiley-kiss.gif',       ':*'     ),
            ':-*' => array('smiley-kiss.gif',       ':-*'    ),
            ':S'  => array('smiley-undecided.gif',   ':S'    ),
            ':-S' => array('smiley-undecided.gif',   ':-S'   ),
        );
        foreach ($map as $body => $expected) {
            $result = emoticons_engine::text2icons($body);
            $this->assertRegExp('/' . preg_quote($expected[0], '/') . '/', $result);
            $this->assertRegExp('/title="' . preg_quote($expected[1], '/') . '"/', $result);
        }
    }
}
program/include/bc.php
@@ -220,11 +220,6 @@
    rcmail::get_instance()->html_editor($mode);
}
function rcmail_replace_emoticons($html)
{
    return rcmail::get_instance()->replace_emoticons($html);
}
function rcmail_deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file=null, $smtp_opts=null)
{
    return rcmail::get_instance()->deliver_message($message, $from, $mailto, $smtp_error, $body_file, $smtp_opts);
program/include/rcmail.php
@@ -1858,7 +1858,20 @@
     */
    public function html_editor($mode = '')
    {
        $hook = $this->plugins->exec_hook('html_editor', array('mode' => $mode));
        $spellcheck       = intval($this->config->get('enable_spellcheck'));
        $spelldict        = intval($this->config->get('spellcheck_dictionary'));
        $disabled_plugins = array();
        $disabled_buttons = array();
        if (!$spellcheck) {
            $disabled_plugins[] = 'spellchecker';
        }
        $hook = $this->plugins->exec_hook('html_editor', array(
                'mode'             => $mode,
                'disabled_plugins' => $disabled_plugins,
                'disabled_buttons' => $disabled_buttons,
        ));
        if ($hook['abort']) {
            return;
@@ -1885,8 +1898,10 @@
            'mode'       => $mode,
            'lang'       => $lang,
            'skin_path'  => $this->output->get_skin_path(),
            'spellcheck' => intval($this->config->get('enable_spellcheck')),
            'spelldict'  => intval($this->config->get('spellcheck_dictionary'))
            'spellcheck' => $spellcheck, // deprecated
            'spelldict'  => $spelldict,
            'disabled_plugins' => $hook['disabled_plugins'],
            'disabled_buttons' => $hook['disabled_buttons'],
        );
        $this->output->add_label('selectimage', 'addimage', 'selectmedia', 'addmedia');
@@ -1894,43 +1909,6 @@
        $this->output->include_css('program/js/tinymce/roundcube/browser.css');
        $this->output->include_script('tinymce/tinymce.min.js');
        $this->output->include_script('editor.js');
    }
    /**
     * Replaces TinyMCE's emoticon images with plain-text representation
     *
     * @param string $html  HTML content
     *
     * @return string HTML content
     */
    public static function replace_emoticons($html)
    {
        $emoticons = array(
            '8-)' => 'smiley-cool',
            ':-#' => 'smiley-foot-in-mouth',
            ':-*' => 'smiley-kiss',
            ':-X' => 'smiley-sealed',
            ':-P' => 'smiley-tongue-out',
            ':-@' => 'smiley-yell',
            ":'(" => 'smiley-cry',
            ':-(' => 'smiley-frown',
            ':-D' => 'smiley-laughing',
            ':-)' => 'smiley-smile',
            ':-S' => 'smiley-undecided',
            ':-$' => 'smiley-embarassed',
            'O:-)' => 'smiley-innocent',
            ':-|' => 'smiley-money-mouth',
            ':-O' => 'smiley-surprised',
            ';-)' => 'smiley-wink',
        );
        foreach ($emoticons as $idx => $file) {
            // <img title="Cry" src="http://.../program/js/tinymce/plugins/emoticons/img/smiley-cry.gif" border="0" alt="Cry" />
            $search[]  = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tinymce\/plugins\/emoticons\/img\/'.$file.'.gif"[^>]+\/>/i';
            $replace[] = $idx;
        }
        return preg_replace($search, $replace, $html);
    }
    /**
@@ -2319,6 +2297,39 @@
        return file_get_contents($name, false);
    }
    /**
     * Converts HTML content into plain text
     *
     * @param string $html    HTML content
     * @param array  $options Conversion parameters (width, links, charset)
     *
     * @return string Plain text
     */
    public function html2text($html, $options)
    {
        $default_options = array(
            'links'   => true,
            'width'   => 75,
            'body'    => $html,
            'charset' => RCUBE_CHARSET,
        );
        $options = array_merge($default_options, $options);
        // Plugins may want to modify HTML in another/additional way
        $options = $this->plugins->exec_hook('html2text', $options);
        // Convert to text
        if (!$options['abort']) {
            $converter = new rcube_html2text($options['body'],
                false, $options['links'], $options['width'], $options['charset']);
            $options['body'] = rtrim($converter->get_text());
        }
        return $options['body'];
    }
    /************************************************************************
     *********          Deprecated methods (to be removed)          *********
program/js/editor.js
@@ -89,7 +89,7 @@
  else {
    $.extend(conf, {
      plugins: 'autolink charmap code colorpicker directionality emoticons link image media nonbreaking'
        + ' paste table tabfocus textcolor searchreplace' + (config.spellcheck ? ' spellchecker' : ''),
        + ' paste table tabfocus textcolor searchreplace spellchecker',
      toolbar: 'bold italic underline | alignleft aligncenter alignright alignjustify'
        + ' | bullist numlist outdent indent ltr rtl blockquote | forecolor backcolor | fontselect fontsizeselect'
        + ' | link unlink table | emoticons charmap image media | code searchreplace undo redo',
@@ -102,6 +102,15 @@
    });
  }
  // disable TinyMCE plugins/buttons from Roundcube plugin
  $.each(config.disabled_plugins || [], function() {
    conf.plugins = conf.plugins.replace(this, '');
  });
  $.each(config.disabled_plugins || [], function() {
    conf.toolbar = conf.toolbar.replace(this, '');
  });
  conf.toolbar = conf.toolbar.replace(/\|\s+\|/g, '|');
  // support external configuration settings e.g. from skin
  if (window.rcmail_editor_settings)
    $.extend(conf, window.rcmail_editor_settings);
program/steps/mail/compose.inc
@@ -499,8 +499,7 @@
                $text = $html = $sql_arr['signature'];
                if ($sql_arr['html_signature']) {
                    $h2t  = new rcube_html2text($html, false, true);
                    $text = trim($h2t->get_text());
                    $text = $RCMAIL->html2text($html);
                }
                else {
                    $t2h  = new rcube_text2html($text, false);
@@ -874,9 +873,8 @@
        if ($part->ctype_secondary == 'html') {
            // use html part if it has been used for message (pre)viewing
            // decrease line length for quoting
            $len = $COMPOSE['mode'] == RCUBE_COMPOSE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH;
            $txt = new rcube_html2text($body, false, true, $len);
            $body = $txt->get_text();
            $len  = $COMPOSE['mode'] == RCUBE_COMPOSE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH;
            $body = $RCMAIL->html2text($body, array('width' => $len));
        }
        else {
            if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') {
program/steps/mail/func.inc
@@ -884,8 +884,7 @@
            $data['body'] = rcube_enriched::to_html($data['body']);
        }
        $txt  = new rcube_html2text($data['body'], false, true);
        $body = $txt->get_text();
        $body = $RCMAIL->html2text($data['body']);
        $part->ctype_secondary = 'plain';
    }
    // text/html
program/steps/mail/sendmail.inc
@@ -398,12 +398,8 @@
    $MAIL_MIME->setHTMLBody($plugin['body']);
    // replace emoticons
    $plugin['body'] = $RCMAIL->replace_emoticons($plugin['body']);
    // add a plain text version of the e-mail as an alternative part.
    $h2t = new rcube_html2text($plugin['body'], false, true, 0, $message_charset);
    $plainTextPart = rcube_mime::wordwrap($h2t->get_text(), $LINE_LENGTH, "\r\n", false, $message_charset);
    $plainTextPart = $RCMAIL->html2text($plugin['body'], array('width' => 0, 'charset' => $message_charset));
    $plainTextPart = rcube_mime::wordwrap($plainTextPart, $LINE_LENGTH, "\r\n", false, $message_charset);
    $plainTextPart = wordwrap($plainTextPart, 998, "\r\n", true);
    // make sure all line endings are CRLF (#1486712)
@@ -412,11 +408,8 @@
    $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body',
        array('body' => $plainTextPart, 'type' => 'alternative', 'message' => $MAIL_MIME));
    // add a plain text version of the e-mail as an alternative part.
    $MAIL_MIME->setTXTBody($plugin['body']);
    // look for "emoticon" images from TinyMCE and change their src paths to
    // be file paths on the server instead of URL paths.
    rcmail_fix_emoticon_paths($MAIL_MIME);
    // Extract image Data URIs into message attachments (#1488502)
    rcmail_extract_inline_images($MAIL_MIME, $from);
@@ -763,61 +756,6 @@
    }
    return false;
}
/**
 * go from this:
 * <img src="http[s]://.../tinymce/plugins/emoticons/img/smiley-cool.gif" border="0" alt="Cool" title="Cool" />
 *
 * to this:
 *
 * <img src="/path/on/server/.../tinymce/plugins/emoticons/img/smiley-cool.gif" border="0" alt="Cool" title="Cool" />
 */
function rcmail_fix_emoticon_paths($mime_message)
{
    global $RCMAIL;
    $body = $mime_message->getHTMLBody();
    // remove any null-byte characters before parsing
    $body = preg_replace('/\x00/', '', $body);
    $searchstr  = 'program/js/tinymce/plugins/emoticons/img/';
    $assets_dir = $RCMAIL->config->get('assets_dir');
    $path       = ($assets_dir ?: INSTALL_PATH) . '/' . $searchstr;
    $offset     = 0;
    // keep track of added images, so they're only added once
    $included_images = array();
    if (preg_match_all('# src=[\'"]([^\'"]+)#', $body, $matches, PREG_OFFSET_CAPTURE)) {
        foreach ($matches[1] as $m) {
            // find emoticon image tags
            if (preg_match('#'.$searchstr.'(.*)$#', $m[0], $imatches)) {
                $image_name = $imatches[1];
                // sanitize image name so resulting attachment doesn't leave images dir
                $image_name = preg_replace('/[^a-zA-Z0-9_\.\-]/i', '', $image_name);
                $img_file   = $path . $image_name;
                if (!in_array($image_name, $included_images)) {
                    // add the image to the MIME message
                    $res = $mime_message->addHTMLImage($img_file, 'image/gif', '', true, $image_name);
                    if (is_a($res, 'PEAR_Error')) {
                        $RCMAIL->output->show_message("emoticonerror", 'error');
                        continue;
                    }
                    array_push($included_images, $image_name);
                }
                $body    = substr_replace($body, $img_file, $m[1] + $offset, strlen($m[0]));
                $offset += strlen($img_file) - strlen($m[0]);
            }
        }
    }
    $mime_message->setHTMLBody($body);
}
/**
program/steps/utils/html2text.inc
@@ -26,15 +26,11 @@
    $html = stripslashes($html);
}
// Replace emoticon images with its text representation
$html = $RCMAIL->replace_emoticons($html);
$params['links'] = (bool) rcube_utils::get_input_value('_do_links', rcube_utils::INPUT_GET);
$params['width'] = (int) rcube_utils::get_input_value('_width', rcube_utils::INPUT_GET);
$do_links = (bool) rcube_utils::get_input_value('_do_links', rcube_utils::INPUT_GET);
$width    = (int) rcube_utils::get_input_value('_width', rcube_utils::INPUT_GET);
$text = $RCMAIL->html2text($html, $params);
// Convert to text
$converter = new rcube_html2text($html, false, $do_links, $width);
header('Content-Type: text/plain; charset=UTF-8');
print rtrim($converter->get_text());
header('Content-Type: text/plain; charset=' . RCUBE_CHARSET);
print $text;
exit;
tests/phpunit.xml
@@ -60,6 +60,7 @@
            <file>./../plugins/database_attachments/tests/DatabaseAttachments.php</file>
            <file>./../plugins/debug_logger/tests/DebugLogger.php</file>
            <file>./../plugins/emoticons/tests/Emoticons.php</file>
            <file>./../plugins/emoticons/tests/EmoticonsEngine.php</file>
            <file>./../plugins/enigma/tests/Enigma.php</file>
            <file>./../plugins/example_addressbook/tests/ExampleAddressbook.php</file>
            <file>./../plugins/filesystem_attachments/tests/FilesystemAttachments.php</file>