"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