<?php
/**
 * Roundcube Plus Skin plugin.
 *
 * Copyright 2019, Tecorama LLC.
 *
 * @license Commercial. See the LICENSE file for details.
 */

require_once(__DIR__ . "/../xframework/common/Plugin.php");

class xskin extends XFramework\Plugin
{
    protected bool $settings = false;
    private string $lookAndFeelUrl = "?_task=settings&_action=preferences&_section=xskin";
    private array $disablePluginsConfig = [];

    public function initialize()
    {
        $this->handleQuickLanguageChange();
        $this->handleQuickSkinChange();
        $this->addSkinInterfaceMenuItem();
        $this->addLanguageInterfaceMenuItem();

        // include scripts (doing it here so the quick skin change works in elastic/larry)
        $this->includeAsset("assets/scripts/xskin.min.js");

        // return if we're not running a Roundcube Plus skin (but add custom css so it applies to all skins)
        if (!$this->rcpSkin) {
            $this->includeCustomCss();
            return;
        }

        if (!$this->elastic) {
            $this->disablePluginsConfig = $this->rcmail->config->get("disable_plugins_on_mobile", []);
        }

        // add hooks
        $this->add_hook("startup", [$this, "startup"]);
        $this->add_hook("config_get", [$this, $this->elastic ? "elasticGetConfig" : "larryGetConfig"]);
        $this->add_hook("render_page", [$this, $this->elastic ? "elasticRenderPage" : "larryRenderPage"]);

        if ($this->rcmail->task == "settings") {
            $this->add_hook('preferences_sections_list', [$this, 'preferencesSectionsList']);
            $this->add_hook("preferences_list", [$this, "preferencesList"]);
            $this->add_hook("preferences_save", [$this, "preferencesSave"]);
        }

        // include assets
        $this->includeAsset("assets/scripts/xskin.min.js");
        $this->includeAsset("assets/styles/styles.css");
        $this->includeSkinConfig();

        if ($this->skinBase == "larry") {
            $this->larrySetSkin();
            $this->addDisableMobileInterfaceMenuItem();

            if ($this->rcmail->output->get_env("xskin_type") == "mobile") {
                $this->includeAsset("assets/scripts/hammer.min.js");
                $this->includeAsset("assets/scripts/jquery.hammer.js");
                $this->includeAsset("assets/scripts/larry_mobile.min.js");
                $this->includeAsset("assets/styles/larry_mobile.css");
                $this->includeAsset("../../skins/$this->skin/assets/styles/mobile.css");
            } else {
                $this->includeAsset("assets/scripts/larry_desktop.min.js");
                $this->includeAsset("assets/styles/larry_desktop.css");
                $this->includeAsset("../../skins/$this->skin/assets/styles/desktop.css");
            }
        } else {
            $this->includeAsset("../../skins/$this->skin/assets/styles/styles.css");
            $this->includeAsset("../../skins/$this->skin/assets/scripts/scripts.min.js");
        }

        // removed the cairo font (included with previous versions) because of line spacing issues - fix any old font settings
        if ($this->rcmail->config->get("xskin_font_family_$this->skin") == "cairo") {
            $this->rcmail->config->set("xskin_font_family_$this->skin", "noto-sans");
        }

        // if remote assets are disabled, set the font to roboto (loaded from elastic) and don't load fonts from google
        if ($this->rcmail->config->get("disable_remote_skin_fonts")) {
            // set these to a value that doesn't exist in _options.scss so it won't set the font
            $this->rcmail->config->set("xskin_font_family", "inherited-local");
            $this->rcmail->config->set("xskin_font_family_$this->skin", "inherited-local");
        } else {
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Roboto&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Noto+Sans&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Ubuntu&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Montserrat+Alternates&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Sarala&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Quattrocento&display=block");
            $this->include_stylesheet("https://fonts.googleapis.com/css2?family=Merienda&display=block");
        }

        $this->ensureSkinLogo();
        $this->setPreviewBranding();
        $this->includeCustomCss();
    }

    public function startup()
    {
        if ($this->elastic) {
            // add labels to env (for creating the mobile interface in js)
            $this->rcmail->output->add_label("login");
        } else {
            // litecube is the only skin not using font icons in desktop; but it does use it on mobile
            if ($this->skin == "litecube" && $this->rcmail->output->get_env("xmobile")) {
                $this->rcmail->config->set("xlarry_font_icons", true);
            }

            // add larry-based classes to body
            $bodyClasses = ["x" . $this->rcmail->output->get_env("xskin_type")];
            $this->rcmail->config->get("xlarry_font_icons") && ($bodyClasses[] = "xlarry-font-icons");
            $this->rcmail->config->get("xlarry_square_ui") && ($bodyClasses[] = "xlarry-square-ui");
            $this->rcmail->config->get("xlarry_light_ui") && ($bodyClasses[] = "xlarry-light-ui");
            $this->rcmail->task == "logout" && ($bodyClasses[] = "login-page");
            $this->addBodyClass(implode(" ", $bodyClasses));

            // add labels to env (for creating the mobile interface in js)
            $this->rcmail->output->add_label("login", "folders", "search", "attachment", "section", "options");

            // disable composing in html on mobile devices unless config option set to allow
            if ($this->rcmail->output->get_env("xmobile") && !$this->rcmail->config->get("allow_mobile_html_composing")) {
                global $CONFIG;
                $CONFIG['htmleditor'] = false;
            }
        }

        $this->rcmail->output->set_env("rcp_skin", $this->rcpSkin);
        $this->addClasses();
    }

    /**
     * Hook retrieving config options (including user settings).
     */
    public function elasticGetConfig($arg)
    {
        // Substitute the skin name retrieved from the config file with "elastic" for the plugins that treat
        // elastic-based skins as "elastic."
        if (empty($arg['name']) || $arg['name'] != "skin" || !array_key_exists(str_replace("_elastic", "", $arg['result']), $this->getSkins())) {
            return $arg;
        }

        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4);

        // this is a call from the rc core, let's hope they fix this
        if (!empty($trace[3]['class']) && $trace[3]['class'] == "jqueryui") {
            $arg['result'] = "elastic";
        }

        // check if the calling file is in the list of plugins to fix or it's a unit test and set the skin to elastic
        $fixPlugins = $this->rcmail->config->get("fix_plugins", []);
        if (!empty($trace[3]['file']) &&
            (in_array(basename(dirname($trace[3]['file'])), $fixPlugins) || basename($trace[3]['file']) == "TestCase.php")
        ) {
            $arg['result'] = "elastic";
        }

        return $arg;
    }

    function larryGetConfig($arg)
    {
        if ($this->rcmail->output->get_env("xskin_type") == "mobile") {
            // disable unwanted plugins on mobile devices
            $disablePlugins = ["preview_pane", "google_ads", "threecol"];

            if (!empty($this->larryDisabledPluginsConfig) && is_array($this->larryDisabledPluginsConfig)) {
                $disablePlugins = array_merge($disablePlugins, $this->larryDisabledPluginsConfig);
            }

            foreach ($disablePlugins as $val) {
                if (isset($arg['name']) && strpos($arg['name'], $val) !== false) {
                    $arg['result'] = false;
                    return $arg;
                }
            }

            // set the layout to list on mobile devices so it can be displayed properly
            // IMPORTANT: we have to unset $_GET['_layout'] because on RC 1.4 setting $arg here results in adding
            // the new layout value to GET, which is then picked up and saved into the database by
            // program/steps/mail/list.inc. So the 'list' value we set here for mobile is then applied to desktop
            // as well. Unsetting GET fixes the issue.
            if (isset($arg['name']) && $arg['name'] == "layout") {
                $arg['result'] = "list";
                unset($_GET['_layout']);
                return $arg;
            }
        }

        // Substitute the skin name retrieved from the config file with "larry" for the plugins that treat larry-based
        // skins as "classic."
        if (empty($arg['name']) || $arg['name'] != "skin" || !$this->isRcpSkin($arg['result'])) {
            return $arg;
        }

        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4);

        // check if the calling file is in the list of plugins to fix or it's a unit test and set the skin to larry
        $fixPlugins = $this->rcmail->config->get("fix_plugins", []);
        if (!empty($trace[3]['file']) &&
            (in_array(basename(dirname($trace[3]['file'])), $fixPlugins) || basename($trace[3]['file']) == "TestCase.php")
        ) {
            $arg['result'] = "larry";
        }

        return $arg;
    }

    public function elasticRenderPage($arg)
    {
        $this->addLoginRcpBranding($arg);
        return $arg;
    }

    public function larryRenderPage($arg)
    {
        // check if it's an error page
        if (strpos($arg['content'], "uibox centerbox errorbox")) {
            return;
        }

        $this->addLoginRcpBranding($arg);

        if ($this->rcmail->task != "login" && $this->rcmail->task != "logout") {
            $this->larryModifyPageHtml($arg);
        }

        return $arg;
    }

    /**
     * Modifies the html of the non-login Roundcube pages.
     * Unit tested via renderPage()
     *
     * @param array $arg
     * @codeCoverageIgnore
     */
    protected function larryModifyPageHtml(array &$arg)
    {
        // check if it's an error page
        if (strpos($arg['content'], "uibox centerbox errorbox")) {
            return;
        }

        // if using a desktop skin on mobile devices after clicked "use desktop skin" show a link to revert to
        // mobile skin in the top bar
        if (isset($_COOKIE['rcs_disable_mobile_skin'])) {
            $this->replace(
                '<div class="topleft">',
                '<div class="topleft">'.
                html::a(
                    [
                        "class" => "enable-mobile-skin",
                        "href" => "javascript:void(0)",
                        "onclick" => "xskin.enableMobileSkin()",
                    ],
                    rcube::Q($this->rcmail->gettext("xskin.enable_mobile_skin"))
                ),
                $arg['content']
            );
        }

        // add the toolbar-bg element that is used by alpha
        $this->replace(
            '<div id="mainscreencontent',
            '<div id="toolbar-bg"></div><div id="mainscreencontent',
            $arg['content']
        );
    }

    /**
     * Adds the skin config files from <skin>/config.inc.php to the main config, if the file exists.
     */
    protected function includeSkinConfig()
    {
        // include the default setting values from the skin's meta.json in the config
        // values from meta.json get automatically included in the config, but at the same time they're included
        // in dontoverride, which is not good because we want admins to be able to include/exclude it from dontoverride
        // so we set the default values in meta as 'xskin_default_*' and here we translate them to 'xskin_*'
        // this way the values 'xskin_*' can be used normally in dontoverride
        foreach ($this->rcmail->config->all() as $key => $val) {
            if (strpos($key, "xskin_default") === 0) {
                $this->rcmail->config->set("xskin" . substr($key, 13), $val);
            }
        }

        $file = RCUBE_INSTALL_PATH . "skins/" . $this->skin . "/config.inc.php";

        if (!file_exists($file)) {
            return;
        }

        $config = [];
        @include($file);

        if (is_array($config)) {
            foreach ($config as $key => $val) {
                $this->rcmail->config->set($key, $val);
            }
        }
    }

    /**
     * Sets the current skin and color and fills in the correct properties for the desktop, tablet and phone skin.
     * Larry only.
     */
    public function larrySetSkin()
    {
        // check if already set
        if ($this->rcmail->output->get_env("xskin")) {
            return;
        }

        if ($this->rcmail->output->get_env("xphone")) {
            $skinType = "mobile";
        } else if ($this->rcmail->output->get_env("xtablet")) {
            $skinType = "mobile";
        } else {
            $skinType = "desktop";
        }

        // litecube-f doesn't support mobile, set the device to desktop to avoid errors
        // also set device to desktop if mobile interface is disabled in config
        if ($this->skin == "litecube-f" || $this->rcmail->config->get("disable_mobile_interface")) {
            $this->setDevice(true);
            $skinType = "desktop";
        }

        // change the skin in the environment
        if (isset($GLOBALS['OUTPUT']) && method_exists($GLOBALS['OUTPUT'], "set_skin")) {
            $GLOBALS['OUTPUT']->set_skin($this->skin);
        }

        // if running a mobile skin, remove the apps menu before it gets added using js
        if ($skinType != "desktop") {
            $this->setJsVar("appsMenu", "");
        }

        // sent environment variables
        $this->rcmail->output->set_env("xskin", $this->skin);
        $this->rcmail->output->set_env("xskin_type", $skinType);
        $this->rcmail->output->set_env("rcp_skin", $this->rcpSkin);
    }

    protected function addLanguageInterfaceMenuItem()
    {
        if ($this->getDontOverride("language") || $this->rcmail->config->get("disable_menu_languages")) {
            return;
        }

        $languages = $this->rcmail->list_languages();
        asort($languages);

        $select = new html_select(["onchange" => "xskin.quickLanguageChange()", "class"=>"form-control", "name" => "quick-language-change"]);
        $select->add(array_values($languages), array_keys($languages));

        $this->addToInterfaceMenu(
            "quick-language-change",
            html::div(
                ["id" => "quick-language-change", "class" => "section"],
                html::div(["class" => "section-title"], $this->gettext("language")) . $select->show($this->rcmail->user->language)
            )
        );
    }

    public function preferencesSectionsList(array $arg): array
    {
        $arg['list']['xskin'] = ['id' => 'xskin', 'section' => $this->gettext("skin_look_and_feel")];
        return $arg;
    }

    /**
     * Replaces the preference skin selection with a dialog-based selection that allows specifying separate desktop
     * table and phone skins.
     *
     * @param array $arg
     * @return array
     */
    public function preferencesList(array $arg): array
    {
        if ($arg['section'] != 'xskin' || $this->getDontOverride("look_and_feel")) {
            return $arg;
        }

        $arg['blocks']['skin_look_and_feel']['name'] = $this->gettext("skin_look_and_feel");
        $skin = $this->skin;

        if (!$this->getDontOverride("xskin_icons") && ($this->elastic || $this->rcmail->config->get("xlarry_font_icons"))) {
            $this->getSettingSelect(
                $arg,
                "skin_look_and_feel",
                "icons_$skin",
                [
                    $this->gettext("icons_solid") => "solid",
                    $this->gettext("icons_traditional") => "traditional",
                    $this->gettext("icons_outlined") => "outlined",
                    $this->gettext("icons_material") => "material",
                    $this->gettext("icons_cartoon") => "cartoon",
                ],
                $this->getCurrentIcons(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xicons', 'html')"],
                "icons"
            );
        }

        if (!$this->getDontOverride("xskin_list_icons") && ($this->elastic || $this->rcmail->config->get("xlarry_font_icons"))) {
            $this->getSettingCheckbox(
                $arg,
                "skin_look_and_feel",
                "list_icons_$skin",
                $this->getCurrentListIcons(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xlist-icons', 'body')"],
                "list_icons"
            );
        }

        // larry-based skins don't have icons on buttons, disabling this option for larry
        if (!$this->getDontOverride("xskin_button_icons") && $this->elastic) {
            $this->getSettingCheckbox(
                $arg,
                "skin_look_and_feel",
                "button_icons_$skin",
                $this->getCurrentButtonIcons(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xbutton-icons', 'body')"],
                "button_icons"
            );
        }

        // if remote assets are disabled, don't give the users the choice of a font because they load from google
        if (!$this->getDontOverride("xskin_font_family") && !$this->rcmail->config->get("disable_remote_skin_fonts")) {
            $fonts = [];
            $fontList = ["Arial", "Courier", "Merienda", "Montserrat", "Noto Sans", "Quattrocento", "Sarala", "Roboto", "Times", "Ubuntu"];

            foreach ($fontList as $font) {
                $fonts[$font] = strtolower(str_replace(" ", "-", $font));
            }

            ksort($fonts);

            $this->getSettingSelect(
                $arg,
                "skin_look_and_feel",
                "font_family_$skin",
                $fonts,
                $this->getCurrentFontFamily(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xfont-family', 'html')"],
                "font_family"
            );
        }

        if (!$this->getDontOverride("xskin_font_size")) {
            $this->getSettingSelect(
                $arg,
                "skin_look_and_feel",
                "font_size_$skin",
                [
                    $this->gettext("font_size_xs") => "xs",
                    $this->gettext("font_size_s") => "s",
                    $this->gettext("font_size_n") => "n",
                    $this->gettext("font_size_l") => "l",
                    $this->gettext("font_size_xl") => "xl",
                ],
                $this->getCurrentFontSize(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xfont-size', 'html')"],
                "font_size"
            );
        }

        if (!$this->getDontOverride("xskin_thick_font")) {
            $this->getSettingCheckbox(
                $arg,
                "skin_look_and_feel",
                "thick_font_$skin",
                $this->getCurrentThickFont(),
                false,
                ["onchange" => "xskin.applySetting(this, 'xthick-font', 'html')"],
                "thick_font"
            );
        }

        if (!$this->getDontOverride("xskin_color")) {
            $colorBoxes = "";
            foreach ($this->rcmail->config->get("xskin_colors") as $color) {
                $colorBoxes .= html::a(
                    [
                        "class" => "color-box",
                        "onclick" => "xskin.applySetting('#xcolor-input', 'xcolor', 'body', '$color')",
                        "style" => "background:#$color !important",
                    ],
                    " "
                );
            }

            $this->addSetting(
                $arg,
                "skin_look_and_feel",
                "color_$skin",
                $colorBoxes . "<input id='xcolor-input' type='hidden' name='color_$skin' value='" .
                $this->getCurrentColor() . "' />",
                "",
                "color"
            );
        }

        $arg['blocks']["skin_look_and_feel"]['options']["save_hint"] = [
            "title" => "",
            "content" => "<span class='xsave-hint'>" . rcube::Q($this->gettext("save_hint")) . "</span>" .
                "<script>xskin.updateIFrameClasses();</script>"
        ];

        return $arg;
    }

    /**
     * Saves the skin selection preferences.
     *
     * @param array $arg
     * @return array
     */
    public function preferencesSave(array $arg): array
    {
        if ($arg['section'] == "xskin") {
            $this->saveSetting($arg, "icons_$this->skin");
            $this->saveSetting($arg, "list_icons_$this->skin");
            $this->saveSetting($arg, "button_icons_$this->skin");
            $this->saveSetting($arg, "font_family_$this->skin");
            $this->saveSetting($arg, "font_size_$this->skin");
            $this->saveSetting($arg, "thick_font_$this->skin");
            $this->saveSetting($arg, "color_$this->skin");
            $this->addClasses();
        }

        return $arg;
    }

    public function addSkinInterfaceMenuItem()
    {
        if ($this->getDontOverride("skin") || $this->rcmail->config->get("disable_menu_skins")) {
            return;
        }

        if ($html = $this->getShortcutSkinsHtml()) {
            $this->addToInterfaceMenu(
                "skin-options",
                html::div(
                    ["id" => "xskin-options", "class" => "section"],
                    html::div(["class" => "section-title"], $this->gettext("skin")) . $html
                )
            );
        }
    }

    protected function getShortcutSkinsHtml()
    {
        if (count($this->getInstalledSkins()) <= 1 ||
            $this->getDontOverride("skin") ||
            $this->rcmail->config->get("disable_menu_skins")
        ) {
            return false;
        }

        $select = new html_select(["onchange" => "xskin.quickSkinChange()", "class" => "form-control", "name" => "quick-skin-change"]);
        $added = 0;

        foreach ($this->getInstalledSkins() as $installedSkin) {
            if (array_key_exists($installedSkin, $this->skins)) {
                $select->add($this->skins[$installedSkin], $installedSkin);
                $added++;
            } else if ($installedSkin == "elastic" || $installedSkin == "larry") {
                $select->add(ucfirst($installedSkin), $installedSkin);
                $added++;
            }
        }

        if ($added > 1) {
            if ($this->rcpSkin) {
                $lookAndFeelHtml = html::div(
                    ["id" => "look-and-feel-shortcut"],
                    html::a(
                        ["href" => $this->lookAndFeelUrl, "class" => "btn btn-sm btn-success"],
                        rcube::Q($this->gettext("skin_look_and_feel_shortcut"))
                    )
                );
            } else {
                $lookAndFeelHtml = "";
            }

            return html::div(["id" => "xshortcut-skins", "class" => "shortcut-item"], $select->show($this->skin)) . $lookAndFeelHtml;
        }

        return false;
    }

    protected function getCurrentColor()
    {
        if ($this->getDontOverride("xskin_color")) {
            return $this->rcmail->config->get("xskin_color");
        }

        $color = $this->rcmail->config->get(
            "xskin_color_" . $this->skin,
            $this->rcmail->config->get("xskin_color", "")
        );

        // have to do strlen because in_array thinks that "0" == "000000"
        $colors = $this->rcmail->config->get("xskin_colors");

        if (strlen($color) != 6 || !is_array($colors) || !in_array($color, $colors)) {
            $color = $this->rcmail->config->get("xskin_color");
        }

        return $color;
    }

    protected function getCurrentFontFamily()
    {
        if ($this->getDontOverride("xskin_font_family")) {
            return $this->rcmail->config->get("xskin_font_family");
        }

        return $this->rcmail->config->get("xskin_font_family_$this->skin", $this->rcmail->config->get("xskin_font_family"));
    }

    protected function getCurrentFontSize()
    {
        if ($this->getDontOverride("xskin_font_size")) {
            return $this->rcmail->config->get("xskin_font_size");
        }

        return $this->rcmail->config->get("xskin_font_size_$this->skin", $this->rcmail->config->get("xskin_font_size"));
    }

    protected function getCurrentThickFont()
    {
        if ($this->getDontOverride("xskin_thick_font")) {
            return $this->rcmail->config->get("xskin_thick_font");
        }

        return $this->rcmail->config->get("xskin_thick_font_$this->skin", $this->rcmail->config->get("xskin_thick_font"));
    }

    protected function getCurrentIcons()
    {
        if ($this->getDontOverride("xskin_icons")) {
            return $this->rcmail->config->get("xskin_icons");
        }

        return $this->rcmail->config->get("xskin_icons_$this->skin", $this->rcmail->config->get("xskin_icons"));
    }

    protected function getCurrentListIcons()
    {
        if ($this->getDontOverride("xskin_list_icons")) {
            return $this->rcmail->config->get("xskin_list_icons");
        }

        return $this->rcmail->config->get("xskin_list_icons_$this->skin", $this->rcmail->config->get("xskin_list_icons"));
    }

    protected function getCurrentButtonIcons()
    {
        if ($this->getDontOverride("xskin_button_icons")) {
            return $this->rcmail->config->get("xskin_button_icons");
        }

        return $this->rcmail->config->get("xskin_button_icons_$this->skin", $this->rcmail->config->get("xskin_button_icons"));
    }

    protected function addClasses()
    {
        // add html classes
        $classes = [
            "xfont-family-" . $this->getCurrentFontFamily(),
            "xfont-size-" . $this->getCurrentFontSize(),
            "xthick-font-" . ($this->getCurrentThickFont() ? "yes" : "no"),
        ];

        $this->addHtmlClass(implode(" ", $classes));

        // add body classes
        $classes = [
            "{$this->rcmail->task}-page",
            "xskin",
            "skin-" . $this->skin,
            "xcolor-" . $this->getCurrentColor(),
            "xlist-icons-" . ($this->getCurrentListIcons() ? "yes" : "no"),
            "xbutton-icons-" . ($this->getCurrentButtonIcons() ? "yes" : "no"),
        ];

        // add body classes from skin's meta.json
        $classes[] = $this->rcmail->config->get("xbody-classes", "");

        if ($this->rcmail->task == "logout") {
            $classes[] = "login-page";
        }

        $this->addBodyClass(implode(" ", $classes));

        // this needs to be added to html so the icon() scss function works properly
        $this->addHtmlClass("xicons-" . $this->getCurrentIcons());
    }

    /**
     * Adds the Roundcube Plus icon to the login page.
     *
     * @param $arg
     */
    protected function addLoginRcpBranding(&$arg)
    {
        if ($this->rcmail->task != "login" && $this->rcmail->task != "logout") {
            return;
        }

        if (!$this->rcmail->config->get("remove_vendor_branding")) {
            $this->replace(
                "</body>",
                html::a(
                    [
                        "id" => "vendor-branding",
                        "href" => "https://roundcubeplus.com",
                        "target" => "_blank",
                        "title" => "More Roundcube skins and plugins at roundcubeplus.com",
                    ],
                    html::span([], "+")
                ).
                "</body>",
                $arg['content']
            );
        }
    }

    /**
     * Performs string replacement with error checking. If the string to search for cannot be found it exits with an
     * error message.
     *
     * @param string $search
     * @param string $replace
     * @param string $subject
     * @param string|int $errorNumber
     * @return int
     * @codeCoverageIgnore
     */
    protected function replace(string $search, string $replace, string &$subject, $errorNumber = ""): int
    {
        $count = 0;
        $subject = str_replace($search, $replace, $subject, $count);

        if ($errorNumber && !$count) {
            exit(
                "<p>ERROR $errorNumber: Roundcube is not running properly or it is not compatible with the Roundcube ".
                "Plus skin. Disable the xskin plugin in config.inc.php and refresh this page to check for errors.</p>"
            );
        }

        return $count;
    }

    protected function handleQuickSkinChange()
    {
        // set skin by a url parameter - this is used by the quick skin change select option in the popup
        if (($skin = rcube_utils::get_input_value('skin', rcube_utils::INPUT_GET)) &&
            !empty($this->userId) &&
            !$this->getDontOverride("skin") &&
            ($pref = $this->rcmail->user->get_prefs())
        ) {
            $pref['skin'] = $skin;
            $this->rcmail->user->save_prefs($pref);
            header("Refresh:0; url=" . XFramework\Utils::removeVarsFromUrl("skin"));
            exit;
        }
    }

    /**
     * Sets the language if it's specified as a url parameter. Applicable only after the user is logged in.
     */
    protected function handleQuickLanguageChange()
    {
        if (($lan = rcube_utils::get_input_value('language', rcube_utils::INPUT_GET)) &&
            !empty($this->userId) &&
            !$this->getDontOverride("language") &&
            array_key_exists($lan, $this->rcmail->list_languages())
        ) {
            // es_419 is too long and doesn't fit to the db field, so RC doesn't save it at all; we're saving it as es_ES
            $lan == "es_419" && ($lan = "es_ES");

            $this->db->update("users", ["language" => $lan], ["user_id" => $this->userId]);
            header("Refresh:0; url=" . XFramework\Utils::removeVarsFromUrl("language"));
            exit;
        }
    }

    protected function setPreviewBranding()
    {
        // set the preview background logo (loaded using js in [skin]/watermark.html)
        $this->rcmail->output->set_env(
            "xwatermark",
            $this->rcmail->config->get("preview_branding", "../../plugins/xskin/assets/images/watermark.png")
        );

    }

    protected function includeCustomCss()
    {
        // include the custom css if specified in the xskin config
        if ($overwriteCss = $this->rcmail->config->get("overwrite_css")) {
            $this->includeAsset($overwriteCss);
        }

        // include the custom css if specified in skin json
        if ($customCss = $this->rcmail->config->get("custom_css")) {
            $this->includeAsset($customCss);
        }
    }

    /**
     * Larry only.
     */
    protected function addDisableMobileInterfaceMenuItem()
    {
        // create the 'use mobile skin' button (added only if user switched to desktop skin on mobile)
        $skinType = $this->rcmail->output->get_env("xskin_type");

        if ($skinType == "desktop" && isset($_COOKIE['rcs_disable_mobile_skin'])) {
            $this->addToInterfaceMenu(
                "enable-mobile-skin",
                html::div(
                    ["id" => "enable-mobile-skin", "class" => "section"],
                    "<input type='button' class='button mainaction' onclick='xskin.enableMobileSkin()' value='" .
                    rcube::Q($this->rcmail->gettext("xskin.enable_mobile_skin")) . "' />"

                )
            );
        } else if ($skinType != "desktop") {
            $this->addToInterfaceMenu(
                "disable-mobile-skin",
                html::div(
                    ["id" => "disable-mobile-skin", "class" => "section"],
                    "<input type='button' class='button mainaction' onclick='xskin.disableMobileSkin()' value='" .
                    rcube::Q($this->rcmail->gettext("xskin.disable_mobile_skin")) . "' />"
                )
            );
        }
    }

    /**
     * Sets the default logo images to RC+ if they're not set up otherwise in the config.
     */
    protected function ensureSkinLogo()
    {
        if (empty($this->rcmail->config->get("skin_logo"))) {
            $this->rcmail->config->set(
                "skin_logo",
                [
                    "*" => "skins/$this->skin/assets/images/logo_header.png",
                    "[print]" => "skins/$this->skin/assets/images/logo_print.png",
                    "login" => false,
                ]
            );
        }
    }
}