Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/acymailing/WpInit/Activation.php
+++ b/acymailing/WpInit/Activation.php
@@ -2,21 +2,28 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingHelpersUpdateHelper;
class Activation
{
+ // Install DB and sample data
public function install(): void
{
$file_name = rtrim(dirname(__DIR__), DS).DS.'back'.DS.'tables.sql';
- $handle = fopen($file_name, 'r');
- $queries = fread($handle, filesize($file_name));
- fclose($handle);
+ $queries = acym_getInternalFileContents($file_name);
+
+ if ($queries === false) {
+ return;
+ }
+ // If it is a network activation (activate on all websites)
if (is_multisite() && is_network_admin()) {
$currentBlog = get_current_blog_id();
- $sites = function_exists('get_sites') ? get_sites() : wp_get_sites();
+ $sites = get_sites();
+ // Install on all websites
foreach ($sites as $site) {
if (is_object($site)) {
$site = get_object_vars($site);
@@ -25,13 +32,14 @@
$this->sampledata($queries);
}
+ // Switch back to network main site
switch_to_blog($currentBlog);
} else {
$this->sampledata($queries);
}
if (file_exists(ACYM_FOLDER.'update.php')) {
- unlink(ACYM_FOLDER.'update.php');
+ acym_deleteFile(ACYM_FOLDER.'update.php');
}
}
@@ -48,6 +56,7 @@
continue;
}
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL schema creation during plugin install, caching and abstraction are not applicable here.
$wpdb->query('CREATE TABLE IF NOT EXISTS'.$oneTable);
}
@@ -65,6 +74,7 @@
return;
}
+ //First we increase the perfs so that we won't have any surprise.
acym_increasePerf();
$updateHelper = new UpdateHelper();
@@ -101,6 +111,7 @@
$config->saveConfig(['installcomplete' => 1]);
+ // Reload conf
acym_config(true);
}
}
--- a/acymailing/WpInit/Beaver.php
+++ b/acymailing/WpInit/Beaver.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
class Beaver
{
public function __construct()
@@ -19,7 +21,34 @@
public function addAcyscriptBeaver()
{
- wp_enqueue_script('select2lib', ACYM_JS.'libraries/select2-full.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'libraries'.DS.'select2-full.min.js'), ['jquery']);
- wp_enqueue_script('acym_script_widget_article_beaver', ACYM_JS.'widget.min.js?v='.time(), ['jquery', 'select2lib'], false, true);
+ wp_enqueue_script(
+ 'select2lib',
+ ACYM_JS.'libraries/select2-full.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'libraries'.DS.'select2-full.min.js'),
+ [
+ 'jquery',
+ ],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
+ );
+ wp_enqueue_script(
+ 'acym_script_widget_article_beaver',
+ ACYM_JS.'widget.min.js?v='.time(),
+ [
+ 'jquery',
+ 'select2lib',
+ ],
+ '11.0.0',
+ [
+ 'in_footer' => true,
+ ]
+ );
+ // CSRF token for the article-search AJAX (dynamics/trigger requires acym_checkToken)
+ wp_add_inline_script(
+ 'acym_script_widget_article_beaver',
+ 'var acym_widget_nonce = '.json_encode(wp_create_nonce('acymnonce')).';',
+ 'before'
+ );
}
}
--- a/acymailing/WpInit/Cron.php
+++ b/acymailing/WpInit/Cron.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingControllersConfigurationController;
use AcyMailingHelpersCronHelper;
@@ -26,17 +28,21 @@
public function triggerAutomatedTasks()
{
+ // Starter versions shouldn't have access to the cron
if (!acym_level(ACYM_ESSENTIAL)) {
acym_deleteScheduledTask(['name' => ConfigurationController::CRON_TASK_NAME]);
return;
}
- if (!acym_isLicenseValidWeekly() && (empty($_SERVER['HTTP_REFERER']) || strpos($_SERVER['HTTP_REFERER'], 'api.acymailing.com') === false)) {
+ //removeIf(development)
+ $httpReferer = acym_getVar('string', 'HTTP_REFERER', '', 'SERVER');
+ if (!acym_isLicenseValidWeekly() && (empty($httpReferer) || strpos($httpReferer, 'api.acymailing.com') === false)) {
acym_deleteScheduledTask(['name' => ConfigurationController::CRON_TASK_NAME]);
return;
}
+ //endRemoveIf(development)
$cronHelper = new CronHelper();
$cronHelper->cron();
--- a/acymailing/WpInit/Elementor.php
+++ b/acymailing/WpInit/Elementor.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
class Elementor
{
public function __construct()
@@ -14,10 +16,48 @@
public function addAcyScriptElementor()
{
- wp_enqueue_script('select2lib', ACYM_JS.'libraries/select2-full.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'libraries'.DS.'select2-full.min.js'), ['jquery']);
- wp_enqueue_script('acym_script_widget_article_elementor', ACYM_JS.'widget.min.js?v='.time(), ['jquery', 'select2lib'], false, true);
- wp_enqueue_script('acymailing-compatibility-elementor', ACYM_JS.'libraries/elementor.min.js', [], false, true);
- wp_enqueue_style('acym_style_widget_article_elementor', ACYM_CSS.'libraries/elementor.min.css?v='.time());
+ wp_enqueue_script(
+ 'select2lib',
+ ACYM_JS.'libraries/select2-full.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'libraries'.DS.'select2-full.min.js'),
+ ['jquery'],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
+ );
+ wp_enqueue_script(
+ 'acym_script_widget_article_elementor',
+ ACYM_JS.'widget.min.js?v='.time(),
+ ['jquery', 'select2lib'],
+ '11.0.0',
+ [
+ 'in_footer' => true,
+ ]
+ );
+ // CSRF token for the article-search AJAX (dynamics/trigger requires acym_checkToken)
+ wp_add_inline_script(
+ 'acym_script_widget_article_elementor',
+ 'var acym_widget_nonce = '.json_encode(wp_create_nonce('acymnonce')).';',
+ 'before'
+ );
+ wp_enqueue_script(
+ 'acymailing-compatibility-elementor',
+ ACYM_JS.'libraries/elementor.min.js',
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => true,
+ ]
+ );
+ wp_enqueue_style(
+ 'acym_style_widget_article_elementor',
+ ACYM_CSS.'libraries/elementor.min.css?v='.time(),
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => true,
+ ]
+ );
}
public function registerWidgets()
--- a/acymailing/WpInit/ElementorForm.php
+++ b/acymailing/WpInit/ElementorForm.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingClassesUserClass;
use AcyMailingClassesListClass;
@@ -19,6 +21,8 @@
public function register_settings_section($widget)
{
+ // Check if an id is provided because this function is called many times sometimes there is no id (and not the rest: settings, form_fields,...)
+ // So when we try to access fields data that is not currently existing a fatal error appears
if ($widget->get_id()) {
$fields = ['' => ''];
foreach ($widget->get_data('settings')['form_fields'] as $field) {
@@ -97,7 +101,7 @@
$newUser->name = $data[$settings['acym_nameField']];
$newUser->email = $data[$settings['acym_emailField']];
- $newUser->creation_date = date('Y-m-d H:i:s');
+ $newUser->creation_date = gmdate('Y-m-d H:i:s');
$newUser->confirmed = $settings['acym_confirmUsers'] === 'yes';
$user = $userClass->getOneByEmail($newUser->email);
@@ -105,6 +109,7 @@
$newUser->id = $user->id;
}
+ // We do that because Elementor submit the form via ajax and in ajax mode WordPress always return true to the function is_admin()
$config = acym_config();
if ($config->get('require_confirmation', 1) == 1) {
$userClass->forceConfAdmin = true;
--- a/acymailing/WpInit/FakePhpMailer.php
+++ b/acymailing/WpInit/FakePhpMailer.php
@@ -1,27 +0,0 @@
-<?php
-
-namespace AcyMailingWpInit;
-
-use AcyMailingHelpersMailerHelper;
-
-class FakePhpMailer
-{
- public function send()
- {
- return true;
- }
-
- public function IsSMTP()
- {
- }
-
- public function addReplyTo($replyto, $name = '')
- {
- return true;
- }
-
- public function setFrom($address, $name = '', $auto = true)
- {
- return true;
- }
-}
--- a/acymailing/WpInit/Forms.php
+++ b/acymailing/WpInit/Forms.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingClassesFormClass;
class Forms
@@ -36,13 +38,18 @@
}
}
- if (!empty($this->formToDisplay)) acym_initModule();
+ if (!empty($this->formToDisplay)) {
+ acym_initModule();
+ }
}
public function displayForms()
{
- if (empty($this->formToDisplay)) return;
+ if (empty($this->formToDisplay)) {
+ return;
+ }
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Generated in partial folder "forms", escaped there.
echo implode('', $this->formToDisplay);
}
--- a/acymailing/WpInit/Gutenberg.php
+++ b/acymailing/WpInit/Gutenberg.php
@@ -2,9 +2,10 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingClassesFieldClass;
use AcyMailingClassesListClass;
-use AcyMailingClassesUserClass;
use AcyMailingCoreAcymParameter;
class Gutenberg
@@ -44,13 +45,19 @@
{
wp_register_script(
'gutenberg-acymailing-subscription-form',
- ACYM_JS.'gutenberg/subscription.min.js?time='.time()
+ ACYM_JS.'gutenberg/subscription.min.js?time='.time(),
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
);
wp_add_inline_script(
'gutenberg-acymailing-subscription-form',
'var acym_lists = '.json_encode($this->lists).';
var ACYM_JS_TXT = '.acym_getJSMessages().';
- var acym_fields = '.json_encode($this->fields).';'
+ var acym_fields = '.json_encode($this->fields).';
+ var acym_subscription_nonce = '.json_encode(wp_create_nonce('acymnonce')).';'
);
$basicAttribute = [
@@ -98,6 +105,10 @@
'type' => 'string',
'default' => '',
],
+ 'trackingconsent' => [
+ 'type' => 'string',
+ 'default' => '0',
+ ],
'unsub' => [
'type' => 'string',
'default' => '0',
@@ -199,10 +210,13 @@
}
$params = new AcymParameter($block_attributes);
- return acym_renderForm(
+ ob_start();
+ acym_renderForm(
$params,
['disableButtons' => strpos(acym_currentURL(), 'block-renderer') !== false]
);
+
+ return ob_get_clean();
}
public function registerBlockProfile()
@@ -211,7 +225,12 @@
wp_register_script(
'gutenberg-acymailing-profile',
- ACYM_JS.'gutenberg/profile.min.js?time='.time()
+ ACYM_JS.'gutenberg/profile.min.js?time='.time(),
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
);
wp_add_inline_script(
'gutenberg-acymailing-profile',
@@ -278,7 +297,12 @@
wp_register_script(
'gutenberg-acymailing-archive',
- ACYM_JS.'gutenberg/archive.min.js?time='.time()
+ ACYM_JS.'gutenberg/archive.min.js?time='.time(),
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
);
wp_add_inline_script(
'gutenberg-acymailing-archive',
--- a/acymailing/WpInit/MailDisabler.php
+++ b/acymailing/WpInit/MailDisabler.php
@@ -0,0 +1,30 @@
+<?php
+// phpcs:disable library_core_files -- Intended to disable the WordPress emails, if the user chooses so.
+
+namespace AcyMailingWpInit;
+
+defined('ABSPATH') || die('Restricted Access');
+
+use AcyMailingHelpersMailerHelper;
+
+class MailDisabler
+{
+ public function send()
+ {
+ return true;
+ }
+
+ public function IsSMTP()
+ {
+ }
+
+ public function addReplyTo($replyto, $name = '')
+ {
+ return true;
+ }
+
+ public function setFrom($address, $name = '', $auto = true)
+ {
+ return true;
+ }
+}
--- a/acymailing/WpInit/Menu.php
+++ b/acymailing/WpInit/Menu.php
@@ -11,16 +11,21 @@
$this->router = $router;
if (defined('WP_ADMIN') && WP_ADMIN) {
+ // Add AcyMailing menu in the back-end's left menu of WordPress
add_action('admin_menu', [$this, 'addMenus'], 99);
}
+ // Add "settings" link for acym on plugins listing
add_filter('plugin_action_links_'.plugin_basename(dirname(__DIR__).'/index.php'), [$this, 'addPluginLinks']);
}
+ // Add AcyMailing menu to WP left menu and define controllers
public function addMenus()
{
+ // Everyone in WordPress can read, the real test is made bellow
$capability = 'read';
+ // Make sure the user is allowed to see admin features
$config = acym_config();
$allowedGroups = explode(',', $config->get('wp_access', 'administrator'));
$userGroups = acym_getGroupsByUser();
@@ -34,7 +39,8 @@
}
if (!$allowed) return;
- $svg = acym_loaderLogo(false);
+ // Add the Acy menu items to the WP menu
+ $svg = acym_fileGetContent(ACYM_IMAGES.'logos/logo_grey.svg');
add_menu_page(
acym_translation('ACYM_DASHBOARD'),
'AcyMailing',
@@ -82,6 +88,7 @@
);
}
+ // Declare invisible menus
$controllers = ['dynamics', 'file', 'language'];
foreach ($controllers as $oneCtrl) {
add_submenu_page(
@@ -94,12 +101,14 @@
);
}
+ // In WordPress, the first submenu is called "AcyMailing" instead of "Dashboard" so we rename it manually
global $submenu;
if (isset($submenu[ACYM_COMPONENT.'_dashboard'])) {
$submenu[ACYM_COMPONENT.'_dashboard'][0][0] = acym_translation('ACYM_DASHBOARD');
}
}
+ // Add links on the plugins listing
public function addPluginLinks($links): array
{
$settings_link = '<a href="admin.php?page='.ACYM_COMPONENT.'_configuration">'.acym_translation('ACYM_SETTINGS').'</a>';
--- a/acymailing/WpInit/Message.php
+++ b/acymailing/WpInit/Message.php
@@ -2,19 +2,36 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
class Message
{
public function __construct()
{
+ // This is the frontend message system, don't load on the backend
if (defined('WP_ADMIN') && WP_ADMIN) return;
if (defined('DOING_AJAX') && DOING_AJAX) return;
+ // When calling acym_getFormToken() while _locale is set, it breaks the backend widget edition for some reason
$locale = acym_getVar('string', '_locale', '');
if (!empty($locale)) return;
- wp_enqueue_style('acy_front_messages_css', ACYM_CSS.'front/messages.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'front'.DS.'messages.min.css'));
- wp_enqueue_script('acy_front_messages_js', ACYM_JS.'front/messages.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'front'.DS.'messages.min.js'));
+ wp_enqueue_style(
+ 'acy_front_messages_css',
+ ACYM_CSS.'front/messages.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'front'.DS.'messages.min.css'),
+ [],
+ '11.0.0'
+ );
+ wp_enqueue_script(
+ 'acy_front_messages_js',
+ ACYM_JS.'front/messages.min.js?v='.filemtime(ACYM_MEDIA.'js'.DS.'front'.DS.'messages.min.js'),
+ [],
+ '11.0.0',
+ [
+ 'in_footer' => false,
+ ]
+ );
wp_add_inline_script(
'acy_front_messages_js',
'var ACYM_AJAX_START = "'.admin_url('admin-ajax.php').'";
--- a/acymailing/WpInit/Oauth.php
+++ b/acymailing/WpInit/Oauth.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
class Oauth
{
public function __construct()
@@ -10,11 +12,11 @@
$state = acym_getVar('string', 'state');
if (!empty($code) && !empty($state)) {
if ($state === 'acymailingsmtp') {
- acym_redirect(acym_completeLink('configuration&auth_type=smtp&code='.$_GET['code'], false, true));
+ acym_redirect(acym_completeLink('configuration&auth_type=smtp&code='.$code, false, true));
}
if ($state === 'acymailingbounce') {
- acym_redirect(acym_completeLink('configuration&auth_type=bounce&code='.$_GET['code'], false, true));
+ acym_redirect(acym_completeLink('configuration&auth_type=bounce&code='.$code, false, true));
}
}
}
--- a/acymailing/WpInit/OverrideEmail.php
+++ b/acymailing/WpInit/OverrideEmail.php
@@ -13,6 +13,16 @@
public function overrideEmailFunction($args)
{
+ // A previous email in the same request may have registered the "blockEmailSending" and "blockEmailSendingPostSMTP" hooks below.
+ // We remove them systematically so each email is evaluated independently and we never missblock an email
+ remove_action('phpmailer_init', [$this, 'blockEmailSending']);
+ remove_filter('post_smtp_do_send_email', [$this, 'blockEmailSendingPostSMTP']);
+
+ // Let integrators exclude specific emails from AcyMailing's handling
+ if (!apply_filters('acym_override_wp_mail', true, $args)) {
+ return $args;
+ }
+
if (empty($args['to'])) {
return $args;
}
@@ -53,6 +63,7 @@
if (stripos($charsetContent, 'boundary=') !== false) {
$boundary = trim(str_replace(['BOUNDARY=', 'boundary=', '"'], '', $charsetContent));
}
+ // Avoid setting an empty $content_type.
} elseif (trim($content) !== '') {
$contentType = trim($content);
}
@@ -105,7 +116,7 @@
public function blockEmailSending(&$phpmailer)
{
- $phpmailer = new FakePhpMailer();
+ $phpmailer = new MailDisabler();
}
public function blockEmailSendingPostSMTP($shouldSend)
--- a/acymailing/WpInit/Router.php
+++ b/acymailing/WpInit/Router.php
@@ -2,17 +2,22 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingClassesPluginClass;
class Router
{
public function __construct()
{
+ // Back router
add_action('wp_ajax_acymailing_router', [$this, 'router']);
+ // Front router
if (!acym_isAdmin()) {
add_action('wp_loaded', [$this, 'frontRouter']);
}
+ // Make sure we can redirect / download / modify headers if needed after some checks
$pages = [
'automation',
'bounces',
@@ -52,8 +57,10 @@
];
foreach ($pages as $page) {
if (in_array($page, $headerPages)) {
+ // Ensure we can set headers in the plugin
add_action('load-acymailing_page_acymailing_'.$page, [$this, 'waitHeaders']);
}
+ // Disable WP emojis in AcyMailing only
add_action('admin_print_scripts-acymailing_page_acymailing_'.$page, [$this, 'disableJsBreakingPages']);
add_action('admin_print_styles-acymailing_page_acymailing_'.$page, [$this, 'removeCssBreakingPages']);
}
@@ -65,12 +72,16 @@
public function protectAcyMailingPages()
{
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Make sure we're on an AcyMailing page.
$page = isset($_REQUEST['page']) ? sanitize_text_field(wp_unslash($_REQUEST['page'])) : '';
- if (empty($page) || strpos($page, 'acymailing_') === false) return;
+ if (empty($page) || strpos($page, 'acymailing_') === false) {
+ return;
+ }
wp_dequeue_script('responsive-lightbox-admin-select2');
wp_dequeue_style('responsive-lightbox-admin-select2');
+ // Remove theme loading select2 from Supreme module pro for divi
wp_dequeue_script('dsm-select-two');
wp_dequeue_style('dsm-select-two');
}
@@ -82,18 +93,25 @@
public function disableJsBreakingPages()
{
+ // Show normal emojis on AcyMailing pages
remove_action('admin_print_scripts', 'print_emoji_detection_script');
+ // Slideshow ck breaks the editor
remove_action('wp_enqueue_media', '\Slideshowck\Tinymce\register_scripts_styles');
+ // Skaut Google Drive gallery breaks the editor
remove_action('wp_enqueue_media', '\Sgdg\Admin\TinyMCE\register_scripts_styles');
+ // Remove theme loading select2 which breaks select2 in vueJS
wp_dequeue_script('select2.js');
+ // The "Checkout Field Manager for WooCommerce" plugin breaks the js on every pages
wp_dequeue_script('checkout_fields_js');
+ // Fixed editor incompatibility
wp_dequeue_script('wp-optimize-minify-admin-purge');
+ // Remove Happy Elementor Addons select2 which breaks our select2
wp_dequeue_script('happy-elementor-addons-select2');
wp_dequeue_script('select2');
}
@@ -105,9 +123,11 @@
wp_dequeue_style('wpml-select-2');
wp_dequeue_style('swcfpc_admin_css');
+ // Remove Happy Elementor Addons select2 which breaks our select2
wp_dequeue_style('happy-elementor-addons-select2');
wp_dequeue_style('select2');
+ // Messes with the tooltips, the schedule date in emails for example
wp_dequeue_style('qlwapp-admin-menu');
}
@@ -121,18 +141,23 @@
public function router(bool $front = false): void
{
- displayFreeTrialMessage();
+ acym_displayFreeTrialMessage();
if (!$front) {
auth_redirect();
+
+ // The router only enforces auth_redirect() and the 'dashboard' controller is exempt from call()'s ACL, so mirror Menu.php's back-office access gate (wp_access groups) to keep arbitrary logged-in users out.
+ if (!acym_hasBackofficeAccess()) {
+ wp_die(esc_html(acym_translation('ACYM_ACCESS_DENIED')), '', ['response' => 403]);
+ }
}
if (file_exists(ACYM_FOLDER.'update.php')) {
$acyActivation = new Activation();
if (is_multisite()) {
$currentBlog = get_current_blog_id();
- $sites = function_exists('get_sites') ? get_sites() : wp_get_sites();
+ $sites = get_sites();
foreach ($sites as $site) {
if (is_object($site)) {
@@ -148,15 +173,16 @@
$acyActivation->updateAcym();
}
- unlink(ACYM_FOLDER.'update.php');
+ acym_deleteFile(ACYM_FOLDER.'update.php');
}
$config = acym_config(true);
+ // Get controller. If not found, take it from the page
$ctrl = acym_getVar('cmd', 'ctrl', '');
$task = acym_getVar('cmd', 'task', '');
- if (!$front && acym_isAdmin() && file_exists(ACYM_NEW_FEATURES_SPLASHSCREEN_JSON) && is_writable(ACYM_NEW_FEATURES_SPLASHSCREEN_JSON)) {
+ if (!$front && acym_isAdmin() && file_exists(ACYM_NEW_FEATURES_SPLASHSCREEN_JSON) && acym_isWritable(ACYM_NEW_FEATURES_SPLASHSCREEN_JSON)) {
$ctrl = 'dashboard';
$task = 'features';
acym_setVar('ctrl', $ctrl);
@@ -174,8 +200,9 @@
$ctrl = str_replace(ACYM_COMPONENT.'_', '', acym_getVar('cmd', 'page', ''));
if (empty($ctrl)) {
- echo acym_translation('ACYM_PAGE_NOT_FOUND');
+ echo esc_html(acym_translation('ACYM_PAGE_NOT_FOUND'));
+ // For Google search console in the frontend to prevent from doing 404 errors
$noCache = acym_getVar('int', 'nocache', 0);
if (!empty($noCache)) {
acym_redirect(acym_rootURI());
@@ -192,7 +219,7 @@
$controllerNamespace = 'AcyMailing\'.$subNamespace.'Controllers\'.ucfirst($ctrl).'Controller';
if (!class_exists($controllerNamespace)) {
- echo acym_translation('ACYM_PAGE_NOT_FOUND').': '.$ctrl;
+ echo esc_html(acym_translation('ACYM_PAGE_NOT_FOUND').': '.$ctrl);
return;
}
@@ -200,16 +227,15 @@
if (!$front && acym_isAdmin() && $task != 'edit' && !(defined('DOING_AJAX') && DOING_AJAX)) {
$pluginClass = new PluginClass();
$installedPlugins = $pluginClass->getAll('title');
- $newPlugins = json_decode(ACYM_AVAILABLE_PLUGINS);
- foreach ($newPlugins as $onePlugin) {
- if (empty($installedPlugins[$onePlugin->name])) continue;
- if ($installedPlugins[$onePlugin->name]->type !== 'ADDON') continue;
+ foreach (ACYM_AVAILABLE_PLUGINS as $onePlugin) {
+ if (empty($installedPlugins[$onePlugin['name']])) continue;
+ if ($installedPlugins[$onePlugin['name']]->type !== 'ADDON') continue;
acym_enqueueMessage(
acym_translationSprintf(
'ACYM_NEW_PLUGIN_FORMAT',
- $onePlugin->name,
- '<a target="_blank" style="color: #00a5ff;" href="'.$onePlugin->downloadlink.'">'.acym_translation('ACYM_CLICK_HERE').'</a>'
+ $onePlugin['name'],
+ '<a target="_blank" style="color: #00a5ff;" href="'.$onePlugin['downloadlink'].'">'.acym_translation('ACYM_CLICK_HERE').'</a>'
),
'error'
);
@@ -261,6 +287,7 @@
acym_redirect($cleanedUrl);
}
+ // Never allow autologin for administrator accounts
if ($user->has_cap('manage_options')) {
acym_redirect($cleanedUrl);
}
@@ -274,6 +301,7 @@
private function deactivateHookAdminFooter()
{
+ //Remove hook function which break AcyMailing pages
remove_action('admin_footer', 'Freemius::_enrich_ajax_url');
remove_action('admin_footer', 'Freemius::_open_support_forum_in_new_page');
}
--- a/acymailing/WpInit/Update.php
+++ b/acymailing/WpInit/Update.php
@@ -1,173 +0,0 @@
-<?php
-
-namespace AcyMailingWpInit;
-
-use AcyMailingHelpersUpdatemeHelper;
-
-class Update
-{
- private bool $cancelUpdate = false;
- private string $pluginSlug;
-
- public function __construct()
- {
- $this->pluginSlug = plugin_basename(dirname(__DIR__).'/index.php');
-
- add_filter('pre_set_site_transient_update_plugins', [$this, 'checkUpdates'], 10, 1);
- add_filter('site_transient_update_plugins', [$this, 'checkUpdates'], 10, 1);
- add_filter('upgrader_package_options', [$this, 'checkDownloadUrl']);
- add_action('upgrader_process_complete', [$this, 'afterUpdate'], 20, 2);
- }
-
- public function checkDownloadUrl($options)
- {
- if (empty($options['hook_extra']['plugin']) || $options['hook_extra']['plugin'] !== $this->pluginSlug || !acym_level(ACYM_ESSENTIAL)) {
- return $options;
- }
-
- if (isset($options['package']) && strpos($options['package'], 'wordpress.org') !== false) {
- $this->checkVersion(true);
- $config = acym_config(true);
- $options['package'] = $config->get('downloadurl', '');
- }
-
- return $options;
- }
-
- public function checkUpdates($transient)
- {
- $this->checkVersion();
-
- if ($this->cancelUpdate) {
- if (!empty($transient->response[$this->pluginSlug])) {
- unset($transient->response[$this->pluginSlug]);
- }
-
- return $transient;
- }
-
- if (!acym_level(ACYM_ESSENTIAL) || !empty($transient->no_update[$this->pluginSlug]) || empty($transient->response[$this->pluginSlug])) {
- return $transient;
- }
-
- $config = acym_config();
- $downloadURL = $config->get('downloadurl', '');
-
- if (strpos($downloadURL, 'http') === false) {
- $downloadURL = '';
- add_action('admin_notices', [$this, 'noticeUpdate'], 110);
- }
-
- $transient->response[$this->pluginSlug]->package = $downloadURL;
-
- return $transient;
- }
-
- public function afterUpdate($upgrader_object, $options): void
- {
- if ($options['action'] !== 'update' || $options['type'] !== 'plugin') {
- return;
- }
-
- if (!empty($options['plugin']) && $options['plugin'] === $this->pluginSlug) {
- $this->resetUpdateData();
- } elseif (!empty($options['plugins'])) {
- foreach ($options['plugins'] as $onePluginSlug) {
- if ($onePluginSlug !== $this->pluginSlug) {
- continue;
- }
-
- $this->resetUpdateData();
- break;
- }
- }
- }
-
- public function noticeUpdate()
- {
- global $pagenow;
- if (!in_array($pagenow, ['update-core.php', 'plugins.php'])) {
- return;
- }
-
- echo '<div class="notice notice-error is-dismissible">
- <p>AcyMailing: '.acym_translation('ACYM_PAID_VERSION_NEED_UPDATE_ERROR_LICENSE_ATTACH').'</p>
- </div>';
- }
-
- private function checkVersion(bool $forceCheck = false): void
- {
- $config = acym_config();
- $lastCheck = $config->get('lastupdatecheck', 0);
-
- static $alreadyChecked = false;
- if (!$forceCheck && ($alreadyChecked || ($lastCheck > time() - 86400 && empty($_REQUEST['force-check'])))) {
- return;
- }
- $alreadyChecked = true;
-
- $url = ACYM_UPDATEME_API_URL.'public/updatexml/component?extension=acymailing&cms=wordpress&version=latest&level=starter';
- if (acym_level(ACYM_ESSENTIAL)) {
- $url .= '&website='.urlencode(ACYM_LIVE);
- }
-
- $updateInformation = acym_fileGetContent($url);
- $xmlPos = strpos($updateInformation, '<?xml');
- if ($xmlPos === false) {
- return;
- }
-
- $updateInformation = substr($updateInformation, $xmlPos);
-
- try {
- $xml = new SimpleXMLElement($updateInformation);
- $latestVersion = (string)$xml->update[0]->version;
- $downloadURL = (string)$xml->update[0]->downloadurl;
- } catch (Exception $err) {
- return;
- }
-
- $currentVersion = $config->get('version');
- if (!empty($currentVersion) && version_compare($currentVersion, $latestVersion, '>=')) {
- $this->cancelUpdate = true;
- }
-
- $newConfig = [
- 'lastupdatecheck' => time(),
- 'latestversion' => $latestVersion,
- 'downloadurl' => $downloadURL,
- ];
-
- if (is_multisite()) {
- $currentBlog = get_current_blog_id();
- $sites = function_exists('get_sites') ? get_sites() : wp_get_sites();
-
- foreach ($sites as $site) {
- if (is_object($site)) {
- $site = get_object_vars($site);
- }
- switch_to_blog($site['blog_id']);
- $config->saveConfig($newConfig);
- }
-
- switch_to_blog($currentBlog);
- }
-
- $config->saveConfig($newConfig);
-
- if (acym_level(ACYM_ESSENTIAL)) {
- UpdatemeHelper::getLicenseInfo();
- }
- }
-
- private function resetUpdateData(): void
- {
- $config = acym_config();
- $config->saveConfig(
- [
- 'downloadurl' => '',
- 'lastupdatecheck' => 0,
- ]
- );
- }
-}
--- a/acymailing/WpInit/UserSync.php
+++ b/acymailing/WpInit/UserSync.php
@@ -2,6 +2,8 @@
namespace AcyMailingWpInit;
+defined('ABSPATH') || die('Restricted Access');
+
use AcyMailingClassesUserClass;
use AcyMailingHelpersRegacyHelper;
@@ -9,10 +11,12 @@
{
public function __construct()
{
+ // Let the users opt in to the AcyMailing lists
add_action('register_form', [$this, 'addRegistrationFields']);
add_action('edit_user_profile', [$this, 'addProfileFields'], 10, 1);
add_action('show_user_profile', [$this, 'addProfileFields'], 10, 1);
+ // Hooks to create/update an Acy user when a WP user is created/updated
add_action('user_register', [$this, 'synchSaveUsers'], 10, 1);
add_action('profile_update', [$this, 'synchSaveUsers'], 10, 2);
add_action('delete_user', [$this, 'synchDeleteUsers']);
@@ -32,8 +36,28 @@
?>
<div class="acym__regacy">
- <label class="acym__regacy__label"><?php echo $regacyHelper->label; ?></label>
- <div class="acym__regacy__values"><?php echo $regacyHelper->listsHtml; ?></div>
+ <label class="acym__regacy__label"><?php echo esc_html($regacyHelper->label); ?></label>
+ <div class="acym__regacy__values">
+ <?php
+ echo wp_kses(
+ $regacyHelper->listsHtml,
+ [
+ 'table' => ['class' => [], 'style' => []],
+ 'tr' => ['style' => []],
+ 'td' => ['style' => []],
+ 'input' => [
+ 'type' => [],
+ 'name' => [],
+ 'id' => [],
+ 'value' => [],
+ 'class' => [],
+ 'checked' => [],
+ ],
+ 'label' => ['for' => [], 'class' => []],
+ ]
+ );
+ ?>
+ </div>
</div>
<?php
}
@@ -41,29 +65,32 @@
public function addProfileFields()
{
$config = acym_config();
- if (!$config->get('regacy', 0)) return;
+ if (!$config->get('regacy', 0)) {
+ return;
+ }
$regacyHelper = new RegacyHelper();
- if (!$regacyHelper->prepareLists([])) return;
+ if (!$regacyHelper->prepareLists([])) {
+ return;
+ }
?>
- <h2><?php echo acym_translation('ACYM_SUBSCRIPTION'); ?></h2>
+ <h2><?php echo esc_html(acym_translation('ACYM_SUBSCRIPTION')); ?></h2>
<table class="form-table">
<tbody>
<?php
foreach ($regacyHelper->lists as $listId => $oneList) {
- $checked = $oneList['checked'] ? 'checked="checked"' : '';
?>
<tr>
<th scope="row">
<label class="acym__regacy__lists__label" for="acym__regacy__lists-<?php echo intval($listId); ?>">
- <?php echo acym_escape($oneList['name']); ?>
+ <?php echo esc_html($oneList['name']); ?>
</label>
</th>
<td>
<input name="regacy_visible_lists_checked[]"
- type="checkbox"
- id="acym__regacy__lists-<?php echo intval($listId); ?>"
- value="<?php echo intval($listId); ?>" <?php echo $checked; ?>>
+ type="checkbox"
+ id="acym__regacy__lists-<?php echo intval($listId); ?>"
+ value="<?php echo intval($listId); ?>" <?php checked($oneList['checked']); ?>>
</td>
</tr>
<?php
@@ -71,7 +98,7 @@
?>
</tbody>
</table>
- <input type="hidden" value="<?php echo implode(',', array_keys($regacyHelper->lists)); ?>" name="regacy_visible_lists" />
+ <input type="hidden" value="<?php echo esc_attr(implode(',', array_keys($regacyHelper->lists))); ?>" name="regacy_visible_lists" />
<input type="hidden" value="WordPress user profile" name="acy_source" />
<?php
}
--- a/acymailing/back/Classes/ActionClass.php
+++ b/acymailing/back/Classes/ActionClass.php
@@ -37,9 +37,9 @@
public function getActionsByConditionId(int $id): array
{
$actions = acym_loadObjectList(
- 'SELECT action.*
- FROM #__acym_action as action
- WHERE action.condition_id = '.intval($id)
+ 'SELECT *
+ FROM #__acym_action
+ WHERE condition_id = '.intval($id)
);
array_map([$this, 'fixTypes'], $actions);
--- a/acymailing/back/Classes/AutomationClass.php
+++ b/acymailing/back/Classes/AutomationClass.php
@@ -77,7 +77,7 @@
continue;
}
- $element->$oneAttribute = is_array($value) ? json_encode($value) : strip_tags($value);
+ $element->$oneAttribute = is_array($value) ? json_encode($value) : acym_stripTags($value);
}
return parent::save($element);
@@ -96,6 +96,10 @@
return parent::delete($elements);
}
+ /**
+ * @param mixed $trigger The identifier of the trigger
+ * @param array $data An array with data for user-type triggers (user id, order, event...)
+ */
public function trigger($triggers, array $data = []): void
{
if (!acym_level(ACYM_ENTERPRISE) || empty($triggers)) {
@@ -117,10 +121,13 @@
$newData = $data;
$execute = false;
+ // If we reached the next execution time we execute
+ // next_execution is only set if one of the time triggers like "asap" or "day" is selected in the automation
if (!empty($step->next_execution) && $step->next_execution <= $newData['time']) {
$execute = true;
}
+ // Call the triggers to set the next execution time
acym_trigger('onAcymExecuteTrigger', [&$step, &$execute, &$newData]);
$newData['automation'] = $this->getOneById($step->automation_id);
@@ -130,7 +137,9 @@
$conditions = $conditionClass->getConditionsByStepId($step->id);
if (!empty($conditions)) {
foreach ($conditions as $condition) {
- if (!$this->verifyCondition($condition->conditions, $newData)) continue;
+ if (!$this->verifyCondition($condition->conditions, $newData)) {
+ continue;
+ }
$actions = $actionClass->getActionsByStepId($step->id);
if (empty($actions)) continue;
@@ -187,14 +196,17 @@
$query->where = $initialWhere;
}
+ //We do the or first
foreach ($action->filters as $or => $orValue) {
if (empty($orValue)) {
continue;
}
$num = 0;
$query->where = $initialWhere;
+ //Next the and
foreach ($orValue as $and => $andValue) {
$num++;
+ //Finally we have all names filter
foreach ($andValue as $filterName => $filterOptions) {
acym_trigger('onAcymProcessFilter_'.$filterName, [&$query, &$filterOptions, &$num]);
}
@@ -232,13 +244,15 @@
return $this->didAnAction;
}
- private function verifyCondition($conditions, array $data = []): bool
+ private function verifyCondition(array $conditions, array $data = []): bool
{
- if (empty($conditions)) return true;
+ if (empty($conditions)) {
+ return true;
+ }
+
$userTriggeringAction = empty($data['userId']) ? 0 : $data['userId'];
$usersTriggeringAction = empty($data['userIds']) ? [] : $data['userIds'];
- $conditions = json_decode($conditions, true);
$query = new AutomationHelper();
$initialWhere = ['1 = 1'];
if (!empty($conditions['type_condition']) && $conditions['type_condition'] == 'user') {
@@ -258,11 +272,14 @@
foreach ($conditions as $or => $orValue) {
if (empty($orValue)) continue;
+ // we increment id condition not validate
$conditionNotValid = 0;
$num = 0;
+ //Next the and
foreach ($orValue as $and => $andValue) {
$num++;
$query->where = $initialWhere;
+ //Finally we have all names condition
foreach ($andValue as $filterName => $filterOptions) {
acym_trigger('onAcymProcessCondition_'.$filterName, [&$query, &$filterOptions, &$num, &$conditionNotValid]);
}
--- a/acymailing/back/Classes/CampaignClass.php
+++ b/acymailing/back/Classes/CampaignClass.php
@@ -309,6 +309,7 @@
$element->click = number_format($element->click / $element->subscribers * 100, 2);
}
+ //Tracking sales
if (!acym_isTrackingSalesActive()) {
return;
}
@@ -395,7 +396,7 @@
$campaign->$oneAttribute = json_encode(empty($value) ? [] : $value);
} else {
if (empty($value)) continue;
- $campaign->$oneAttribute = strip_tags($value);
+ $campaign->$oneAttribute = acym_stripTags($value);
}
}
@@ -432,11 +433,11 @@
return false;
}
- if (acym_isAdmin()) {
+ if (acym_isAdmin() && acym_isAllowed('campaigns')) {
return true;
}
- $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
+ $query = 'SELECT COUNT(*) FROM #__acym_campaign AS campaign
JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
$condition = 'mail.creator_id = '.intval($userId);
@@ -454,6 +455,10 @@
return acym_loadResult($query) > 0;
}
+ /**
+ * Delete a campaign. Needs to delete the tag associated with the campaign and the mail attached to the campaign.
+ * Deleting a mail of a campaign needs to clean the association table mail_has_list and its tags
+ */
public function delete(array $elements): int
{
acym_arrayToInteger($elements);
@@ -557,6 +562,7 @@
public function send(int $campaignID, int $result = 0, bool $abTestFinal = false)
{
+ // Make sure the email we're trying to send exists
$campaign = $this->getOneById($campaignID);
if (empty($campaign->mail_id)) {
@@ -572,15 +578,18 @@
$filters = [0 => []];
acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[0], &$pluginIsExisting]);
} else {
+ // Adds the special campaigns conditions to the "OR" blocs of the segment
foreach ($filters as $key => $filter) {
acym_trigger('onAcymSendCampaignSpecial', [$campaign, &$filters[$key], &$pluginIsExisting]);
}
}
+ // This is a special campaign type, but the required plugin is not installed
if (!$pluginIsExisting) {
return false;
}
+ // Make sure some receivers have been selected
$lists = acym_loadResultArray('SELECT list_id FROM #__acym_mail_has_list WHERE mail_id = '.intval($campaign->mail_id));
if (empty($lists)) {
$this->errors[] = acym_translation('ACYM_NO_LIST_SELECTED');
@@ -639,6 +648,7 @@
$select = [intval($campaign->mail_id), 'ul.`user_id`', acym_escapeDB($date)];
}
+ // Resending a campaign only to users who didn't receive it
if (!empty($campaign->sending_params['resendTarget']) && 'new' === $campaign->sending_params['resendTarget']) {
if (acym_isMultilingual()) {
$automationHelper->leftjoin['us'] = '`#__acym_user_stat` AS `us` ON `us`.`user_id` = `user`.`id` AND `us`.`mail_id` IN (SELECT id FROM #__acym_mail WHERE parent_id = '.intval(
@@ -662,8 +672,9 @@
'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select1)
);
+ // If we have an odd number of users to send, we send one more to the first mail
$numberOfUsersToSend2 = $numberOfUsersToSend1 * 2 > $numberOfUsersToSend ? $numberOfUsersToSend1 - 1 : $numberOfUsersToSend1;
- $automationHelper->limit = $numberOfUsersToSend.', '.$numberOfUsersToSend2;
+ $automationHelper->limit = $numberOfUsersToSend1.', '.$numberOfUsersToSend2;
$select2 = [intval($campaign->sending_params['abtest']['B']), 'ul.`user_id`', acym_escapeDB($date)];
$numberUsersInsertedByMailId[intval($campaign->sending_params['abtest']['B'])] = acym_query(
'INSERT IGNORE INTO `#__acym_queue` (`mail_id`, `user_id`, `sending_date`) '.$automationHelper->getQuery($select2)
@@ -737,6 +748,7 @@
{
acym_arrayToInteger($mailIds);
+ //TODO move these methods in mail stat class
$query = 'SELECT SUM(sent) AS sent, SUM(open_unique) AS open_unique FROM #__acym_mail_stat
WHERE mail_id IN ('.implode(',', $mailIds).')';
@@ -825,28 +837,35 @@
public function getLastNewsletters(array &$params): array
{
+ // Init select elements
$querySelect = 'SELECT mail.*, campaign.sending_date ';
$queryCountSelect = 'SELECT COUNT(*) FROM (SELECT DISTINCT mail.id ';
+ // Form the query
$query = 'FROM #__acym_campaign AS campaign
JOIN #__acym_mail AS mail ON campaign.mail_id = mail.id ';
+ // We may need some joins depending on the selected options
if (isset($params['userId']) || isset($params['lists'])) {
$query .= 'JOIN #__acym_mail_has_list AS maillist ON mail.id = maillist.mail_id ';
if (isset($params['userId'])) $query .= 'JOIN #__acym_user_has_list AS userlist ON maillist.list_id = userlist.list_id ';
}
+ // Make sure we display only active campaigns
$where = 'WHERE campaign.active = 1 AND campaign.sent = 1 AND mail.type = '.acym_escapeDB(MailClass::TYPE_STANDARD).' AND campaign.visible = 1 ';
+ // If we want an archive of some specific lists
if (isset($params['lists'])) {
acym_arrayToInteger($params['lists']);
$where .= 'AND maillist.list_id IN ('.implode(', ', $params['lists']).') ';
}
+ // If we want an archive for a specific user
if (isset($params['userId']) && !empty($params['displayUserListOnly'])) {
$where .= 'AND userlist.user_id = '.intval($params['userId']).' ';
}
+ // If the user search for a newsletter
if (isset($params['search'])) {
$search = acym_escapeDB('%'.acym_utf8Encode($params['search']).'%');
$where .= 'AND (mail.subject LIKE '.$search.' OR mail.body LIKE '.$search.')';
@@ -856,9 +875,11 @@
$return = [];
$return['count'] = (int)acym_loadResult($queryCountSelect.$query.') AS r ');
+ // Make sure we display campaigns only once
$endQuerySelect = 'GROUP BY mail.id ';
$endQuerySelect .= 'ORDER BY campaign.sending_date DESC';
+ // Init the pagination
$page = $params['page'] ?? 0;
$numberPerPage = $params['numberPerPage'] ?? 0;
$lastNewsletters = $params['limit'] ?? 0;
@@ -945,6 +966,7 @@
$time = time();
foreach ($activeAutoCampaigns as $campaign) {
+ // Check the start date
$nextTrigger = $campaign->next_trigger;
if (empty($nextTrigger) && !empty($campaign->sending_params['start_date'])) {
$nextTrigger = $campaign->sending_params['start_date'];
@@ -954,6 +976,7 @@
continue;
}
+ //check if we trigger the campaign
$step = new stdClass();
$step->triggers = $campaign->sending_params;
$step->last_execution = $campaign->last_generated;
@@ -968,6 +991,7 @@
continue;
}
+ //update the campaign
$campaignMail = $mailClass->getOneById($campaign->mail_id);
$lastGenerated = $campaign->last_generated;
@@ -978,6 +1002,7 @@
continue;
}
+ //We generate the new campaign
$generatedCampaign = $this->generateCampaign($campaign, $campaignMail, $lastGenerated, $mailClass);
if (empty($generatedCampaign)) {
$this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_FAILED_GENERATING', $campaign->name);
@@ -986,6 +1011,7 @@
$this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_GENERATED', $campaign->name, $campaign->sending_params['number_generated']);
+ // We send it directly if no confirmation is needed
if (empty($campaign->sending_params['need_confirm_to_send'])) {
$this->send($generatedCampaign->id);
} elseif (!empty($campaign->sending_params['admin_notification_emails']) && !empty($adminNotificationEmail)) {
@@ -1002,10 +1028,12 @@
private function shouldGenerateCampaign(object $campaign, object $campaignMail): bool
{
+ // The generateByCategory function is the only one that can stop a campaign generation, with min number of items
$results = acym_trigger('generateByCategory', [&$campaignMail], null, function ($plugin) {
$plugin->generateCampaignResult->status = true;
});
+ // If one of the return statuses is "false", we won't generate the campaign
foreach ($results as $oneResult) {
if (isset($oneResult->status) && !$oneResult->status) {
$this->messages[] = acym_translationSprintf('ACYM_CAMPAIGN_NOT_GENERATED', $campaign->name, $oneResult->message);
@@ -1053,6 +1081,7 @@
$newCampaign->id = $this->save($newCampaign);
+ // Replace content in the generated mail. MUST be done after campaign has been saved
acym_trigger('replaceContent', [&$newMail, false]);
$mailClass->save($newMail);
--- a/acymailing/back/Classes/ConditionClass.php
+++ b/acymailing/back/Classes/ConditionClass.php
@@ -42,7 +42,7 @@
}
if ($oneAttribute !== 'conditions') {
- $element->$oneAttribute = is_array($value) ? json_encode($value) : strip_tags($value);
+ $element->$oneAttribute = is_array($value) ? json_encode($value) : acym_stripTags($value);
}
}
@@ -51,8 +51,16 @@
public function getConditionsByStepId(int $id): array
{
- $query = 'SELECT acycondition.* FROM #__acym_condition as acycondition LEFT JOIN #__acym_step AS step ON step.id = acycondition.step_id WHERE step.id = '.intval($id);
+ $conditions = acym_loadObjectList(
+ 'SELECT *
+ FROM #__acym_condition
+ WHERE `step_id` = '.intval($id)
+ );
- return acym_loadObjectList($query);
+ foreach ($conditions as $condition) {
+ $condition->conditions = empty($condition->conditions) ? [] : json_decode($condition->conditions, true);
+ }
+
+ return $conditions;
}
}
--- a/acymailing/back/Classes/ConfigurationClass.php
+++ b/acymailing/back/Classes/ConfigurationClass.php
@@ -31,6 +31,12 @@
return $default;
}
+ /**
+ * @param object|array $element
+ *
+ * @Deprecated 10.6.0 No longer used because of type mismatch.
+ * @See ConfigurationClass::saveConfig() as a replacement.
+ */
public function save($element): ?int
{
$this->saveConfig((array)$element);
@@ -46,6 +52,7 @@
$previousCronSecurityKey = $this->get('cron_key');
$params = [];
foreach ($newConfig as $name => $value) {
+ //If it's a password containing only * then we just consider the user saved again the config but there is no modification on the password
if (!empty($value)) {
if (strpos($name, 'password') !== false && trim($value, '*') === '') {
continue;
@@ -67,13 +74,15 @@
$value = implode(',', $value);
}
+ //We update the current instance in the same time
if (empty($this->values[$name])) {
$this->values[$name] = new stdClass();
}
$this->values[$name]->value = $value;
+ // We do a strip tags to avoid HTML injections
if ($escape && !is_null($value)) {
- $params[] = '('.acym_escapeDB(strip_tags($name)).','.acym_escapeDB(strip_tags($value)).')';
+ $params[] = '('.acym_escapeDB(acym_stripTags($name)).','.acym_escapeDB(acym_stripTags($value)).')';
} else {
$params[] = '('.acym_escapeDB($name).','.acym_escapeDB($value).')';
}
@@ -82,6 +91,7 @@
$activeCron = $this->get('active_cron', 0);
$newCronSecurity = $this->get('cron_security', 0);
$newCronSecurityKey = $this->get('cron_key');
+ // Handle cron key activation or modification while automated tasks are active
if (!empty($activeCron) && !empty($newCronSecurity) && (empty($previousCronSecurity) || $previousCronSecurityKey !== $newCronSecurityKey)) {
$configurationController = new ConfigurationController();
$deactivationResult = $configurationController->modifyCron('deactivateCron');
@@ -95,13 +105,14 @@
}
try {
+ // We do a replace so that values are always kept up to date and added if necessary in the mean time
$status = acym_query('REPLACE INTO #__acym_configuration (`name`, `value`) VALUES '.implode(',', $params));
} catch (Exception $e) {
$status = false;
}
if ($status === false) {
- acym_display(isset($e) ? $e->getMessage() : substr(strip_tags(acym_getDBError()), 0, 200).'...', 'error');
+ acym_display(isset($e) ? $e->getMessage() : substr(acym_stripTags(acym_getDBError()), 0, 200).'...', 'error');
}
$newFollowupPriority = $this->get('followup_max_priority', 0) == 1;
--- a/acymailing/back/Classes/FieldClass.php
+++ b/acymailing/back/Classes/FieldClass.php
@@ -3,6 +3,7 @@
namespace AcyMailingClasses;
use AcyMailingCoreAcymClass;
+use AcyMailingHelpersSecurityHelper;
class FieldClass extends AcymClass
{
@@ -142,7 +143,8 @@
public function store(int $userID, array $fields, bool $ajax = false): void
{
- if (!empty($_FILES['customField'])) {
+ $customField = acym_getVar('array', 'customField', [], 'FILES');
+ if (!empty($customField)) {
$uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
$uploadPath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS);
if (!file_exists($uploadPath)) {
@@ -150,11 +152,12 @@
}
$allowedExtensions = explode(',', $this->config->get('allowed_files'));
- foreach ($_FILES['customField']['tmp_name'] as $key => $value) {
+ foreach ($customField['tmp_name'] as $key => $value) {
if (is_array($value) && isset($value[0])) $value = $value[0];
if (empty($value)) continue;
- $fileName = $_FILES['customField']['name'][$key];
+ // Get the uploaded file name
+ $fileName = $customField['name'][$key];
while (is_array($fileName) && isset($fileName[0])) {
$fileName = $fileName[0];
}
@@ -164,14 +167,14 @@
if ($ajax) {
$this->errors[] = acym_translationSprintf(
'ACYM_ACCEPTED_TYPE',
- acym_escape($ext),
+ esc_html($ext),
implode(', ', $allowedExtensions)
);
} else {
acym_enqueueMessage(
acym_translationSprintf(
'ACYM_ACCEPTED_TYPE',
- acym_escape($ext),
+ esc_html($ext),
implode(', ', $allowedExtensions)
),
'error',
@@ -191,7 +194,7 @@
continue;
}
- $fields[$key] = $_FILES['customField']['name'][$key];
+ $fields[$key] = $customField['name'][$key];
}
}
@@ -246,6 +249,7 @@
$value = substr($value, 0, $fieldOptions->max_characters);
}
+ // If deleting a file field, also delete the physical file
if ($field->type === 'file' && strlen($value) === 0) {
$oldValue = acym_loadResult(
'SELECT `value` FROM #__acym_user_has_field WHERE `user_id` = '.intval($userID).' AND `field_id` = '.intval($id)
@@ -256,14 +260,16 @@
if (empty($fileName)) $fileName = $oldValue;
$uploadFolder = trim(acym_cleanPath(html_entity_decode(acym_getFilesFolder(true))), DS.' ').DS;
+ // Try new path (with user folder) first, then legacy path (without user folder)
$filePath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$userID.DS.$fileName);
if (!file_exists($filePath)) {
$filePath = acym_cleanPath(ACYM_ROOT.$uploadFolder.'userfiles'.DS.$fileName);
}
- acym_deleteFile($filePath, false);
+ acym_deleteFile($filePath);
}
}
+ // The user removed a value, don't add an empty line and remove any previous value from the bd
if (strlen($value) === 0) {
$query = 'DELETE FROM `#__acym_user_has_field`
WHERE `user_id` = '.intval($userID).'
@@ -354,6 +360,7 @@
});
if (empty($defaultValues)) {
+ // Options from database
if (!empty($one->field_value)) {
$fieldOptions = json_decode($one->option, true);
if (!empty($fieldOptions['fieldDB'])) {
@@ -367,6 +3