2025-02-23 20:52:25 +01:00

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;
}
}
}
}