Initial commit

This commit is contained in:
gustavooth
2026-07-23 22:56:30 -03:00
commit 22a1006598
1816 changed files with 410742 additions and 0 deletions
@@ -0,0 +1,70 @@
<?php
namespace FluentCrm\App\Models;
/**
* Activity Log Model - DB Model for Activity Logs
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class ActivityLog extends Model
{
protected $table = 'fc_activity_logs';
protected $guarded = ['id'];
protected $fillable = [
'object_type',
'object_id',
'action',
'source',
'description',
'activity_by',
'created_at',
'updated_at'
];
// Ensure the key is added to every serialized row
protected $appends = ['activity_by_email'];
// Cast the description column to an array (or object)
protected $casts = [
'description' => 'array', // Automatically decodes JSON to array
// Use 'object' instead of 'array' if you prefer stdClass objects
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->created_at)) {
$model->created_at = fluentCrmTimestamp();
}
if (empty($model->activity_by)) {
$model->activity_by = 0;
}
$model->updated_at = fluentCrmTimestamp();
});
static::updated(function ($model) {
$model->updated_at = fluentCrmTimestamp();
});
}
public function getActivityByEmailAttribute()
{
$user = User::where('ID', $this->activity_by)->first();
if (!$user) {
return null;
}
return $user->display_name . ' (' . $user->user_email . ')';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,667 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\BlockParser;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Support\Str;
/**
* CampaignEmail Model - DB Model for Campaign Emails
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class CampaignEmail extends Model
{
protected $table = 'fc_campaign_emails';
protected $guarded = ['id'];
protected $appends = ['email_type_label'];
/**
* Define the canonical email types and any legacy or module-specific aliases.
*
* @return array<string, array<int, string>>
*/
public static function getEmailTypeAliases()
{
return [
'funnel_email_campaign' => ['funnel_email_campaign', 'automation'],
'recurring_campaign' => ['recurring_campaign', 'recurring_email_campaign'],
'custom_email_campaign' => ['custom_email_campaign', 'custom_email'],
'campaign' => ['campaign'],
'sequence' => ['sequence', 'email_sequence', 'sequence_email'],
];
}
/**
* Map canonical email types to the user-facing labels used in reporting.
*
* @return array<string, string>
*/
public static function getEmailTypeLabels()
{
return [
'funnel_email_campaign' => __('Automation', 'fluent-crm'),
'recurring_campaign' => __('Recurring Campaign', 'fluent-crm'),
'custom_email_campaign' => __('Custom Email', 'fluent-crm'),
'campaign' => __('Campaign', 'fluent-crm'),
'sequence' => __('Sequence', 'fluent-crm'),
];
}
/**
* Resolve the canonical email type key for filtering and reporting.
*
* @param string|null $emailType
* @return string
*/
public static function normalizeEmailType($emailType)
{
$emailType = sanitize_text_field((string)$emailType);
foreach (static::getEmailTypeAliases() as $canonicalType => $aliases) {
if (in_array($emailType, $aliases, true)) {
return $canonicalType;
}
}
return $emailType ?: 'campaign';
}
/**
* Expand selected canonical types into the raw slugs stored in email rows.
*
* @param array<int, string> $selectedTypes
* @return array<int, string>
*/
public static function expandEmailTypes(array $selectedTypes)
{
$expandedTypes = [];
$aliases = static::getEmailTypeAliases();
foreach ($selectedTypes as $selectedType) {
$canonicalType = static::normalizeEmailType($selectedType);
$expandedTypes = array_merge($expandedTypes, $aliases[$canonicalType] ?? [$canonicalType]);
}
return array_values(array_unique($expandedTypes));
}
/**
* Resolve the human-readable email type label for reports and tables.
*
* @return string
*/
public function getEmailTypeLabelAttribute()
{
return static::resolveEmailTypeLabel($this->email_type);
}
/**
* Convert a stored email type slug into a stable UI label.
*
* @param string|null $emailType
* @return string
*/
public static function resolveEmailTypeLabel($emailType)
{
$emailType = static::normalizeEmailType($emailType);
$labels = static::getEmailTypeLabels();
if (isset($labels[$emailType])) {
return $labels[$emailType];
}
return ucwords(str_replace(['_', '-'], ' ', $emailType ?: 'campaign'));
}
/**
* One2One: CampaignEmail belongs to one Campaign
* @return Model
*/
public function campaign()
{
return $this->belongsTo(
__NAMESPACE__ . '\Campaign', 'campaign_id', 'id'
)->withoutGlobalScope('type');
}
/**
* One2One: CampaignEmail belongs to one Subscriber
* @return Model
*/
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
);
}
/**
* One2One: CampaignEmail belongs to one Subject
*
* Note: The email_subject_id will be inserted by calculating the prioroty
* from subjects table where the subjects are related to a parent Campaign.
* So, when creating a campaign email, there will be an option to select a
* subject from a list and that list will contain subjects related to the
* parent campaign because a campaign can have many subjects and the campaign
* email will get only one from that list by calculating the priority from subjects.
*
* @return Model
*/
public function subject()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subject', 'email_subject_id', 'id'
);
}
public function markAs($status)
{
$this->status = $status;
$this->save();
return $this;
}
public function markAsSent($status = 'sent')
{
return $this->markAs($status);
}
public function markAsFailed($status = 'failed')
{
return $this->markAs($status);
}
/**
* Data for the email to be sent
* @return array
*/
public function data()
{
$email_subject = $this->getEmailSubject();
$email_body = $this->getEmailBody();
$headers = Helper::getMailHeader($this->email_headers);
return [
'to' => [
'email' => $this->email_address,
'name' => ($this->subscriber) ? $this->subscriber->full_name : ''
],
'headers' => $headers,
'subject' => $email_subject,
'body' => $email_body,
'campaign_id' => $this->campaign_id,
'id' => $this->id,
'subscriber_id' => $this->subscriber_id
];
}
/**
* Build preview data for one queued/sent email row.
*
* Route: GET /campaigns/emails/{email_id}/preview via CampaignController::previewEmail().
* This is the contact/campaign/all-emails history preview, not the draft editor preview
* route (POST /campaigns/email-preview-html). Keep the body rendering rules aligned with
* CampaignController::getEmailPreviewBody(): raw/classic-builder templates must bypass
* BlockParser, while block-editor templates should continue through BlockParser.
*
* @return array
*/
public function previewData()
{
$emailSettings = fluentcrmGetGlobalSettings('email_settings', []);
$campaign = $this->campaign;
$subscriber = $this->subscriber;
$emailBody = ($this->email_body) ? $this->email_body : (($campaign) ? $campaign->email_body : '');
$designTemplate = ($campaign) ? $campaign->design_template : '';
if (!$designTemplate) {
$designTemplate = 'plain';
}
$rawTemplates = [
'raw_html',
'visual_builder',
'raw_classic'
];
if (in_array($designTemplate, $rawTemplates, true) || ($this->is_parsed && $this->email_body)) {
$emailBody = wp_unslash($emailBody);
} else {
$emailBody = (new BlockParser($subscriber))->parse($emailBody);
}
/**
* Determine the campaign email body content text.
*
* This filter allows you to modify the email body content before it is sent to the subscriber.
*
* @param string $emailBody The email body content.
* @param object $this->subscriber The subscriber object.
* @since 2.7.0
*
*/
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
$templateConfig = wp_parse_args(
($campaign) ? Arr::get($campaign->settings, 'template_config', []) : [],
Helper::getTemplateConfig($designTemplate)
);
$emailFooterConfig = ($campaign) ? Helper::getFooterConfig($campaign) : [];
$footerText = Arr::get($emailFooterConfig, 'footer_content', '');
if ($subscriber) {
$subscriber->campaign_id = $this->campaign_id;
/**
* Determine the footer text of a campaign email for previewing.
*
* This filter allows you to modify the footer text of a campaign email before it is sent to the subscriber.
*
* @param string $footerText The footer text of the campaign email.
* @param object $subscriber The subscriber object.
* @since 2.7.0
*
*/
$footerText = apply_filters('fluent_crm/parse_campaign_email_text', $footerText, $subscriber);
}
$preHeader = ($campaign) ? $campaign->email_pre_header : '';
if ($preHeader && $subscriber) {
/**
* Filter the pre-header text of a campaign email for previewing.
*
* @param string $preHeader The pre-header text of the campaign email.
* @param object $subscriber The subscriber object.
* @since 2.7.0
*
*/
$preHeader = apply_filters('fluent_crm/parse_campaign_email_text', $preHeader, $subscriber);
}
$emailFooterConfig['footer_content'] = $footerText;
/**
* Filter the email body content using a specific email design template.
*
* This filter allows customization of the email body content by applying a specific design template.
*
* @param string $emailBody The original email body content before applying the design template.
* @param array {
* Contextual information for the email design template.
*
* @type string $preHeader The pre-header text for the email, if available.
* @type string $email_body The original email body content.
* @type string $footer_text The footer text for the email, if any.
* @type array $config Configuration settings for the email template.
* }
* @param object|null $this->campaign The campaign object, if available.
* @param object|null $this->subscriber The subscriber object, if available.
* @since 1.0.0
*
*/
$email_body = apply_filters(
'fluent_crm/email-design-template-' . $designTemplate,
$emailBody,
[
'preHeader' => $preHeader,
'email_body' => $emailBody,
'footer_text' => $footerText,
'footer_config' => $emailFooterConfig,
'config' => $templateConfig
],
$campaign,
$subscriber
);
if (Str::contains($email_body, ['##crm.', '{{crm.'])) {
/**
* Filter the email body content for a campaign email and parse SmartCodes.
*
* This filter allows customization of the email body content before it is sent to the subscriber. There are FluentCRM-specific SmartCodes.
*
* @param string $email_body The email body content to be filtered.
* @param object $this ->subscriber The subscriber object containing subscriber details.
* @since 2.7.0
*
*/
$email_body = apply_filters('fluent_crm/parse_extended_crm_text', $email_body, $subscriber);
}
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
$email_body = str_replace(['##web_preview_url##', '{{crm_global_email_footer}}', '{{crm_preheader_text}}'], [$preViewUrl, $footerText, $preHeader], $email_body);
$email_body = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $email_body);
return [
'to' => [
'email' => $this->email_address,
'name' => ($subscriber) ? $subscriber->full_name : ''
],
'from' => [
'name' => Arr::get($emailSettings, 'from_name'),
'email' => Arr::get($emailSettings, 'from_email')
],
'reply' => null,
'subject' => $this->email_subject,
'body' => $email_body,
'campaign_id' => $this->campaign_id,
'id' => $this->id,
'subscriber_id' => $this->subscriber_id
];
}
public function getEmailSubject()
{
return $this->email_subject;
}
public function getEmailBody()
{
$subscriber = $this->subscriber;
if ($subscriber) {
$subscriber->email_id = $this->id;
}
$designTemplate = 'classic';
$campaign = $this->campaign;
if ($campaign) {
$designTemplate = $campaign->design_template;
}
if ($this->is_parsed && !$this->email_body && $campaign && $campaign->email_body) {
// Recover unsent queue rows that were marked parsed after their body was cleared.
$this->is_parsed = 0;
}
if (!$this->is_parsed) {
$rawTemplates = [
'raw_html',
'visual_builder',
'raw_classic'
];
$emailBody = ($campaign) ? $campaign->email_body : $this->email_body;
// Don't cache URL map if body has conditional blocks or merge tags inside href URLs
$canCache = !Helper::hasConditionOnString($emailBody)
&& !preg_match('/href=["\'][^"\']*\{\{/', $emailBody);
if (in_array($designTemplate, $rawTemplates)) {
$emailBody = $this->campaign->email_body;
} else {
$emailBody = $this->getParsedEmailBody();
}
$emailBody = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $emailBody);
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
$emailBody = apply_filters('fluentcrm_email_body_text', $emailBody, $subscriber, $this);
if ($campaign && $trackingType = $campaign->getClickTrackingStatus()) {
$campaignUrls = $this->getCampaignUrls($emailBody, $canCache);
if ($campaignUrls) {
if ($trackingType === 'anonymous') {
$emailBody = Helper::attachAnonymousUrls($emailBody, $campaignUrls, $this->id, $this->email_hash);
} else {
$emailBody = Helper::attachUrls($emailBody, $campaignUrls, $this->id, $this->email_hash);
}
}
}
$this->email_body = $emailBody;
$this->is_parsed = 1;
// Not saved to DB here — the parsed body is kept in memory for Mailer::send().
// BaseHandler's mark-as-sent UPDATE persists is_parsed=1 and clears email_body.
// On rare retry (process crash), the email re-parses from campaign body which
// is correct. This avoids a ~15ms LONGTEXT write per email during bulk sends.
}
$emailFooterConfig = [];
$footerText = '';
if ($subscriber) {
$subscriber->campaign_id = $this->campaign_id;
static $footerConfigCache = [];
$cacheKey = $this->campaign_id ?: 0;
if (isset($footerConfigCache[$cacheKey])) {
$emailFooterConfig = $footerConfigCache[$cacheKey];
} else {
$emailFooterConfig = Helper::getFooterConfig($campaign);
$footerConfigCache[$cacheKey] = $emailFooterConfig;
}
$footerText = Arr::get($emailFooterConfig, 'footer_content', '');
if ($footerText) {
/**
* Filter the footer text of the campaign email.
*
* This filter allows you to modify the footer text of the campaign email before it is sent to the subscriber.
*
* @param string $footerText The footer text of the campaign email.
* @param object $subscriber The subscriber object.
* @since 2.7.0
*
*/
$footerText = apply_filters('fluent_crm/parse_campaign_email_text', $footerText, $subscriber);
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
$footerText = str_replace('##web_preview_url##', $preViewUrl, $footerText);
$emailFooterConfig['footer_content'] = $footerText;
}
}
static $templateConfigCache = [];
$templateCacheKey = $this->campaign_id ?: 0;
if (isset($templateConfigCache[$templateCacheKey])) {
$templateConfig = $templateConfigCache[$templateCacheKey];
} else {
if ($this->campaign && Arr::get($this->campaign->settings, 'template_config')) {
$templateConfig = wp_parse_args($this->campaign->settings['template_config'], Helper::getTemplateConfig($this->campaign->design_template));
} else {
$templateConfig = Helper::getTemplateConfig();
}
$templateConfigCache[$templateCacheKey] = $templateConfig;
}
$preHeader = ($this->campaign) ? $this->campaign->email_pre_header : '';
if ($preHeader && $subscriber) {
/**
* Filter the pre-header text of a campaign email.
*
* This filter allows you to modify the pre-header text of a campaign email before it is sent.
*
* @param string $preHeader The pre-header text of the campaign email.
* @param object $subscriber The subscriber object containing subscriber details.
* @since 2.7.0
*
*/
$preHeader = apply_filters('fluent_crm/parse_campaign_email_text', $preHeader, $subscriber);
}
$footerUrls = $this->getCampaignUrls($footerText, false);
if ($footerUrls) {
$trackingType = $campaign ? $campaign->getClickTrackingStatus() : null;
if ($trackingType === 'anonymous') {
$footerText = Helper::attachAnonymousUrls($footerText, $footerUrls, $this->id, $this->email_hash);
} else {
$footerText = Helper::attachUrls($footerText, $footerUrls, $this->id, $this->email_hash);
}
}
$templateData = [
'preHeader' => $preHeader,
'email_body' => $this->email_body,
'footer_text' => $footerText,
'config' => $templateConfig,
'footer_config' => $emailFooterConfig,
];
/**
* Filter the email design template content.
*
* This filter allows customization of the email design template content based on the template type.
*
* @param string $this ->email_body The original email body content.
* @param array $templateData The data used for the template.
* @param object $this ->campaign The campaign object.
* @param object $this ->subscriber The subscriber object.
* @since 1.0.0
*
*/
$content = apply_filters(
'fluent_crm/email-design-template-' . $designTemplate,
$this->email_body,
$templateData,
$this->campaign,
$this->subscriber
);
$preViewUrl = site_url('?fluentcrm=1&route=email_preview&_e_hash=' . $this->email_hash);
$content = str_replace(['##web_preview_url##', '{{crm_global_email_footer}}', '{{crm_preheader_text}}'], [$preViewUrl, $footerText, $preHeader], $content);
if (Str::contains($content, ['##crm.', '{{crm'])) {
/**
* Filter the content to parse extended CRM text such as SmartCodes.
*
* This filter allows you to modify the content by parsing extended CRM text. There are FluentCRM-specific SmartCodes available.
*
* @param string $content The content to be filtered.
* @param object $subscriber The subscriber object.
* @since 2.7.0
*
*/
$content = apply_filters('fluent_crm/parse_extended_crm_text', $content, $subscriber);
}
return Helper::injectTrackerPixel($content, $this->email_hash, $this->id);
}
private function getParsedEmailBody()
{
if (!$this->campaign_id || !$this->campaign) {
// return (new BlockParser($this->subscriber))->parse($this->email_body);
return (new BlockParser($this->subscriber))->parse($this->email_body);
}
static $parsedEmailBody = [];
$originalBody = $this->campaign->email_body;
$hasConditions = Helper::hasConditionOnString($originalBody);
if (isset($parsedEmailBody[$this->campaign_id]) && !$hasConditions) {
return $parsedEmailBody[$this->campaign_id];
}
if ($this->campaign->status == 'archived' && !$hasConditions) {
$cachedEmailBody = fluentcrm_get_campaign_meta($this->campaign_id, '_cached_email_body', true);
if ($cachedEmailBody) {
$parsedEmailBody[$this->campaign_id] = $cachedEmailBody;
return $parsedEmailBody[$this->campaign_id];
}
}
$rawTemplates = [
'raw_html',
'visual_builder',
'raw_classic'
];
if (in_array($this->campaign->design_template, $rawTemplates)) {
$emailBody = $originalBody;
} else {
// $emailBody = (new BlockParser($this->subscriber))->parse($originalBody);
$emailBody = (new BlockParser($this->subscriber))->parse($originalBody);
}
if ($hasConditions) {
return $emailBody;
}
$parsedEmailBody[$this->campaign_id] = $emailBody;
return $emailBody;
}
public function getCampaignUrls($emailBody, $cached = false)
{
$trackingType = fluentcrmTrackClicking();
if (!$trackingType) {
return [];
}
if (!$cached || !$this->campaign_id) {
return Helper::urlReplaces($emailBody);
}
static $campaignUrls = [];
if (isset($campaignUrls[$this->campaign_id])) {
return $campaignUrls[$this->campaign_id];
}
$campaignUrls[$this->campaign_id] = Helper::urlReplaces($emailBody);
return $campaignUrls[$this->campaign_id];
}
public function getClicks()
{
return fluentCrmDb()->table('fc_campaign_url_metrics')
->select(['fc_campaign_url_metrics.counter', 'fc_url_stores.url', 'fc_campaign_url_metrics.id'])
->where('type', 'click')
->where('fc_campaign_url_metrics.subscriber_id', $this->subscriber_id)
->where('fc_campaign_url_metrics.campaign_id', $this->campaign_id)
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
->get();
}
public function getSubjectCount($campaignId)
{
return static::select(
'fc_campaign_emails.email_subject_id',
fluentCrmDb()->raw('count(*) as total'),
'fc_meta.value',
'fc_meta.key'
)
->where('fc_campaign_emails.campaign_id', $campaignId)
->groupBy('fc_campaign_emails.email_subject_id')
->join('fc_meta', 'fc_meta.id', '=', 'fc_campaign_emails.email_subject_id')
->get();
}
public function getOpenCount($subjectId)
{
return static::where('email_subject_id', $subjectId)
->where('is_open', '>', 0)
->count();
}
public function setEmailHeadersAttribute($headers)
{
$this->attributes['email_headers'] = \maybe_serialize($headers);
}
public function getEmailHeadersAttribute($settings)
{
return \maybe_unserialize($settings);
}
}
@@ -0,0 +1,324 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
/**
* CampaignUrlMetric Model - DB Model for Email URL Metrics
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class CampaignUrlMetric extends Model
{
protected $table = 'fc_campaign_url_metrics';
protected $guarded = ['id'];
public function campaign()
{
return $this->belongsTo(__NAMESPACE__ . '\Campaign', 'campaign_id', 'id')
->withoutGlobalScope('type');
}
public function subscriber()
{
return $this->belongsTo(__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id');
}
public function url_stores()
{
return $this->belongsTo(__NAMESPACE__ . '\UrlStores', 'url_id', 'id');
}
public static function maybeInsert($data)
{
$query = static::where([
'campaign_id' => $data['campaign_id'],
'subscriber_id' => $data['subscriber_id'],
'type' => $data['type']
])->when(!empty($data['url_id']), function ($query) use ($data) {
return $query->where('url_id', $data['url_id']);
});
if ($instance = $query->first()) {
$instance->counter += 1;
$instance->save();
return $instance;
}
return static::create($data);
}
public function getLinksReport($campaign)
{
if (is_numeric($campaign)) {
$campaign = Campaign::withoutGlobalScopes()->find($campaign);
}
if (!$campaign) {
return [];
}
$settings = $campaign->settings;
$clickTracker = $settings['click_tracker'] ?? true;
// is anonimous tracking enabled?
if ($clickTracker === 'anonymous') {
// get from meta
$links = fluentcrm_get_campaign_meta($campaign->id, '_ano_url_clicks', true);
$formattedLinks = [];
if ($links && is_array($links)) {
$index = 1;
foreach ($links as $link => $count) {
$formattedLinks[] = [
'id' => $index,
'url' => esc_url_raw($link),
'total' => $count
];
$index++;
}
}
// sort by total desc
usort($formattedLinks, function ($a, $b) {
return $b['total'] <=> $a['total'];
});
return $this->maybeTransformSmartLinks($formattedLinks);
}
if ($clickTracker === false) {
return [];
}
$stats = static::select(
fluentCrmDb()->raw('count(*) as total'),
'fc_url_stores.url',
'fc_url_stores.id'
)
->where('fc_campaign_url_metrics.campaign_id', $campaign->id)
->where('fc_campaign_url_metrics.type', 'click')
->groupBy('fc_campaign_url_metrics.url_id')
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
->orderBy('total', 'DESC')
->get()->toArray();
$formatedLinks = [];
foreach ($stats as $stat) {
$url = str_replace(['&amp;'], ['&'], $stat['url']);
$url = esc_url_raw($url);
if (isset($formatedLinks[$url])) {
$formatedLinks[$url]['total'] += $stat['total'];
continue;
}
$formatedLinks[$url] = [
'id' => $stat['id'],
'url' => $url,
'total' => $stat['total']
];
}
$sortedLinks = array_values($formatedLinks);
usort($sortedLinks, function ($a, $b) {
return $b['total'] <=> $a['total'];
});
return $this->maybeTransformSmartLinks($sortedLinks);
}
public function getCampaignAnalytics($campaign)
{
if (is_numeric($campaign)) {
$campaign = Campaign::withoutGlobalScopes()->find($campaign);
}
if (!$campaign) {
return [];
}
$unsubscribeCount = CampaignUrlMetric::where('campaign_id', $campaign->id)
->where('type', 'unsubscribe')
->distinct()
->count('subscriber_id');
$formattedStatus = [];
if ($campaign->getOpenTrackingStatus(false) === 'anonymous') {
$openCount = fluentcrm_get_campaign_meta($campaign->id, '_ano_open_count', true);
if (!$openCount) {
$openCount = 0;
}
} else {
$openCount = fluentCrmDb()->table('fc_campaign_emails')
->where('campaign_id', $campaign->id)
->where(function ($q) {
$q->where('is_open', 1)
->orWhereNotNull('click_counter');
})
->count();
}
if ($campaign->getClickTrackingStatus(false) === 'anonymous') {
$clicks = fluentcrm_get_campaign_meta($campaign->id, '_ano_url_clicks', true);
$clickCount = 0;
if ($clicks && is_array($clicks)) {
$clickCount = array_sum($clicks);
}
} else {
$clickCount = fluentCrmDb()->table('fc_campaign_emails')
->where('campaign_id', $campaign->id)
->whereNotNull('click_counter')
->count();
}
if ($openCount) {
$formattedStatus['open'] = [
'total' => $openCount,
/* translators: %d: number of opens */
'label' => sprintf(__('Open Rate (%d)', 'fluent-crm'), $openCount),
'type' => 'open',
'is_percent' => true,
'icon_class' => 'dashicons dashicons-buddicons-pm'
];
}
if ($clickCount) {
$formattedStatus['click'] = [
'total' => $clickCount,
/* translators: %d: number of clicks */
'label' => sprintf(__('Click Rate (%d)', 'fluent-crm'), $clickCount),
'type' => 'click',
'is_percent' => true,
'icon_class' => 'el-icon el-icon-position'
];
}
if ($openCount && $clickCount) {
$formattedStatus['ctor'] = [
'total' => number_format(($clickCount / $openCount) * 100, 2) . '%',
'label' => __('Click To Open Rate', 'fluent-crm'),
'type' => 'ctor',
'icon_class' => 'el-icon el-icon-chat-dot-square'
];
}
if ($unsubscribeCount) {
$formattedStatus['unsubscribe'] = [
'total' => $unsubscribeCount,
/* translators: %d: number of unsubscribes */
'label' => sprintf(__('Unsubscribe (%d)', 'fluent-crm'), $unsubscribeCount),
'type' => 'unsubscribe',
'is_percent' => true,
'icon_class' => 'el-icon el-icon-warning-outline'
];
}
$revenue = fluentcrm_get_campaign_meta($campaign->id, '_campaign_revenue');
if ($revenue && $revenue->value) {
$data = (array)$revenue->value;
foreach ($data as $currency => $cents) {
if ($cents && $currency !== 'orderIds') {
$formattedStatus['revenue'] = [
'label' => __('Revenue', 'fluent-crm') . ' (' . $currency . ')',
'type' => 'revenue',
'total' => number_format($cents / 100, 2),
'icon_class' => 'el-icon el-icon-money'
];
}
}
}
return $formattedStatus;
}
public function getSubjectStats($campaign)
{
$subjects = $campaign->subjects()->get();
if ($subjects->isEmpty()) {
return [];
}
$subjectCounts = (new CampaignEmail)->getSubjectCount($campaign->id);
$totalClicks = 0;
$totalOpens = 0;
foreach ($subjectCounts as $subjectCount) {
$metric = $this->getSubjectMetric(
$subjectCount->email_subject_id, $campaign->id
);
$totalClicks += $metric['total_clicks'];
$totalOpens += $metric['total_opens'];
$subjectCount->metric = $metric;
}
return [
'subjects' => $subjectCounts,
'total_clicks' => $totalClicks,
'total_opens' => $totalOpens
];
}
private function getSubjectMetric($subjectId, $campaignId)
{
$clickMetrics = $this->getClickMetrics($campaignId, $subjectId);
$openCount = (new CampaignEmail)->getOpenCount($subjectId);
$clickTotal = array_sum($clickMetrics->pluck('total')->toArray());
return [
'clicks' => $clickMetrics,
'total_clicks' => $clickTotal,
'total_opens' => $openCount
];
}
public function getClickMetrics($campaignId, $subjectId)
{
return static::select(
fluentCrmDb()->raw('count(*) as total'),
'fc_url_stores.url'
)
->where('fc_campaign_url_metrics.campaign_id', $campaignId)
->where('fc_campaign_url_metrics.type', 'click')
->where('fc_campaign_emails.email_subject_id', $subjectId)
->groupBy('fc_campaign_url_metrics.url_id')
->join('fc_url_stores', 'fc_url_stores.id', '=', 'fc_campaign_url_metrics.url_id')
->join('fc_campaign_emails', 'fc_campaign_emails.subscriber_id', '=', 'fc_campaign_url_metrics.subscriber_id')
->orderBy('total', 'DESC')
->get();
}
private function maybeTransformSmartLinks($links)
{
if (!apply_filters('fluent_crm/has_smartlink', false)) {
return $links;
}
foreach ($links as $index => $link) {
$url = $link['url'];
if (strpos($url, 'route=smart_url&slug=') !== false) {
// this is a smart-link
$smartLink = apply_filters('fluent_crm/smartlink_by_short_url', null, $url);
if ($smartLink) {
$links[$index]['destination'] = $smartLink->target_url;
$links[$index]['title'] = $smartLink->title;
}
}
}
return $links;
}
}
@@ -0,0 +1,177 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Models\Model;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\Framework\Support\Arr;
class Company extends Model
{
protected $table = 'fc_companies';
protected $guarded = ['id'];
protected $fillable = [
'hash',
'name',
'owner_id',
'industry',
'type',
'email',
'phone',
'address_line_1',
'address_line_2',
'postal_code',
'city',
'state',
'country',
'timezone',
'employees_number',
'description',
'logo',
'linkedin_url',
'facebook_url',
'twitter_url',
'meta',
'website',
'date_of_start',
'created_at',
'updated_at'
];
/**
* Get subscriber mappable fields.
*
* @return array
*/
public static function mappables()
{
return [
'name' => __('Company Name *', 'fluent-crm'),
'owner_email' => __('Owner Email', 'fluent-crm'),
'owner_name' => __('Owner Name', 'fluent-crm'),
'industry' => __('Industry', 'fluent-crm'),
'description' => __('Company Description', 'fluent-crm'),
'logo' => __('Company Logo URL', 'fluent-crm'),
'type' => __('Type', 'fluent-crm'),
'email' => __('Company Email', 'fluent-crm'),
'phone' => __('Company Phone', 'fluent-crm'),
'address_line_1' => __('Address Line 1', 'fluent-crm'),
'address_line_2' => __('Address Line 2', 'fluent-crm'),
'postal_code' => __('Postal Code', 'fluent-crm'),
'city' => __('City', 'fluent-crm'),
'state' => __('State', 'fluent-crm'),
'country' => __('Country', 'fluent-crm'),
'employees_number' => __('Employees Number', 'fluent-crm'),
'linkedin_url' => __('LinkedIn URL', 'fluent-crm'),
'facebook_url' => __('Facebook URL', 'fluent-crm'),
'twitter_url' => __('Twitter URL', 'fluent-crm'),
'website' => __('Website URL', 'fluent-crm')
];
}
protected $searchable = [
'name',
'phone',
'description',
'email'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->hash = md5(wp_generate_uuid4() . '_' . time() . '_' . wp_rand(1000, 9999));
});
}
/**
* Local scope to filter companies by search/query string
*/
public function scopeSearchBy($query, $search)
{
if ($search) {
$fields = $this->searchable;
$query->where(function ($query) use ($fields, $search) {
$query->where(array_shift($fields), 'LIKE', "%$search%");
foreach ($fields as $field) {
$query->orWhere($field, 'LIKE', "%$search%");
}
});
}
return $query;
}
public function scopeOfType($query, $status)
{
return $query->where('type', $status);
}
public function scopeOfIndustry($query, $status)
{
return $query->where('industry', $status);
}
/**
* Get all of the subscribers that belongs to the company.
*
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsToMany
*/
public function subscribers()
{
return $this->belongsToMany(
__NAMESPACE__ . '\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
)->where('object_type', __CLASS__);
}
public function owner()
{
return $this->belongsTo(Subscriber::class, 'owner_id', 'id');
}
public function getContactsCount()
{
return $this->subscribers()->count();
}
/**
* A Company has many notes and activities.
*
* @return \FluentCrm\Framework\Database\Orm\Relations\HasMany
*/
public function notes()
{
return $this->hasMany(CompanyNote::class, 'subscriber_id', 'id');
}
public function setMetaAttribute($meta)
{
$this->attributes['meta'] = \maybe_serialize($meta);
}
public function getMetaAttribute($meta)
{
$metaData = \maybe_unserialize($meta);
if (!$metaData) {
return [
'custom_values' => []
];
}
$metaDefaults = [
'custom_values' => []
];
return array_merge($metaDefaults, $metaData);
}
public function getCustomValues()
{
return Arr::get($this->meta, 'custom_values', []);
}
}
@@ -0,0 +1,91 @@
<?php
namespace FluentCrm\App\Models;
/**
* SubscriberNote Model - DB Model for Contact's notes
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class CompanyNote extends Model
{
protected $table = 'fc_subscriber_notes';
protected $guarded = ['id'];
protected $fillable = [
'subscriber_id',
'parent_id',
'created_by',
'type',
'title',
'description',
'created_at'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
if(empty($model->created_at)) {
$model->created_at = fluentCrmTimestamp();
}
$model->status = '_company_note_';
$model->updated_at = fluentCrmTimestamp();
$model->created_by = $model->created_by ?: get_current_user_id();
});
static::updated(function ($model) {
$model->updated_at = fluentCrmTimestamp();
});
static::addGlobalScope('status', function ($builder) {
$builder->where('status', '_company_note_'); // This disguised the Company Note from SubscriberNote
});
}
/**
* One2One: CompanyNote belongs to one Company
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
*/
public function company()
{
return $this->belongsTo(
__NAMESPACE__.'\Company', 'subscriber_id', 'id'
);
}
public function markAs($status)
{
$this->status = $status;
$this->save();
return $this;
}
public function createdBy()
{
if(!$this->created_by) {
return false;
}
$user = get_user_by('ID', $this->created_by);
if (!$user) {
return false;
}
return [
'ID' => $user->ID,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'display_name' => $user->display_name
];
}
}
@@ -0,0 +1,36 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\Framework\Support\Arr;
/**
* CustomCompanyField Model - DB Model for Company Contact Fields
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 2.8.50
*/
class CustomCompanyField extends CustomContactField
{
protected $globalMetaName = 'company_custom_fields';
public function getFieldGroups()
{
$fieldGroups = fluentcrm_get_option('company_field_groups');
if (!$fieldGroups) {
$fieldGroups = [
[
'slug' => 'default',
'title' => __('Custom Company Data', 'fluent-crm')
]
];
}
return $fieldGroups;
}
}
@@ -0,0 +1,269 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\Framework\Support\Arr;
/**
* CustomContactField Model - DB Model for Custom Contact Fields
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class CustomContactField
{
protected $globalMetaName = 'contact_custom_fields';
public function getGlobalFields($with = [])
{
$data['fields'] = fluentcrm_get_option($this->globalMetaName, []);
if (in_array('field_types', $with)) {
$data['field_types'] = $this->getFieldTypes();
}
if (in_array('field_groups', $with)) {
$data['field_groups'] = $this->getFieldGroups();
}
return $data;
}
public function getFieldTypes()
{
/**
* Modify the global custom contact field types for FluentCRM custom contact fields.
*
* The default field types are: 'text', 'textarea', 'number', 'single-select', 'multi-select', 'radio', 'checkbox', 'date', 'date_time'.
*
* @since 2.7.0
*
* @param array {
* An associative array of field types.
*
* @type array $text {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $textarea {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $number {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $single-select {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $multi-select {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $radio {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $checkbox {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $date {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* @type array $date_time {
* @type string $type The type of the field.
* @type string $label The label for the field.
* @type string $value_type The value type of the field.
* }
* }
*/
return apply_filters('fluent_crm/global_field_types', [
'text' => [
'type' => 'text',
'label' => __('Single Line Text', 'fluent-crm'),
'value_type' => 'string'
],
'textarea' => [
'type' => 'textarea',
'label' => __('Multi Line Text', 'fluent-crm'),
'value_type' => 'string'
],
'number' => [
'type' => 'number',
'label' => __('Numeric Field', 'fluent-crm'),
'value_type' => 'numeric'
],
'single-select' => [
'type' => 'select-one',
'label' => __('Select choice', 'fluent-crm'),
'value_type' => 'string'
],
'multi-select' => [
'type' => 'select-multi',
'label' => __('Multiple Select choice', 'fluent-crm'),
'value_type' => 'array'
],
'radio' => [
'type' => 'radio',
'label' => __('Radio Choice', 'fluent-crm'),
'value_type' => 'string'
],
'checkbox' => [
'type' => 'checkbox',
'label' => __('Checkboxes', 'fluent-crm'),
'value_type' => 'array'
],
'date' => [
'type' => 'date',
'label' => __('Date', 'fluent-crm'),
'value_type' => 'date'
],
'date_time' => [
'type' => 'date_time',
'label' => __('Date and Time', 'fluent-crm'),
'value_type' => 'datetime'
]
]);
}
public function saveGlobalFields($fields)
{
$slugs = [];
foreach ($fields as $field) {
if (isset($field['slug'])) {
$slugs[] = $field['slug'];
}
}
$formattedFields = [];
$keys = [];
foreach ($fields as $field) {
if (empty($field['slug'])) {
$field['slug'] = $this->generateSlug($field, $slugs);
}
if (in_array($field['slug'], $keys)) {
continue;
}
$keys[] = $field['slug'];
$formattedFields[] = $field;
}
fluentcrm_update_option($this->globalMetaName, $formattedFields);
return $formattedFields;
}
protected function generateSlug($field, $slugs)
{
$label = str_replace(' ', '_', $field['label']);
$label = sanitize_title($label, 'custom_field', 'view');
$label = substr($label, 0, 25);
$originalLabel = $label;
if (is_numeric($label)) {
$label = 'cf_' . $label;
}
$mainColumns = array_merge(
(new Subscriber)->getFillable(),
['id', 'updated_at']
);
if (in_array($label, $mainColumns)) {
$label = 'cf_' . $label;
}
$index = 1;
while (in_array($label, $slugs)) {
$label = $originalLabel . '_' . $index;
$index++;
}
return $label;
}
public function formatCustomFieldValues($values, $fields = [])
{
if (!$values) {
return $values;
}
if (!$fields) {
$rawFields = fluentcrm_get_option($this->globalMetaName, []);
foreach ($rawFields as $field) {
$fields[$field['slug']] = $field;
}
}
foreach ($values as $valueKey => $value) {
$isArrayType = Arr::get($fields, $valueKey . '.type') == 'checkbox' || Arr::get($fields, $valueKey . '.type') == 'select-multi';
if (!is_array($value) && $isArrayType) {
$itemValues = explode(',', $value);
$trimmedvalues = [];
foreach ($itemValues as $itemValue) {
$trimmedvalues[] = trim($itemValue);
}
if ($itemValue) {
$values[$valueKey] = $trimmedvalues;
}
}
}
return $values;
}
public function getFieldGroups()
{
$fieldGroups = fluentcrm_get_option('contact_field_groups');
if (!$fieldGroups) {
$fieldGroups = [
[
'slug' => 'default',
'title' => __('Custom Profile Data', 'fluent-crm')
]
];
}
return $fieldGroups;
}
public function updateGroupName($oldName, $newName)
{
$currentCustomFields = fluentcrm_get_option($this->globalMetaName);
$updatedCustomFields = [];
foreach ($currentCustomFields as $customField) {
if (isset($customField['group']) && $customField['group'] == $oldName) {
$customField['group'] = $newName;
}
$updatedCustomFields[] = $customField;
}
fluentcrm_update_option($this->globalMetaName, $updatedCustomFields);
return $updatedCustomFields;
}
}
@@ -0,0 +1,45 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\Helper;
/**
* CustomEmailCampaign Model - DB Model for Custom Emails
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class CustomEmailCampaign extends Campaign
{
protected static $type = 'custom_email_campaign';
public static function getMock()
{
$defaultTemplate = Helper::getDefaultEmailTemplate();
return [
'id' => '',
'title' => __('Custom Email', 'fluent-crm'),
'status' => 'published',
'template_id' => '',
'email_subject' => '',
'email_pre_header' => '',
'email_body' => '',
'utm_status' => 0,
'utm_source' => '',
'utm_medium' => '',
'utm_campaign' => '',
'utm_term' => '',
'utm_content' => '',
'design_template' => $defaultTemplate,
'settings' => (object)[
'template_config' => Helper::getTemplateConfig($defaultTemplate)
]
];
}
}
@@ -0,0 +1,50 @@
<?php
namespace FluentCrm\App\Models;
/**
* SubscriberNote Model - DB Model for Contact's notes
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class EventTracker extends Model
{
protected $table = 'fc_event_tracking';
protected $guarded = ['id'];
protected $fillable = [
'subscriber_id',
'counter',
'created_by',
'provider',
'event_key',
'title',
'value'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->created_by = $model->created_by ?: get_current_user_id();
});
}
/**
* One2One: SubscriberNote belongs to one Subscriber
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
*/
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
);
}
}
@@ -0,0 +1,206 @@
<?php
namespace FluentCrm\App\Models;
/**
* Funnel Model - DB Model for Automation Funnels
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Funnel extends Model
{
private static $type = 'funnels';
protected $table = 'fc_funnels';
protected $fillable = [
'type',
'title',
'trigger_name',
'status',
'conditions',
'settings',
'created_by',
'updated_at'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->type = self::$type;
});
static::addGlobalScope('type', function ($builder) {
$builder->where('fc_funnels.type', '=', self::$type);
});
}
public function scopePublished($query)
{
return $query->where('status', 'published');
}
public function actions()
{
return $this->hasMany(
__NAMESPACE__ . '\FunnelSequence', 'funnel_id', 'id'
);
}
public function subscribers()
{
return $this->hasMany(
__NAMESPACE__ . '\FunnelSubscriber', 'funnel_id', 'id'
);
}
public function setSettingsAttribute($settings)
{
$this->attributes['settings'] = \maybe_serialize($settings);
}
public function getSettingsAttribute($settings)
{
return \maybe_unserialize($settings);
}
public function setConditionsAttribute($conditions)
{
$this->attributes['conditions'] = \maybe_serialize($conditions);
}
public function getConditionsAttribute($conditions)
{
return \maybe_unserialize($conditions);
}
public function getSubscribersCount()
{
return $this->subscribers()->count();
}
public function updateMeta($key, $value)
{
fluentcrm_update_meta($this->id, __CLASS__, $key, $value);
}
public function getMeta($key, $default = '')
{
$meta = fluentcrm_get_meta($this->id, __CLASS__, $key);
if($meta) {
return $meta->value;
}
return $default;
}
public function deleteMeta($key)
{
fluentcrm_delete_meta($this->id, __CLASS__, $key);
}
public function labelsTerm()
{
return $this->belongsToMany(Label::class, 'fc_term_relations', 'object_id', 'term_id')
->wherePivot('object_type', __CLASS__);
}
public function labels()
{
$labelIds = TermRelation::where('object_id', $this->id)
->where('object_type', __CLASS__)
->pluck('term_id')
->toArray();
return Label::whereIn('id', $labelIds)->get();
}
public function getFormattedLabels()
{
$labels = $this->labels();
return $labels->map(function ($label) {
return [
'id' => $label->id,
'slug' => $label->slug,
'title' => $label->title,
'color' => $label->settings['color'] ?? ''
];
});
}
public function attachLabels($labelIds)
{
if (!is_array($labelIds)) {
$labelIds = [$labelIds];
}
$existingLabelIds = TermRelation::where('object_id', $this->id)
->where('object_type', __CLASS__)
->pluck('term_id')
->toArray();
$newLabelIds = array_diff($labelIds, $existingLabelIds);
if (!empty($newLabelIds)) {
foreach ($newLabelIds as $labelId) {
TermRelation::create([
'object_id' => $this->id,
'object_type' => __CLASS__,
'term_id' => $labelId
]);
}
}
return $this;
}
/**
* Replace existing funnel labels with the provided label IDs.
*
* @param array|int $labelIds
* @return $this
*/
public function syncLabels($labelIds)
{
if (!is_array($labelIds)) {
$labelIds = [$labelIds];
}
$existingLabelIds = TermRelation::where('object_id', $this->id)
->where('object_type', __CLASS__)
->pluck('term_id')
->toArray();
$labelIds = array_unique(array_filter(array_map('intval', $labelIds)));
$labelsToDetach = array_diff($existingLabelIds, $labelIds);
$labelsToAttach = array_diff($labelIds, $existingLabelIds);
if (!empty($labelsToDetach)) {
$this->detachLabels($labelsToDetach);
}
if (!empty($labelsToAttach)) {
$this->attachLabels($labelsToAttach);
}
return $this;
}
public function detachLabels($labelIds)
{
if (!is_array($labelIds)) {
$labelIds = [$labelIds];
}
TermRelation::where('object_id', $this->id)
->where('object_type', __CLASS__)
->whereIn('term_id', $labelIds)
->delete();
return $this;
}
}
@@ -0,0 +1,309 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\BlockParser;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
/**
* FunnelCampaign Model - DB Model for Automation Campaigns
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class FunnelCampaign extends Campaign
{
protected static $type = 'funnel_email_campaign';
protected $guarded = ['id'];
public static function getMock()
{
$defaultTemplate = Helper::getDefaultEmailTemplate();
return [
'id' => '',
'parent_id' => '',
'title' => __('Funnel Campaign Holder', 'fluent-crm'),
'status' => 'published',
'template_id' => '',
'email_subject' => '',
'email_pre_header' => '',
'email_body' => '',
'utm_status' => 0,
'utm_source' => '',
'utm_medium' => '',
'utm_campaign' => '',
'utm_term' => '',
'utm_content' => '',
'design_template' => $defaultTemplate,
'settings' => (object)[
'template_config' => Helper::getTemplateConfig($defaultTemplate),
'mailer_settings' => [
'from_name' => '',
'from_email' => '',
'reply_to_name' => '',
'reply_to_email' => '',
'is_custom' => 'no'
]
]
];
}
public function sendToCustomAddresses($addresses = [], $args = [], $refSubscriber = false)
{
if (!$addresses) {
return;
}
$time = current_time('mysql');
foreach ($addresses as $address) {
if (!is_email($address)) {
continue;
}
// check if the email has any subscriber
$subscriber = Subscriber::where('email', $address)->first();
if ($subscriber && $subscriber->status != 'subscribed') {
continue;
}
// We have to handle manually
$emailBody = (new BlockParser($refSubscriber))->parse($this->email_body);
$emailSubject = $this->email_subject;
if ($refSubscriber) {
/**
* Filter the campaign email body content.
*
* This filter allows you to modify the email body content before it is sent to the subscriber.
*
* @param string $emailBody The email body content.
* @param object $refSubscriber The subscriber object reference.
* @since 2.7.0
*
*/
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $refSubscriber);
/**
* Filter the email subject text for a campaign.
*
* This filter allows you to modify the email subject text before it is sent to the subscriber.
*
* @param string $emailSubject The original email subject text.
* @param object $refSubscriber The subscriber object reference.
*
* @return string The filtered email subject text.
* @since 2.7.0
*
*/
$emailSubject = apply_filters('fluent_crm/parse_campaign_email_text', $emailSubject, $refSubscriber);
}
$email = [
'campaign_id' => $this->id,
'email_address' => $address,
'email_subject' => $emailSubject,
'email_body' => $emailBody,
'created_at' => $time,
'updated_at' => $time,
'is_parsed' => 1,
'note' => __('Email Sent From Funnel', 'fluent-crm')
];
if ($subscriber) {
$email['subscriber_id'] = $subscriber->id;
}
if ($args) {
$email = wp_parse_args($email, $args);
}
$insertId = CampaignEmail::insert($email);
$emailHash = Helper::generateEmailHash($insertId);
CampaignEmail::where('id', $insertId)
->update([
'email_hash' => $emailHash
]);
}
}
/**
* Add one or more subscribers to the campaign
* @param array $subscriberIds
* @param array $emailArgs extra campaign_email args
* @param bool $isModel if the $subscriberIds is collection or not
* @return array
*/
public function subscribe($subscriberIds, $emailArgs = [], $isModel = false)
{
$updateIds = [];
$mailHeaders = Helper::getMailHeadersFromSettings(Arr::get($this->settings, 'mailer_settings', []));
if ($isModel) {
$subscribers = $subscriberIds;
} else {
$subscribers = Subscriber::whereIn('id', $subscriberIds)->get();
}
$sendableStatuses = fluentCrmEmailSendableStatuses();
foreach ($subscribers as $subscriber) {
if (!in_array($subscriber->status, $sendableStatuses, true)) {
continue; // We don't want to send emails to non-subscribed members
}
$time = fluentCrmTimestamp();
$email = [
'campaign_id' => $this->id,
'status' => $this->status,
'subscriber_id' => $subscriber->id,
'email_address' => $subscriber->email,
'email_headers' => $mailHeaders,
'created_at' => $time,
'updated_at' => $time,
'email_body' => '',
];
$subjectItem = $this->guessEmailSubject();
$emailSubject = $this->email_subject;
// Let's create the email body here
$rawTemplates = [
'raw_html',
'visual_builder',
'raw_classic'
];
$emailBody = $this->email_body;
if (!in_array($this->design_template, $rawTemplates)) {
$emailBody = (new BlockParser($subscriber))->parse($emailBody);
}
$emailBody = str_replace(['https://fonts.googleapis.com/css2', 'https://fonts.googleapis.com/css'], 'https://fonts.bunny.net/css', $emailBody);
/**
* Filter the email body content for a campaign.
*
* This filter allows you to modify the email body content before it is sent to the subscriber.
*
* @param string $emailBody The original email body content.
* @param object $subscriber The subscriber object containing subscriber details.
*
* @return string The filtered email body content.
* @since 2.8.44
*
*/
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
// email body creation done
if ($subjectItem && !empty($subjectItem->value)) {
$emailSubject = $subjectItem->value;
$email['email_subject_id'] = $subjectItem->id;
}
/**
* Filter the campaign email subject text.
*
* This filter allows you to modify the email subject text for a campaign.
*
* @param string $emailSubject The original email subject text.
* @param object $subscriber The subscriber object.
*
* @return string The filtered email subject text.
* @since 2.8.40
*
*/
$email['email_subject'] = apply_filters('fluent_crm/parse_campaign_email_text', $emailSubject, $subscriber);
if ($emailArgs) {
$email = wp_parse_args($emailArgs, $email);
}
$inserted = CampaignEmail::create($email);
$subscriber->campaign_id = $this->id;
$subscriber->email_id = $inserted->id;
$emailHash = Helper::generateEmailHash($inserted->id);
/**
* Filter the email body content of a campaign email.
*
* This filter allows you to modify the email body content before it is sent to the subscriber.
*
* @param string $emailBody The email body content.
* @param object $subscriber The subscriber object containing subscriber details.
*
* @return string The filtered email body content.
* @since 2.8.44
*
*/
$emailBody = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
$trackingType = fluentcrmTrackClicking();
if ($trackingType) {
$campaignUrls = Helper::urlReplaces($emailBody);
if ($campaignUrls) {
if ($trackingType === 'anonymous') {
$emailBody = Helper::attachAnonymousUrls($emailBody, $campaignUrls, $inserted->id, $emailHash);
} else {
$emailBody = Helper::attachUrls($emailBody, $campaignUrls, $inserted->id, $emailHash);
}
}
}
CampaignEmail::where('id', $inserted->id)
->update([
'email_hash' => $emailHash,
'email_body' => $emailBody,
'is_parsed' => 1
]);
$updateIds[] = $inserted->id;
}
$emailCount = $this->getEmailCount();
if ($emailCount != $this->recipients_count) {
$this->recipients_count = $emailCount;
$this->save();
}
return $updateIds;
}
public function processAndSubscribe($subscriber, $refData = [], $args = [])
{
foreach ($refData as $refKey => $data) {
$subscriber->{$refKey} = $data;
}
/*
* Note: We are not using the parse_campaign_email_text filter here
* Have a plan to remove this below commented code
*/
// We have to handle manually
// $emailBody = (new BlockParser($this->subscriber))->parse($this->email_body);
// $args['email_body'] = apply_filters('fluent_crm/parse_campaign_email_text', $emailBody, $subscriber);
// $args['email_subject'] = apply_filters('fluent_crm/parse_campaign_email_text', $this->email_subject, $subscriber);
// $args['is_parsed'] = 1;
return $this->subscribe([$subscriber], $args, true);
}
public function getOpenTrackingStatus($globalFallback = true)
{
return fluentcrmTrackEmailOpen();
}
public function getClickTrackingStatus($globalFallback = true)
{
return fluentcrmTrackClicking();
}
}
@@ -0,0 +1,55 @@
<?php
namespace FluentCrm\App\Models;
/**
* FunnelMetric Model - DB Model for Automation Analytics
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class FunnelMetric extends Model
{
protected $table = 'fc_funnel_metrics';
protected $fillable = [
'funnel_id',
'sequence_id',
'subscriber_id',
'benchmark_value',
'benchmark_currency',
'status',
'notes'
];
public function scopeStatus($query, $status = 'completed')
{
return $query->where('status', $status);
}
public function funnel()
{
return $this->belongsTo(
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
);
}
public function sequence()
{
return $this->belongsTo(
__NAMESPACE__ . '\FunnelSequence', 'sequence_id', 'id'
);
}
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
);
}
}
@@ -0,0 +1,98 @@
<?php
namespace FluentCrm\App\Models;
/**
* FunnelSequence Model - DB Model for Automation Sequences
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class FunnelSequence extends Model
{
protected $table = 'fc_funnel_sequences';
protected $fillable = [
'funnel_id',
'action_name',
'parent_id',
'condition_type',
'title',
'description',
'status',
'conditions',
'settings',
'delay',
'c_delay',
'sequence',
'created_by',
'type',
'note',
];
public static function boot()
{
parent::boot();
static::updating(function ($model) {
if (isset($model->settings) && is_array($model->settings)) {
$model->settings = \maybe_serialize($model->settings);
}
if (isset($model->conditions) && is_array($model->conditions)) {
$model->conditions = \maybe_serialize($model->conditions);
}
});
}
public function funnel()
{
return $this->belongsTo(
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
);
}
public function parent()
{
return $this->belongsTo(
__NAMESPACE__ . '\FunnelSequence', 'parent_id', 'id'
);
}
public function children()
{
return $this->hasMany(
__NAMESPACE__ . '\FunnelSequence', 'parent_id', 'id'
);
}
public function setSettingsAttribute($settings)
{
if (is_array($settings)) {
$this->attributes['settings'] = \maybe_serialize($settings);
} else {
$this->attributes['settings'] = $settings;
}
}
public function getSettingsAttribute($settings)
{
return \maybe_unserialize($settings);
}
public function setConditionsAttribute($conditions)
{
if (is_array($conditions)) {
$this->attributes['conditions'] = \maybe_serialize($conditions);
} else {
$this->attributes['conditions'] = $conditions;
}
}
public function getConditionsAttribute($conditions)
{
return \maybe_unserialize($conditions);
}
}
@@ -0,0 +1,76 @@
<?php
namespace FluentCrm\App\Models;
/**
* FunnelSubscriber Model - DB Model for Automation Subscribers
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class FunnelSubscriber extends Model
{
protected $table = 'fc_funnel_subscribers';
protected $fillable = [
'funnel_id',
'subscriber_id',
'status',
'type',
'next_sequence',
'next_sequence_id',
'last_sequence_id',
'last_sequence_status',
'last_executed_time',
'next_execution_time',
'starting_sequence_id',
'source_trigger_name',
'source_ref_id',
'notes'
];
public function scopeActive($query)
{
return $query->where('status', 'active');
}
public function funnel()
{
return $this->belongsTo(
__NAMESPACE__ . '\Funnel', 'funnel_id', 'id'
);
}
public function next_sequence_item()
{
return $this->belongsTo(
__NAMESPACE__ . '\FunnelSequence', 'next_sequence_id', 'id'
);
}
public function last_sequence()
{
return $this->belongsTo(
__NAMESPACE__ . '\FunnelSequence', 'last_sequence_id', 'id'
);
}
public function metrics()
{
return $this->hasMany(
__NAMESPACE__ . '\FunnelMetric', 'subscriber_id', 'subscriber_id'
);
}
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
);
}
}
@@ -0,0 +1,47 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\Framework\Database\Orm\Builder;
class Label extends Model
{
protected $table = 'fc_terms';
protected $guarded = ['id'];
/*
* taxonomy_name: global_label
* taxonomy_name global_label is the default taxonomy name/type for the Label
* so it is not required to pass the taxonomy_name while creating a label
*/
protected $fillable = ['parent_id', 'slug', 'title', 'description', 'position', 'settings', 'created_at', 'updated_at'];
protected $hidden = ['taxonomy_name'];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->taxonomy_name = $model->taxonomy_name ?: 'global_label'; // default type is label
});
static::addGlobalScope('taxonomy_name', function (Builder $builder) {
$builder->where('taxonomy_name', '=', 'global_label');
});
}
public function getSettingsAttribute($value)
{
return \maybe_unserialize($value);
}
public function setSettingsAttribute($value)
{
$this->attributes['settings'] = maybe_serialize($value);
}
}
@@ -0,0 +1,83 @@
<?php
namespace FluentCrm\App\Models;
/**
* Lists Model - DB Model for Contact Lists
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Lists extends Model
{
protected $table = 'fc_lists';
protected $guarded = ['id'];
/**
* $searchable Columns in table to search
* @var array
*/
protected $searchable = [
'title',
'slug',
'description'
];
/**
* Local scope to filter subscribers by search/query string
* @param ModelQueryBuilder $query
* @param string $search
* @return ModelQueryBuilder
*/
public function scopeSearchBy($query, $search)
{
if ($search) {
$fields = $this->searchable;
$query->where(function ($query) use ($fields, $search) {
$query->where(array_shift($fields), 'LIKE', "%$search%");
foreach ($fields as $field) {
$query->orWhere($field, 'LIKE', "$search%");
}
});
}
return $query;
}
/**
* Many2Many: List belongs to many Subscriber
*
* @return \FluentCrm\App\Models\Base\Collection
*/
public function subscribers()
{
return $this->belongsToMany(
__NAMESPACE__.'\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
)->where('object_type', __CLASS__);
}
public function totalCount()
{
return fluentCrmDb()->table('fc_subscriber_pivot')
->where('object_type', 'FluentCrm\App\Models\Lists')
->where('object_id', $this->id)
->count();
}
public function countByStatus($status = 'subscribed')
{
return fluentCrmDb()->table('fc_subscriber_pivot')
->where('fc_subscriber_pivot.object_type', 'FluentCrm\App\Models\Lists')
->where('fc_subscriber_pivot.object_id', $this->id)
->join('fc_subscribers', 'fc_subscribers.id', '=', 'fc_subscriber_pivot.subscriber_id')
->where('fc_subscribers.status', $status)
->count();
}
}
@@ -0,0 +1,41 @@
<?php
namespace FluentCrm\App\Models;
/**
* Meta Model - DB Model for Meta table
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Meta extends Model
{
protected $table = 'fc_meta';
protected $primaryKey = 'id';
protected $guarded = ['id'];
protected $fillable = [
'object_type',
'object_id',
'key',
'value',
'created_at',
'updated_at'
];
public function setValueAttribute($value)
{
$this->attributes['value'] = maybe_serialize($value);
}
public function getValueAttribute($value)
{
return maybe_unserialize($value);
}
}
@@ -0,0 +1,68 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\Framework\Database\Orm\Model as BaseModel;
use FluentCrm\Framework\Support\Str;
class Model extends BaseModel
{
public function __construct($attributes = [])
{
parent::__construct($attributes);
}
public function scopeLatest($query, $field = 'created_at')
{
return $query->orderBy($field, 'desc');
}
public function scopeNewest($query, $field = 'created_at')
{
return $query->orderBy($field, 'asc');
}
public function getPerPage()
{
return (isset($_REQUEST['per_page'])) ? intval($_REQUEST['per_page']) : 15;
}
/**
* Get a fresh timestamp for the model.
*
* @return \DateTime
*/
public function freshTimestamp()
{
return new \FluentCrm\Framework\Support\DateTime(current_time('mysql'));
}
protected function serializeDate(\DateTimeInterface $date)
{
return $date->format('Y-m-d H:i:s');
}
public function getTimezone()
{
return wp_timezone();
}
protected function asDateTime($value)
{
if (is_string($value) && Str::contains($value, 'T')) {
return new \FluentCrm\Framework\Support\DateTime($value);
}
return parent::asDateTime($value);
}
protected function originalIsNumericallyEquivalent($key)
{
$current = $this->attributes[$key];
$original = $this->original[$key];
return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;
}
}
@@ -0,0 +1,47 @@
<?php
namespace FluentCrm\App\Models;
/**
* Subject Model - DB Model for Email Subjects
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Subject extends Model
{
protected $table = 'fc_meta';
protected $guarded = ['id'];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->object_type = __class__;
});
static::saving(function ($model) {
$model->object_type = __class__;
});
static::addGlobalScope('object_type', function ($builder) {
$builder->where('object_type', '=', __class__);
});
}
public function campaign()
{
return $this->belongsTo(__NAMESPACE__.'\Campaign', 'object_id', 'id')
->withoutGlobalScope('type');
}
public function emails()
{
return $this->hasMany(__NAMESPACE__.'\CampaignEmail', 'email_subject_id', 'id');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
<?php
namespace FluentCrm\App\Models;
/**
* SubscriberMeta Model - DB Model for Contact meta data
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class SubscriberMeta extends Model
{
protected $table = 'fc_subscriber_meta';
protected $guarded = ['id'];
/**
* One2One: SubscriberNote belongs to one Subscriber
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
*/
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__.'\Subscriber', 'subscriber_id', 'id'
);
}
public function scopeFilterByKey($query, $key)
{
if ($key) {
$query->where('key', $key);
}
return $query;
}
public function setValueAttribute($value)
{
$this->attributes['value'] = maybe_serialize($value);
}
public function getValueAttribute($value)
{
return maybe_unserialize($value);
}
}
@@ -0,0 +1,93 @@
<?php
namespace FluentCrm\App\Models;
/**
* SubscriberNote Model - DB Model for Contact's notes
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class SubscriberNote extends Model
{
protected $table = 'fc_subscriber_notes';
protected $guarded = ['id'];
protected $fillable = [
'subscriber_id',
'parent_id',
'created_by',
'type',
'title',
'description',
'created_at'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->created_at)) {
$model->created_at = fluentCrmTimestamp();
}
$model->updated_at = fluentCrmTimestamp();
$model->created_by = $model->created_by ?: get_current_user_id();
});
static::updated(function ($model) {
$model->updated_at = fluentCrmTimestamp();
});
static::addGlobalScope('status', function ($builder) {
$builder->whereNotIn('status', ['_company_note_', '_system_log_']);
});
}
/**
* One2One: SubscriberNote belongs to one Subscriber
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsTo
*/
public function subscriber()
{
return $this->belongsTo(
__NAMESPACE__ . '\Subscriber', 'subscriber_id', 'id'
);
}
public function markAs($status)
{
$this->status = $status;
$this->save();
return $this;
}
public function createdBy()
{
if (!$this->created_by) {
return false;
}
$user = User::find($this->created_by);
if (!$user) {
return false;
}
if (!$user) {
return false;
}
return [
'ID' => $user->ID,
'display_name' => $user->display_name,
'photo' => $user->photo
];
}
}
@@ -0,0 +1,105 @@
<?php
namespace FluentCrm\App\Models;
/**
* SubscriberPivot Model - DB Model for Contact's relationships
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class SubscriberPivot extends Model
{
protected $table = 'fc_subscriber_pivot';
protected $guarded = ['id'];
public function scopeFilter($query, $constraints)
{
foreach ($constraints as $filed => $value) {
$query->where($filed, $value);
}
return $query;
}
/**
* Save an entry to the subscriber pivot table.
*
* @param array $attributes
* @return int
*/
public static function store($attributes)
{
$attributes += [
'created_at' => $now = current_time('mysql'),
'updated_at' => $now
];
return static::insert($attributes);
}
/**
* Attach tags/lists to the subscriber.
*
* @param array $items
* @param int $subscriber
* @param string $type
*/
public static function attach($items, $subscriber, $type)
{
$objectIds = [];
foreach ($items as $objectId) {
$objectIds = array_merge($objectIds, [$objectId]);
static::firstOrCreate([
'subscriber_id' => $subscriber,
'object_id' => $objectId,
'object_type' => $type
]);
}
if ($objectIds) {
$function = static::getFunctionName($type, __FUNCTION__);
$function($objectIds, Subscriber::find($subscriber));
}
}
/**
* Detach tags/lists from the subscriber.
*
* @param array $items
* @param int $subscriber
* @param string $type
*/
public static function detach($items, $subscriber, $type)
{
if ($items) {
static::where('subscriber_id', $subscriber)
->where('object_type', $type)
->whereIn('object_id', $items)
->delete();
$function = static::getFunctionName($type, __FUNCTION__);
$function($items, Subscriber::find($subscriber));
}
}
private static function getFunctionName($type, $prefix)
{
$parts = explode('\\', $type);
$typeOfObject = end($parts);
$function = $typeOfObject == 'Tag' ? 'tags' : 'lists';
if ($prefix == 'attach') {
return "fluentcrm_contact_added_to_$function";
} else if ($prefix == 'detach') {
return "fluentcrm_contact_removed_from_$function";
}
}
}
@@ -0,0 +1,56 @@
<?php
namespace FluentCrm\App\Models;
/**
* System Log Model - DB Model for System Logs & Activities
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class SystemLog extends Model
{
protected $table = 'fc_subscriber_notes';
protected $guarded = ['id'];
protected $fillable = [
'subscriber_id',
'parent_id',
'created_by',
'type',
'title',
'description',
'created_at'
];
public static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->created_at)) {
$model->created_at = fluentCrmTimestamp();
}
if (empty($model->subscriber_id)) {
$model->subscriber_id = 0;
}
$model->status = '_system_log_';
$model->updated_at = fluentCrmTimestamp();
});
static::updated(function ($model) {
$model->updated_at = fluentCrmTimestamp();
});
static::addGlobalScope('status', function ($builder) {
$builder->where('status', '_system_log_');
});
}
}
@@ -0,0 +1,82 @@
<?php
namespace FluentCrm\App\Models;
/**
* Tag Model - DB Model for Contact's Tags
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Tag extends Model
{
protected $table = 'fc_tags';
protected $guarded = ['id'];
/**
* $searchable Columns in table to search
* @var array
*/
protected $searchable = [
'title',
'slug',
'description'
];
/**
* Local scope to filter subscribers by search/query string
* @param \FluentCrm\Framework\Database\Query\Builder $query
* @param string $search
* @return \FluentCrm\Framework\Database\Query\Builder
*/
public function scopeSearchBy($query, $search)
{
if ($search) {
$fields = $this->searchable;
$query->where(function ($query) use ($fields, $search) {
$query->where(array_shift($fields), 'LIKE', "%$search%");
foreach ($fields as $field) {
$query->orWhere($field, 'LIKE', "$search%");
}
});
}
return $query;
}
/**
* Get all of the subscribers that belongs to the tag.
*
* @return \FluentCrm\Framework\Database\Orm\Relations\BelongsToMany
*/
public function subscribers()
{
return $this->belongsToMany(
__NAMESPACE__.'\Subscriber', 'fc_subscriber_pivot', 'object_id', 'subscriber_id'
)->where('object_type', __CLASS__);
}
public function totalCount()
{
return fluentCrmDb()->table('fc_subscriber_pivot')
->where('object_type', 'FluentCrm\App\Models\Tag')
->where('object_id', $this->id)
->count();
}
public function countByStatus($status = 'subscribed')
{
return fluentCrmDb()->table('fc_subscriber_pivot')
->where('fc_subscriber_pivot.object_type', 'FluentCrm\App\Models\Tag')
->where('fc_subscriber_pivot.object_id', $this->id)
->join('fc_subscribers', 'fc_subscribers.id', '=', 'fc_subscriber_pivot.subscriber_id')
->where('fc_subscribers.status', $status)
->count();
}
}
@@ -0,0 +1,52 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\Libs\Parser\Parser;
/**
* Template Model - DB Model for Email templates
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Template extends Model
{
const CREATED_AT = 'post_date';
const UPDATED_AT = 'post_modified';
protected $table = 'posts';
protected $primaryKey = 'ID';
public function scopeEmailTemplates($query, $types = ['publish'])
{
return $query->where(
'post_type', fluentcrmTemplateCPTSlug()
)->whereIn('post_status', $types);
}
public function scopeCampaignTemplate($query)
{
return $query->where(
'post_type', fluentcrmCampaignTemplateCPTSlug()
)->where('post_status', 'publish');
}
public function campaign()
{
return $this->hasOne(__NAMESPACE__.'\\'.'Campaign', 'template_id', 'ID');
}
public function render($content = null)
{
$content = $content ?: $this->post_content;
return Parser::parse($content, []);
}
}
@@ -0,0 +1,24 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\Framework\Database\Orm\Model;
class TermRelation extends Model
{
protected $table = 'fc_term_relations';
protected $fillable = [
'term_id',
'object_type',
'object_id',
'settings'
];
public $timestamps = false;
protected $casts = [
'settings' => 'array'
];
}
@@ -0,0 +1,83 @@
<?php
namespace FluentCrm\App\Models;
/**
* UrlStores Model - DB Model for Short Urls
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class UrlStores extends Model
{
protected $table = 'fc_url_stores';
protected $guarded = ['id'];
public static function getUrlSlug($longUrl)
{
// Normalize URL before lookup and storage
$longUrl = str_replace("\xE2\x80\x8B", '', $longUrl);
$longUrl = htmlspecialchars_decode($longUrl);
static $urls = [];
$cacheKey = md5($longUrl);
if (isset($urls[$cacheKey])) {
return $urls[$cacheKey];
}
$isExist = self::where('url', $longUrl)->first();
if ($isExist) {
$urls[$cacheKey] = $isExist->short;
return $isExist->short;
}
$maxRetries = 3;
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
$short = self::generateRandomSlug();
try {
self::insert([
'url' => $longUrl,
'short' => $short,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
]);
$urls[$cacheKey] = $short;
return $short;
} catch (\Exception $e) {
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
continue;
}
throw $e;
}
}
return '';
}
public static function generateRandomSlug($length = 6)
{
$chars = '0123456789abcdefghijklmnopqrstuvwxyz';
$charsLen = strlen($chars);
$slug = '';
$bytes = random_bytes($length);
for ($i = 0; $i < $length; $i++) {
$slug .= $chars[ord($bytes[$i]) % $charsLen];
}
return $slug;
}
public static function getRowByShort($short)
{
global $wpdb;
return $wpdb->get_row($wpdb->prepare("SELECT * FROM " . $wpdb->prefix . "fc_url_stores WHERE BINARY `short` = %s ORDER BY `id` DESC LIMIT 1", $short));
}
}
@@ -0,0 +1,48 @@
<?php
namespace FluentCrm\App\Models;
/**
* User Model - DB Model for WordPress Users Table
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class User extends Model
{
protected $table = 'users';
protected $primaryKey = 'ID';
protected $hidden = ['user_pass', 'user_activation_key'];
protected $appends = [ 'photo'];
/**
* Accessor to get dynamic photo attribute
* @return string
*/
public function getPhotoAttribute()
{
$contact = Subscriber::where('user_id', $this->ID);
if(!empty($this->attributes['user_email'])) {
$contact->orWhere('email', $this->attributes['user_email']);
}
$contact = $contact->first();
if($contact) {
return $contact->photo;
}
if(empty($this->attributes['user_email'])) {
return '';
}
return fluentcrmGravatar($this->attributes['user_email'], $this->attributes['display_name']);
}
}
@@ -0,0 +1,105 @@
<?php
namespace FluentCrm\App\Models;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
/**
* Webhook Model - DB Model for Webhooks
*
* Database Model
*
* @package FluentCrm\App\Models
*
* @version 1.0.0
*/
class Webhook extends Meta
{
protected $fillable = [
'id',
'key',
'value',
'object_type'
];
public static function boot()
{
parent::boot();
static::addGlobalScope('type', function ($builder) {
$builder->where(function($query) {
$query->where('object_type', '=', 'webhook')
->orWhere('object_type', 'LIKE', 'webhook_%');
});
});
}
public function getFields()
{
$contactFields = [
'fields' => [],
'custom_fields' => []
];
foreach (Subscriber::mappables() as $key => $column) {
$contactFields['fields'][] = ['key' => $key, 'field' => $column];
}
foreach ((new CustomContactField)->getGlobalFields()['fields'] as $field) {
$contactFields['custom_fields'][] = ['key' => $field['slug'], 'field' => $field['label']];
}
return $contactFields;
}
public function getSchema()
{
$schema = [
'name' => '',
'lists' => [],
'tags' => [],
'url' => '',
'status' => ''
];
if (Helper::isCompanyEnabled()) {
$schema['companies'] = [];
}
return $schema;
}
public function store($data)
{
$key = wp_generate_uuid4();
$webhookUrl = site_url("?fluentcrm=1&route=contact&hash={$key}");
return static::create([
'object_type' => 'webhook',
'key' => $key,
'value' => array_merge($data, [
'url' => $webhookUrl
]),
]);
}
public function saveChanges($data)
{
$data['tags'] = Arr::get($data, 'tags', []);
$data['lists'] = Arr::get($data, 'lists', []);
$data['companies'] = Arr::get($data, 'companies', []);
$this->value = array_merge(
$this->value,
array_diff_key($data, [
'id' => '', 'url' => ''
])
);
$this->save();
return $this;
}
}