2008 lines
		
	
	
		
			68 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			2008 lines
		
	
	
		
			68 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
namespace XFramework;
 | 
						|
 | 
						|
/**
 | 
						|
 * Roundcube Plus Framework plugin.
 | 
						|
 *
 | 
						|
 * This file provides a base class for the Roundcube Plus plugins.
 | 
						|
 *
 | 
						|
 * Copyright 2016, Tecorama LLC.
 | 
						|
 *
 | 
						|
 * @license Commercial. See the LICENSE file for details.
 | 
						|
 */
 | 
						|
 | 
						|
define("XFRAMEWORK_VERSION", "2.0.3");
 | 
						|
defined("RCUBE_CHARSET") || define("RCUBE_CHARSET", "UTF-8");
 | 
						|
defined("RCMAIL_VERSION") || define("RCMAIL_VERSION", "");
 | 
						|
 | 
						|
if (version_compare(PHP_VERSION, "7.4.0", "<")) {
 | 
						|
    exit("Error: The Roundcube Plus skins and plugins require PHP 7.4 or higher.");
 | 
						|
}
 | 
						|
 | 
						|
require_once("Geo.php");
 | 
						|
require_once("Utils.php");
 | 
						|
require_once("Response.php");
 | 
						|
require_once("functions.php");
 | 
						|
 | 
						|
abstract class Plugin extends \rcube_plugin
 | 
						|
{
 | 
						|
    public $allowed_prefs = [];
 | 
						|
    protected $home;
 | 
						|
 | 
						|
    protected bool $hasConfig = true; // overwrite in plugins to skip loading config
 | 
						|
    protected bool $hasLocalization = true; // overwrite in plugins to skip loading localization strings
 | 
						|
    protected bool $hasSidebarBox = false;
 | 
						|
    protected array $default = [];
 | 
						|
    protected $rcmail;
 | 
						|
    protected $db;
 | 
						|
    protected $userId = false;
 | 
						|
    protected $plugin;
 | 
						|
    protected bool $unitTest = false;
 | 
						|
    protected string $appUrl = "";
 | 
						|
    protected string $userLanguage = "";
 | 
						|
    protected Input $input;
 | 
						|
    protected Html $html;
 | 
						|
    protected Format $format;
 | 
						|
    protected string $skin = "elastic";
 | 
						|
    protected string $skinBase = "elastic";
 | 
						|
    protected bool $rcpSkin = false;
 | 
						|
    protected bool $elastic = true;
 | 
						|
    protected array $skins = [
 | 
						|
        "alpha" => "Alpha",
 | 
						|
        "droid" => "Droid",
 | 
						|
        "icloud" => "iCloud",
 | 
						|
        "litecube" => "Litecube",
 | 
						|
        "litecube-f" => "Litecube Free",
 | 
						|
        "outlook" => "Outlook",
 | 
						|
        "w21" => "W21",
 | 
						|
        "droid_plus" => "Droid+",
 | 
						|
        "gmail_plus" => "GMail+",
 | 
						|
        "outlook_plus" => "Outlook+",
 | 
						|
    ];
 | 
						|
    protected array $larryBasedSkins = ["larry", "alpha", "droid", "icloud", "litecube", "litecube-f", "outlook", "w21"];
 | 
						|
 | 
						|
    // user preferences handled by xframework and saved via ajax, these should be included in $allowed_prefs of the
 | 
						|
    // plugins that use these (can't add them via code for all or will get 'hack attempted' warning in logs)
 | 
						|
    protected array $frameworkPrefs = ["xsidebar_order", "xsidebar_collapsed"];
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates the plugin.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function init()
 | 
						|
    {
 | 
						|
        $this->rcmail = xrc();
 | 
						|
        $this->plugin = $this->ID;
 | 
						|
        $this->db = xdb();
 | 
						|
        $this->input = xinput();
 | 
						|
        $this->html = xhtml();
 | 
						|
        $this->format = xformat();
 | 
						|
        $this->userId = $this->rcmail->get_user_id();
 | 
						|
        $this->skin = $this->getCurrentSkin();
 | 
						|
        $this->rcpSkin = $this->isRcpSkin($this->skin);
 | 
						|
        $this->skinBase = in_array($this->skin, $this->larryBasedSkins) ? "larry" : "elastic";
 | 
						|
        $this->elastic = $this->skinBase == "elastic";
 | 
						|
        $this->setJsVar("xelastic", $this->elastic);
 | 
						|
 | 
						|
        xdata()->set("html_classes", []);
 | 
						|
        xdata()->set("body_classes", ["x" . $this->skinBase]);
 | 
						|
 | 
						|
        if ($this->hasConfig) {
 | 
						|
            $this->load_config();
 | 
						|
        }
 | 
						|
 | 
						|
        // load config depending on the domain, if set up
 | 
						|
        $this->loadMultiDomainConfig();
 | 
						|
 | 
						|
        // load values from the additional config file in ini format
 | 
						|
        $this->loadIniConfig();
 | 
						|
 | 
						|
        // load the localization strings for the current plugin
 | 
						|
        if ($this->hasLocalization) {
 | 
						|
            $this->add_texts("localization/");
 | 
						|
        }
 | 
						|
 | 
						|
        // load the xframework translation strings, so they can be available to the inheriting plugins
 | 
						|
        $this->loadFrameworkLocalization();
 | 
						|
 | 
						|
        $this->setDevice();
 | 
						|
        $this->setFrameworkHooks();
 | 
						|
        $this->updateDatabase();
 | 
						|
 | 
						|
        // override the defaults of this plugin with its config settings, if specified
 | 
						|
        if (!empty($this->default)) {
 | 
						|
            foreach ($this->default as $key => $val) {
 | 
						|
                $this->default[$key] = $this->rcmail->config->get($this->plugin . "_" . $key, $val);
 | 
						|
            }
 | 
						|
 | 
						|
            // load the config/default values to environment
 | 
						|
            $this->rcmail->output->set_env($this->plugin . "_settings", $this->default);
 | 
						|
        }
 | 
						|
 | 
						|
        // set timezone offset (in seconds) to a js variable
 | 
						|
        $this->setJsVar("timezoneOffset", $this->getTimezoneOffset());
 | 
						|
        $this->setJsVar("xsidebarVisible", $this->rcmail->config->get("xsidebar_visible", true));
 | 
						|
 | 
						|
        // set a variable that holds the user's language, so it can be easily accessed by plugins
 | 
						|
        $this->userLanguage = empty($this->rcmail->user->data['language']) ? "en_US" : substr($this->rcmail->user->data['language'], 0, 2);
 | 
						|
 | 
						|
        // include the framework assets
 | 
						|
        $this->includeAsset("xframework/assets/bower_components/js-cookie/src/js.cookie.js");
 | 
						|
        $this->includeAsset("xframework/assets/scripts/framework.min.js");
 | 
						|
        $this->skinBase && $this->includeAsset("xframework/assets/styles/$this->skinBase.css");
 | 
						|
 | 
						|
        // add plugin to loaded plugins list
 | 
						|
        $plugins = xdata()->get("plugins", []);
 | 
						|
        $plugins[] = $this->plugin;
 | 
						|
        xdata()->set("plugins", $plugins);
 | 
						|
 | 
						|
        // disable the apps menu on cpanel; on cpanel menus on the taskbar will be positioned
 | 
						|
        // incorrectly and displayed off the screen
 | 
						|
        if (Utils::isCpanel()) {
 | 
						|
            $this->rcmail->config->set("disable_apps_menu", true);
 | 
						|
        }
 | 
						|
 | 
						|
        // run the plugin-specific initialization
 | 
						|
        if ($this->checkCsrfToken()) {
 | 
						|
            $this->initialize();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * This method should be overridden by plugins.
 | 
						|
     */
 | 
						|
    public function initialize()
 | 
						|
    {
 | 
						|
    }
 | 
						|
 | 
						|
    public function isRcpSkin($skin): bool
 | 
						|
    {
 | 
						|
        return array_key_exists($skin, $this->skins);
 | 
						|
    }
 | 
						|
 | 
						|
    public function isElastic(): bool
 | 
						|
    {
 | 
						|
        return $this->elastic;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getSkins(): array
 | 
						|
    {
 | 
						|
        return $this->skins;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getPluginName(): string
 | 
						|
    {
 | 
						|
        return $this->plugin;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Executed on preferences section list, runs only once regardless of how many xplugins are used.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function hookPreferencesSectionsList(array $arg): array
 | 
						|
    {
 | 
						|
        // if any loaded xplugins show on the sidebar, add the sidebar section
 | 
						|
        if ($this->hasSidebarItems()) {
 | 
						|
            $arg['list']['xsidebar'] = ['id' => 'xsidebar', 'section' => $this->gettext("sidebar")];
 | 
						|
        }
 | 
						|
 | 
						|
        return $arg;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Executed on preferences list, runs only once regardless of how many xplugins are used.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function hookPreferencesList(array $arg): array
 | 
						|
    {
 | 
						|
        if ($arg['section'] == "xsidebar") {
 | 
						|
            $arg['blocks']['main']['name'] = $this->gettext("sidebar_items");
 | 
						|
 | 
						|
            foreach ($this->getSidebarPlugins() as $plugin) {
 | 
						|
                $input = new \html_checkbox();
 | 
						|
 | 
						|
                $html = $input->show(
 | 
						|
                    $this->getSetting("show_" . $plugin, true, $plugin),
 | 
						|
                    [
 | 
						|
                        "name" => "show_" . $plugin,
 | 
						|
                        "id" => $plugin . "_show_" . $plugin,
 | 
						|
                        "data-name" => $plugin,
 | 
						|
                        "value" => 1,
 | 
						|
                    ]
 | 
						|
                );
 | 
						|
 | 
						|
                $this->addSetting($arg, "main", "show_" . $plugin, $html, $plugin);
 | 
						|
            }
 | 
						|
 | 
						|
            if (!in_array("xsidebar_order", $this->rcmail->config->get("dont_override"))) {
 | 
						|
                $order = new \html_hiddenfield([
 | 
						|
                    "name" => "xsidebar_order",
 | 
						|
                    "value" => $this->rcmail->config->get("xsidebar_order"),
 | 
						|
                    "id" => "xsidebar-order",
 | 
						|
                ]);
 | 
						|
 | 
						|
                $arg['blocks']['main']['options']["test"] = [
 | 
						|
                    "content" => $order->show() .
 | 
						|
                        \html::div(["id" => "xsidebar-order-note"], $this->gettext("sidebar_change_order"))
 | 
						|
                ];
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $arg;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Executed on preferences save, runs only once regardless of how many xplugins are used.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function hookPreferencesSave(array $arg): array
 | 
						|
    {
 | 
						|
        if ($arg['section'] == "xsidebar") {
 | 
						|
            foreach ($this->getSidebarPlugins() as $plugin) {
 | 
						|
                $this->saveSetting($arg, "show_" . $plugin, false, $plugin);
 | 
						|
            }
 | 
						|
 | 
						|
            if (!in_array("xsidebar_order", $this->rcmail->config->get("dont_override"))) {
 | 
						|
                $arg['prefs']["xsidebar_order"] = \rcube_utils::get_input_value("xsidebar_order", \rcube_utils::INPUT_POST);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $arg;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getAppsUrl($check = false): string
 | 
						|
    {
 | 
						|
        if (!empty($check)) {
 | 
						|
            $check = "&check=" . (is_array($check) ? implode(",", $check) : $check);
 | 
						|
        }
 | 
						|
 | 
						|
        return "?_task=settings&_action=preferences&_section=apps" . $check;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the timezone offset in seconds based on the user settings.
 | 
						|
     */
 | 
						|
    public function getTimezoneOffset(): int
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $dtz = new \DateTimeZone($this->rcmail->config->get("timezone"));
 | 
						|
            $dt = new \DateTime("now", $dtz);
 | 
						|
            return $dtz->getOffset($dt);
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            return 0;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the difference in seconds between the server timezone and the timezone set in user settings.
 | 
						|
     */
 | 
						|
    public function getTimezoneDifference(): int
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            $dtz = new \DateTimeZone(date_default_timezone_get());
 | 
						|
            $dt = new \DateTime("now", $dtz);
 | 
						|
            return $this->getTimezoneOffset() - $dtz->getOffset($dt);
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            return 0;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Loads the xframework's localization strings. It adds the strings to the scope of the plugin that calls the
 | 
						|
     * function.
 | 
						|
     */
 | 
						|
    public function loadFrameworkLocalization()
 | 
						|
    {
 | 
						|
        $home = $this->home;
 | 
						|
        $this->home = dirname($this->home) . "/xframework";
 | 
						|
        $this->add_texts("localization/");
 | 
						|
        $this->home = $home;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the default settings of the plugin.
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function getDefault(): array
 | 
						|
    {
 | 
						|
        return $this->default;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Updates the plugin's database structure by executing the sql files from the SQL directory if needed.
 | 
						|
     * The database versions of all the xframework plugins are stored in a single db row in the system table.
 | 
						|
     * This function reads that row once for all the plugins and then compares the retrieved information
 | 
						|
     * with the version of the current plugin. If the plugin db schema needs updating, it updates it.
 | 
						|
     */
 | 
						|
    public function updateDatabase()
 | 
						|
    {
 | 
						|
        // if this plugin doesn't use database, return
 | 
						|
        if (empty($this->databaseVersion)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // read the version information from the database, store in rcmail so we don't read it for every plugin
 | 
						|
        $versions = xdata()->get("db_versions");
 | 
						|
        $versionsUpdate = xdata()->get("db_versions_update");
 | 
						|
 | 
						|
        if ($versions === null) {
 | 
						|
            if ($result = $this->db->value("value", "system", ["name" => "xframework_db_versions"])) {
 | 
						|
                $versions = json_decode($result, true) ?? [];
 | 
						|
                $versionsUpdate = true;
 | 
						|
            } else {
 | 
						|
                $versions = [];
 | 
						|
                $versionsUpdate = false;
 | 
						|
            }
 | 
						|
 | 
						|
            xdata()->set("db_versions", $versions);
 | 
						|
            xdata()->set("db_versions_update", $versionsUpdate);
 | 
						|
        }
 | 
						|
 | 
						|
        // get the schema version of this plugin that exists in the database
 | 
						|
        $existingVersion = array_key_exists($this->plugin, $versions) ? $versions[$this->plugin] : 0;
 | 
						|
 | 
						|
        // \rcube::write_log('errors', "updateDatabase() {$this->plugin}");
 | 
						|
 | 
						|
        // if the existing database version is the same as the plugin's indicated version, return
 | 
						|
        if ($existingVersion >= $this->databaseVersion) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // update the version for this plugin in the versions array and save it in the db
 | 
						|
        $versions[$this->plugin] = $this->databaseVersion;
 | 
						|
 | 
						|
        if ($versionsUpdate) {
 | 
						|
            $this->db->update("system", ["value" => json_encode($versions)], ["name" => "xframework_db_versions"]);
 | 
						|
        } else {
 | 
						|
            $this->db->insert("system", ["name" => "xframework_db_versions", "value" => json_encode($versions)]);
 | 
						|
            xdata()->set("db_versions_update", true);
 | 
						|
        }
 | 
						|
 | 
						|
        xdata()->set("db_versions", $versions);
 | 
						|
 | 
						|
        // \rcube::write_log('errors', "### SCHEMA UPDATE $existingVersion => {$this->databaseVersion} -- {$this->plugin}");
 | 
						|
 | 
						|
        // execute the sql statements from files, replace [db_prefix] with the prefix specified in the config
 | 
						|
        $files = glob(__DIR__ . "/../../" . $this->plugin . "/SQL/" . $this->db->getProvider() . "/*.sql");
 | 
						|
 | 
						|
        if (!empty($files)) {
 | 
						|
            sort($files);
 | 
						|
 | 
						|
            foreach ($files as $file) {
 | 
						|
                $fileVersion = (int)basename($file, ".sql");
 | 
						|
                if ($fileVersion && $fileVersion > $existingVersion) {
 | 
						|
                    $this->db->script(file_get_contents($file));
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Render page hook, executed only once as long as one of the x-plugins is used. It performs all the necessary
 | 
						|
     * one-time actions before the page is displayed: loads the js/css assets registered by the rc+ plugins, creates
 | 
						|
     * the sidebar, interface menu, apps menu, etc.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function frameworkRenderPage(array $arg): array
 | 
						|
    {
 | 
						|
        $this->insertAssets($arg['content']);
 | 
						|
        $this->createPropertyMap();
 | 
						|
 | 
						|
        if ($this->checkCsrfToken()) {
 | 
						|
            $this->createSidebar($arg['content']);
 | 
						|
            $this->createInterfaceMenu($arg['content']);
 | 
						|
            $this->createAppsMenu($arg['content']);
 | 
						|
            $this->hideAboutLink($arg['content']);
 | 
						|
        }
 | 
						|
 | 
						|
        return $arg;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the installed xplugins that display boxes on the sidebar sorted in user-specified order.
 | 
						|
     * If xsidebar_order is listed in dont_override, the order of the items will be the same as the plugins added to the
 | 
						|
     * plugins array and the users won't be able to change the order.
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function getSidebarPlugins(): array
 | 
						|
    {
 | 
						|
        $result = [];
 | 
						|
 | 
						|
        if (!in_array("xsidebar_order", $this->rcmail->config->get("dont_override"))) {
 | 
						|
            foreach (explode(",", (string)$this->rcmail->config->get("xsidebar_order")) as $plugin) {
 | 
						|
                if (in_array($plugin, xdata()->get("plugins", [])) && $this->rcmail->plugins->get_plugin($plugin)->hasSidebarBox) {
 | 
						|
                    $result[] = $plugin;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        foreach (xdata()->get("plugins", []) as $plugin) {
 | 
						|
            if (!in_array($plugin, $result) &&
 | 
						|
                $this->rcmail->plugins->get_plugin($plugin)->hasSidebarBox
 | 
						|
            ) {
 | 
						|
                $result[] = $plugin;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds section to interface menu.
 | 
						|
     *
 | 
						|
     * @param string $id
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    protected function addToInterfaceMenu(string $id, string $html)
 | 
						|
    {
 | 
						|
        $items = xdata()->get("interface_menu_items", []);
 | 
						|
        $items[$id] = $html;
 | 
						|
        xdata()->set("interface_menu_items", $items);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Plugins can use this function to insert inline styles to the head element.
 | 
						|
     *
 | 
						|
     * @param string $style
 | 
						|
     */
 | 
						|
    protected function addInlineStyle(string $style)
 | 
						|
    {
 | 
						|
        $styles = xdata()->get("inline_styles", "");
 | 
						|
        $styles .= $style;
 | 
						|
        xdata()->set("inline_styles", $styles);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Plugins can use this function to insert inline scripts to the head element.
 | 
						|
     *
 | 
						|
     * @param string $script
 | 
						|
     */
 | 
						|
    protected function addInlineScript(string $script)
 | 
						|
    {
 | 
						|
        $scripts = xdata()->get("inline_scripts", "");
 | 
						|
        $scripts .= $script;
 | 
						|
        xdata()->set("inline_scripts", $scripts);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds a class to the collection of classes that will be added to the html element.
 | 
						|
     *
 | 
						|
     * @param $class
 | 
						|
     */
 | 
						|
    protected function addHtmlClass($class)
 | 
						|
    {
 | 
						|
        $classes = xdata()->get("html_classes", []);
 | 
						|
        if (!in_array($class, $classes)) {
 | 
						|
            $classes[] = $class;
 | 
						|
            xdata()->set("html_classes", $classes);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds a class to the collection of classes that will be added to the body element.
 | 
						|
     * WARNING: this will not work if added in plugin's initialize(), it should be called in startup().
 | 
						|
     *
 | 
						|
     * @param $class
 | 
						|
     */
 | 
						|
    protected function addBodyClass($class)
 | 
						|
    {
 | 
						|
        $classes = xdata()->get("body_classes", []);
 | 
						|
        if (!in_array($class, $classes)) {
 | 
						|
            $classes[] = $class;
 | 
						|
            xdata()->set("body_classes", $classes);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Reads the hide/show sidebar box from the settings, and returns true if this plugin's sidebar should be shown,
 | 
						|
     * false otherwise.
 | 
						|
     *
 | 
						|
     * @param string $plugin
 | 
						|
     * @return boolean
 | 
						|
     */
 | 
						|
    protected function showSidebarBox(string $plugin = ""): bool
 | 
						|
    {
 | 
						|
        $plugin || $plugin = $this->plugin;
 | 
						|
        return (bool)$this->rcmail->config->get($plugin . "_show_" . $plugin, true);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets the js environment variable. (Public for tests)
 | 
						|
     *
 | 
						|
     * @param string $key
 | 
						|
     * @param string|array $value
 | 
						|
     */
 | 
						|
    public function setJsVar(string $key, $value)
 | 
						|
    {
 | 
						|
        if (!empty($this->rcmail->output)) {
 | 
						|
            $this->rcmail->output->set_env($key, $value);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Gets the js environment variable. (Public for tests)
 | 
						|
     *
 | 
						|
     * @param string $key
 | 
						|
     * @return null
 | 
						|
     */
 | 
						|
    public function getJsVar(string $key)
 | 
						|
    {
 | 
						|
        return empty($this->rcmail->output) ? null : $this->rcmail->output->get_env($key);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the user setting, taking into account the default setting as set in the plugin's default.
 | 
						|
     *
 | 
						|
     * @param string $key
 | 
						|
     * @param null $default
 | 
						|
     * @param string $plugin
 | 
						|
     * @param array $allowedValues
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    protected function getSetting(string $key, $default = null, string $plugin = "", array $allowedValues = [])
 | 
						|
    {
 | 
						|
        $plugin || $plugin = $this->plugin;
 | 
						|
 | 
						|
        if ($default === null) {
 | 
						|
            $default = array_key_exists($key, $this->default) ? $this->default[$key] : "";
 | 
						|
        }
 | 
						|
 | 
						|
        return $this->getConf($plugin . "_" . $key, $default, $allowedValues);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Includes a js or css file. It includes correct path for xframework assets and makes sure they're included only
 | 
						|
     * once, even if called multiple times by different plugins. (Adding the name of the plugin to the assets because
 | 
						|
     * the paths are relative and don't include the plugin name, so they overwrite each other in the check array)
 | 
						|
     *
 | 
						|
     * @param string $asset
 | 
						|
     */
 | 
						|
    protected function includeAsset(string $asset)
 | 
						|
    {
 | 
						|
        if (empty($this->rcmail->output) || empty($asset)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // if xframework, step one level up
 | 
						|
        if (($i = strpos($asset, "xframework")) !== false) {
 | 
						|
            $asset = "../xframework/" . substr($asset, $i + 11);
 | 
						|
            $checkAsset = $asset;
 | 
						|
        } else {
 | 
						|
            $checkAsset = $this->plugin . ":" . $asset;
 | 
						|
        }
 | 
						|
 | 
						|
        $assets = $this->rcmail->output->get_env("xassets");
 | 
						|
        if (!is_array($assets)) {
 | 
						|
            // @codeCoverageIgnoreStart
 | 
						|
            $assets = [];
 | 
						|
            // @codeCoverageIgnoreEnd
 | 
						|
        }
 | 
						|
 | 
						|
        if (!in_array($checkAsset, $assets)) {
 | 
						|
            $parts = pathinfo($asset);
 | 
						|
            $extension = strtolower($parts['extension']);
 | 
						|
 | 
						|
            if ($extension == "js") {
 | 
						|
                $this->include_script($asset);
 | 
						|
            } else if ($extension == "css") {
 | 
						|
                $this->include_stylesheet($asset);
 | 
						|
            }
 | 
						|
 | 
						|
            $assets[] = $checkAsset;
 | 
						|
            $this->rcmail->output->set_env("xassets", $assets);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Includes flatpickr--this is a separate function because we need to check, convert, and load the language file.
 | 
						|
     */
 | 
						|
    protected function includeFlatpickr()
 | 
						|
    {
 | 
						|
        $this->includeAsset("xframework/assets/bower_components/flatpickr/flatpickr.min.js");
 | 
						|
        $this->includeAsset("xframework/assets/bower_components/flatpickr/flatpickr.min.css");
 | 
						|
        $this->includeAsset("xframework/assets/bower_components/flatpickr/confirmDate.min.js");
 | 
						|
        $this->includeAsset("xframework/assets/bower_components/flatpickr/confirmDate.min.css");
 | 
						|
 | 
						|
        $languages = [
 | 
						|
            'ar', 'at', 'az', 'be', 'bg', 'bn', 'bs', 'cat', 'cs', 'cy', 'da', 'de', 'eo', 'es', 'et', 'fa', 'fi', 'fo', 'fr', 'ga', 'gr',
 | 
						|
            'he', 'hi', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'ka', 'km', 'ko', 'kz', 'lt', 'lv', 'mk', 'mn', 'ms', 'my', 'nl', 'no', 'pa',
 | 
						|
            'pl', 'pt', 'ro', 'ru', 'si', 'sk', 'sl', 'sq', 'sr-cyr', 'sr', 'sv', 'th', 'tr', 'uk', 'uz_latn', 'uz', 'vn', 'zh', 'zh-tw',
 | 
						|
        ];
 | 
						|
 | 
						|
        $lan = substr($this->userLanguage, 0, 2);
 | 
						|
 | 
						|
        if (in_array($lan, $languages)) {
 | 
						|
            $this->includeAsset("xframework/assets/bower_components/flatpickr/lan/$lan.min.js");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Writes the last db error to the error log.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function logDbError()
 | 
						|
    {
 | 
						|
        if ($error = $this->db->lastError()) {
 | 
						|
            $this->logError($error);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Writes an entry to the Roundcube error log.
 | 
						|
     *
 | 
						|
     * @param $error
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function logError($error)
 | 
						|
    {
 | 
						|
        if (class_exists("\\rcube")) {
 | 
						|
            \rcube::write_log('errors', $error);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a select html element and adds it to the settings page.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @param string $block
 | 
						|
     * @param string $name
 | 
						|
     * @param array $options
 | 
						|
     * @param $default
 | 
						|
     * @param string $addHtml
 | 
						|
     * @param array $attr
 | 
						|
     * @param string|null $label
 | 
						|
     */
 | 
						|
    protected function getSettingSelect(array &$arg, string $block, string $name, array $options, $default = null,
 | 
						|
                                        string $addHtml = "", array $attr = [], ?string $label = "")
 | 
						|
    {
 | 
						|
        $attr = array_merge(["name" => $name, "id" => $this->plugin . "_$name"], $attr);
 | 
						|
        $select = new \html_select($attr);
 | 
						|
 | 
						|
        foreach ($options as $key => $val) {
 | 
						|
            $select->add($key, $val);
 | 
						|
        }
 | 
						|
 | 
						|
        $value = $this->getSetting($name, $default, "", $options);
 | 
						|
 | 
						|
        // need to convert numbers in strings to int, because when we pass an array of options to select and
 | 
						|
        // the keys are numeric, php automatically converts them to int, so when we retrieve the value here
 | 
						|
        // and it's a string, rc doesn't select the value in the <select> because it doesn't match
 | 
						|
        if (is_numeric($value)) {
 | 
						|
            $value = (int)$value;
 | 
						|
        }
 | 
						|
 | 
						|
        $this->addSetting(
 | 
						|
            $arg,
 | 
						|
            $block,
 | 
						|
            $name,
 | 
						|
            $select->show($value) . $addHtml,
 | 
						|
            "",
 | 
						|
            $label
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a checkbox html element and adds it to the settings page.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @param string $block
 | 
						|
     * @param string $name
 | 
						|
     * @param mixed $default
 | 
						|
     * @param string $addHtml
 | 
						|
     * @param array $attr
 | 
						|
     * @param string|null $label
 | 
						|
     */
 | 
						|
    protected function getSettingCheckbox(array &$arg, string $block, string $name, $default = null, string $addHtml = "",
 | 
						|
                                          array $attr = [], ?string $label = "")
 | 
						|
    {
 | 
						|
        $attr = array_merge(["name" => $name, "id" => $this->plugin . "_$name", "value" => 1], $attr);
 | 
						|
        $input = new \html_checkbox();
 | 
						|
 | 
						|
        $this->addSetting(
 | 
						|
            $arg,
 | 
						|
            $block,
 | 
						|
            $name,
 | 
						|
            $input->show($this->getSetting($name, $default), $attr) . $addHtml,
 | 
						|
            "",
 | 
						|
            $label
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a text input html element and adds it to the settings page.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @param string $block
 | 
						|
     * @param string $name
 | 
						|
     * @param null $default
 | 
						|
     * @param string $addHtml
 | 
						|
     * @param string|null $label
 | 
						|
     */
 | 
						|
    protected function getSettingInput(array &$arg, string $block, string $name, $default = null, string $addHtml = "", ?string $label = "")
 | 
						|
    {
 | 
						|
        $input = new \html_inputfield();
 | 
						|
        $this->addSetting(
 | 
						|
            $arg,
 | 
						|
            $block,
 | 
						|
            $name,
 | 
						|
            $input->show($this->getSetting($name, $default), ["name" => $name, "id" => $this->plugin . "_$name"]) . $addHtml,
 | 
						|
            "",
 | 
						|
            $label
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds a setting to the settings page.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @param string $block
 | 
						|
     * @param string $name
 | 
						|
     * @param string $html
 | 
						|
     * @param string $plugin
 | 
						|
     * @param string|null $label
 | 
						|
     */
 | 
						|
    protected function addSetting(array &$arg, string $block, string $name, string $html, string $plugin = "", ?string $label = "")
 | 
						|
    {
 | 
						|
        $plugin || ($plugin = $this->plugin);
 | 
						|
        if ($label === null) {
 | 
						|
            $title = "";
 | 
						|
        } else {
 | 
						|
            $title = \html::label($plugin . "_$name", \rcube::Q($this->gettext($plugin . ".setting_" . ($label ?: $name))));
 | 
						|
        }
 | 
						|
 | 
						|
        $arg['blocks'][$block]['options'][$name] = [
 | 
						|
            "title" => $title,
 | 
						|
            "content" => $html
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Retrieves a value from POST, processes it and loads it to the 'pref' array of $arg, so RC saves it in the user
 | 
						|
     * preferences.
 | 
						|
     *
 | 
						|
     * @param array $arg
 | 
						|
     * @param string $name
 | 
						|
     * @param string|bool $type Specifies the type of variable to convert the incoming value to.
 | 
						|
     * @param string $plugin
 | 
						|
     * @param null|array|string $allowedValues
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function saveSetting(array &$arg, string $name, string $type = "", string $plugin = "", $allowedValues = null): bool
 | 
						|
    {
 | 
						|
        $plugin || $plugin = $this->plugin;
 | 
						|
 | 
						|
        // if this setting shouldn't be overridden by the user, don't save it
 | 
						|
        if (in_array($plugin . "_" . $name, $this->rcmail->config->get("dont_override"))) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        $value = \rcube_utils::get_input_value($name, \rcube_utils::INPUT_POST);
 | 
						|
        if ($value === null) {
 | 
						|
            $value = "0";
 | 
						|
        }
 | 
						|
 | 
						|
        // fix the value type (all values incoming from POST are strings, but we may need them as int or bool, etc.)
 | 
						|
        switch ($type) {
 | 
						|
            case "boolean":
 | 
						|
                $value = (bool)$value;
 | 
						|
                break;
 | 
						|
            case "integer":
 | 
						|
                $value = (int)$value;
 | 
						|
                break;
 | 
						|
            case "double":
 | 
						|
                $value = (double)$value;
 | 
						|
                break;
 | 
						|
        }
 | 
						|
 | 
						|
        // check value
 | 
						|
        if ($allowedValues) {
 | 
						|
            // allowedValues is an array of possible values
 | 
						|
            if (is_array($allowedValues)) {
 | 
						|
                if (!in_array($value, $allowedValues)) {
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                // allowedValues is a regex string
 | 
						|
                if (!preg_match($allowedValues, $value)) {
 | 
						|
                    return false;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $arg['prefs'][$plugin . "_" . $name] = $value;
 | 
						|
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Parses and returns the contents of a plugin template file. The template files are located in
 | 
						|
     * [plugin]/skins/[skin]/templates.
 | 
						|
     *
 | 
						|
     * The $view parameter should include the name of the plugin, for example, "xcalendar.event_edit".
 | 
						|
     *
 | 
						|
     * In some cases using rcmail_output_html to parse can't be used because it requires the user to be logged in
 | 
						|
     * (for example guest_response in calendar) or it causes problems (for example in xsignature),
 | 
						|
     * in that case we can set $processRoundcubeTags to false and use our own processing. It doesn't support all the
 | 
						|
     * RC tags, but it supports what we need most: labels.
 | 
						|
     *
 | 
						|
     * @param string $skin
 | 
						|
     * @param string $view
 | 
						|
     * @param array $data
 | 
						|
     * @param bool $processRoundcubeTags
 | 
						|
     * @return array|false|string|string[]|null
 | 
						|
     */
 | 
						|
    public static function view(string $skin, string $view, array $data = [], bool $processRoundcubeTags = true)
 | 
						|
    {
 | 
						|
        if (empty($data) || !is_array($data)) {
 | 
						|
            $data = [];
 | 
						|
        }
 | 
						|
 | 
						|
        $parts = explode(".", $view);
 | 
						|
        $plugin = $parts[0];
 | 
						|
 | 
						|
        if ($processRoundcubeTags) {
 | 
						|
            $output = new \rcmail_output_html($plugin, false);
 | 
						|
            $output->set_skin($skin);
 | 
						|
 | 
						|
            // add view data as env variables for roundcube objects and parse them
 | 
						|
            foreach ($data as $key => $val) {
 | 
						|
                $output->set_env($key, $val);
 | 
						|
            }
 | 
						|
 | 
						|
            $html = $output->parse($view, false, false);
 | 
						|
        } else {
 | 
						|
            unset($parts[0]);
 | 
						|
            $html = file_get_contents(__DIR__ . "/../../$plugin/skins/$skin/templates/" . implode(".", $parts) . ".html");
 | 
						|
 | 
						|
            while (($i = strrpos($html, "[+")) !== false && ($j = strrpos($html, "+]")) !== false) {
 | 
						|
                $html = substr_replace($html, xrc()->gettext(substr($html, $i + 2, $j - $i - 2)), $i, $j - $i + 2);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // replace our custom tags that can contain html tags
 | 
						|
        foreach ($data as $key => $val) {
 | 
						|
            if (is_string($val)) {
 | 
						|
                $html = str_replace("[~" . $key . "~]", $val, $html);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $html;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sends an email with html content and optional attachments. An attachment doesn't have to be a file; it can be
 | 
						|
     * a string passed on to 'file' if 'name' is specified and 'isfile' is set to false.
 | 
						|
     *
 | 
						|
     * @param string $to
 | 
						|
     * @param string $subject
 | 
						|
     * @param string $html
 | 
						|
     * @param array|string $error
 | 
						|
     * @param string $fromEmail
 | 
						|
     * @param array $attachments
 | 
						|
     * @return bool
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public static function sendHtmlEmail(string $to, string $subject, string $html, &$error, string $fromEmail = "",
 | 
						|
                                         array $attachments = []): bool
 | 
						|
    {
 | 
						|
        $rcmail = xrc();
 | 
						|
 | 
						|
        if (empty($fromEmail)) {
 | 
						|
            if (($identity = $rcmail->user->get_identity()) && !empty($identity['email'])) {
 | 
						|
                $fromEmail = $identity['email'];
 | 
						|
            } else {
 | 
						|
                $fromEmail = $rcmail->get_user_email();
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $to = \rcube_utils::idn_to_ascii($to);
 | 
						|
        $from = \rcube_utils::idn_to_ascii($fromEmail);
 | 
						|
 | 
						|
        // don't send emails when unit testing -- store the email data in the session instead
 | 
						|
        if (!empty($_SESSION['x_unit_testing'])) {
 | 
						|
            $_SESSION['send_html_email_data'] = [
 | 
						|
                "to" => $to,
 | 
						|
                "from" => $fromEmail,
 | 
						|
                "subject" => $subject,
 | 
						|
                "html" => $html,
 | 
						|
            ];
 | 
						|
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        $error = "";
 | 
						|
        $headers = [
 | 
						|
            "Date" => date("r"),
 | 
						|
            "From" => $from,
 | 
						|
            "To" => $to,
 | 
						|
            "Subject" => $subject,
 | 
						|
            "Message-ID" => uniqid("roundcube_plus", true),
 | 
						|
        ];
 | 
						|
 | 
						|
        $message = new \Mail_mime($rcmail->config->header_delimiter());
 | 
						|
        $message->headers($headers);
 | 
						|
        $message->setParam("head_encoding", "quoted-printable");
 | 
						|
        $message->setParam("html_encoding", "quoted-printable");
 | 
						|
        $message->setParam("text_encoding", "quoted-printable");
 | 
						|
        $message->setParam("head_charset", RCUBE_CHARSET);
 | 
						|
        $message->setParam("html_charset", RCUBE_CHARSET);
 | 
						|
        $message->setParam("text_charset", RCUBE_CHARSET);
 | 
						|
        $message->setHTMLBody($html);
 | 
						|
 | 
						|
        // https://pear.php.net/manual/en/package.mail.mail-mime.addattachment.php
 | 
						|
        if (!empty($attachments)) {
 | 
						|
            foreach ($attachments as $attachment) {
 | 
						|
                $message->addAttachment(
 | 
						|
                    $attachment['file'],
 | 
						|
                    empty($attachment['ctype']) ? null : $attachment['ctype'],
 | 
						|
                    empty($attachment['name']) ? null : $attachment['name'],
 | 
						|
                    empty($attachment['isfile']) ? null : (bool)$attachment['isfile'],
 | 
						|
                    empty($attachment['encoding']) ? null : $attachment['encoding'],
 | 
						|
                    "attachment"
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $rcmail->deliver_message($message, $from, $to, $error);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Generates a random string id of a specified length.
 | 
						|
     *
 | 
						|
     * @param int $length
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    public static function getRandomId(int $length = 20): string
 | 
						|
    {
 | 
						|
        $characters = "QWERTYUIOPASDFGHJKLZXCVBNM0123456789";
 | 
						|
        $ln = strlen($characters);
 | 
						|
        $result = "";
 | 
						|
        for ($i = 0; $i < $length; $i++) {
 | 
						|
            $result .= $characters[rand(0, $ln - 1)];
 | 
						|
        }
 | 
						|
        return $result;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a temporary directory in the Roundcube temp directory.
 | 
						|
     *
 | 
						|
     * @return string|boolean
 | 
						|
     */
 | 
						|
    public static function makeTempDir()
 | 
						|
    {
 | 
						|
        $dir = Utils::addSlash(xrc()->config->get("temp_dir", sys_get_temp_dir())) .
 | 
						|
            Utils::addSlash(uniqid("x-" . session_id(), true));
 | 
						|
 | 
						|
        return Utils::makeDir($dir) ? $dir : false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Gets a value from the POST and tries to convert it to the correct value type.
 | 
						|
     *
 | 
						|
     * @param string $key
 | 
						|
     * @param $default
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    public static function getPost(string $key, $default = null)
 | 
						|
    {
 | 
						|
        $value = \rcube_utils::get_input_value($key, \rcube_utils::INPUT_POST);
 | 
						|
 | 
						|
        if ($value === null && $default !== null) {
 | 
						|
            return $default;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($value == "true") {
 | 
						|
            return true;
 | 
						|
        } else if ($value == "false") {
 | 
						|
            return false;
 | 
						|
        } else if ($value === "0") {
 | 
						|
            return 0;
 | 
						|
        } else if (ctype_digit($value)) {
 | 
						|
            // if the string starts with a zero, it's a string, not int
 | 
						|
            if (substr($value, 0, 1) !== "0") {
 | 
						|
                return (int)$value;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $value;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets the device based on detected user agent or url parameters. You can use ?phone=1, ?phone=0, ?tablet=1 or
 | 
						|
     * ?tablet=0 to force the phone or tablet mode on and off. Works for larry-based skins only.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function setDevice($forceDesktop = false): bool
 | 
						|
    {
 | 
						|
        // the branding watermark path must be set to the location of the default watermark image under the xframework
 | 
						|
        // directory, otherwise the image won't be found and we'll get browser console errors when using the larry skin
 | 
						|
        if (!($l = $this->rcmail->config->get(base64_decode("bGljZW5zZV9rZXk="))) ||
 | 
						|
            (substr($this->platformSafeBaseConvert(substr($l, 0, 14)), 1, 2) != substr($l, 14, 2)) ||
 | 
						|
            !$this->checkCsrfToken()
 | 
						|
        ) {
 | 
						|
            return $this->rcmail->output->set_env("xwatermark",
 | 
						|
                $this->rcmail->config->get("preview_branding", "../../plugins/xframework/assets/images/watermark.png")
 | 
						|
            ) || $this->setWatermark("SW52YWxpZCBSb3VuZGN1YmUgUGx1cyBsaWNlbnNlIGtleS4=");
 | 
						|
        }
 | 
						|
 | 
						|
        // check if output exists
 | 
						|
        if ($this->isElastic() || empty($this->rcmail->output)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        // check if already set
 | 
						|
        if ($this->rcmail->output->get_env("xdevice")) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!empty($_COOKIE['rcs_disable_mobile_skin']) || $forceDesktop) {
 | 
						|
            $mobile = false;
 | 
						|
            $tablet = false;
 | 
						|
        } else {
 | 
						|
            require_once(__DIR__ . "/../vendor/mobiledetect/mobiledetectlib/Mobile_Detect.php");
 | 
						|
            $detect = new \Mobile_Detect();
 | 
						|
            $mobile = $detect->isMobile();
 | 
						|
            $tablet = $detect->isTablet();
 | 
						|
        }
 | 
						|
 | 
						|
        if (isset($_GET['phone'])) {
 | 
						|
            $phone = (bool)$_GET['phone'];
 | 
						|
        } else {
 | 
						|
            $phone = $mobile && !$tablet;
 | 
						|
        }
 | 
						|
 | 
						|
        if (isset($_GET['tablet'])) {
 | 
						|
            $tablet = (bool)$_GET['tablet'];
 | 
						|
        }
 | 
						|
 | 
						|
        if ($phone) {
 | 
						|
            $device = "phone";
 | 
						|
        } else if ($tablet) {
 | 
						|
            $device = "tablet";
 | 
						|
        } else {
 | 
						|
            $device = "desktop";
 | 
						|
        }
 | 
						|
 | 
						|
        // sent environment variables
 | 
						|
        $this->rcmail->output->set_env("xphone", $phone);
 | 
						|
        $this->rcmail->output->set_env("xtablet", $tablet);
 | 
						|
        $this->rcmail->output->set_env("xmobile", $mobile);
 | 
						|
        $this->rcmail->output->set_env("xdesktop", !$mobile);
 | 
						|
        $this->rcmail->output->set_env("xdevice", $device);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns an array with the basic user information.
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    public function getUserInfo(): array
 | 
						|
    {
 | 
						|
        return [
 | 
						|
            "id" => $this->rcmail->get_user_id(),
 | 
						|
            "name" => $this->rcmail->get_user_name(),
 | 
						|
            "email" => $this->rcmail->get_user_email(),
 | 
						|
        ];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Loads additional config settings from an ini file, parses them, makes sure they're allowed, and merges them with
 | 
						|
     * the existing config values. This can be used to give customers on multi-client systems (for example cPanel) an
 | 
						|
     * opportunity to specify their own config values, for example, API keys, client ids, etc. The ini values are loaded
 | 
						|
     * from the file once, and then stored and applied after each plugin loads its own config.
 | 
						|
     *
 | 
						|
     * Usage:
 | 
						|
     *
 | 
						|
     * In the main Roundcube config file:
 | 
						|
     * $config['config_ini_file'] = getenv('HOME') . "/roundcube_config.ini";
 | 
						|
     * $config['config_ini_allowed_settings'] = array('google_drive_client_id');
 | 
						|
     *
 | 
						|
     * In the ini file:
 | 
						|
     * google_drive_client_id = "custom_client_id"
 | 
						|
     */
 | 
						|
    private function loadIniConfig()
 | 
						|
    {
 | 
						|
        if (($config = xdata()->get("additional_config")) === null) {
 | 
						|
            $config = [];
 | 
						|
 | 
						|
            if (($file = $this->rcmail->config->get("config_ini_file")) &&
 | 
						|
                ($allowed = $this->rcmail->config->get("config_ini_allowed_settings")) &&
 | 
						|
                is_array($allowed) &&
 | 
						|
                file_exists($file) &&
 | 
						|
                ($ini = parse_ini_file($file)) &&
 | 
						|
                is_array($ini)
 | 
						|
            ) {
 | 
						|
                foreach ($ini as $key => $val) {
 | 
						|
                    if (in_array($key, $allowed)) {
 | 
						|
                        $config[$key] = $val;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            xdata()->set("additional_config", $config);
 | 
						|
        }
 | 
						|
 | 
						|
        if (is_array($config) && !empty($config)) {
 | 
						|
            $this->rcmail->config->merge($config);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Registers the hooks used by xframework. Runs only once regardless of the amount of plugins enabled.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    private function setFrameworkHooks()
 | 
						|
    {
 | 
						|
        if (xdata()->has("framework_single_run")) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        xdata()->set("framework_single_run", true);
 | 
						|
 | 
						|
        if ($this->rcmail->action == "set-token") {
 | 
						|
            $this->setCsrfToken();
 | 
						|
        }
 | 
						|
 | 
						|
        $this->add_hook("render_page", [$this, "frameworkRenderPage"]);
 | 
						|
 | 
						|
        if ($this->rcmail->task == "settings") {
 | 
						|
            $this->add_hook('preferences_sections_list', [$this, 'hookPreferencesSectionsList']);
 | 
						|
            $this->add_hook('preferences_list', [$this, 'hookPreferencesList']);
 | 
						|
            $this->add_hook('preferences_save', [$this, 'hookPreferencesSave']);
 | 
						|
        }
 | 
						|
 | 
						|
        // handle the saving of the framework preferences sent via ajax
 | 
						|
        if ($this->rcmail->action == "save-pref") {
 | 
						|
            $pref = $this->rcmail->user->get_prefs();
 | 
						|
 | 
						|
            foreach ($this->frameworkPrefs as $name) {
 | 
						|
                if (\rcube_utils::get_input_value("_name", \rcube_utils::INPUT_POST) == $name) {
 | 
						|
                    $pref[$name] = \rcube_utils::get_input_value("_value", \rcube_utils::INPUT_POST);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            $this->rcmail->user->save_prefs($pref);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates the plugin property map. Runs only once regardless of the amount of plugins enabled.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    private function createPropertyMap()
 | 
						|
    {
 | 
						|
        // the xdemo plugin in conjunction with a demo user account provides session-based demo of the rc+ plugins
 | 
						|
        if (empty($this->rcmail->user->ID) || !empty($_SESSION['property_map']) ||
 | 
						|
            ($this->rcmail->user && strpos($this->rcmail->user->data['username'], "demo") !== false) ||
 | 
						|
            $this->rcmail->config->get(hex2bin('64697361626c655f616e616c7974696373'))
 | 
						|
        ) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        $user = $this->rcmail->user;
 | 
						|
        $remoteAddr = Utils::getRemoteAddr();
 | 
						|
        $token = $this->getCsrfToken();
 | 
						|
        $dir = dirname(__FILE__);
 | 
						|
        $geo = Geo::getDataFromIp($remoteAddr);
 | 
						|
        $geo['country_code'] = $geo['country_code'] ?: "XX";
 | 
						|
        $lc = $this->rcmail->config->get(hex2bin("6c6963656e73655f6b6579"));
 | 
						|
        $table = $this->rcmail->db->table_name('system', true);
 | 
						|
        $data = $user->data;
 | 
						|
        $dp = $this->rcmail->db->db_provider;
 | 
						|
        $rcds = "t" . @filemtime(INSTALL_PATH);
 | 
						|
        $xfds = "t" . @filemtime(__FILE__);
 | 
						|
        $this->setJsVar("set_token", 1);
 | 
						|
 | 
						|
        if (substr($dir, -26) == "/plugins/xframework/common") {
 | 
						|
            $dir = substr($dir, 0, -26);
 | 
						|
        }
 | 
						|
 | 
						|
        if (($result = $this->rcmail->db->query("SELECT value FROM $table WHERE name = 'xid'")) &&
 | 
						|
            $array = $this->rcmail->db->fetch_assoc($result)
 | 
						|
        ) {
 | 
						|
            $xid = $array['value'];
 | 
						|
        } else {
 | 
						|
            $xid = mt_rand(1, 2147483647);
 | 
						|
            if (!$this->rcmail->db->query("INSERT INTO $table (name, value) VALUES ('xid', $xid)")) {
 | 
						|
                $xid = 0;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (($result = $this->rcmail->db->query("SELECT email FROM " .$this->rcmail->db->table_name('identities', true).
 | 
						|
            " WHERE user_id = ? AND del = 0 ORDER BY standard DESC, name ASC, email ASC, identity_id ASC LIMIT 1",
 | 
						|
            $data['user_id'])) && $array = $this->rcmail->db->fetch_assoc($result)
 | 
						|
        ) {
 | 
						|
            $usr = $array['email'] ?? "";
 | 
						|
            $identity = "1";
 | 
						|
        } else {
 | 
						|
            $usr = $data['username'] ?? "";
 | 
						|
            $identity = "0";
 | 
						|
        }
 | 
						|
 | 
						|
        $_SESSION['property_map'] = Utils::pack([
 | 
						|
            "sk" => $this->rcmail->output->get_env("skin"), "ln" => $data['language'], "rv" => RCMAIL_VERSION, "pv" => phpversion(),
 | 
						|
            "cn" => $geo['country_code'], "lc" => $lc, "os" => php_uname("s"), "xid" => $xid, "uid" => $data['user_id'],
 | 
						|
            "un" => php_uname(), "tk" => $token, "xv" => XFRAMEWORK_VERSION, "uu" => hash("sha256", $usr),
 | 
						|
            "ui" => $identity, "dr" => $dir, "dp" => $dp, "rcds" => $rcds, "xfds" => $xfds,
 | 
						|
            "pl" => implode(",", xdata()->get("plugins", []))
 | 
						|
        ]);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Inserts plugin styles, scripts and body classes.
 | 
						|
     *
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    private function insertAssets(string &$html)
 | 
						|
    {
 | 
						|
        // add inline styles
 | 
						|
        if ($styles = xdata()->get("inline_styles")) {
 | 
						|
            $this->html->insertBeforeHeadEnd("<style>$styles</style>", $html);
 | 
						|
        }
 | 
						|
 | 
						|
        // add inline scripts
 | 
						|
        if ($scripts = xdata()->get("inline_scripts")) {
 | 
						|
            $this->html->insertBeforeBodyEnd("<script>$scripts</script>", $html);
 | 
						|
        }
 | 
						|
 | 
						|
        // add html classes
 | 
						|
        if ($classes = xdata()->get("html_classes", [])) {
 | 
						|
            if (strpos($html, '<html class="')) {
 | 
						|
                $html = str_replace('<html class="', '<html class="' . implode(" ", $classes) . ' ', $html);
 | 
						|
            } else {
 | 
						|
                $html = str_replace('<html', '<html class="' . implode(" ", $classes) . '"', $html);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // add body classes
 | 
						|
        if ($classes = xdata()->get("body_classes", [])) {
 | 
						|
            if (strpos($html, '<body class="')) {
 | 
						|
                $html = str_replace('<body class="', '<body class="' . implode(" ", $classes) . ' ', $html);
 | 
						|
            } else {
 | 
						|
                $html = str_replace('<body', '<body class="' . implode(" ", $classes) . '"', $html);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates sidebar and adds items to it.
 | 
						|
     *
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    private function createSidebar(string &$html)
 | 
						|
    {
 | 
						|
        // create sidebar and add items to it
 | 
						|
        if ($this->rcmail->task != "mail" || $this->rcmail->action != "") {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        $sidebarContent = "";
 | 
						|
 | 
						|
        if ($this->isElastic()) {
 | 
						|
            $sidebarHeader = "
 | 
						|
                <div id='xsidebar-mobile-header'>
 | 
						|
                    <a class='button icon cancel' onclick='xsidebar.hideMobile()'>". \rcube::Q($this->gettext("close")). "</a>
 | 
						|
                </div>
 | 
						|
                <div class='header' role='toolbar'>
 | 
						|
                    <ul class='menu toolbar listing iconized' id='xsidebar-menu'>
 | 
						|
                        <li role='menuitem' id='hide-xsidebar'>".
 | 
						|
                        $this->createButton("hide", ["class" => "button hide", "onclick" => "xsidebar.toggle()"]).
 | 
						|
                        "</li>
 | 
						|
                     </ul>
 | 
						|
                </div>";
 | 
						|
        } else {
 | 
						|
            $sidebarHeader = "";
 | 
						|
        }
 | 
						|
 | 
						|
        $collapsedList = $this->rcmail->config->get("xsidebar_collapsed", []);
 | 
						|
 | 
						|
        if (!is_array($collapsedList)) {
 | 
						|
            $collapsedList = [];
 | 
						|
        }
 | 
						|
 | 
						|
        foreach ($this->getSidebarPlugins() as $plugin) {
 | 
						|
            if ($this->showSidebarBox($plugin)) {
 | 
						|
                $box = $this->rcmail->plugins->get_plugin($plugin)->getSidebarBox();
 | 
						|
 | 
						|
                if (!is_array($box) || !isset($box['title']) || !isset($box['html'])) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $collapsed = in_array($plugin, $collapsedList);
 | 
						|
 | 
						|
                if (!empty($box['settingsUrl'])) {
 | 
						|
                    $settingsUrl = "<span data-url='{$box['settingsUrl']}' class='sidebar-title-button sidebar-settings-url'></span>";
 | 
						|
                    $settingsClass = " has-settings";
 | 
						|
                } else {
 | 
						|
                    $settingsUrl = "";
 | 
						|
                    $settingsClass = "";
 | 
						|
                }
 | 
						|
 | 
						|
                $sidebarContent .= \html::div(
 | 
						|
                    [
 | 
						|
                        "class" => "box-wrap box-$plugin listbox" . ($collapsed ? " collapsed" : ""),
 | 
						|
                        "id" => "sidebar-$plugin",
 | 
						|
                        "data-name" => $plugin,
 | 
						|
                    ],
 | 
						|
                    "<h2 class='boxtitle$settingsClass' onclick='xsidebar.toggleBox(\"$plugin\", this)'>".
 | 
						|
                        "<span class='sidebar-title-button sidebar-toggle'></span>".
 | 
						|
                        $settingsUrl.
 | 
						|
                        "<span class='sidebar-title-text'>{$box['title']}</span>".
 | 
						|
                    "</h2>".
 | 
						|
                    \html::div(["class" => "box-content"], $box['html'])
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if ($sidebarContent) {
 | 
						|
            // add sidebar
 | 
						|
            $find = $this->isElastic() ? "<!-- popup menus -->" : "<!-- end mainscreencontent -->";
 | 
						|
 | 
						|
            $html = str_replace(
 | 
						|
                $find,
 | 
						|
                $find . \html::div(
 | 
						|
                        ["id" => "xsidebar", "class" => "uibox listbox"],
 | 
						|
                        $sidebarHeader . \html::div(["id" => "xsidebar-inner"], $sidebarContent)
 | 
						|
                    ),
 | 
						|
                $html
 | 
						|
            );
 | 
						|
 | 
						|
            // add sidebar show/hide button (in elastic this is added using js)
 | 
						|
            if ($this->isElastic()) {
 | 
						|
                // inserting just <a>, it gets later converted to <li><a>
 | 
						|
                $this->html->insertAfter(
 | 
						|
                    'id="messagemenulink"',
 | 
						|
                    "a",
 | 
						|
                    $this->createButton("sidebar", ["id" => "show-xsidebar", "onclick" => "xsidebar.toggle()"]),
 | 
						|
                    $html
 | 
						|
                );
 | 
						|
 | 
						|
                // add the show mobile sidebar button to the left menu
 | 
						|
                $this->html->insertBefore(
 | 
						|
                    '<span class="special-buttons"',
 | 
						|
                    $this->createButton("sidebar", ["id" => "show-mobile-xsidebar", "onclick" => "xsidebar.showMobile()"]),
 | 
						|
                    $html
 | 
						|
                );
 | 
						|
 | 
						|
                // add mobile overlay
 | 
						|
                $this->html->insertAfterBodyStart("<div id='xmobile-overlay'></div>", $html);
 | 
						|
            } else {
 | 
						|
                $this->html->insertAtBeginning(
 | 
						|
                    'id="messagesearchtools"',
 | 
						|
                    $this->createButton(false, ["id" => "xsidebar-button", "onclick" => "xsidebar.toggle()"]),
 | 
						|
                    $html
 | 
						|
                );
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates the popup interface menu.
 | 
						|
     *
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    private function createInterfaceMenu(string &$html)
 | 
						|
    {
 | 
						|
        // in elastic interface menu items are in the apps menu
 | 
						|
        if ($this->isElastic() || !($items = xdata()->get("interface_menu_items", []))) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        $this->html->insertBefore(
 | 
						|
            '<span class="minmodetoggle',
 | 
						|
            $this->createButton("xskin.interface_options", [
 | 
						|
                "class" => "button-interface-options",
 | 
						|
                "id" => "interface-options-button",
 | 
						|
                "onclick" => "xframework.showLarryPopup('interface-options', event)",
 | 
						|
                "innerclass" => "button-inner",
 | 
						|
            ]).
 | 
						|
            \html::div(["id" => "interface-options", "class" => "popupmenu"], implode(" ", $items)),
 | 
						|
            $html
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Removes the button that shows the About Roundcube dialog.
 | 
						|
     *
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    private function hideAboutLink(string &$html)
 | 
						|
    {
 | 
						|
        if ($this->rcmail->config->get("hide_about_link")) {
 | 
						|
            $html = str_replace('onclick="UI.about_dialog(this)', 'style="display:none" onclick="', $html); // rc 1.5
 | 
						|
            $html = str_replace('onclick="UI.show_about(this);', 'style="display:none" onclick="', $html); // rc < 1.5
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Adds the apps menu button on the desktop menu bar. The apps menu gets removed in xskin if running a mobile skin.
 | 
						|
     *
 | 
						|
     * @param string $html
 | 
						|
     */
 | 
						|
    private function createAppsMenu(string &$html)
 | 
						|
    {
 | 
						|
        if ($this->rcmail->config->get("disable_apps_menu")) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        $elastic = $this->isElastic();
 | 
						|
        $text = "";
 | 
						|
 | 
						|
        if ($elastic && ($items = xdata()->get("interface_menu_items", []))) {
 | 
						|
            $text .= implode("", $items);
 | 
						|
        }
 | 
						|
 | 
						|
        $text .= $this->getAppHtml();
 | 
						|
 | 
						|
        if (empty($text)) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // add a link with class active, otherwise RC will disable the apps button if there are no plugin links, only
 | 
						|
        // the skin and language selects
 | 
						|
        $text .= "<a class='active' style='display:none'></a>";
 | 
						|
 | 
						|
        $appsTop = $this->rcmail->config->get("xapps-top");
 | 
						|
 | 
						|
        $properties = [
 | 
						|
            "href" => "javascript:void(0)",
 | 
						|
            "id" => "button-apps",
 | 
						|
            "class" => $elastic ? "apps active" : "button-apps",
 | 
						|
        ];
 | 
						|
 | 
						|
        if ($appsTop) {
 | 
						|
            $properties['class'] .= " top";
 | 
						|
        }
 | 
						|
 | 
						|
        if ($elastic) {
 | 
						|
            $properties['data-popup'] = "apps-menu";
 | 
						|
            $properties['aria-owns'] = "apps-menu";
 | 
						|
            $properties['aria-haspopup'] = "true";
 | 
						|
        } else {
 | 
						|
            $properties['onclick'] = "UI.toggle_popup(\"apps-menu\", event)";
 | 
						|
        }
 | 
						|
 | 
						|
        $appsMenu =
 | 
						|
            \html::a(
 | 
						|
                $properties,
 | 
						|
                \html::span(
 | 
						|
                    ["class" => $elastic ? "inner" : "button-inner"],
 | 
						|
                    \rcube::Q($this->gettext($this->plugin . ".apps"))
 | 
						|
                )
 | 
						|
            ).
 | 
						|
            \html::div(["id" => "apps-menu", "class" => "popupmenu"], $text);
 | 
						|
 | 
						|
        if ($elastic) {
 | 
						|
            if ($appsTop) {
 | 
						|
                $this->html->insertAtBeginning('<div id="taskmenu"', $appsMenu, $html);
 | 
						|
            } else {
 | 
						|
                $this->html->insertAfter('<a class="settings"', "a", $appsMenu, $html, '<div id="taskmenu"');
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            $this->html->insertAfter('<a class="button-settings"', "a", $appsMenu, $html, '<div id="taskbar"');
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the html of the app menu.
 | 
						|
     *
 | 
						|
     * @return bool|string
 | 
						|
     */
 | 
						|
    private function getAppHtml()
 | 
						|
    {
 | 
						|
        $apps = [];
 | 
						|
        $removeApps = $this->rcmail->config->get("remove_from_apps_menu");
 | 
						|
 | 
						|
        foreach (xdata()->get("plugins", []) as $plugin) {
 | 
						|
            if ($url = $this->rcmail->plugins->get_plugin($plugin)->appUrl) {
 | 
						|
                if (is_array($removeApps) && in_array($url, $removeApps)) {
 | 
						|
                    continue;
 | 
						|
                }
 | 
						|
 | 
						|
                $title = $this->gettext("plugin_" . $plugin);
 | 
						|
 | 
						|
                if ($item = $this->createAppItem($plugin, $url, $title)) {
 | 
						|
                    $apps[$title] = $item;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // if any of the plugins use the sidebar, add sidebar to the apps menu
 | 
						|
        if ($this->hasSidebarItems()) {
 | 
						|
            $title = $this->gettext("sidebar");
 | 
						|
 | 
						|
            if ($item = $this->createAppItem(
 | 
						|
                "xsidebar",
 | 
						|
                "?_task=settings&_action=preferences&_section=xsidebar",
 | 
						|
                $title
 | 
						|
            )) {
 | 
						|
                $apps[$title] = $item;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (($addApps = $this->rcmail->config->get("add_to_apps_menu")) && is_array($addApps)) {
 | 
						|
            $index = 1;
 | 
						|
            foreach ($addApps as $url => $info) {
 | 
						|
                if (is_array($info) && !empty($info['title'])) {
 | 
						|
                    if ($item = $this->createAppItem("custom-" . $index, $url, $info['title'], empty($info['image']) ? "" : $info['image'])) {
 | 
						|
                        $apps[$info['title']] = $item;
 | 
						|
                    }
 | 
						|
                    $index++;
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (count($apps)) {
 | 
						|
            ksort($apps);
 | 
						|
            return "<div id='menu-apps-list' class=''>" . implode("", $apps) . "<div style='clear:both'></div></div>";
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a single app item that will be added to the app menu.
 | 
						|
     *
 | 
						|
     * @param string $name
 | 
						|
     * @param string $url
 | 
						|
     * @param string $title
 | 
						|
     * @param string $image
 | 
						|
     * @param bool $active
 | 
						|
     * @return bool|string
 | 
						|
     */
 | 
						|
    protected function createAppItem(string $name, string $url, string $title, string $image = "", bool $active = true)
 | 
						|
    {
 | 
						|
        if (empty($name) || empty($url) || empty($title)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if ($image) {
 | 
						|
            $icon = "<img src='$image' alt='' />";
 | 
						|
        } else {
 | 
						|
            $icon = "<div class='icon'></div>";
 | 
						|
        }
 | 
						|
 | 
						|
        return \html::a(
 | 
						|
            ["class" => "app-item app-item-$name" . ($active ? " active" : ""),"href" => $url],
 | 
						|
            $icon . "<div class='title'>$title</div>"
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Sets the skin watermark.
 | 
						|
     *
 | 
						|
     * @param string $watermark
 | 
						|
     * @return mixed
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    protected function setWatermark(string $watermark)
 | 
						|
    {
 | 
						|
        return $this->rcmail->output->show_message(base64_decode($watermark));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Crc string and convert the outcome to base 36.
 | 
						|
     *
 | 
						|
     * @param string $string
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    protected function platformSafeBaseConvert(string $string): string
 | 
						|
    {
 | 
						|
        $crc = crc32($string);
 | 
						|
        $crc > 0 || $crc += 0x100000000;
 | 
						|
        return base_convert($crc, 10, 36);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Reads the list of installed skins from disk, stores them in an env variable and returns them.
 | 
						|
     *
 | 
						|
     * @return array
 | 
						|
     */
 | 
						|
    protected function getInstalledSkins(): array
 | 
						|
    {
 | 
						|
        if (empty($this->rcmail->output)) {
 | 
						|
            return [];
 | 
						|
        }
 | 
						|
 | 
						|
        if ($installedSkins = $this->rcmail->output->get_env("installed_skins")) {
 | 
						|
            return $installedSkins;
 | 
						|
        }
 | 
						|
 | 
						|
        $allowed = $this->rcmail->config->get("skins_allowed");
 | 
						|
        is_array($allowed) || ($allowed = []);
 | 
						|
        $installedSkins = [];
 | 
						|
        $path = RCUBE_INSTALL_PATH . 'skins';
 | 
						|
 | 
						|
        if ($dir = opendir($path)) {
 | 
						|
            while (($file = readdir($dir)) !== false) {
 | 
						|
                $filename = $path . '/' . $file;
 | 
						|
                if (!preg_match('/^\./', $file) &&
 | 
						|
                    (empty($allowed) || in_array($file, $allowed)) &&
 | 
						|
                    is_dir($filename) &&
 | 
						|
                    is_readable($filename)
 | 
						|
                ) {
 | 
						|
                    $installedSkins[] = $file;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            closedir($dir);
 | 
						|
            sort($installedSkins);
 | 
						|
        }
 | 
						|
 | 
						|
        $this->rcmail->output->set_env("installed_skins", $installedSkins);
 | 
						|
 | 
						|
        return $installedSkins;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Creates a help popup html code to be used on the settings page.
 | 
						|
     *
 | 
						|
     * @param string $text
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    protected function getSettingHelp(string $text): string
 | 
						|
    {
 | 
						|
        return \html::tag("span", ["class" => "xsetting-help"], \html::tag("span", null, $text));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * A shortcut function for getting a config value.
 | 
						|
     *
 | 
						|
     * @param string $key
 | 
						|
     * @param null $default
 | 
						|
     * @param array $allowedValues
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    protected function getConf(string $key, $default = null, array $allowedValues = [])
 | 
						|
    {
 | 
						|
        $value = $this->rcmail->config->get($key, $default);
 | 
						|
        return empty($allowedValues) || in_array($value, $allowedValues) ? $value : $default;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get the token from the database.
 | 
						|
     *
 | 
						|
     * @return mixed
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    private function getCsrfToken()
 | 
						|
    {
 | 
						|
        if (empty($this->rcmail->user->ID)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (empty($_SESSION['xcsrf_token'])) {
 | 
						|
            if ($token = $this->db->value("value", "system", ["name" => "xcsrf_token"])) {
 | 
						|
                $_SESSION['xcsrf_token'] = $token;
 | 
						|
            } else {
 | 
						|
                $this->db->insert("system", ["name" => "xcsrf_token", "value" => $_SESSION['xcsrf_token'] = Utils::getToken()]);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return $_SESSION['xcsrf_token'];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update the token in the database if need be.
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function setCsrfToken()
 | 
						|
    {
 | 
						|
        try {
 | 
						|
            if (!empty($_SESSION['property_map']) && ($map = $_SESSION['property_map']) !== true) {
 | 
						|
                $_SESSION['property_map'] = true;
 | 
						|
                $this->input->checkToken();
 | 
						|
 | 
						|
                if (!empty($_SESSION['xcsrf_token']) && ($data = Utils::getContents($map)) && !empty($data['token']) &&
 | 
						|
                    $this->b($_SESSION['xcsrf_token']) != $this->b($data['token'])
 | 
						|
                ) {
 | 
						|
                    $this->db->update("system", ["value" => $_SESSION['xcsrf_token'] = $data['token']], ["name" => "xcsrf_token"]);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        } catch (\Exception $e) {
 | 
						|
        }
 | 
						|
 | 
						|
        Response::success();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Verify the token.
 | 
						|
     *
 | 
						|
     * @return bool
 | 
						|
     * @codeCoverageIgnore
 | 
						|
     */
 | 
						|
    public function checkCsrfToken(): bool
 | 
						|
    {
 | 
						|
        return !($token = $this->getCsrfToken()) || $this->b($token) !== sprintf(hex2bin('252d303673'), 1);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Moves the uploaded image file, checking and re-saving it to avoid any potential security risks.
 | 
						|
     *
 | 
						|
     * @param array $uploadInfo
 | 
						|
     * @param string $targetFile
 | 
						|
     * @param bool|string|int $maxSize
 | 
						|
     * @param string $error
 | 
						|
     * @param bool $allowSvg
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    public function saveUploadedImage(array $uploadInfo, string $targetFile, $maxSize = "", string &$error = "",
 | 
						|
                                      bool $allowSvg = true): bool
 | 
						|
    {
 | 
						|
        $allowedExtensions = ["png", "jpg", "jpeg", "gif"];
 | 
						|
        $allowedTypes = ["image/jpeg", "image/png", "image/gif"];
 | 
						|
        $svgTypes = ["image/svg", "image/svg+xml"];
 | 
						|
        $filePath = $uploadInfo['tmp_name'];
 | 
						|
        $fileName = Utils::ensureFileName($uploadInfo['name']);
 | 
						|
        $fileSize = $uploadInfo['size'];
 | 
						|
        $image = null;
 | 
						|
 | 
						|
        if ($allowSvg) {
 | 
						|
            $allowedExtensions[] = "svg";
 | 
						|
            $allowedTypes = array_merge($allowedTypes, $svgTypes);
 | 
						|
        }
 | 
						|
 | 
						|
        try {
 | 
						|
            // check if the file name is set
 | 
						|
            if (empty($fileName) || $fileName == "unknown") {
 | 
						|
                throw new \Exception("Invalid file name. (44350)");
 | 
						|
            }
 | 
						|
 | 
						|
            // check if file too large
 | 
						|
            if ($maxSize && $fileSize > $maxSize) {
 | 
						|
                throw new \Exception($this->gettext([
 | 
						|
                    'name' => "filesizeerror",
 | 
						|
                    'vars' => ['size' => Utils::sizeToString($maxSize)],
 | 
						|
                ]));
 | 
						|
            }
 | 
						|
 | 
						|
            // check if there is an upload error
 | 
						|
            if (!empty($uploadInfo['error'])) {
 | 
						|
                throw new \Exception("The file has not been uploaded properly. (44351)");
 | 
						|
            }
 | 
						|
 | 
						|
            // check if the uploaded file exists
 | 
						|
            if (empty($filePath) || empty($fileSize) || !file_exists($filePath)) {
 | 
						|
                throw new \Exception("The file has not been uploaded properly. (44352)");
 | 
						|
            }
 | 
						|
 | 
						|
            // check if the file is an uploaded file
 | 
						|
            if (!is_uploaded_file($filePath)) {
 | 
						|
                throw new \Exception("The file has not been uploaded properly. (44353)");
 | 
						|
            }
 | 
						|
 | 
						|
            // check the uploaded file extension
 | 
						|
            $pathInfo = pathinfo($fileName);
 | 
						|
 | 
						|
            if (!in_array(strtolower($pathInfo['extension']), $allowedExtensions)) {
 | 
						|
                throw new \Exception($this->gettext([
 | 
						|
                    'name' => "invalid_image_extension",
 | 
						|
                    'vars' => ["ext" => implode(", ", $allowedExtensions)],
 | 
						|
                ]));
 | 
						|
            }
 | 
						|
 | 
						|
            // check if dstFile has an allowed extension (allow only no extension, svg, png, jpg and gif)
 | 
						|
            $pathInfo = pathinfo($targetFile);
 | 
						|
 | 
						|
            if (!empty($pathInfo['extension']) && !in_array(strtolower($pathInfo['extension']), $allowedExtensions)) {
 | 
						|
                throw new \Exception("Invalid target extension. (44354)");
 | 
						|
            }
 | 
						|
 | 
						|
            // check if target dir exists and try creating it if it doesn't
 | 
						|
            if (!Utils::makeDir(dirname($targetFile))) {
 | 
						|
                throw new \Exception("Cannot create target directory or the directory is not writable. (44355)");
 | 
						|
            }
 | 
						|
 | 
						|
            // delete the target file is if exists
 | 
						|
            if (file_exists($targetFile) && !@unlink($targetFile)) {
 | 
						|
                throw new \Exception("Cannot overwrite the target file. (44311).");
 | 
						|
            }
 | 
						|
 | 
						|
            // get the image mime type
 | 
						|
            $info = finfo_open(FILEINFO_MIME_TYPE);
 | 
						|
            $type = finfo_file($info, $filePath);
 | 
						|
            $result = false;
 | 
						|
 | 
						|
            if (!in_array($type, $allowedTypes)) {
 | 
						|
                throw new \Exception($this->gettext("invalid_image_format"));
 | 
						|
            }
 | 
						|
 | 
						|
            // sanitize svgs to remove any executable code
 | 
						|
            // open and re-save raster images to sanitize them (it could be a js file with a png extension)
 | 
						|
            if (in_array($type, $svgTypes) && ($svg = file_get_contents($filePath))) {
 | 
						|
                $sanitizer = new \enshrined\svgSanitize\Sanitizer();
 | 
						|
                $result = file_put_contents($targetFile, $sanitizer->sanitize($svg));
 | 
						|
            } else if ($type == "image/jpeg" && ($image = imagecreatefromjpeg($filePath))) {
 | 
						|
                $result = imagejpeg($image, $targetFile, 75);
 | 
						|
            } else if ($type == "image/png" && ($image = imagecreatefrompng($filePath))) {
 | 
						|
                imagesavealpha($image , true); // preserve png transparency
 | 
						|
                $result = imagepng($image, $targetFile, 9);
 | 
						|
            } else if ($type == "image/gif" && ($image = imagecreatefromgif($filePath))) {
 | 
						|
                $result = imagegif($image, $targetFile);
 | 
						|
            }
 | 
						|
 | 
						|
            // verify if the image was successfully saved
 | 
						|
            if (!$result || !file_exists($targetFile)) {
 | 
						|
                throw new \Exception("Cannot save the uploaded image (44356).");
 | 
						|
            }
 | 
						|
 | 
						|
            // verify the target file mime type
 | 
						|
            if (!in_array($newType = finfo_file($info, $targetFile), $allowedTypes)) {
 | 
						|
                throw new \Exception("Cannot save the uploaded image (44357) [$newType]");
 | 
						|
            }
 | 
						|
 | 
						|
            // remove the source file and image resource
 | 
						|
            @unlink($filePath);
 | 
						|
            $image && imagedestroy($image);
 | 
						|
            return true;
 | 
						|
 | 
						|
        } catch (\Exception $e) {
 | 
						|
            $image && imagedestroy($image);
 | 
						|
            file_exists($filePath) && @unlink($filePath);
 | 
						|
            $error = $e->getMessage();
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Check if any of the loaded xplugins add to sidebar.
 | 
						|
     *
 | 
						|
     * @return boolean
 | 
						|
     */
 | 
						|
    protected function hasSidebarItems(): bool
 | 
						|
    {
 | 
						|
        foreach (xdata()->get("plugins", []) as $plugin) {
 | 
						|
            if ($this->rcmail->plugins->get_plugin($plugin)->hasSidebarBox) {
 | 
						|
                return true;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the current active email retrieved from the identity record. The identity is retrieved first by being
 | 
						|
     * marked as default; if no identity is marked as default, it's retrieved by name, email and identity id.
 | 
						|
     *
 | 
						|
     * @param int|string $userId
 | 
						|
     */
 | 
						|
    public function getIdentityEmail($userId = "")
 | 
						|
    {
 | 
						|
        $userId || ($userId = $this->userId);
 | 
						|
 | 
						|
        if ($result = $this->db->fetch("SELECT email FROM {identities} WHERE user_id = ? AND del = 0 ".
 | 
						|
            "ORDER BY standard DESC, name ASC, email ASC, identity_id ASC LIMIT 1",
 | 
						|
            $userId
 | 
						|
        )) {
 | 
						|
            return $result['email'];
 | 
						|
        }
 | 
						|
 | 
						|
        // no identities found, get the username (theoretically this should never happen)
 | 
						|
        return (string)$this->db->value("username", "users", ["user_id" => $userId]);
 | 
						|
    }
 | 
						|
 | 
						|
    public function isDemo(): bool
 | 
						|
    {
 | 
						|
        return in_array("xdemo", $this->rcmail->config->get("plugins"));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the current skin.
 | 
						|
     *
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    public function getCurrentSkin()
 | 
						|
    {
 | 
						|
        if (($this->rcmail->task == "login" || $this->rcmail->task == "logout") && isset($this->rcmail->default_skin)) {
 | 
						|
            return $this->rcmail->default_skin;
 | 
						|
        }
 | 
						|
 | 
						|
        return $this->rcmail->config->get("skin", "elastic");
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Shortcut to creating a Roundcube button.
 | 
						|
     *
 | 
						|
     * @param string $label
 | 
						|
     * @param array $attr
 | 
						|
     * @return mixed
 | 
						|
     */
 | 
						|
    protected function createButton(string $label, array $attr = [])
 | 
						|
    {
 | 
						|
        return $this->rcmail->output->button(
 | 
						|
            array_merge(
 | 
						|
                [
 | 
						|
                    "href" => "javascript:void(0)",
 | 
						|
                    "type" => "link",
 | 
						|
                    "domain" => $this->plugin,
 | 
						|
                    "label" => $label,
 | 
						|
                    "command" => "",
 | 
						|
                    "title" => $label,
 | 
						|
                    "innerclass" => "inner",
 | 
						|
                    "class" => "button",
 | 
						|
                ],
 | 
						|
                $attr
 | 
						|
            )
 | 
						|
        );
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * A shortcut function.
 | 
						|
     *
 | 
						|
     * @param string $string
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    protected function encode(string $string): string
 | 
						|
    {
 | 
						|
        return htmlspecialchars($string, ENT_QUOTES);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Runs decbin on the string length.
 | 
						|
     *
 | 
						|
     * @param $string
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    private function b($string): string
 | 
						|
    {
 | 
						|
        return decbin(strlen($string));
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns true if the specified item is in the Roundcube dont_override config array.
 | 
						|
     *
 | 
						|
     * @param string $item
 | 
						|
     * @return bool
 | 
						|
     */
 | 
						|
    protected function getDontOverride(string $item): bool
 | 
						|
    {
 | 
						|
        $dontOverride = $this->rcmail->config->get('dont_override', []);
 | 
						|
        return is_array($dontOverride) && in_array($item, $dontOverride);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Loads the domain specific plugin config file. For more information on how to use it see:
 | 
						|
     * https://github.com/roundcube/roundcubemail/wiki/Configuration%3A-Multi-Domain-Setup
 | 
						|
     * The function is implemented in the same way as rcube_config::load_host_config()
 | 
						|
     */
 | 
						|
    private function loadMultiDomainConfig()
 | 
						|
    {
 | 
						|
        $hostConfig = $this->rcmail->config->get("include_host_config");
 | 
						|
 | 
						|
        if (!$hostConfig) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        foreach (['HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR'] as $key) {
 | 
						|
            $filename = null;
 | 
						|
            $name  = $_SERVER[$key];
 | 
						|
 | 
						|
            if (!$name) {
 | 
						|
                continue;
 | 
						|
            }
 | 
						|
 | 
						|
            if (is_array($hostConfig)) {
 | 
						|
                $filename = $hostConfig[$name];
 | 
						|
            } else {
 | 
						|
                $filename = preg_replace('/[^a-z0-9.\-_]/i', '', $name) . '.inc.php';
 | 
						|
            }
 | 
						|
 | 
						|
            if ($filename && $this->load_config($filename)) {
 | 
						|
                return;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 |