Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/fluent-crm/app/ComposerScript.php
+++ b/fluent-crm/app/ComposerScript.php
@@ -0,0 +1,125 @@
+<?php
+// phpcs:disable
+namespace FluentCrmApp;
+
+use ComposerScriptEvent;
+use InvalidArgumentException;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
+
+class ComposerScript
+{
+ public static function postInstall(Event $event)
+ {
+ static::postUpdate($event);
+ }
+
+ public static function postUpdate(Event $event)
+ {
+ $vendorDir = $event->getComposer()->getConfig()->get('vendor-dir');
+ $composerJson = json_decode(file_get_contents($vendorDir . '/../composer.json'), true);
+ $namespace = $composerJson['extra']['wpfluent']['namespace']['current'];
+
+ if (!$namespace) {
+ throw new InvalidArgumentException("Namespace not set in composer.json file.");
+ }
+
+ $itr = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
+ $vendorDir.'/wpfluent/framework/src/', RecursiveDirectoryIterator::SKIP_DOTS
+ ), RecursiveIteratorIterator::SELF_FIRST);
+
+ foreach ($itr as $file) {
+ if ($file->isDir()) {
+ continue;
+ }
+
+ $fileName = $file->getPathname();
+
+ $content = file_get_contents($fileName);
+ $content = str_replace(
+ ['WPFluent\', 'WPFluentPackage\'],
+ [$namespace . '\Framework\', $namespace . '\'],
+ $content
+ );
+
+ file_put_contents($fileName, $content);
+ }
+
+ static::updateVendorComposerFiles($vendorDir, $namespace);
+ }
+
+ protected static function updateVendorComposerFiles($vendorDir, $namespace)
+ {
+ $composerInstalledJson = json_decode(file_get_contents(
+ $installedJsonFile = $vendorDir . '/composer/installed.json'
+ ), true);
+
+ foreach ($composerInstalledJson['packages'] as &$package) {
+ if ($package['name'] == 'wpfluent/framework') {
+ $package['autoload']['psr-4'] = [
+ $namespace . "\Framework\" => "src/WPFluent"
+ ];
+ } else {
+ $packageDir = $vendorDir . "/{$package['name']}/src/";
+
+ if(!is_dir($packageDir)) {
+ continue;
+ }
+
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator(
+ $packageDir, RecursiveDirectoryIterator::SKIP_DOTS
+ ),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $item) {
+
+ if ($item->isDir()) {
+ continue;
+ }
+
+ $fileName = $item->getPathname();
+ $content = file_get_contents($fileName);
+ $content = str_replace(
+ ['WPFluent\', 'WPFluentPackage\'],
+ [$namespace . '\Framework\', $namespace . '\'],
+ $content
+ );
+
+ file_put_contents($fileName, $content);
+ }
+
+ $psr4 = array_keys($package['autoload']['psr-4']);
+
+ $replaced = str_replace(
+ 'WPFluentPackage', $namespace, $psr4[0]
+ );
+
+ $package['autoload']['psr-4'] = [
+ $replaced => "src/"
+ ];
+
+ $packageComposerJson = json_decode(file_get_contents(
+ $vendorDir .'/' . $package['name'] . '/composer.json'
+ ), true);
+
+ $packageComposerJson['autoload']['psr-4'] = [
+ $replaced => "src/"
+ ];
+
+ file_put_contents(
+ $vendorDir .'/' . $package['name'] . '/composer.json',
+ json_encode($packageComposerJson, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
+ );
+ }
+ }
+
+ file_put_contents(
+ $installedJsonFile,
+ json_encode($composerInstalledJson, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)
+ );
+
+ exec('composer dump-autoload');
+ }
+}
--- a/fluent-crm/app/Functions/helpers.php
+++ b/fluent-crm/app/Functions/helpers.php
@@ -64,14 +64,16 @@
if (!function_exists('fluentCrmMix')) {
/**
* Generate URL for static assets for plugin's assets directory
+ * Now uses Vite helper for HMR support in dev mode
*
* @param string $path
+ * @param string $manifestDirectory (deprecated, kept for compatibility)
*
* @return string
*/
function fluentCrmMix($path, $manifestDirectory = '')
{
- return FluentCrm('url.assets') . ltrim($path, '/');
+ return FluentCrmAppVite::getEnqueuePath(ltrim($path, '/'));
}
}
@@ -342,6 +344,29 @@
return fluentcrm_delete_meta($campaignId, 'FluentCrmAppModelsCampaign', $key);
}
+function fluentcrm_get_sms_campaign_meta($campaignId, $key, $returnValue = false)
+{
+ $item = fluentcrm_get_meta($campaignId, 'FluentCampaignAppModulesSMSModelsSMSCampaign', $key);
+ if ($returnValue) {
+ if ($item) {
+ return $item->value;
+ }
+ return false;
+ }
+
+ return $item;
+}
+
+function fluentcrm_update_sms_campaign_meta($campaignId, $key, $value)
+{
+ return fluentcrm_update_meta($campaignId, 'FluentCampaignAppModulesSMSModelsSMSCampaign', $key, $value);
+}
+
+function fluentcrm_delete_sms_campaign_meta($campaignId, $key = '')
+{
+ return fluentcrm_delete_meta($campaignId, 'FluentCampaignAppModulesSMSModelsSMSCampaign', $key);
+}
+
/**
* Get Email Campaign meta value
* @param int $templateId ID of the template
@@ -526,6 +551,41 @@
}
+function fluentcrm_subscriber_sms_statuses($isOptions = false)
+{
+ $core_statuses = [
+ 'sms_subscribed',
+ 'sms_pending',
+ 'sms_unsubscribed',
+ 'sms_bounced'
+ ];
+
+ $statuses = apply_filters('fluent_crm/contact_sms_statuses', $core_statuses);
+
+ if (!$isOptions) {
+ return $statuses;
+ }
+
+ $formattedStatues = [];
+ $transMaps = [
+ 'sms_subscribed' => __('SMS Subscribed', 'fluent-crm'),
+ 'sms_pending' => __('SMS Pending', 'fluent-crm'),
+ 'sms_unsubscribed' => __('SMS Unsubscribed', 'fluent-crm'),
+ 'sms_bounced' => __('SMS Bounced', 'fluent-crm')
+ ];
+
+ foreach ($statuses as $status) {
+ $title = isset($transMaps[$status]) ? $transMaps[$status] : ucwords(str_replace('_', ' ', $status));
+ $formattedStatues[] = [
+ 'id' => $status,
+ 'slug' => $status,
+ 'title' => $title
+ ];
+ }
+
+ return $formattedStatues;
+}
+
/**
* Get all subscriber editable status options.
*
@@ -628,21 +688,21 @@
* @param array $types {
* An associative array of contact activity types.
*
- * @type string $note Note activity type.
- * @type string $call Call activity type.
- * @type string $email Email activity type.
- * @type string $meeting Meeting activity type.
- * @type string $quote_sent Quote sent activity type.
- * @type string $quote_accepted Quote accepted activity type.
- * @type string $quote_refused Quote refused activity type.
- * @type string $invoice_sent Invoice sent activity type.
- * @type string $invoice_part_paid Invoice part paid activity type.
- * @type string $invoice_paid Invoice paid activity type.
- * @type string $invoice_refunded Invoice refunded activity type.
- * @type string $transaction Transaction activity type.
- * @type string $feedback Feedback activity type.
- * @type string $tweet Tweet activity type.
- * @type string $facebook_post Facebook post activity type.
+ * @type string $note Note activity type.
+ * @type string $call Call activity type.
+ * @type string $email Email activity type.
+ * @type string $meeting Meeting activity type.
+ * @type string $quote_sent Quote sent activity type.
+ * @type string $quote_accepted Quote accepted activity type.
+ * @type string $quote_refused Quote refused activity type.
+ * @type string $invoice_sent Invoice sent activity type.
+ * @type string $invoice_part_paid Invoice part paid activity type.
+ * @type string $invoice_paid Invoice paid activity type.
+ * @type string $invoice_refunded Invoice refunded activity type.
+ * @type string $transaction Transaction activity type.
+ * @type string $feedback Feedback activity type.
+ * @type string $tweet Tweet activity type.
+ * @type string $facebook_post Facebook post activity type.
* }
*/
$types = apply_filters('fluent_crm/contact_activity_types', [
@@ -767,7 +827,7 @@
$complianceSettings = FluentCrmAppServicesHelper::getComplianceSettings();
$gravatarEnabled = $complianceSettings['enable_gravatar'] == 'yes' ? true : false;
- $fallbackEnabled = $complianceSettings['gravatar_fallback'] == 'yes' ? true : false;
+ $fallbackEnabled = $complianceSettings['gravatar_fallback'] == 'yes' ? true : false;
if (!$gravatarEnabled) {
return apply_filters('fluent_crm/default_avatar', FLUENTCRM_PLUGIN_URL . 'assets/images/avatar.png', $email);
@@ -797,6 +857,36 @@
}
/**
+ * Get avatar HTML for admin-facing UI and guarantee a safe placeholder.
+ *
+ * WordPress get_avatar() can return false when avatars are disabled, which
+ * breaks consumers that expect markup. In that case we fall back to the
+ * plugin's avatar URL helper and return a simple image tag.
+ *
+ * @param string $email
+ * @param string $name
+ * @param int $size
+ * @return string
+ */
+function fluentcrmGetAvatarHtml($email, $name = '', $size = 128)
+{
+ $avatarHtml = get_avatar($email, $size);
+
+ if ($avatarHtml) {
+ return $avatarHtml;
+ }
+
+ $avatarUrl = fluentcrmGravatar($email, $name);
+
+ return sprintf(
+ '<img alt="%1$s" src="%2$s" class="avatar avatar-%3$d photo" height="%3$d" width="%3$d" />',
+ esc_attr($name),
+ esc_url($avatarUrl),
+ absint($size)
+ );
+}
+
+/**
* get FluentCRM's Global Settings
* @param string $key key of the setting
* @param mixed $default default value
@@ -829,10 +919,18 @@
/**
* get if click tracking is enabled or disabled
- * @return bool
+ * @return bool|string
*/
function fluentcrmTrackClicking()
{
+
+ static $tracking = null;
+
+ if ($tracking !== null) {
+ return $tracking;
+ }
+
+
/**
* Determine if click tracking is enabled for FluentCRM Emails.
*
@@ -840,7 +938,85 @@
*
* @return bool True if click tracking is enabled, false otherwise.
*/
- return apply_filters('fluent_crm/track_click', true);
+ $trackClick = apply_filters('fluent_crm/track_click', true);
+
+ if (!$trackClick) {
+ $tracking = false;
+ return false; // disabled by filter
+ }
+
+ $complianceSettings = FluentCrmAppServicesHelper::getComplianceSettings();
+
+ $value = $complianceSettings['email_click_tracking'] ?? 'yes';
+
+ if ($value === 'yes') {
+ $tracking = true;
+ return true;
+ }
+
+ if ($value === 'no') {
+ $tracking = false;
+ return false;
+ }
+
+ if ($value === 'anonymous') {
+ $tracking = 'anonymous';
+ return 'anonymous';
+ }
+
+ return true; // default enabled
+}
+
+
+/**
+ * get if open tracking is enabled or disabled or anonymous
+ * @return bool|string
+ */
+function fluentcrmTrackEmailOpen()
+{
+
+ static $tracking = null;
+
+ if ($tracking !== null) {
+ return $tracking;
+ }
+
+
+ /**
+ * Determine if open tracking is enabled for FluentCRM Emails.
+ *
+ * This filter allows you to enable or disable open tracking in FluentCRM.
+ *
+ * @return bool True if open tracking is disable, true otherwise.
+ */
+ $disableTracking = apply_filters('fluentcrm_disable_email_open_tracking', false);
+
+ if ($disableTracking) {
+ $tracking = false;
+ return false; // disabled by filter
+ }
+
+ $complianceSettings = FluentCrmAppServicesHelper::getComplianceSettings();
+
+ $value = $complianceSettings['email_open_tracking'] ?? 'yes';
+
+ if ($value === 'yes') {
+ $tracking = true;
+ return $tracking;
+ }
+
+ if ($value === 'no') {
+ $tracking = false;
+ return $tracking;
+ }
+
+ if ($value === 'anonymous') {
+ $tracking = 'anonymous';
+ return $tracking;
+ }
+
+ $tracking = true; // default enabled
+ return $tracking;
}
@@ -1036,7 +1212,25 @@
* @param string The base URL for the FluentCRM admin menu.
*/
$url = apply_filters('fluent_crm/menu_url_base', admin_url('admin.php?page=fluentcrm-admin#/'));
- if($ext) {
+ if ($ext) {
+ $url .= $ext;
+ }
+
+ return $url;
+}
+
+function fluent_crm_menu_url_base_new($ext = '')
+{
+ /**
+ * Define the base URL for the FluentCRM admin menu.
+ *
+ * This filter allows customization of the base URL used in the FluentCRM admin menu.
+ * By default, it points to the FluentCRM admin page within the WordPress admin dashboard.
+ *
+ * @param string The base URL for the FluentCRM admin menu.
+ */
+ $url = apply_filters('fluent_crm/new_menu_url_base', admin_url('admin.php?page=fluent-crm-v3#/'));
+ if ($ext) {
$url .= $ext;
}
@@ -1085,8 +1279,8 @@
*
* This filter allows modification of the lifetime value of a contact profile.
*
- * @param int $lifeTimeValue The initial lifetime value, default is 0.
- * @param array $profile The contact profile data.
+ * @param int $lifeTimeValue The initial lifetime value, default is 0.
+ * @param array $profile The contact profile data.
*
* @return int The modified lifetime value.
*/
@@ -1339,9 +1533,12 @@
}
/**
- * Get FluentCRM Query Builder instance
+ * Get FluentCRM Database Connection instance.
*
- * @return FluentCrmFrameworkDatabaseQueryWPDBConnection
+ * Note: ->table()->get() returns FluentCrmFrameworkSupportCollection (not array).
+ * Use ->isEmpty() instead of !$result to check for empty results.
+ *
+ * @return FluentCrmFrameworkDatabaseQueryWPDBConnection
*/
function fluentCrmDb()
{
@@ -1350,7 +1547,8 @@
function fluentCrmIsMemoryExceeded($percent = 75)
{
- $memory_limit = fluentCrmGetMemoryLimit() * ($percent / 100);
+ $memoryLimit = fluentCrmGetMemoryLimit();
+ $memory_limit = $memoryLimit * ($percent / 100);
$current_memory = memory_get_usage(true);
return $current_memory >= $memory_limit;
@@ -1371,7 +1569,7 @@
$memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce
}
- if (!$memory_limit || -1 === $memory_limit || '-1' === $memory_limit) {
+ if (!$memory_limit || -1 === $memory_limit || '-1' === $memory_limit || '-1M' === $memory_limit) {
// Unlimited, set to 12GB.
$memory_limit = '12G';
}
@@ -1394,7 +1592,7 @@
}
if ($limit < 104857600) {
- return 104857600;
+ return 104857600 * 2;
}
return $limit;
@@ -1433,7 +1631,7 @@
function fluentCrmGetContactSecureHash($contactId)
{
if (!$contactId) {
- return false;
+ return '';
}
$exist = SubscriberMeta::where('subscriber_id', $contactId)
@@ -1461,20 +1659,27 @@
function fluentCrmGetContactManagedHash($contactId)
{
+ static $cache = [];
+
+ if (isset($cache[$contactId])) {
+ return $cache[$contactId];
+ }
+
$exist = SubscriberMeta::where('subscriber_id', $contactId)
->where('key', '_secure_managed_hash')
->first();
if ($exist) {
- $cutOutTime = time() - 60 * 60 * 24 * 30;
- if (time() - strtotime($exist->updated_at) > $cutOutTime) {
+ if (time() - strtotime($exist->updated_at) > 60 * 60 * 24 * 30) {
$hash = md5(wp_generate_uuid4() . '_' . $contactId . '_' . '_' . time()) . '__' . $contactId;
$exist->value = $hash;
- $exist->updated_at = gmdate('Y-m-d H:i:s');
+ $exist->updated_at = current_time('mysql');
$exist->save();
+ $cache[$contactId] = $hash;
return $hash;
}
+ $cache[$contactId] = $exist->value;
return $exist->value;
}
@@ -1488,6 +1693,7 @@
'value' => $hash
]);
+ $cache[$contactId] = $hash;
return $hash;
}
@@ -1561,6 +1767,11 @@
return ['campaign', 'recurring_mail'];
}
+function fluentCrmAutoProcessSmsCampaignTypes()
+{
+ return ['campaign'];
+}
+
function fluentCrmRunTimeCache($key, $value = NULL)
{
static $items = [];
--- a/fluent-crm/app/Hooks/CLI/Commands.php
+++ b/fluent-crm/app/Hooks/CLI/Commands.php
@@ -195,7 +195,7 @@
$offset += $limit;
- if (!$customers) {
+ if ($customers->isEmpty()) {
$processingStatus = false;
} else {
foreach ($customers as $customer) {
@@ -415,7 +415,7 @@
$offset += 10;
- if (!$customers) {
+ if ($customers->isEmpty()) {
$processingStatus = false;
} else {
foreach ($customers as $customer) {
@@ -634,7 +634,7 @@
$offset += 10;
- if (!$students) {
+ if ($students->isEmpty()) {
$processingStatus = false;
} else {
foreach ($students as $student) {
@@ -869,7 +869,7 @@
return;
}
- if (!$licenses) {
+ if ($licenses->isEmpty()) {
WP_CLI::line('No users found');
return;
}
@@ -934,7 +934,7 @@
return;
}
- if (!$licenses) {
+ if ($licenses->isEmpty()) {
WP_CLI::line('No users found');
return;
}
@@ -1101,4 +1101,9 @@
WP_CLI::line('User ids for contacts has been synced');
}
+ public function simulate_funnel($args, $assoc_args)
+ {
+ (new SimulateFunnelCommand())->handle($args, $assoc_args);
+ }
+
}
--- a/fluent-crm/app/Hooks/CLI/SimulateFunnelCommand.php
+++ b/fluent-crm/app/Hooks/CLI/SimulateFunnelCommand.php
@@ -0,0 +1,481 @@
+<?php
+
+namespace FluentCrmAppHooksCLI;
+
+use FluentCrmAppModelsFunnel;
+use FluentCrmAppModelsFunnelMetric;
+use FluentCrmAppModelsFunnelSequence;
+use FluentCrmAppModelsFunnelSubscriber;
+use FluentCrmAppModelsSubscriber;
+use FluentCrmAppServicesFunnelFunnelProcessor;
+
+class SimulateFunnelCommand
+{
+ /*
+ * Fast-forward a subscriber through an automation funnel, skipping wait times.
+ * Real actions will fire (tags applied, emails sent, etc.) — only delays are shortened.
+ *
+ * Usage:
+ * wp fluent_crm simulate_funnel --funnel_id=123 --email=john@example.com
+ * wp fluent_crm simulate_funnel --funnel_id=123 --subscriber_id=456
+ * wp fluent_crm simulate_funnel --funnel_id=123 --email=john@example.com --sleep=1 --max_steps=50
+ * wp fluent_crm simulate_funnel --funnel_id=123 --email=john@example.com --sleep=0
+ *
+ * --sleep=0 runs one step at a time (step mode). Run the command again to advance to the next step.
+ */
+ public function handle($args, $assoc_args)
+ {
+ $funnelId = WP_CLIUtilsget_flag_value($assoc_args, 'funnel_id');
+ $subscriberId = WP_CLIUtilsget_flag_value($assoc_args, 'subscriber_id');
+ $email = WP_CLIUtilsget_flag_value($assoc_args, 'email');
+ $sleepSeconds = intval(WP_CLIUtilsget_flag_value($assoc_args, 'sleep', 2));
+ $maxSteps = max(1, intval(WP_CLIUtilsget_flag_value($assoc_args, 'max_steps', 100)));
+ $stepMode = $sleepSeconds === 0;
+
+ if ($sleepSeconds < 0) {
+ $sleepSeconds = 0;
+ }
+
+ if (!$funnelId) {
+ WP_CLI::error('--funnel_id is required');
+ }
+
+ $funnel = Funnel::find(intval($funnelId));
+ if (!$funnel) {
+ WP_CLI::error('Funnel not found');
+ }
+
+ if ($subscriberId) {
+ $subscriber = Subscriber::find(intval($subscriberId));
+ } elseif ($email) {
+ $subscriber = Subscriber::where('email', sanitize_email($email))->first();
+ } else {
+ WP_CLI::error('--subscriber_id or --email is required');
+ return;
+ }
+
+ if (!$subscriber) {
+ WP_CLI::error('Subscriber not found');
+ }
+
+ WP_CLI::line('---');
+ WP_CLI::line(sprintf('Funnel: %s (#%d) - Status: %s', $funnel->title, $funnel->id, $funnel->status));
+ WP_CLI::line(sprintf('Subscriber: %s (#%d) - Status: %s', $subscriber->email, $subscriber->id, $subscriber->status));
+ if ($stepMode) {
+ WP_CLI::line('Mode: step-by-step (--sleep=0)');
+ } else {
+ WP_CLI::line(sprintf('Wait times will be reduced to %d second(s)', $sleepSeconds));
+ }
+ WP_CLI::line('---');
+
+ // Print funnel step map
+ $this->printFunnelSteps($funnel->id);
+
+ if ($funnel->status !== 'published') {
+ WP_CLI::warning('This funnel is not published. Proceeding anyway...');
+ }
+
+ // Check existing enrollment
+ $funnelSub = FunnelSubscriber::where('funnel_id', $funnel->id)
+ ->where('subscriber_id', $subscriber->id)
+ ->first();
+
+ if ($funnelSub) {
+ if (in_array($funnelSub->status, ['completed', 'cancelled'])) {
+ WP_CLI::line(sprintf('Subscriber already %s this funnel.', $funnelSub->status));
+ WP_CLI::confirm('Re-enroll and start fresh?');
+ $this->resetFunnelEnrollment($funnel->id, $subscriber->id, $funnelSub->id);
+ $funnelSub = null;
+ } elseif (in_array($funnelSub->status, ['active', 'waiting'])) {
+ $nextSeq = $funnelSub->next_sequence_id ? FunnelSequence::find($funnelSub->next_sequence_id) : null;
+ $nextLabel = $nextSeq ? ($nextSeq->title ?: $nextSeq->action_name) : 'unknown';
+ WP_CLI::line(sprintf('Subscriber is already in this funnel (status: %s, next: %s).', $funnelSub->status, $nextLabel));
+
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
+ fwrite(STDOUT, 'Resume or Restart? (resume/restart): ');
+ $choice = strtolower(trim(fgets(STDIN)));
+
+ if ($choice === 'restart') {
+ $this->resetFunnelEnrollment($funnel->id, $subscriber->id, $funnelSub->id);
+ $funnelSub = null;
+ WP_CLI::line('Restarting from the beginning...');
+ } else {
+ WP_CLI::line('Resuming from current position...');
+ }
+ } else {
+ WP_CLI::line(sprintf('Current enrollment status: %s', $funnelSub->status));
+ }
+ }
+
+ // In auto-advance mode, minimize wait times so we don't actually wait days
+ if (!$stepMode) {
+ $filterDelay = max(1, $sleepSeconds);
+ add_filter('fluent_crm/funnel_seq_delay_in_seconds', function () use ($filterDelay) {
+ return $filterDelay;
+ }, 99999, 4);
+ }
+
+ $processor = new FunnelProcessor();
+
+ $firedHooks = [];
+
+ // Enroll if not already
+ if (!$funnelSub) {
+ WP_CLI::line('Enrolling subscriber into funnel...');
+
+ $hooksBefore = $this->snapshotHooks();
+ $processor->startSequences($subscriber, $funnel);
+ $firedHooks = array_merge($firedHooks, $this->diffHooks($hooksBefore));
+
+ $funnelSub = FunnelSubscriber::where('funnel_id', $funnel->id)
+ ->where('subscriber_id', $subscriber->id)
+ ->first();
+
+ if (!$funnelSub) {
+ WP_CLI::error('Failed to enroll — funnel may have no sequences');
+ }
+
+ $this->showExecutedMetrics($funnel->id, $subscriber->id, 'Enrollment');
+
+ // In step mode, stop after enrollment — next run will resume
+ if ($stepMode) {
+ $this->showStepModeNextUp($funnelSub);
+ $this->askAndShowFiredHooks($firedHooks);
+ $this->showFinalStatus($funnel->id, $subscriber->id, $funnelSub->id);
+ return;
+ }
+ }
+
+ // In step mode, process exactly one batch then stop
+ if ($stepMode) {
+ $lastMetricId = (int) FunnelMetric::where('funnel_id', $funnel->id)
+ ->where('subscriber_id', $subscriber->id)
+ ->max('id');
+
+ $hooksBefore = $this->snapshotHooks();
+ $this->processOneStep($processor, $funnelSub);
+ $firedHooks = array_merge($firedHooks, $this->diffHooks($hooksBefore));
+
+ // Show what was processed in this step
+ $newMetrics = FunnelMetric::where('funnel_id', $funnel->id)
+ ->where('subscriber_id', $subscriber->id)
+ ->where('id', '>', $lastMetricId)
+ ->orderBy('id', 'ASC')
+ ->get();
+
+ if ($newMetrics->count()) {
+ WP_CLI::line(sprintf('Processed %d action(s):', $newMetrics->count()));
+ foreach ($newMetrics as $metric) {
+ $seq = FunnelSequence::find($metric->sequence_id);
+ if ($seq) {
+ WP_CLI::line(sprintf(' > [%s] %s', $seq->action_name, $seq->title ?: ''));
+ }
+ }
+ }
+
+ $funnelSub = FunnelSubscriber::find($funnelSub->id);
+ $this->showStepModeNextUp($funnelSub);
+ $this->askAndShowFiredHooks($firedHooks);
+ $this->showFinalStatus($funnel->id, $subscriber->id, $funnelSub->id);
+ return;
+ }
+
+ // Fast-forward remaining steps
+ $step = 0;
+
+ while ($step < $maxSteps) {
+ $shouldBreak = $this->checkTerminalStatus($funnelSub);
+ if ($shouldBreak) {
+ break;
+ }
+
+ $step++;
+
+ // Show what's about to execute
+ $nextSeq = $funnelSub->next_sequence_id ? FunnelSequence::find($funnelSub->next_sequence_id) : null;
+ if ($nextSeq) {
+ WP_CLI::line(sprintf('[Step %d] %s: %s', $step, $nextSeq->action_name, $nextSeq->title ?: ''));
+ }
+
+ // Force execution time to now
+ FunnelSubscriber::where('id', $funnelSub->id)->update([
+ 'next_execution_time' => current_time('mysql'),
+ ]);
+ $funnelSub->next_execution_time = current_time('mysql');
+
+ // Process the next step
+ $hooksBefore = $this->snapshotHooks();
+ $processor->processFunnelAction($funnelSub);
+ $firedHooks = array_merge($firedHooks, $this->diffHooks($hooksBefore));
+
+ sleep($sleepSeconds);
+
+ // Reload for next iteration
+ $funnelSub = FunnelSubscriber::find($funnelSub->id);
+ }
+
+ if ($step >= $maxSteps) {
+ WP_CLI::warning(sprintf('Reached max steps limit (%d). Use --max_steps to increase.', $maxSteps));
+ }
+
+ $this->askAndShowFiredHooks($firedHooks);
+ $this->showFinalStatus($funnel->id, $subscriber->id, $funnelSub ? $funnelSub->id : null);
+ }
+
+ private function resetFunnelEnrollment($funnelId, $subscriberId, $funnelSubId)
+ {
+ FunnelMetric::where('funnel_id', $funnelId)
+ ->where('subscriber_id', $subscriberId)
+ ->delete();
+ FunnelSubscriber::where('id', $funnelSubId)->delete();
+ }
+
+ private function showExecutedMetrics($funnelId, $subscriberId, $label)
+ {
+ $metrics = FunnelMetric::where('funnel_id', $funnelId)
+ ->where('subscriber_id', $subscriberId)
+ ->orderBy('id', 'ASC')
+ ->get();
+
+ if ($metrics->count()) {
+ WP_CLI::line(sprintf('%s processed %d step(s):', $label, $metrics->count()));
+ foreach ($metrics as $metric) {
+ $seq = FunnelSequence::find($metric->sequence_id);
+ if ($seq) {
+ WP_CLI::line(sprintf(' > [%s] %s', $seq->action_name, $seq->title ?: ''));
+ }
+ }
+ }
+ }
+
+ private function processOneStep($processor, $funnelSub)
+ {
+ $funnelSub = FunnelSubscriber::find($funnelSub->id);
+
+ $shouldBreak = $this->checkTerminalStatus($funnelSub);
+ if ($shouldBreak) {
+ return;
+ }
+
+ // Force execution time to now so processFunnelAction picks it up
+ FunnelSubscriber::where('id', $funnelSub->id)->update([
+ 'next_execution_time' => current_time('mysql'),
+ ]);
+ $funnelSub->next_execution_time = current_time('mysql');
+
+ // Use the real processor — SequencePoints resolves next batch,
+ // processSequencePoints executes it
+ $processor->processFunnelAction($funnelSub);
+ }
+
+ private function showStepModeNextUp($funnelSub)
+ {
+ if (!$funnelSub) {
+ return;
+ }
+
+ if ($funnelSub->status === 'active' && $funnelSub->next_sequence_id) {
+ $upNext = FunnelSequence::find($funnelSub->next_sequence_id);
+ WP_CLI::line(sprintf(
+ 'Up next: [%s] %s',
+ $upNext ? $upNext->action_name : '?',
+ $upNext ? ($upNext->title ?: '') : ''
+ ));
+ WP_CLI::line('Run the command again to advance.');
+ }
+ }
+
+ private function checkTerminalStatus($funnelSub)
+ {
+ if (!$funnelSub) {
+ WP_CLI::error('Funnel subscriber record not found');
+ return true;
+ }
+
+ if ($funnelSub->status === 'completed') {
+ WP_CLI::success('Funnel completed!');
+ return true;
+ }
+
+ if ($funnelSub->status === 'cancelled') {
+ WP_CLI::warning('Funnel cancelled (subscriber may not be in a processable status)');
+ return true;
+ }
+
+ if ($funnelSub->status === 'waiting') {
+ $seq = $funnelSub->next_sequence_id ? FunnelSequence::find($funnelSub->next_sequence_id) : null;
+ WP_CLI::warning(sprintf(
+ 'Blocked on benchmark: %s — cannot auto-advance past goals.',
+ $seq ? ($seq->title ?: $seq->action_name) : 'unknown'
+ ));
+ return true;
+ }
+
+ if ($funnelSub->status === 'pending') {
+ WP_CLI::warning('Subscriber is pending (needs double opt-in). Cannot auto-advance.');
+ return true;
+ }
+
+ if ($funnelSub->status !== 'active' || !$funnelSub->next_execution_time) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function snapshotHooks()
+ {
+ global $wp_actions;
+ return $wp_actions ?: [];
+ }
+
+ private function diffHooks($before)
+ {
+ global $wp_actions;
+ $after = $wp_actions ?: [];
+ $fired = [];
+
+ foreach ($after as $hook => $count) {
+ $prevCount = isset($before[$hook]) ? $before[$hook] : 0;
+ if ($count > $prevCount) {
+ // Only include fluentcrm-related hooks
+ if (strpos($hook, 'fluentcrm') !== false || strpos($hook, 'fluent_crm') !== false) {
+ $fired[$hook] = $count - $prevCount;
+ }
+ }
+ }
+
+ return $fired;
+ }
+
+ private function askAndShowFiredHooks($firedHooks)
+ {
+ if (empty($firedHooks)) {
+ return;
+ }
+
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
+ fwrite(STDOUT, sprintf('Show fired hooks? (%d hooks) (yes/no): ', count($firedHooks)));
+ $answer = strtolower(trim(fgets(STDIN)));
+
+ if ($answer !== 'yes' && $answer !== 'y') {
+ return;
+ }
+
+ WP_CLI::line('Fired hooks:');
+ foreach ($firedHooks as $hook => $count) {
+ $suffix = $count > 1 ? sprintf(' (x%d)', $count) : '';
+ WP_CLI::line(sprintf(' > %s%s', $hook, $suffix));
+ }
+ }
+
+ private function showFinalStatus($funnelId, $subscriberId, $funnelSubId)
+ {
+ $funnelSub = $funnelSubId ? FunnelSubscriber::find($funnelSubId) : null;
+ $totalMetrics = FunnelMetric::where('funnel_id', $funnelId)
+ ->where('subscriber_id', $subscriberId)
+ ->count();
+
+ WP_CLI::line('---');
+ WP_CLI::line(sprintf('Final status: %s', $funnelSub ? $funnelSub->status : 'unknown'));
+ WP_CLI::line(sprintf('Total actions executed: %d', $totalMetrics));
+ }
+
+ private function printFunnelSteps($funnelId)
+ {
+ $sequences = FunnelSequence::where('funnel_id', $funnelId)
+ ->orderBy('sequence', 'ASC')
+ ->get();
+
+ if ($sequences->isEmpty()) {
+ WP_CLI::line('No steps in this funnel.');
+ WP_CLI::line('---');
+ return;
+ }
+
+ // Group children by parent_id and condition_type
+ $topLevel = [];
+ $children = []; // $children[$parentId][$conditionType][]
+ foreach ($sequences as $seq) {
+ if (!$seq->parent_id) {
+ $topLevel[] = $seq;
+ } else {
+ $children[$seq->parent_id][$seq->condition_type][] = $seq;
+ }
+ }
+
+ WP_CLI::line('Funnel steps:');
+ $this->printSequenceList($topLevel, $children, ' ');
+ WP_CLI::line('---');
+ }
+
+ private function printSequenceList($sequences, $children, $indent)
+ {
+ $count = count($sequences);
+ foreach ($sequences as $i => $seq) {
+ $label = $this->formatSequenceLabel($seq);
+ $isLast = ($i === $count - 1);
+ $connector = $isLast ? '└─' : '├─';
+ WP_CLI::line($indent . $connector . ' ' . $label);
+
+ // If conditional/ab-test, print branches
+ if ($seq->type === 'conditional' && isset($children[$seq->id])) {
+ $childIndent = $indent . ($isLast ? ' ' : '│ ');
+ $branches = $children[$seq->id];
+
+ if (isset($branches['yes'])) {
+ WP_CLI::line($childIndent . '├─ [YES]:');
+ $this->printSequenceList($branches['yes'], $children, $childIndent . '│ ');
+ }
+ if (isset($branches['no'])) {
+ WP_CLI::line($childIndent . '└─ [NO]:');
+ $this->printSequenceList($branches['no'], $children, $childIndent . ' ');
+ }
+ }
+ }
+ }
+
+ private function formatSequenceLabel($seq)
+ {
+ $type = $seq->type ?: 'action';
+ $title = $seq->title ?: $seq->action_name;
+
+ if ($seq->action_name === 'fluentcrm_wait_times') {
+ $wait = $this->formatWaitTime($seq->settings);
+ return sprintf('(%s) %s — %s', $type, $title, $wait);
+ }
+
+ if ($seq->action_name === 'end_this_funnel') {
+ return sprintf('(%s) End Funnel', $type);
+ }
+
+ return sprintf('(%s) %s', $type, $title);
+ }
+
+ private function formatWaitTime($settings)
+ {
+ if (!is_array($settings)) {
+ return '';
+ }
+
+ $waitType = $settings['wait_type'] ?? '';
+
+ if ($waitType === 'timestamp_wait') {
+ return 'until ' . ($settings['wait_date_time'] ?? '?');
+ }
+
+ if ($waitType === 'to_day') {
+ $day = $settings['wait_day_of_week'] ?? '?';
+ $time = $settings['wait_time_of_day'] ?? '';
+ return sprintf('next %s%s', $day, $time ? ' at ' . $time : '');
+ }
+
+ if ($waitType === 'by_custom_field') {
+ return 'until custom field date';
+ }
+
+ $amount = $settings['wait_time_amount'] ?? '?';
+ $unit = $settings['wait_time_unit'] ?? 'days';
+ return sprintf('%s %s', $amount, $unit);
+ }
+}
--- a/fluent-crm/app/Hooks/Handlers/AdminBar.php
+++ b/fluent-crm/app/Hooks/Handlers/AdminBar.php
@@ -24,24 +24,22 @@
$contactPermission = PermissionManager::currentUserCan('fcrm_read_contacts');
/**
- * Determine whether the FluentCRM global search is enabled or not.
+ * Determine whether the FluentCRM admin bar search is enabled or not.
*
- * @return bool False Default is false or disabled.
+ * @return bool False Default is false or disabled.
*/
- if ( !is_admin() || !$contactPermission || apply_filters('fluent_crm/disable_global_search', false) ) {
+ if (!is_admin() || !$contactPermission || apply_filters('fluent_crm/disable_adminbar_search', apply_filters('fluent_crm/disable_global_search', false))) {
return;
}
- add_action('admin_bar_menu', [$this, 'addGlobalSearch'], 999);
-
+ add_action('admin_bar_menu', [$this, 'addAdminBarSearch'], 999);
}
- public function addGlobalSearch($adminBar)
+ public function addAdminBarSearch($adminBar)
{
-
wp_enqueue_script(
- 'fluentcrm_global_search',
- fluentCrmMix('/admin/js/global-search.js'),
+ 'fluentcrm_adminbar_search',
+ fluentCrmMix('/admin/js/adminbar-search.js'),
['jquery']
);
@@ -68,7 +66,7 @@
}
}
- wp_localize_script('fluentcrm_global_search', 'fc_bar_vars', [
+ wp_localize_script('fluentcrm_adminbar_search', 'fcrm_adminbar_search_vars', [
'rest' => $this->getRestInfo(),
'links' => (new Stats)->getQuickLinks(),
'subscriber_base' => $urlBase . 'subscribers/',
@@ -79,13 +77,14 @@
'Type to search contacts' => __('Type to search contacts', 'fluent-crm'),
'Quick Links' => __('Quick Links', 'fluent-crm'),
'Sorry no contact found' => __('Sorry no contact found', 'fluent-crm'),
- 'Load More' => __('Load More', 'fluent-crm')
+ 'Load More' => __('Load More', 'fluent-crm'),
+ 'Close' => __('Close', 'fluent-crm')
]
]);
$args = [
'parent' => 'top-secondary',
- 'id' => 'fc_global_search',
+ 'id' => 'fcrm_adminbar_search',
'title' => __('Search Contacts', 'fluent-crm'),
'href' => '#',
'meta' => false
--- a/fluent-crm/app/Hooks/Handlers/AdminMenu.php
+++ b/fluent-crm/app/Hooks/Handlers/AdminMenu.php
@@ -3,8 +3,6 @@
namespace FluentCrmAppHooksHandlers;
use FluentCrmAppModelsLists;
-use FluentCrmAppModelsMeta;
-use FluentCrmAppModelsSubscriber;
use FluentCrmAppModelsTag;
use FluentCrmAppServicesHelper;
use FluentCrmAppServicesPermissionManager;
@@ -32,7 +30,6 @@
if (isset($_GET['page']) && $_GET['page'] == 'fluentcrm-admin' && is_admin()) {
$this->mayBeRedirect();
- $this->maybeInitExperimentalNavigation();
// Maybe we have to update the database tables
UpgradationHandler::maybeUpdateDbTables();
@@ -50,11 +47,14 @@
return;
}
- $dashboardPermission = 'fcrm_view_dashboard';
$isAdmin = false;
if (in_array('administrator', $permissions)) {
$dashboardPermission = 'manage_options';
$isAdmin = true;
+ } else {
+ $user = get_user_by('ID', get_current_user_id());
+ $roles = array_values((array)$user->roles);
+ $dashboardPermission = Arr::get($roles, 0);
}
$title = __('FluentCRM', 'fluent-crm');
@@ -80,45 +80,49 @@
array($this, 'render')
);
- add_submenu_page(
- 'fluentcrm-admin',
- __('Contacts', 'fluent-crm'),
- __('Contacts', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_contacts',
- 'fluentcrm-admin#/subscribers',
- array($this, 'render')
- );
- if (in_array('fcrm_read_contacts', $permissions)) {
- if (Helper::isCompanyEnabled()) {
- add_submenu_page(
- 'fluentcrm-admin',
- __('Companies', 'fluent-crm'),
- __('Companies', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_contact_cats',
- 'fluentcrm-admin#/contact-groups/companies',
- array($this, 'render')
- );
- }
+ if (in_array('fcrm_read_contacts', $permissions)) {
add_submenu_page(
'fluentcrm-admin',
- __('Lists', 'fluent-crm'),
- __('Lists', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_contact_cats',
- 'fluentcrm-admin#/contact-groups/lists',
+ __('Contacts', 'fluent-crm'),
+ __('Contacts', 'fluent-crm'),
+ $dashboardPermission,
+ 'fluentcrm-admin#/subscribers',
array($this, 'render')
);
- add_submenu_page(
- 'fluentcrm-admin',
- __('Tags', 'fluent-crm'),
- __('Tags', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_contact_cats',
- 'fluentcrm-admin#/contact-groups/tags',
- array($this, 'render')
- );
+ if (in_array('fcrm_manage_contact_cats', $permissions)) {
+ if (Helper::isCompanyEnabled()) {
+ add_submenu_page(
+ 'fluentcrm-admin',
+ __('Companies', 'fluent-crm'),
+ __('Companies', 'fluent-crm'),
+ $dashboardPermission,
+ 'fluentcrm-admin#/contact-groups/companies',
+ array($this, 'render')
+ );
+ }
+
+ add_submenu_page(
+ 'fluentcrm-admin',
+ __('Lists', 'fluent-crm'),
+ __('Lists', 'fluent-crm'),
+ $dashboardPermission,
+ 'fluentcrm-admin#/contact-groups/lists',
+ array($this, 'render')
+ );
+
+ add_submenu_page(
+ 'fluentcrm-admin',
+ __('Tags', 'fluent-crm'),
+ __('Tags', 'fluent-crm'),
+ $dashboardPermission,
+ 'fluentcrm-admin#/contact-groups/tags',
+ array($this, 'render')
+ );
+ }
}
if (in_array('fcrm_read_emails', $permissions)) {
@@ -126,7 +130,7 @@
'fluentcrm-admin',
__('Campaigns', 'fluent-crm'),
__('Campaigns', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_emails',
+ $dashboardPermission,
'fluentcrm-admin#/email/campaigns',
array($this, 'render')
);
@@ -135,7 +139,7 @@
'fluentcrm-admin',
__('Recurring Campaigns', 'fluent-crm'),
__('Recurring Campaigns', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_emails',
+ $dashboardPermission,
'fluentcrm-admin#/email/recurring-campaigns',
array($this, 'render')
);
@@ -144,7 +148,7 @@
'fluentcrm-admin',
__('Email Sequences', 'fluent-crm'),
__('Email Sequences', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_emails',
+ $dashboardPermission,
'fluentcrm-admin#/email/sequences',
array($this, 'render')
);
@@ -153,7 +157,7 @@
'fluentcrm-admin',
__('Email Templates', 'fluent-crm'),
__('Email Templates', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_emails',
+ $dashboardPermission,
'fluentcrm-admin#/email/templates',
array($this, 'render')
);
@@ -164,7 +168,7 @@
'fluentcrm-admin',
__('Forms', 'fluent-crm'),
__('Forms', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_forms',
+ $dashboardPermission,
'fluentcrm-admin#/forms',
array($this, 'render')
);
@@ -175,7 +179,7 @@
'fluentcrm-admin',
__('Automations', 'fluent-crm'),
__('Automations', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_read_funnels',
+ $dashboardPermission,
'fluentcrm-admin#/funnels',
array($this, 'render')
);
@@ -189,7 +193,7 @@
'fluentcrm-admin',
__('Settings', 'fluent-crm'),
__('Settings', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_settings',
+ $dashboardPermission,
'fluentcrm-admin#/settings',
array($this, 'render')
);
@@ -198,7 +202,7 @@
'fluentcrm-admin',
__('Reports', 'fluent-crm'),
__('Reports', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_settings',
+ $dashboardPermission,
'fluentcrm-admin#/reports',
array($this, 'render')
);
@@ -207,7 +211,7 @@
'fluentcrm-admin',
__('Addons', 'fluent-crm'),
__('Addons', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_manage_settings',
+ $dashboardPermission,
'fluentcrm-admin#/add-ons',
array($this, 'render')
);
@@ -218,7 +222,7 @@
'fluentcrm-admin',
__('SMTP', 'fluent-crm'),
__('SMTP', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_view_dashboard',
+ $dashboardPermission,
'fluentcrm-admin#/settings/smtp_settings',
array($this, 'render')
);
@@ -229,7 +233,7 @@
'fluentcrm-admin',
__('Help', 'fluent-crm'),
__('Help', 'fluent-crm'),
- ($isAdmin) ? $dashboardPermission : 'fcrm_view_dashboard',
+ $dashboardPermission,
'fluentcrm-admin#/documentation',
array($this, 'render')
);
@@ -249,19 +253,35 @@
wp_enqueue_script(
'fluentcrm_admin_app_start',
- fluentCrmMix('/admin/js/start.js'),
+ fluentCrmMix('/admin/js/app.js'),
array('fluentcrm_admin_app_boot'),
$this->version
);
+ /**
+ * Controls whether the FluentCRM admin top menu bar should be rendered.
+ *
+ * @param bool $renderTopMenuBar Whether to render the top menu bar.
+ */
+ $renderTopMenuBar = apply_filters('fluent_crm/render_top_menu_bar', true);
+
$urlBase = fluentcrm_menu_url_base();
$menuItems = $this->getMenuItems($urlBase);
- $app['view']->render('admin.menu_page', [
- 'menuItems' => $menuItems,
- 'logo' => FLUENTCRM_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
- 'base_url' => $urlBase
+ $proData = [
+ 'label' => __('Upgrade to Pro', 'fluent-crm'),
+ 'permalink' => 'https://fluentcrm.com?utm_source=dashboard&utm_medium=plugin&utm_campaign=pro&utm_id=wp',
+ 'has_pro' => defined('FLUENTCAMPAIGN')
+ ];
+
+ $app['view']->render('admin.new_menu_page', [
+ 'menuItems' => $menuItems,
+ 'settingsUrl' => $urlBase . 'settings',
+ 'logo' => FLUENTCRM_PLUGIN_URL . 'assets/images/fluentcrm-logo.svg',
+ 'base_url' => $urlBase,
+ 'proData' => $proData,
+ 'renderTopMenuBar' => $renderTopMenuBar
]);
}
@@ -273,21 +293,21 @@
if (!defined('DISABLE_WP_CRON')) {
$doc_url = 'https://fluentcrm.com/docs/fluentcrm-cron-job-basics-and-checklist/';
$extraHtml = ' ' . sprintf(
- wp_kses(
+ wp_kses(
/* translators: %1$s: Opening <a> tag linking to FluentCRM cron job docs. %2$s: Closing </a> tag. */
- __('Server-Side Cron Job is not enabled %1$sView Documentation%2$s.', 'fluent-crm'),
- array(
- 'a' => array(
- 'href' => array(),
- 'target' => array(),
- 'rel' => array(),
- 'style' => array(),
+ __('Server-Side Cron Job is not enabled %1$sView Documentation%2$s.', 'fluent-crm'),
+ array(
+ 'a' => array(
+ 'href' => array(),
+ 'target' => array(),
+ 'rel' => array(),
+ 'style' => array(),
+ )
)
- )
- ),
- '<a style="font-weight: 500;" target="_blank" rel="noopener" href="' . esc_url($doc_url) . '">',
- '</a>'
- );
+ ),
+ '<a style="font-weight: 500;" target="_blank" rel="noopener" href="' . esc_url($doc_url) . '">',
+ '</a>'
+ );
}
/* translators: %s: the FluentCRM website URL (used in the href of the link) */
return sprintf(wp_kses(__('Thank you for using <a href="%s">FluentCRM</a>.', 'fluent-crm'), array('a' => array('href' => array()))), esc_url($url)) . '<span title="based on your WP timezone settings" style="margin-left: 10px;" data-timestamp="' . current_time('timestamp') . '" id="fc_server_timestamp"></span>. ' . $extraHtml;
@@ -307,6 +327,7 @@
$urlBase = fluentcrm_menu_url_base();
}
+
$permissions = PermissionManager::currentUserPermissions();
$menuItems = [
@@ -322,45 +343,61 @@
'key' => 'contacts',
'label' => __('Contacts', 'fluent-crm'),
'permalink' => $urlBase . 'subscribers',
- 'layout_class' => 'fc_2_col_menu',
+ 'layout_class' => 'fc_1_col_menu',
'sub_items' => [
[
'key' => 'all_contacts',
'label' => __('All Contacts', 'fluent-crm'),
'permalink' => $urlBase . 'subscribers',
- 'description' => __('Browse all your subscribers and customers', 'fluent-crm')
+ 'description' => __('Browse all your subscribers and customers', 'fluent-crm'),
+ 'icon' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M5 3.75C4.65482 3.75 4.375 4.02982 4.375 4.375V5.625H5.625V5H14.375V15H5.625V14.375H4.375V15.625C4.375 15.9702 4.65482 16.25 5 16.25H15C15.3452 16.25 15.625 15.9702 15.625 15.625V4.375C15.625 4.02982 15.3452 3.75 15 3.75H5ZM8.125 12.5C8.125 11.4644 8.96444 10.625 10 10.625C11.0356 10.625 11.875 11.4644 11.875 12.5H8.125ZM10 10C9.30963 10 8.75 9.44037 8.75 8.75C8.75 8.05964 9.30963 7.5 10 7.5C10.6904 7.5 11.25 8.05964 11.25 8.75C11.25 9.44037 10.6904 10 10 10ZM6.25 8.125V6.875H3.75V8.125H6.25ZM6.25 9.375V10.625H3.75V9.375H6.25ZM6.25 13.125V11.875H3.75V13.125H6.25Z" fill="currentColor"/>
+ </svg>'
]
]
];
if (in_array('fcrm_manage_contact_cats', $permissions)) {
- if (Helper::isCompanyEnabled()) {
- $contactMenu['sub_items'][] = [
- 'key' => 'companies',
- 'label' => __('Companies', 'fluent-crm'),
- 'permalink' => $urlBase . 'contact-groups/companies',
- 'description' => __('Browse and Manage contact business/companies', 'fluent-crm')
- ];
- }
-
$contactMenu['sub_items'][] = [
'key' => 'lists',
'label' => __('Lists', 'fluent-crm'),
'permalink' => $urlBase . 'contact-groups/lists',
- 'description' => __('Browse and Manage your lists associate with contact', 'fluent-crm')
+ 'description' => __('Browse and Manage your lists associate with contact', 'fluent-crm'),
+ 'icon' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M7 4H16.75V5.5H7V4ZM4.375 5.875C4.07663 5.875 3.79048 5.75647 3.5795 5.5455C3.36853 5.33452 3.25 5.04837 3.25 4.75C3.25 4.45163 3.36853 4.16548 3.5795 3.9545C3.79048 3.74353 4.07663 3.625 4.375 3.625C4.67337 3.625 4.95952 3.74353 5.1705 3.9545C5.38147 4.16548 5.5 4.45163 5.5 4.75C5.5 5.04837 5.38147 5.33452 5.1705 5.5455C4.95952 5.75647 4.67337 5.875 4.375 5.875ZM4.375 11.125C4.07663 11.125 3.79048 11.0065 3.5795 10.7955C3.36853 10.5845 3.25 10.2984 3.25 10C3.25 9.70163 3.36853 9.41548 3.5795 9.2045C3.79048 8.99353 4.07663 8.875 4.375 8.875C4.67337 8.875 4.95952 8.99353 5.1705 9.2045C5.38147 9.41548 5.5 9.70163 5.5 10C5.5 10.2984 5.38147 10.5845 5.1705 10.7955C4.95952 11.0065 4.67337 11.125 4.375 11.125ZM4.375 16.3C4.07663 16.3 3.79048 16.1815 3.5795 15.9705C3.36853 15.7595 3.25 15.4734 3.25 15.175C3.25 14.8766 3.36853 14.5905 3.5795 14.3795C3.79048 14.1685 4.07663 14.05 4.375 14.05C4.67337 14.05 4.95952 14.1685 5.1705 14.3795C5.38147 14.5905 5.5 14.8766 5.5 15.175C5.5 15.4734 5.38147 15.7595 5.1705 15.9705C4.95952 16.1815 4.67337 16.3 4.375 16.3ZM7 9.25H16.75V10.75H7V9.25ZM7 14.5H16.75V16H7V14.5Z" fill="currentColor"/>
+ </svg>'
];
$contactMenu['sub_items'][] = [
'key' => 'tags',
'label' => __('Tags', 'fluent-crm'),
'permalink' => $urlBase . 'contact-groups/tags',
- 'description' => __('Browse and Manage your tags associate with contact', 'fluent-crm')
+ 'description' => __('Browse and Manage your tags associate with contact', 'fluent-crm'),
+ 'icon' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9.17489 2.57495L16.5991 3.6362L17.6596 11.0612L10.7656 17.9552C10.625 18.0958 10.4343 18.1748 10.2354 18.1748C10.0365 18.1748 9.84578 18.0958 9.70514 17.9552L2.28014 10.5302C2.13953 10.3896 2.06055 10.1988 2.06055 9.99995C2.06055 9.80108 2.13953 9.61035 2.28014 9.4697L9.17489 2.57495ZM9.70514 4.16645L3.87089 9.99995L10.2354 16.3637L16.0689 10.5302L15.2739 4.96145L9.70514 4.16645ZM11.2951 8.93945C11.0138 8.65799 10.8557 8.27629 10.8558 7.87831C10.8559 7.68125 10.8947 7.48613 10.9701 7.30409C11.0456 7.12204 11.1561 6.95664 11.2955 6.81733C11.4349 6.67801 11.6003 6.56751 11.7824 6.49213C11.9645 6.41675 12.1596 6.37797 12.3567 6.37801C12.7546 6.37808 13.1363 6.53624 13.4176 6.8177C13.699 7.09916 13.857 7.48087 13.857 7.87884C13.8569 8.27682 13.6987 8.65846 13.4173 8.93983C13.1358 9.22119 12.7541 9.37921 12.3561 9.37914C11.9581 9.37907 11.5765 9.22091 11.2951 8.93945Z" fill="currentColor"/>
+</svg>'
];
+
+ if (Helper::isCompanyEnabled()) {
+ $contactMenu['sub_items'][] = [
+ 'key' => 'companies',
+ 'label' => __('Companies', 'fluent-crm'),
+ 'permalink' => $urlBase . 'contact-groups/companies',
+ 'description' => __('Browse and Manage contact business/companies', 'fluent-crm'),
+ 'icon' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.75 15.25H18.25V16.75H1.75V15.25H3.25V4C3.25 3.80109 3.32902 3.61032 3.46967 3.46967C3.61032 3.32902 3.80109 3.25 4 3.25H11.5C11.6989 3.25 11.8897 3.32902 12.0303 3.46967C12.171 3.61032 12.25 3.80109 12.25 4V15.25H15.25V9.25H13.75V7.75H16C16.1989 7.75 16.3897 7.82902 16.5303 7.96967C16.671 8.11032 16.75 8.30109 16.75 8.5V15.25ZM4.75 4.75V15.25H10.75V4.75H4.75ZM6.25 9.25H9.25V10.75H6.25V9.25ZM6.25 6.25H9.25V7.75H6.25V6.25Z" fill="currentColor"/>
+</svg>'
+ ];
+ }
+
$contactMenu['sub_items'][] = [
'key' => 'dynamic_segments',
'label' => __('Segments', 'fluent-crm'),
'permalink' => $urlBase . 'contact-groups/dynamic-segments',
- 'description' => __('Manage your dynamic contact segments', 'fluent-crm')
+ 'description' => __('Manage your dynamic contact segments', 'fluent-crm'),
+ 'icon' => '<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M10 11.5V13C8.80653 13 7.66193 13.4741 6.81802 14.318C5.97411 15.1619 5.5 16.3065 5.5 17.5H4C4 15.9087 4.63214 14.3826 5.75736 13.2574C6.88258 12.1321 8.4087 11.5 10 11.5V11.5ZM10 10.75C7.51375 10.75 5.5 8.73625 5.5 6.25C5.5 3.76375 7.51375 1.75 10 1.75C12.4862 1.75 14.5 3.76375 14.5 6.25C14.5 8.73625 12.4862 10.75 10 10.75ZM10 9.25C11.6575 9.25 13 7.9075 13 6.25C13 4.5925 11.6575 3.25 10 3.25C8.3425 3.25 7 4.5925 7 6.25C7 7.9075 8.3425 9.25 10 9.25ZM11.9462 15.109C11.8512 14.7088 11.8512 14.2919 11.9462 13.8917L11.2022 13.462L11.9522 12.163L12.6962 12.5927C12.9949 12.3099 13.3558 12.1013 13.75 11.9838V11.125H15.25V11.9838C15.649 12.1023 16.009 12.3137 16.3037 12.5927L17.0477 12.163L17.7977 13.462L17.0537 13.8917C17.1487 14.2917 17.1487 14.7083 17.0537 15.1083L17.7977 15.538L17.0477 16.837L16.3037 16.4072C16.0051 16.6901 15.6442 16.8987 15.25 17.0162V17.875H13.75V17.0162C13.3558 16.8987 12.9949 16.6901 12.6962 16.4072L11.9522 16.837L11.2022