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,41 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\ActivityLog;
use FluentCrm\Framework\Http\Request\Request;
class ActivityLogController extends Controller
{
/**
* Get all the System Logs
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return array || \WP_REST_Response
*/
public function index(Request $request)
{
$search = sanitize_text_field($request->get('search'));
$logs = ActivityLog::orderBy('id', 'DESC');
if (!empty($search)) {
$logs = $logs->where('action', 'LIKE', "%{$search}%")
->orWhere('description', 'LIKE', "%{$search}%");
}
$logs = $logs->paginate($request->per_page ?: 20);
return [
'logs' => $logs
];
}
public function deleteAll(Request $request)
{
ActivityLog::where('id', '>', 0)->delete();
return [
'message' => __('All activity logs have been deleted', 'fluent-crm')
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,489 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Campaign;
use FluentCrm\App\Models\CampaignUrlMetric;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Http\Request\Request;
/**
* CampaignAnalyticsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class CampaignAnalyticsController extends Controller
{
public function getLinksReport(CampaignUrlMetric $campaignUrlMetric, $campaignId)
{
$campaign = Campaign::withoutGlobalScopes()->findOrFail($campaignId);
$clickStatus = $campaign->settings['click_tracker'] ?? '';
$openStatus = $campaign->settings['open_tracker'] ?? '';
if ($clickStatus === '') {
$clickStatus = fluentcrmTrackClicking();
}
if ($openStatus === '') {
$openStatus = fluentcrmTrackEmailOpen();
}
$links = array_values($campaignUrlMetric->getLinksReport($campaign));
return $this->sendSuccess([
'links' => $links,
'click_status' => $clickStatus,
'open_status' => $openStatus
]);
}
public function getRevenueReport(Request $request, $campaignId)
{
$limit = intval($request->get('per_page', 10));
$offset = (intval($request->get('page', 1)) - 1) * $limit;
$sources = $this->getActiveRevenueSources();
$multiSource = count($sources) > 1;
if (empty($sources)) {
return [
'orders' => [],
'labels' => $this->getRevenueLabels(false),
'total' => 0
];
}
// Build a single newest-first index across every active commerce source so
// pagination spans them all. Within each source, ids stay in DB-newest order.
$index = [];
foreach ($sources as $source) {
foreach ($this->getAttributedOrderIds($source, $campaignId) as $orderId) {
$index[] = ['source' => $source, 'order_id' => (int) $orderId];
}
}
$totalOrders = count($index);
$pageEntries = array_slice($index, $offset, $limit);
$orders = [];
foreach ($pageEntries as $entry) {
$row = $this->formatRevenueRow($entry['source'], $entry['order_id'], $multiSource);
if ($row) {
$orders[] = $row;
}
}
return [
'orders' => $orders,
'labels' => $this->getRevenueLabels($multiSource),
'total' => $totalOrders
];
}
public function getRevenueReSyncReport(Request $request, $campaignId)
{
$sources = $this->getActiveRevenueSources();
if (empty($sources)) {
return [
'message' => __('No revenue found for this campaign', 'fluent-crm')
];
}
$revenueData = ['orderIds' => []];
$primaryCurrency = null;
foreach ($sources as $source) {
$sourceData = $this->reSyncSourceRevenue($source, $campaignId);
foreach ($sourceData['orderIds'] as $oid) {
if (!in_array($oid, $revenueData['orderIds'])) {
$revenueData['orderIds'][] = $oid;
}
}
foreach ($sourceData['totals'] as $currency => $cents) {
if (!isset($revenueData[$currency])) {
$revenueData[$currency] = 0;
if ($primaryCurrency === null) {
$primaryCurrency = $currency;
}
}
$revenueData[$currency] += $cents;
}
}
if (empty($revenueData['orderIds'])) {
return [
'message' => __('No order found to re-sync', 'fluent-crm')
];
}
fluentcrm_update_campaign_meta($campaignId, '_campaign_revenue', $revenueData);
$primaryTotal = $primaryCurrency ? $revenueData[$primaryCurrency] : 0;
return [
'message' => __('Revenue has been re-synced successfully', 'fluent-crm'),
'total' => number_format($primaryTotal / 100, 2)
];
}
/**
* Active commerce sources that participate in campaign revenue attribution.
* Order matters: it determines display precedence within the merged report.
*/
protected function getActiveRevenueSources()
{
$sources = [];
if (defined('WC_PLUGIN_FILE')) {
$sources[] = 'woo';
}
if (Helper::isEdd3()) {
$sources[] = 'edd';
}
if (defined('FLUENTCART_VERSION')) {
$sources[] = 'fct';
}
return $sources;
}
/**
* Lightweight index query — returns just order IDs attributed to this campaign,
* newest-first per source. Used both for paginated report rendering and re-sync.
*/
protected function getAttributedOrderIds($source, $campaignId)
{
if ($source === 'woo') {
if (Helper::isWooHposEnabled()) {
return fluentCrmDb()->table('wc_orders_meta')
->where('meta_key', '_fc_cid')
->where('meta_value', $campaignId)
->orderBy('id', 'DESC')
->get()
->pluck('order_id')
->map(function ($orderId) {
return intval($orderId);
})
->all();
}
return fluentCrmDb()->table('postmeta')
->where('meta_key', '_fc_cid')
->where('meta_value', $campaignId)
->orderBy('meta_id', 'DESC')
->get()
->pluck('post_id')
->map(function ($orderId) {
return intval($orderId);
})
->all();
}
if ($source === 'edd') {
/*
* EDD 3 writes order attribution meta to edd_ordermeta via the
* order meta API. Do not read legacy postmeta/edd_payment records.
*/
return fluentCrmDb()->table('edd_ordermeta')
->where('meta_key', '_fc_cid')
->where('meta_value', $campaignId)
->orderBy('meta_id', 'DESC')
->get()
->pluck('edd_order_id')
->map(function ($orderId) {
return intval($orderId);
})
->all();
}
if ($source === 'fct') {
return fluentCrmDb()->table('fct_order_meta')
->where('meta_key', '_fc_cid')
->where('meta_value', $campaignId)
->orderBy('id', 'DESC')
->get()
->pluck('order_id')
->map(function ($orderId) {
return intval($orderId);
})
->all();
}
return [];
}
/**
* Sum NET revenue per currency for one source — i.e. only orders in a successful
* (paid/completed) status, with refunded amounts subtracted. Returns
* `['orderIds' => [int...], 'totals' => ['usd' => cents, ...]]`.
* Orders that net to zero or below (fully refunded, cancelled, pending) are skipped
* so they don't pollute the order list with non-revenue rows.
*/
protected function reSyncSourceRevenue($source, $campaignId)
{
$result = ['orderIds' => [], 'totals' => []];
$orderIds = $this->getAttributedOrderIds($source, $campaignId);
if (!$orderIds) {
return $result;
}
if ($source === 'woo') {
$paidStatuses = function_exists('wc_get_is_paid_statuses') ? wc_get_is_paid_statuses() : ['processing', 'completed'];
$currency = strtolower(get_woocommerce_currency());
foreach ($orderIds as $orderId) {
$order = wc_get_order($orderId);
if (!$order || !$order->get_id()) {
continue;
}
if (!in_array($order->get_status(), $paidStatuses, true)) {
continue;
}
$netCents = intval(((float) $order->get_total() - (float) $order->get_total_refunded()) * 100);
if ($netCents <= 0) {
continue;
}
$result['orderIds'][] = (int) $order->get_id();
$result['totals'][$currency] = ($result['totals'][$currency] ?? 0) + $netCents;
}
return $result;
}
if ($source === 'edd') {
// EDD 3 keeps canonical status and refund data in order tables.
$completeStatuses = ['complete', 'completed', 'partially_refunded'];
foreach ($orderIds as $orderId) {
$payment = new \EDD_Payment($orderId);
if (!$payment || !$payment->ID) {
continue;
}
if (!in_array($payment->status, $completeStatuses, true)) {
continue;
}
$netTotal = function_exists('edd_get_order_total')
? edd_get_order_total($payment->ID)
: $payment->total;
$netCents = intval(((float) $netTotal) * 100);
if ($netCents <= 0) {
continue;
}
$currency = strtolower(edd_get_payment_currency_code($payment->ID) ?: 'usd');
$result['orderIds'][] = (int) $payment->ID;
$result['totals'][$currency] = ($result['totals'][$currency] ?? 0) + $netCents;
}
return $result;
}
if ($source === 'fct') {
// Canonical "successful" set: paid, partially_paid, partially_refunded.
// Net revenue subtracts total_refund below so partial refunds still contribute.
$successStatuses = \FluentCart\App\Helpers\Status::getOrderPaymentSuccessStatuses();
$orders = \FluentCart\App\Models\Order::query()
->whereIn('id', $orderIds)
->whereIn('payment_status', $successStatuses)
->get();
foreach ($orders as $order) {
$netCents = (int) $order->total_amount - (int) ($order->total_refund ?? 0);
if ($netCents <= 0) {
continue;
}
$currency = strtolower($order->currency ?: 'usd');
$result['orderIds'][] = (int) $order->id;
$result['totals'][$currency] = ($result['totals'][$currency] ?? 0) + $netCents;
}
return $result;
}
return $result;
}
/**
* Render a single order row for the merged revenue table. The `source` key
* is added when more than one commerce platform is contributing data.
*/
protected function formatRevenueRow($source, $orderId, $multiSource)
{
$row = null;
if ($source === 'woo') {
$row = $this->formatWooOrderRow($orderId);
} else if ($source === 'edd') {
$row = $this->formatEddOrderRow($orderId);
} else if ($source === 'fct') {
$row = $this->formatFluentCartOrderRow($orderId);
}
if (!$row) {
return null;
}
if ($multiSource) {
$row = ['source' => $this->getSourceLabel($source)] + $row;
}
return $row;
}
protected function getSourceLabel($source)
{
$labels = [
'woo' => 'WooCommerce',
'edd' => 'EDD',
'fct' => 'FluentCart',
];
return $labels[$source] ?? $source;
}
protected function getRevenueLabels($multiSource)
{
$labels = [
'order' => '#',
'title' => __('Customer', 'fluent-crm'),
'status' => __('Status', 'fluent-crm'),
'date' => __('Date', 'fluent-crm'),
'total' => __('Total', 'fluent-crm'),
'action' => __('View', 'fluent-crm'),
];
if ($multiSource) {
$labels = ['source' => __('Source', 'fluent-crm')] + $labels;
}
return $labels;
}
protected function formatWooOrderRow($orderId)
{
$order = wc_get_order($orderId);
if (!$order || !$order->get_id()) {
return null;
}
/* translators: 1: billing first name, 2: billing last name */
$buyer = trim(sprintf(_x('%1$s %2$s', 'full name', 'fluent-crm'), $order->get_billing_first_name(), $order->get_billing_last_name()));
$order_timestamp = $order->get_date_created() ? $order->get_date_created()->getTimestamp() : '';
if (!$order_timestamp) {
$show_date = '&ndash;';
} else if ($order_timestamp > strtotime('-1 day', time()) && $order_timestamp <= time()) {
$show_date = sprintf(
/* translators: %s: human-readable time difference */
_x('%s ago', '%s = human-readable time difference', 'fluent-crm'),
human_time_diff($order->get_date_created()->getTimestamp(), time())
);
} else {
/**
* Determine the date format for displaying the order creation date in the WooCommerce admin in FluentCRM.
*
* @param string The date format to be used. Default is 'M j, Y'.
* @param string The context for the date format. Default is 'woocommerce'.
* @since 2.2.0
*/
$show_date = $order->get_date_created()->date_i18n(apply_filters('woocommerce_admin_order_date_format', __('M j, Y', 'fluent-crm')));
}
$editUrl = admin_url('post.php?post=' . absint($order->get_id()) . '&action=edit');
return [
'order' => '#' . esc_html($order->get_order_number()),
'title' => '<a href="' . esc_url($editUrl) . '" class="order-view"><strong>' . esc_html($buyer) . '</strong></a>',
'status' => wc_get_order_status_name($order->get_status()),
'date' => $show_date,
'total' => $order->get_formatted_order_total(),
'action' => '<a href="' . esc_url($editUrl) . '">' . esc_html__('View', 'fluent-crm') . '</a>',
];
}
protected function formatEddOrderRow($orderId)
{
$payment = new \EDD_Payment($orderId);
if (!$payment || !$payment->ID) {
return null;
}
$orderActionHtml = '<a href="' . add_query_arg('id', $payment->ID, admin_url('edit.php?post_type=download&page=edd-payment-history&view=view-order-details')) . '">' . esc_html__('View', 'fluent-crm') . '</a>';
$amount = !empty($payment->total) ? $payment->total : 0;
$customer_id = edd_get_payment_customer_id($payment->ID);
if (!empty($customer_id)) {
$customer = new \EDD_Customer($customer_id);
$customerName = '<a href="' . esc_url(admin_url("edit.php?post_type=download&page=edd-customers&view=overview&id=$customer_id")) . '">' . esc_html($customer->name) . '</a>';
} else {
$email = edd_get_payment_user_email($payment->ID);
$customerName = '<a href="' . esc_url(admin_url("edit.php?post_type=download&page=edd-payment-history&s=$email")) . '">' . esc_html__('(customer missing)', 'fluent-crm') . '</a>';
}
return [
'order' => '#' . $payment->number,
'title' => $customerName,
'status' => $payment->status_nicename,
'date' => date_i18n(get_option('date_format'), strtotime($payment->date)),
'total' => edd_currency_filter(edd_format_amount($amount), edd_get_payment_currency_code($payment->ID)),
'action' => $orderActionHtml,
];
}
protected function formatFluentCartOrderRow($orderId)
{
$order = \FluentCart\App\Models\Order::with('customer')->find($orderId);
if (!$order) {
return null;
}
$customerName = '';
if ($order->customer) {
$customerName = trim($order->customer->first_name . ' ' . $order->customer->last_name);
if (!$customerName) {
$customerName = $order->customer->email;
}
}
$orderUrl = admin_url('admin.php?page=fluent-cart#/orders/' . $order->id . '/view');
return [
'order' => '#' . ($order->invoice_no ?: $order->id),
'title' => '<a target="_blank" rel="noopener" href="' . esc_url($orderUrl) . '">' . esc_html($customerName) . '</a>',
'status' => esc_html(\FluentCrm\App\Services\Helper::getStatusText($order->status)),
'date' => date_i18n(get_option('date_format'), strtotime($order->created_at)),
'total' => \FluentCart\App\Helpers\Helper::toDecimal($order->total_amount, true, $order->currency),
'action' => '<a target="_blank" rel="noopener" href="' . esc_url($orderUrl) . '">' . esc_html__('View', 'fluent-crm') . '</a>',
];
}
public function getUnsubscribers(Request $request, $campaignId)
{
$unsubscribes = CampaignUrlMetric::with('subscriber')
->where('campaign_id', $campaignId)
->where('type', 'unsubscribe')
->paginate();
foreach ($unsubscribes as $unsubscribe) {
$unsubscribe->subscriber->reason = $unsubscribe->subscriber->unsubscribeReason();
}
return [
'unsubscribes' => $unsubscribes
];
}
public function getSegmentedContacts(Request $request, $campaignId)
{
$campaign = Campaign::findOrFail($campaignId);
$contactsModel = $campaign->getSubscribersModel();
$search = $request->getSafe('search', 'sanitize_text_field');
if ($search) {
$contactsModel->searchBy($search);
}
if ($orderBy = $request->getSafe('sort_by', 'sanitize_sql_orderby', 'id')) {
$orderType = $request->getSafe('sort_type', 'sanitize_sql_orderby', 'desc');
$contactsModel->orderBy($orderBy, $orderType);
}
$contacts = $contactsModel->with(['lists', 'tags'])->paginate();
return [
'subscribers' => $contacts
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,924 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Http\Controllers\Controller;
use FluentCrm\App\Models\Company;
use FluentCrm\App\Models\CompanyNote;
use FluentCrm\App\Models\CustomCompanyField;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\SubscriberNote;
use FluentCrm\App\Services\AutoSubscribe;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Libs\FileSystem;
use FluentCrm\App\Services\Sanitize;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Support\Collection;
class CompanyController extends Controller
{
public function index(Request $request)
{
$order = [
'by' => $request->getSafe('sort_by', 'sanitize_sql_orderby', 'id'),
'order' => $request->getSafe('sort_order', 'sanitize_sql_orderby', 'DESC')
];
$companies = Company::orderBy($order['by'], $order['order'])
->with(['owner'])
->searchBy($request->getSafe('search', 'sanitize_text_field'));
$inlineFilters = $request->get('inline_filters', []);
if ($inlineFilters && is_array($inlineFilters)) {
$inlineFilters = array_filter($inlineFilters);
foreach ($inlineFilters as $key => $values) {
if (!is_array($values)) {
continue;
}
$values = array_map('sanitize_text_field', $values);
if ($key == 'company_categories') {
$companies->whereIn('industry', $values);
} else if ($key == 'company_types') {
$companies->whereIn('type', $values);
}
}
}
$companies = $companies->paginate();
foreach ($companies as $company) {
$company->contacts_count = $company->getContactsCount();
}
return [
'companies' => $companies
];
}
public function searchCompanies(Request $request)
{
$search = $request->getSafe('search', 'sanitize_text_field');
$companies = Company::orderBy('name', 'ASC')
->searchBy($search);
$subscriberId = $request->getSafe('subscriber_id', 'intval');
if ($subscriberId) {
$companies = $companies->doesnthave('subscribers', 'and', function ($query) use ($subscriberId) {
$query->where('fc_subscribers.id', $subscriberId);
});
}
$companies = $companies->limit(50)->get();
$formatted = [];
$values = (array)$request->get('values', []);
$pushedIds = [];
foreach ($companies as $company) {
$pushedIds[] = $company->id;
$formatted[] = [
'id' => $company->id,
'name' => $company->name,
'email' => $company->email,
'logo' => $company->logo,
'phone' => $company->phone,
'website' => $company->website
];
}
if ($values && $newIds = array_diff($values, $pushedIds)) {
$newItems = Company::whereIn('id', $newIds)
->get();
foreach ($newItems as $item) {
$formatted[] = [
'id' => $item->id,
'name' => $item->name,
'email' => $item->email,
'logo' => $item->logo,
'phone' => $item->phone,
'website' => $item->website
];
}
}
return [
'results' => $formatted,
'has_more' => Company::count() >= 50
];
}
public function searchUnattachedContacts(Request $request)
{
$search = $request->getSafe('search', 'sanitize_text_field');
$companyId = $request->getSafe('company_id', 'intval', '');
$contacts = Subscriber::orderBy('id', 'DESC')
->searchBy($search)
->whereDoesntHave('companies', function ($query) use ($companyId) {
$query->where('fc_companies.id', $companyId);
})
->limit($request->getSafe('limit', 'intval', 20))
->get();
return [
'results' => $contacts
];
}
public function attachSubscribers(Request $request)
{
$subscriberIds = $request->get('subscriber_ids');
$companyIds = $request->get('company_ids');
$result = FluentCrmApi('companies')->attachContactsByIds($subscriberIds, $companyIds);
if (!$result) {
return $this->sendError('Invalid data', 422);
}
return [
'message' => __('Selected Companies have been attached successfully', 'fluent-crm'),
'companies' => $result['companies']
];
}
public function detachSubscribers(Request $request)
{
$subscriberIds = $request->get('subscriber_ids');
$companyIds = $request->get('company_ids');
$result = FluentCrmApi('companies')->detachContactsByIds($subscriberIds, $companyIds);
if (!$result) {
return $this->sendError('Invalid data', 422);
}
$result['message'] = __('Company has been successfully detached', 'fluent-crm');
return $result;
}
/**
* Find a company.
*/
public function find(Request $request, $id)
{
$findBy = $request->getSafe('find_by', 'sanitize_text_field', 'id');
$findByValue = $request->getSafe('find_by_value', 'sanitize_text_field');
$customFindBys = ['name', 'email', 'phone'];
if (in_array($findBy, $customFindBys)) {
$company = Company::where($findBy, $findByValue)->first();
if (!$company) {
return $this->sendError('Company not found', 422);
}
} else {
$company = Company::findOrFail($id);
}
$company->load(['owner']);
if ($company->owner) {
$company->owner->stats = $company->owner->stats();
}
$company->contacts_count = $company->getContactsCount();
return [
'company' => $company
];
}
/**
* Store a company.
* @param Request $request
* @return \WP_REST_Response | array
*/
public function create(Request $request)
{
$allData = $request->all();
$allData = $this->validate($allData, [
'name' => 'required|unique:fc_companies,name'
]);
$data = $this->getSanitizedData($allData);
if (empty($data['logo']) && !empty($allData['website']) && Helper::isExperimentalEnabled('company_auto_logo')) {
$data['logo'] = $this->getLogoWebsiteUrl($allData['website']);
}
$company = FluentCrmApi('companies')->createOrUpdate($data);
if ($contactId = $request->getSafe('intended_contact_id', 'intval')) {
$contact = Subscriber::find($contactId);
if ($contact) {
$contact->attachCompanies([$company->id]);
if (!$contact->company_id) {
$contact->company_id = $company->id;
$contact->save();
}
}
}
return [
'message' => __('Company has been created successfully', 'fluent-crm'),
'company' => $company
];
}
public function update(Request $request, $id = 0)
{
if ($id == 0) {
return $this->create($request);
}
$company = Company::findOrFail($id);
$allData = $request->all();
$name = sanitize_text_field($allData['name']);
if (Company::where('id', '!=', $id)->where('name', $name)->first()) {
return $this->sendError([
'message' => __('Company name already exists. Please use a different company name', 'fluent-crm')
], 422);
}
$data = $this->getSanitizedData($allData);
$company = FluentCrmApi('companies')->createOrUpdate($data);
return [
'message' => __('Company has been updated', 'fluent-crm'),
'company' => $company
];
}
public function updateProperty()
{
$column = $this->request->getSafe('property', 'sanitize_text_field');
$value = $this->request->getSafe('value', 'sanitize_text_field');
$companyIds = $this->request->get('companies');
if (!is_array($companyIds)) {
$companyIds = [$companyIds];
}
$companyIds = array_map('intval', $companyIds);
$companyIds = array_filter($companyIds);
$validColumns = ['type', 'logo', 'owner_id', 'refetch_logo'];
$types = Helper::companyTypes();
$statuses = Helper::companyTypes();
$this->validate([
'column' => $column,
'value' => $value,
'company_ids' => $companyIds
], [
'column' => 'required',
'value' => 'required',
'company_ids' => 'required'
]);
if (!in_array($column, $validColumns)) {
return $this->sendError([
'message' => __('Column is not valid', 'fluent-crm')
]);
}
if ($column == 'type' && !in_array($value, $types)) {
return $this->sendError([
'message' => __('Value is not valid', 'fluent-crm')
]);
} else if ($column == 'status' && !in_array($value, $statuses)) {
return $this->sendError([
'message' => __('Value is not valid', 'fluent-crm')
]);
}
$companies = Company::whereIn('id', $companyIds)->get();
foreach ($companies as $company) {
if ($column == 'refetch_logo') {
$newLogo = $this->getLogoWebsiteUrl($company->website);
if ($newLogo) {
$company->logo = $newLogo;
$company->save();
return [
'message' => __('Logo has been updated successfully', 'fluent-crm'),
'updated_logo' => $newLogo
];
}
return $this->sendError([
'message' => __('Sorry, we could not find the logo from website. Please upload manually', 'fluent-crm')
]);
}
$oldValue = $company->{$column};
if ($oldValue != $value) {
$company->{$column} = $value;
$company->save();
if (in_array($column, ['type', 'status', 'owner_id'])) {
do_action('fluent_crm/company_' . $column . '_to_' . $value, $company, $oldValue);
}
}
}
return $this->sendSuccess([
'message' => __('Company successfully updated', 'fluent-crm')
]);
}
public function delete(Request $request, $id)
{
$company = Company::findOrFail($id);
do_action('fluent_crm/before_company_delete', $company);
$company->delete();
do_action('fluent_crm/company_deleted', $id);
return [
'message' => __('Company has been deleted successfully', 'fluent-crm')
];
}
public function handleBulkActions(Request $request)
{
$actionName = sanitize_text_field($request->get('action_name', ''));
$companyIds = array_map('intval', $request->get('company_ids', []));
$companyIds = array_filter($companyIds);
$lastId = $request->get('last_id', 0);
if (!$companyIds) {
$companyQuery = Company::orderBy('id', 'ASC')
->searchBy($request->getSafe('search', 'sanitize_text_field'));
$inlineFilters = $request->get('company_query.inline_filters', []);
if ($inlineFilters && is_array($inlineFilters)) {
$inlineFilters = array_filter($inlineFilters);
foreach ($inlineFilters as $key => $values) {
if (!is_array($values)) {
continue;
}
$values = array_map('sanitize_text_field', $values);
if ($key == 'company_categories') {
$companyQuery->whereIn('industry', $values);
} else if ($key == 'company_types') {
$companyQuery->whereIn('type', $values);
}
}
}
$companyQuery = $companyQuery->limit(50)
->where('id', '>', $lastId);
} else {
$companyQuery = Company::whereIn('id', $companyIds);
}
$companies = $companyQuery->get();
if ($companies->isEmpty()) {
return [
'is_completed' => true,
'completed_companies' => 0,
'message' => __('All companies have been processed', 'fluent-crm')
];
}
$companyIds = $companyQuery->pluck('id')->toArray();
$lastCompanyId = end($companyIds);
if ($actionName == 'delete_companies') {
foreach ($companies as $company) {
$id = $company->id;
do_action('fluent_crm/before_company_delete', $company);
$company->delete();
do_action('fluent_crm/company_deleted', $id);
}
return $this->sendSuccess([
'last_company_id' => $lastCompanyId,
'completed_companies' => count($companyIds),
'message' => __('Selected Companies have been deleted permanently', 'fluent-crm'),
]);
} elseif ($actionName == 'change_company_status') {
$newStatus = sanitize_text_field($request->get('new_status', ''));
if (!$newStatus) {
return $this->sendError([
'message' => __('Please select status', 'fluent-crm')
]);
}
foreach ($companies as $company) {
$oldStatus = $company->status;
if ($oldStatus != $newStatus) {
$company->status = $newStatus;
$company->save();
do_action('fluent_crm/company_status_to_' . $newStatus, $company, $oldStatus);
}
}
return [
'last_company_id' => $lastCompanyId,
'completed_companies' => count($companyIds),
'message' => __('Status has been changed for the selected companies', 'fluent-crm')
];
} else if ($actionName == 'change_company_type') {
$newType = sanitize_text_field($request->get('new_status', ''));
if (!$newType) {
return $this->sendError([
'message' => __('Please select new type', 'fluent-crm')
]);
}
foreach ($companies as $company) {
$oldType = $company->type;
if ($oldType != $newType) {
$company->type = $newType;
$company->save();
do_action('fluent_crm/company_type_to_' . $newType, $company, $oldType);
}
}
return [
'last_company_id' => $lastCompanyId,
'completed_companies' => count($companyIds),
'message' => __('Company Type has been updated for the selected companies', 'fluent-crm')
];
} else if ($actionName == 'change_company_category') {
$newCategory = sanitize_text_field($request->get('new_status', ''));
if (!$newCategory) {
return $this->sendError([
'message' => __('Please select new category', 'fluent-crm')
]);
}
foreach ($companies as $company) {
$oldCategory = $company->industry;
if ($oldCategory != $newCategory) {
$company->industry = $newCategory;
$company->save();
do_action('fluent_crm/company_category_to_' . $newCategory, $company, $oldCategory);
}
}
return [
'last_company_id' => $lastCompanyId,
'completed_companies' => count($companyIds),
'message' => __('Company Category has been updated for the selected companies', 'fluent-crm')
];
}
return [
'last_company_id' => $lastCompanyId,
'completed_companies' => count($companyIds),
'message' => __('Selected bulk action has been successfully completed', 'fluent-crm')
];
}
private function getSanitizedData($allData)
{
$rules = [
'name' => 'required'
];
if (Arr::get($allData, 'website')) {
$allData['website'] = $this->makeHttpUrl($allData['website']);
$rules['website'] = 'url';
}
if (Arr::get($allData, 'linkedin_url')) {
$allData['linkedin_url'] = $this->makeHttpUrl($allData['linkedin_url']);
$rules['linkedin_url'] = 'url';
}
if (Arr::get($allData, 'facebook_url')) {
$allData['facebook_url'] = $this->makeHttpUrl($allData['facebook_url']);
$rules['facebook_url'] = 'url';
}
if (Arr::get($allData, 'twitter_url')) {
$allData['twitter_url'] = $this->makeHttpUrl($allData['twitter_url']);
$rules['twitter_url'] = 'url';
}
$allData = $this->validate($allData, $rules);
$data = Sanitize::company($allData);
return Arr::only($data, array_keys($allData));
}
private function makeHttpUrl($url)
{
if (!$url) {
return $url;
}
$parsed_url = wp_parse_url($url);
if (!$parsed_url || empty($parsed_url['scheme'])) {
$url = 'https://' . $url;
}
return $url;
}
/**
* Returns true only if the URL resolves to a public, routable IP address.
* Blocks private/reserved ranges to prevent SSRF attacks.
*/
private function isSSRFSafeUrl($url)
{
$parsed = wp_parse_url($url);
if (!$parsed || empty($parsed['host'])) {
return false;
}
$scheme = strtolower($parsed['scheme'] ?? '');
if (!in_array($scheme, ['http', 'https'])) {
return false;
}
$host = $parsed['host'];
// Strip IPv6 brackets if present
$host = trim($host, '[]');
// If it looks like a raw IP, validate directly; otherwise resolve the hostname
if (filter_var($host, FILTER_VALIDATE_IP)) {
$ip = $host;
} else {
$ip = gethostbyname($host);
// gethostbyname() returns the original string on failure
if ($ip === $host && !filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
}
// Reject private, loopback, link-local, and other reserved ranges
return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}
private function getLogoWebsiteUrl($url)
{
if (!$url) {
return NULL;
}
$url = $this->makeHttpUrl($url);
if (!$this->isSSRFSafeUrl($url)) {
return NULL;
}
$response = wp_remote_get($url, [
'sslverify' => false, // Disable SSL verification to avoid 403 Forbidden error
'timeout' => 10, // Set a timeout of 10 seconds
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' // Set a User-Agent header to avoid 403 Forbidden error
]);
// Check for errors in the response
if (is_wp_error($response)) {
return NULL;
}
// Extract the HTML content from the response
$html = wp_remote_retrieve_body($response);
preg_match('/<link rel="apple-touch-icon"(?:.*?)href="([^"]+)"/i', $html, $matches);
// Use regular expressions to find the logo image URL
if (!isset($matches[1])) {
preg_match('/<link rel="(?:shortcut|icon)"(?:.*?)href="([^"]+)"/i', $html, $matches);
}
// If a logo URL is found, download the image to the uploads directory
if (isset($matches[1])) {
$logoUrl = $matches[1];
// Resolve relative URLs against the base domain
if (!preg_match('/^https?:\/\//i', $logoUrl)) {
$parsedBase = wp_parse_url($url);
$baseOrigin = ($parsedBase['scheme'] ?? 'https') . '://' . ($parsedBase['host'] ?? '');
$logoUrl = $baseOrigin . '/' . ltrim($logoUrl, '/');
}
$extension = strtolower(substr($logoUrl, strrpos($logoUrl, '.') + 1));
if (!in_array($extension, ['png', 'jpg', 'jpeg', 'gif', 'ico'])) {
return NULL;
}
// Block SSRF on the logo URL too (the link tag href may point to a different host)
if (!$this->isSSRFSafeUrl($logoUrl)) {
return NULL;
}
$uploadDir = wp_upload_dir(); // Get the uploads directory
$filename = md5($url . time()) . '-' . basename($logoUrl); // Get the filename from the URL
$filepath = $uploadDir['basedir'] . '/fluentcrm/' . $filename; // Combine the uploads directory path with the filename
// Download the image using wp_remote_get() and save it to the uploads directory
$image = wp_remote_get($logoUrl, [
'timeout' => 10, // Set a timeout of 10 seconds
'sslverify' => false // Disable SSL verification to avoid 403 Forbidden error
]);
if (!is_wp_error($image)) {
// Check if the downloaded file is actually an image
$headers = wp_remote_retrieve_headers($image);
$imageBody = wp_remote_retrieve_body($image);
if (defined('FILEINFO_MIME_TYPE') && class_exists('\finfo')) {
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$content_type = $finfo->buffer($imageBody);
} else {
$content_type = wp_remote_retrieve_header($headers, 'content-type');
if (!$content_type) {
$content_type = Arr::get($headers, 'content-type');
}
if (strpos($content_type, 'image/') !== 0) {
return null;
}
// Temporary file to validate the image
$tmpFilePath = tempnam(sys_get_temp_dir(), 'tmpimg');
file_put_contents($tmpFilePath, $imageBody);
$imgSize = getimagesize($tmpFilePath);
wp_delete_file($tmpFilePath);
if (!$imgSize) {
return null;
}
}
if (strpos($content_type, 'image/') === 0) {
global $wp_filesystem;
if (!$wp_filesystem) {
require_once(ABSPATH . '/wp-admin/includes/file.php');
WP_Filesystem();
}
FileSystem::setCustomUploadDir([
'baseurl' => $uploadDir['baseurl'],
'basedir' => $uploadDir['basedir'],
]);
$wp_filesystem->put_contents($filepath, $imageBody);
// Return the URL of the saved image
return $uploadDir['baseurl'] . FLUENTCRM_UPLOAD_DIR . '/' . $filename;
} else {
// If the downloaded file is not an image, delete the file and return null
wp_delete_file($filepath);
}
}
}
// If no logo URL is found, or if an error occurs, or if the downloaded file is not an image, return null
return NULL;
}
public function getNotes()
{
$companyId = $this->request->get('id');
$search = $this->request->get('search');
$includeId = intval($this->request->get('include_id', 0));
$notes = CompanyNote::where('subscriber_id', $companyId);
if (!empty($search)) {
global $wpdb;
$notes = $notes->where('title', 'LIKE', '%' . $wpdb->esc_like(sanitize_text_field($search)) . '%');
}
$notes = $notes->orderBy('id', 'DESC')
->paginate();
foreach ($notes as $note) {
$note->added_by = $note->createdBy();
}
$fields['fields'] = Helper::getNoteSyncFields();
$response = [
'notes' => $notes,
'fields' => $fields
];
if ($includeId) {
$noteIds = (new Collection($notes->items()))->pluck('id')->toArray();
if (!in_array($includeId, $noteIds)) {
$includedNote = CompanyNote::where('id', $includeId)
->where('subscriber_id', $companyId)
->first();
if ($includedNote) {
$includedNote->added_by = $includedNote->createdBy();
$response['included_note'] = $includedNote;
}
}
}
return $this->sendSuccess($response);
}
public function addNote(Request $request, $id)
{
$company = Company::findOrFail($id);
$note = $this->validate($request->get('note'), [
'title' => 'required',
'description' => 'required',
'type' => 'required',
'created_at' => 'nullable|date'
]);
if (empty($note['created_at'])) {
$note['created_at'] = current_time('mysql');
}
$note['subscriber_id'] = $id;
$note = Sanitize::contactNote($note);
$subscriberNote = CompanyNote::create(wp_unslash($note));
/**
* Subscriber's Note Added
*
* @param SubscriberNote $subscriberNote Note Model.
* @param Subscriber $subscriber Contact Model.
* @param array $note Contact Note Data Array.
* @since 1.0
*/
do_action('fluent_crm/company_note_added', $subscriberNote, $company, $note);
return $this->sendSuccess([
'note' => $subscriberNote,
'message' => __('Note has been successfully added', 'fluent-crm')
]);
}
public function updateNote(Request $request, $id, $noteId)
{
$company = Company::findOrFail($id);
$note = $this->validate($request->get('note'), [
'title' => 'required',
'description' => 'required',
'type' => 'required',
'created_at' => 'sometimes|date'
]);
$note = Arr::only(wp_unslash($note), ['title', 'description', 'type', 'created_at']);
if (empty($note['created_at'])) {
unset($note['created_at']);
}
$note = Sanitize::contactNote($note);
$companyNote = CompanyNote::findOrFail($noteId);
$companyNote->fill($note);
$companyNote->save();
/**
* Subscriber's Note Updated
*
* @param CompanyNote $companyNote Note Model.
* @param Company $company Contact Model.
* @param array $note Contact Note Data Array.
* @since 1.0
*/
do_action('fluent_crm/company_note_updated', $companyNote, $company, $note);
return $this->sendSuccess([
'note' => $companyNote,
'message' => __('Note successfully updated', 'fluent-crm')
]);
}
public function deleteNote($id, $noteId)
{
$company = Company::findOrFail($id);
CompanyNote::where('id', $noteId)->delete();
/**
* Subscriber's Note Delete
*
* @param int $noteId Note ID.
* @param Company $company Company Model.
* @since 1.0
*/
do_action('fluent_crm/company_note_deleted', $noteId, $company);
return $this->sendSuccess([
'message' => __('Note successfully deleted', 'fluent-crm')
]);
}
public function bulkDeleteNotes(Request $request, $id)
{
$company = Company::findOrFail($id);
$noteIds = array_filter(array_map('intval', (array) $request->get('note_ids', [])));
if (empty($noteIds)) {
return $this->sendError([
'message' => __('No note IDs provided', 'fluent-crm')
]);
}
if (count($noteIds) > 200) {
return $this->sendError([
'message' => __('Too many notes selected. Please delete 200 or fewer notes at a time.', 'fluent-crm')
]);
}
// Scope delete to this company so users cannot delete notes belonging to other companies.
$deletableNoteIds = CompanyNote::where('subscriber_id', $company->id)
->whereIn('id', $noteIds)
->pluck('id')
->toArray();
$deletedCount = 0;
if ($deletableNoteIds) {
$deletedCount = CompanyNote::whereIn('id', $deletableNoteIds)->delete();
foreach ($deletableNoteIds as $deletedNoteId) {
do_action('fluent_crm/company_note_deleted', $deletedNoteId, $company);
}
}
return $this->sendSuccess([
'message' => sprintf(
/* translators: %d: number of deleted notes */
_n('%d note deleted', '%d notes deleted', $deletedCount, 'fluent-crm'),
$deletedCount
)
]);
}
public function getCustomGlobalFields(CustomCompanyField $model)
{
return $this->sendSuccess(
$model->getGlobalFields(
$this->request->get('with', [])
)
);
}
public function saveCustomGlobalFields(CustomCompanyField $model)
{
$fields = $model->saveGlobalFields(
Helper::parseArrayOrJson($this->request->get('fields'))
);
return $this->sendSuccess([
'fields' => $fields,
'message' => __('Fields saved successfully!', 'fluent-crm')
]);
}
public function updateCustomFieldGroupName(CustomCompanyField $model)
{
$oldName = sanitize_text_field($this->request->get('old_name'));
$newName = sanitize_text_field($this->request->get('new_name'));
$updatedCustomFields = $model->updateGroupName($oldName, $newName);
return $this->sendSuccess([
'fields' => $updatedCustomFields,
'message' => __('Group name updated successfully!', 'fluent-crm')
]);
}
public function getCompanyExternalView(Request $request, $companyId)
{
$company = Company::findOrFail($companyId);
$sectionId = $request->get('section_provider');
return apply_filters('fluent_crm/company_profile_section_' . $sectionId, [
'heading' => '',
'content_html' => ''
], $company);
}
public function saveExternalViewData(Request $request, $companyId)
{
$company = Company::findOrFail($companyId);
$sectionId = $request->get('section_provider');
$response = apply_filters('fluent_crm/company_profile_section_save_' . $sectionId, '', $request->get('data', []), $company);
if (!$response) {
return $this->sendError([
'message' => __('Handler could not be found.', 'fluent-crm')
]);
}
return $response;
}
}
@@ -0,0 +1,103 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\App;
use FluentCrm\Framework\Validator\ValidationException;
use FluentCrm\Framework\Validator\Validator;
/**
* abstract REST API Controller Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
abstract class Controller
{
/**
* @var \FluentCrm\App\App
*/
protected $app = null;
/**
* @var \FluentCrm\Framework\Http\Request\Request
*/
protected $request = null;
/**
* @var \FluentCrm\Framework\Http\Response\Response
*/
protected $response = null;
public function __construct()
{
$this->app = App::getInstance();
$this->request = $this->app['request'];
$this->response = $this->app['response'];
}
public function validate($data, $rules, $messages = [])
{
$validator = new Validator($data, $rules, $messages);
if ($validator->validate()->fails()) {
// Sanitize validation error messages before returning them
$errors = $validator->errors();
if (is_array($errors)) {
array_walk_recursive($errors, function (&$value) {
if (is_string($value)) {
$value = sanitize_text_field($value);
}
});
}
// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Sanitization is already done above
throw new ValidationException(
esc_html__('Unprocessable Entity!', 'fluent-crm'),
422,
null,
$errors
);
}
return $data;
}
public function send($data = null, $code = 200)
{
return $this->response->send($data, $code);
}
public function sendSuccess($data = null, $code = 200)
{
return $this->response->sendSuccess($data, $code);
}
public function sendError($data = null, $code = 422)
{
return $this->response->sendError($data, $code);
}
public function validationErrors($data = null, $code = 422)
{
if ($data instanceof ValidationException) {
$data = $data->errors();
}
// Sanitize error payload before sending the response to prevent unescaped output
if (is_array($data)) {
array_walk_recursive($data, function (&$value) {
if (is_string($value)) {
$value = sanitize_text_field($value);
}
});
} elseif (is_string($data)) {
$data = sanitize_text_field($data);
}
return $this->sendError($data, $code);
}
}
@@ -0,0 +1,494 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Company;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Libs\FileSystem;
use FluentCrm\App\Services\Sanitize;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\App\Models\Subscriber;
/**
* CsvController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class CsvController extends Controller
{
/**
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
* @throws \FluentCrm\Framework\Validator\ValidationException
*/
public function upload(Request $request)
{
if (is_multisite()) {
add_filter('upload_mimes', function ($types) {
if (empty($types['csv'])) {
$types['csv'] = 'text/csv';
}
return $types;
});
}
$files = $this->validate($this->request->files(), [
'file' => 'mimetypes:' . implode(',', fluentcrmCsvMimes())
], [
'file.mimetypes' => __('The file must be a valid CSV.', 'fluent-crm')
]);
$delimeter = $request->get('delimiter', 'comma');
if ($delimeter == 'comma') {
$delimeter = ',';
} else {
$delimeter = ';';
}
$uploadedFiles = FileSystem::put($files);
try {
$csv = $this->getCsvReader(FileSystem::get($uploadedFiles[0]['file']));
$csv->setDelimiter($delimeter);
$headers = $csv->fetchOne();
} catch (\Exception $exception) {
return $this->sendError([
'message' => $exception->getMessage()
]);
}
if (count($headers) != count(array_unique($headers))) {
return $this->sendError([
'message' => __('Looks like your csv has same name header multiple times. Please fix your csv first and remove any duplicate header column', 'fluent-crm')
]);
}
if ($request->get('type') == 'company') {
$mappables = Company::mappables();
} else {
$mappables = Subscriber::mappables();
}
$headerItems = array_values(array_filter($headers));
$subscriberColumns = array_keys($mappables);
$maps = [];
$customFields = fluentcrm_get_custom_contact_fields();
$fieldsMap = [];
if ($customFields) {
foreach ($customFields as $field) {
$fieldsMap[$field['slug']] = $field['label'];
}
}
foreach ($headerItems as $headerItem) {
$tableMap = (in_array($headerItem, $subscriberColumns)) ? $headerItem : null;
if (!$tableMap) {
$santizedItem = str_replace(' ', '_', strtolower($headerItem));
if (in_array($santizedItem, $subscriberColumns)) {
$tableMap = $santizedItem;
}
}
if (!empty($fieldsMap) && in_array($headerItem, $fieldsMap)) {
$tableMap = array_search($headerItem, $fieldsMap);
}
$maps[] = [
'csv' => $headerItem,
'table' => $tableMap
];
}
if ($request->get('type') == 'company') {
/**
* Determine the columns of the company table in FluentCRM.
*
* This filter allows you to modify the columns of the company table in the CSV export.
*
* @since 2.8.0
*
* @param array $subscriberColumns An array of default subscriber columns.
*/
$columns = apply_filters(
'fluent_crm/company_table_columns', $subscriberColumns
);
} else {
/**
* Determine the columns of the subscriber table in FluentCRM.
*
* This filter allows you to modify the columns displayed in the subscriber table.
*
* @since 2.8.0
*
* @param array $subscriberColumns An array of default subscriber table columns.
*/
$columns = apply_filters(
'fluent_crm/subscriber_table_columns', $subscriberColumns
);
}
return $this->send([
'file' => $uploadedFiles[0]['file'],
'headers' => $headerItems,
'fields' => $mappables,
'columns' => $columns,
'map' => $maps
]);
}
public function import()
{
$inputs = $this->request->only([
'map', 'tags', 'lists', 'file', 'update', 'new_status', 'double_optin_email', 'import_silently', 'force_update_status'
]);
if (Arr::get($inputs, 'import_silently') == 'yes') {
if (!defined('FLUENTCRM_DISABLE_TAG_LIST_EVENTS')) {
define('FLUENTCRM_DISABLE_TAG_LIST_EVENTS', true);
}
}
$forceStatusChange = Arr::get($inputs, 'force_update_status') == 'yes';
$delimeter = $this->request->get('delimiter', 'comma');
if ($delimeter == 'comma') {
$delimeter = ',';
} else {
$delimeter = ';';
}
$status = $inputs['new_status'];
try {
$reader = $this->getCsvReader(FileSystem::get($inputs['file']));
$reader->setDelimiter($delimeter);
if (method_exists($reader, 'getRecords')) {
$aHeaders = $reader->fetchOne(0);
$allRecords = $reader->getRecords($aHeaders);
if (!is_array($allRecords)) {
$allRecords = iterator_to_array($allRecords, true);
}
unset($allRecords[0]);
$allRecords = array_values($allRecords);
} else {
$aHeaders = $reader->fetchOne(0);
$allRecords = $reader->fetchAssoc($aHeaders);
if (!is_array($allRecords)) {
$allRecords = iterator_to_array($allRecords, true);
}
unset($allRecords[0]);
$allRecords = array_values($allRecords);
}
} catch (\Exception $exception) {
return $this->sendError([
'message' => $exception->getMessage()
]);
}
$page = $this->request->get('importing_page', 1);
$processPerRequest = apply_filters('fluent_crm/csv_import_contact_limit_per_request', 100);
$offset = ($page - 1) * $processPerRequest;
$records = array_slice($allRecords, $offset, $processPerRequest);
$customFieldKeys = $this->customFieldKeys();
$subscribers = [];
$skipped = [];
$isCompanyEnabled = Helper::isCompanyEnabled();
foreach ($records as $record) {
if (!array_filter($record)) {
continue;
}
$subscriber = [
'custom_values' => []
];
foreach ($inputs['map'] as $map) {
if (!$map['table']) {
continue;
}
if (isset($map['csv'], $map['table'])) {
if (in_array($map['table'], ['tags', 'lists'])) {
//if tags or lists are mapped to be imported
if ($map['table'] == 'tags') {
$subscriber['tags'] = !empty($record[$map['csv']]) ? explode(',', $record[$map['csv']]) : [];
} else {
$subscriber['lists'] = !empty($record[$map['csv']]) ? explode(',', $record[$map['csv']]) : [];
}
}
else if (in_array($map['table'], $customFieldKeys)) {
$subscriber['custom_values'][$map['table']] = $record[$map['csv']];
} else {
$subscriber[$map['table']] = $record[$map['csv']];
}
}
}
if (!array_key_exists('email', $subscriber)) {
return $this->sendError(['email' => __('The email field is required.', 'fluent-crm')], 422);
}
$subscriber['email'] = is_string($subscriber['email']) ? trim($subscriber['email']) : $subscriber['email'];
if ($subscriber['email'] && is_email($subscriber['email'])) {
if (isset($subscriber['company_id']) && $subscriber['company_id'] && $isCompanyEnabled) {
$companyNameOrId = $subscriber['company_id'];
if (is_string($companyNameOrId)) {
$company = Company::query()->firstOrCreate([
'name' => $subscriber['company_id']
], [
'name' => $subscriber['company_id']
]);
if ($company) {
$subscriber['company_id'] = $company->id;
} else {
unset($subscriber['company_id']);
}
} else {
$company = Company::find($subscriber['company_id']);
if (!$company) {
unset($subscriber['company_id']);
}
}
}
$subscribers[] = Sanitize::contact($subscriber);
} else {
$skipped[] = $subscriber;
}
}
if (!isset($inputs['tags'])) {
$inputs['tags'] = [];
}
if (!isset($inputs['lists'])) {
$inputs['lists'] = [];
}
$sendDoubleOptin = Arr::get($inputs, 'double_optin_email') == 'yes';
$result = Subscriber::import(
$subscribers, $inputs['tags'], $inputs['lists'], $inputs['update'], $status, $sendDoubleOptin, $forceStatusChange, 'csv'
);
$totalSkipped = count($result['skips']) + count($skipped);
$completed = $offset + count($records);
$totalCount = count($allRecords);
$hasMore = $completed < $totalCount;
if (!$hasMore) {
FileSystem::delete($inputs['file']);
}
return $this->sendSuccess([
'total' => $totalCount,
'completed' => $completed,
'total_page' => ceil($totalCount / $processPerRequest),
'skipped' => $totalSkipped,
'invalid_contacts' => $skipped,
'skipped_contacts' => $result['skips'],
'invalid_email_counts' => count($skipped),
'inserted' => count($result['inserted']),
'updated' => count($result['updated']),
'has_more' => $hasMore,
'last_page' => $page,
'tags' => $inputs['tags'],
'lists' => $inputs['lists'],
'offset' => $offset,
'result' => $result
]);
}
public function importCompanies()
{
$inputs = $this->request->only([
'map', 'file', 'update', 'create_owner'
]);
$delimeter = $this->request->get('delimiter', 'comma');
if ($delimeter == 'comma') {
$delimeter = ',';
} else {
$delimeter = ';';
}
try {
$reader = $this->getCsvReader(FileSystem::get($inputs['file']));
$reader->setDelimiter($delimeter);
if (method_exists($reader, 'getRecords')) {
$aHeaders = $reader->fetchOne(0);
$allRecords = $reader->getRecords($aHeaders);
if (!is_array($allRecords)) {
$allRecords = iterator_to_array($allRecords, true);
}
unset($allRecords[0]);
$allRecords = array_values($allRecords);
} else {
$aHeaders = $reader->fetchOne(0);
$allRecords = $reader->fetchAssoc($aHeaders);
if (!is_array($allRecords)) {
$allRecords = iterator_to_array($allRecords, true);
}
unset($allRecords[0]);
$allRecords = array_values($allRecords);
}
} catch (\Exception $exception) {
return $this->sendError([
'message' => $exception->getMessage()
]);
}
$page = $this->request->get('importing_page', 1);
$processPerRequest = 100;
$offset = ($page - 1) * $processPerRequest;
$records = array_slice($allRecords, $offset, $processPerRequest);
$willCreateOwner = $this->request->get('create_owner') == 'yes';
$willUpdate = $this->request->get('update') == 'yes';
$customFields = fluentcrm_get_custom_company_fields();
$companies = [];
$skipped = [];
foreach ($records as $record) {
if (!array_filter($record)) {
continue;
}
$company = [];
foreach ($inputs['map'] as $map) {
if (!$map['table']) {
continue;
}
if (isset($map['csv'], $map['table'])) {
$company[$map['table']] = trim($record[$map['csv']]);
}
}
if (empty($company['name'])) {
return $this->sendError(['email' => __('The company name field is required.', 'fluent-crm')], 422);
}
if (!$willUpdate) {
// check if exists
if (Company::where('name', $company['name'])->first()) {
$skipped[] = $company;
continue;
}
}
if ($customFields) {
$customValues = [];
foreach ($company as $dataKey => $dataValue) {
if (strpos($dataKey, '_custom_') === 0) {
$customKey = str_replace('_custom_', '', $dataKey);
$customValues[$customKey] = $dataValue;
unset($company[$dataKey]);
}
}
$company['custom_values'] = $customValues;
}
$company = Sanitize::company($company);
if (!empty($company['owner_email']) && is_email($company['owner_email'])) {
$ownerEmail = sanitize_email($company['owner_email']);
} else {
$ownerEmail = null;
}
if ($ownerEmail) {
$owner = FluentCrmApi('contacts')->getContact($ownerEmail);
if ($owner) {
$company['owner_id'] = $owner->id;
} else if ($willCreateOwner) {
$owner = FluentCrmApi('contacts')->createOrUpdate([
'full_name' => sanitize_text_field(Arr::get($company, 'owner_name')),
'email' => $ownerEmail,
'status' => 'subscribed'
]);
if ($owner) {
$company['owner_id'] = $owner->id;
}
}
}
$createdCompany = FluentCrmApi('companies')->createOrUpdate($company);
$companies[] = $createdCompany;
}
$completed = $offset + count($companies);
$totalCount = count($allRecords);
$hasMore = $completed < $totalCount;
if (!$hasMore) {
FileSystem::delete($inputs['file']);
}
return $this->sendSuccess([
'total' => $totalCount,
'completed' => count($companies),
'total_page' => ceil($totalCount / $processPerRequest),
'skipped' => count($skipped),
'has_more' => $hasMore,
'last_page' => $page,
'offset' => $offset
]);
}
protected function customFieldKeys()
{
$fields = fluentcrm_get_option('contact_custom_fields', []);
$keys = [];
foreach ($fields as $field) {
$keys[] = $field['slug'];
}
return $keys;
}
private function getCsvReader($file)
{
if (!class_exists(' \League\Csv\Reader')) {
include FLUENTCRM_PLUGIN_PATH . 'app/Services/Libs/csv/autoload.php';
}
return \League\Csv\Reader::createFromString($file);
}
}
@@ -0,0 +1,51 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\CustomContactField;
use FluentCrm\App\Services\Helper;
/**
* CustomContactFieldsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class CustomContactFieldsController extends Controller
{
public function getGlobalFields(CustomContactField $model)
{
return $this->sendSuccess(
$model->getGlobalFields(
$this->request->get('with', [])
)
);
}
public function saveGlobalFields(CustomContactField $model)
{
$fields = $model->saveGlobalFields(
Helper::parseArrayOrJson($this->request->get('fields'))
);
return $this->sendSuccess([
'fields' => $fields,
'message' => __('Fields saved successfully!', 'fluent-crm')
]);
}
public function updateGroupName(CustomContactField $model)
{
$oldName = sanitize_text_field($this->request->get('old_name'));
$newName = sanitize_text_field($this->request->get('new_name'));
$updatedCustomFields = $model->updateGroupName($oldName, $newName);
return $this->sendSuccess([
'fields' => $updatedCustomFields,
'message' => __('Group name updated successfully!', 'fluent-crm')
]);
}
}
@@ -0,0 +1,268 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\CampaignEmail;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Stats;
use FluentCrm\Framework\Support\Arr;
/**
* DashboardController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class DashboardController extends Controller
{
public function getStats(Stats $stats)
{
$overallStats = $stats->getCounts();
$nextMinuteTask = Helper::getNextMinuteTaskTimeStamp();
$notices = [];
if ((time() - $nextMinuteTask) > 120) {
$notices[] = '<div class=""><b>Attention: </b> Looks like the scheduled cron jobs are not running timely. Please consider setup server side cron. <a href="' . admin_url('admin.php?page=fluentcrm-admin#/settings/settings_tools') . '">Click here to check the status</a></div>';
}
$systemTips = '';
$emailsCount = Arr::get($overallStats, 'email_sent.count', 0);
if ($emailsCount > 400000) {
$lastEmail = CampaignEmail::orderBy('id', 'ASC')->first();
if ($lastEmail && strtotime($lastEmail->created_at) < strtotime('-120 days')) {
$emailsCount = number_format($emailsCount, 0);
$sysBody = '<div class="fc_system_tips">';
/* translators: %s: number of emails in the database */
$sysBody .= '<p>' . sprintf(__('You have %s email history in the database. Consider cleaning up old email history to speed up your next email campaign.', 'fluent-crm'), $emailsCount) . '</p>';
$sysBody .= '<a href="' . fluentcrm_menu_url_base('settings/settings_tools') . '" class="el-button fcrm_primary_btn">' . __('View Data Cleanup', 'fluent-crm') . '</a>';
$sysBody .= '</div>';
$systemTips = [
'title' => __('Database Cleanup Suggestion', 'fluent-crm'),
'body' => $sysBody,
];
}
}
/**
* Define the FluentCRM dashboard notices.
*
* This filter allows modification of the notices displayed on the FluentCRM dashboard.
*
* @since 2.8.40
*
* @param array $notices An array of notices to be displayed on the dashboard.
*/
$notices = apply_filters('fluent_crm/dashboard_notices', $notices);
/**
* Define the dashboard data for FluentCRM.
*
* @since 2.9.23
*
* @param array {
* The dashboard data array.
*
* @type array $stats Overall statistics.
* @type array $sales Sales statistics.
* @type array $dashboard_notices Notices to be displayed on the dashboard.
* @type array $onboarding Onboarding statistics.
* @type array $quick_links Quick links for the dashboard.
* @type array $ff_config FluentForm configuration.
* @type array $recommendation Recommendations for the user.
* @type array $system_tips System tips for the user.
* }
*/
return apply_filters('fluent_crm/dashboard_data', [
'stats' => $overallStats,
/**
* Determine the FluentCRMsales statistics data.
*
* This filter allows modification of the sales statistics data before it is used.
*
* @since 2.7.0
*
* @param array An array of sales statistics data.
*/
'sales' => apply_filters('fluent_crm/sales_stats', []),
'dashboard_notices' => $notices,
'onboarding' => $stats->getOnboardingStat(),
'quick_links' => $stats->getQuickLinks(),
'ff_config' => [
'is_installed' => defined('FLUENTFORM'),
'create_form_link' => admin_url('admin.php?page=fluent_forms#add=1')
],
'recommendation' => $this->recommendation(),
'system_tips' => $systemTips,
'recent_contacts' => $stats->getRecentContacts(3),
'active_automations' => $stats->getActiveAutomations(3),
'recent_campaigns' => $stats->getRecentCampaigns(3),
'triggers' => $this->getTriggers()
]);
}
private function recommendation()
{
if (defined('FLUENTCAMPAIGN')) {
return false;
}
$recommendations = [];
if (defined('WC_PLUGIN_FILE')) {
$recommendations[] = [
'provider' => 'WooCommerce',
'title' => __('Do more with WooCommerce + FluentCRM', 'fluent-crm'),
'description' => __('Integrate FluentCRM with WooCommerce and segment your customers by purchase behavior, send super targeted emails, onboarding emails, cross promotions and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/woocommerce-marketing-automation/',
'base_title' => __('Supercharge your WooCommerce store by upgrading FluentCRM Pro', 'fluent-crm')
];
$recommendations[] = [
'provider' => 'WooCommerce',
'title' => __('Do more with WooCommerce + FluentCRM', 'fluent-crm'),
'description' => __('Integrate FluentCRM with WooCommerce and segment your customers by purchase behavior, send super targeted emails, onboarding emails, cross promotions and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/woocommerce-marketing-automation/',
'base_title' => __('Supercharge your WooCommerce store by upgrading FluentCRM Pro', 'fluent-crm')
];
}
if (Helper::isEdd3()) {
$recommendations[] = [
'provider' => 'EDD',
'title' => __('Do more with EDD + FluentCRM', 'fluent-crm'),
'description' => __('Integrate FluentCRM with Easy Digital Downloads and segment your customers by purchase behavior, send super targeted emails, onboarding emails, cross promotions and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/easy-digital-downloads-integration-fluentcrm/',
'base_title' => __('Supercharge your Digital Downloads store by upgrading FluentCRM Pro', 'fluent-crm')
];
}
if (defined('LLMS_PLUGIN_FILE')) {
$recommendations[] = [
'provider' => 'LifterLMS',
'title' => __('Do more with LifterLMS + FluentCRM', 'fluent-crm'),
'description' => __('Integrate LifterLMS with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/lifterlms/',
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
$recommendations[] = [
'provider' => 'LifterLMS',
'title' => __('Do more with LifterLMS + FluentCRM', 'fluent-crm'),
'description' => __('Integrate LifterLMS with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/lifterlms/',
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (defined('LEARNDASH_VERSION')) {
$recommendations[] = [
'provider' => 'LearnDash',
'title' => __('Do more with LearnDash + FluentCRM', 'fluent-crm'),
'description' => __('Integrate LearnDash with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/learndash-integration-fluentcrm/',
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
$recommendations[] = [
'provider' => 'LearnDash',
'title' => __('Do more with LearnDash + FluentCRM', 'fluent-crm'),
'description' => __('Integrate LearnDash with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/integrations/learndash-integration-fluentcrm/',
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (defined('TUTOR_VERSION')) {
$recommendations[] = [
'provider' => 'TutorLMS',
'title' => __('Do more with TutorLMS + FluentCRM', 'fluent-crm'),
'description' => __('Integrate TutorLMS with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/docs/tutorlms-integration-with-fluentcrm/',
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (defined('LP_PLUGIN_FILE')) {
$recommendations[] = [
'provider' => 'LearnPress',
'title' => __('Do more with LearnPress + FluentCRM', 'fluent-crm'),
'description' => __('Integrate LearnPress with FluentCRM and segment your students by courses, send super targeted emails, onboarding emails, cross promote more courses and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'learn_more' => 'https://fluentcrm.com/docs/learpress-integration-with-fluentcrm/',
'base_title' => __('Supercharge your LMS by upgrading FluentCRM Pro', 'fluent-crm')
];
}
if (defined('PMPRO_VERSION')) {
$recommendations[] = [
'provider' => 'PaidMembership Pro',
'title' => __('Do more with PaidMembership Pro + FluentCRM', 'fluent-crm'),
'description' => __('Integrate PaidMembership Pro with FluentCRM and segment your members by membership levels, send super targeted emails, onboarding emails, cross promote more levels and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your Membership Site by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (defined('WLM3_PLUGIN_VERSION')) {
$recommendations[] = [
'provider' => 'Wishlist Member',
'title' => __('Do more with Wishlist Member + FluentCRM', 'fluent-crm'),
'description' => __('Integrate Wishlist Member with FluentCRM and segment your members by membership levels, send super targeted emails, onboarding emails, cross promote more levels and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your Membership Site by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (defined('MEPR_PLUGIN_NAME')) {
$recommendations[] = [
'provider' => 'MemberPress',
'title' => __('Do more with MemberPress + FluentCRM', 'fluent-crm'),
'description' => __('Integrate MemberPress with FluentCRM and segment your members by membership levels, send super targeted emails, onboarding emails, cross promote more levels and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your Membership Site by upgrading FluentCRM Pro', 'fluent-crm')
];
} else if (class_exists('\Restrict_Content_Pro')) {
$recommendations[] = [
'provider' => 'Restrict Content Pro',
'title' => __('Do more with Restrict Content Pro + FluentCRM', 'fluent-crm'),
'description' => __('Integrate Restrict Content Pro with FluentCRM and segment your members by membership levels, send super targeted emails, onboarding emails, cross promote more levels and many more.', 'fluent-crm'),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your Membership Site by upgrading FluentCRM Pro', 'fluent-crm')
];
}
if (defined('BP_REQUIRED_PHP_VERSION') && function_exists('\buddypress')) {
$title = defined('BP_PLATFORM_VERSION') ? 'BuddyBoss' : 'BuddyPress';
$recommendations[] = [
'provider' => $title,
/* translators: %s: plugin name (BuddyBoss or BuddyPress) */
'title' => sprintf(__('Do more with %s + FluentCRM', 'fluent-crm'), $title),
/* translators: %s: plugin name (BuddyBoss or BuddyPress) */
'description' => sprintf(__('Integrate %s with FluentCRM and segment your members by different group, send super targeted emails, onboarding emails, cross promote more groups and many more.', 'fluent-crm'), $title),
'btn_text' => __('Upgrade to Pro', 'fluent-crm'),
'base_title' => __('Supercharge your Community Site by upgrading FluentCRM Pro', 'fluent-crm')
];
}
if (!$recommendations) {
return false;
}
return $recommendations[array_rand($recommendations)];
}
private function getTriggers()
{
/**
* Determine the list of funnel triggers in FluentCRM.
*
* This filter allows you to modify the array of funnel triggers.
*
* @since 1.0.0
*
* @param array An array of funnel triggers.
*/
return apply_filters('fluentcrm_funnel_triggers', []);
}
}
@@ -0,0 +1,195 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\Framework\Support\Arr;
/**
* DocsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class DocsController extends Controller
{
private $restApi = 'https://fluentcrm.com/wp-json/wp/v2/';
public function index()
{
$formattedDocs = $this->getDocsPerChunk($this->restApi . 'docs?per_page=100', 'fluentcrm_all_docs');
$moreDocs = $this->getDocsPerChunk($this->restApi . 'docs?per_page=100&offset=100', 'fluentcrm_all_docs_2');
if ($moreDocs) {
$formattedDocs = array_merge($formattedDocs, $moreDocs);
}
return [
'docs' => $formattedDocs
];
}
public function getDoc($docId)
{
$request = wp_remote_get($this->restApi . 'docs/' . $docId);
if (is_wp_error($request)) {
return [
'content' => 'sorry, we could not fetch the doc at this moment. Please try again',
'is_error' => true
];
}
$doc = json_decode(wp_remote_retrieve_body($request), true);
return [
'title' => sanitize_text_field($doc['title']['rendered']),
'content' => links_add_target(Helper::sanitizeHtml($doc['content']['rendered'])),
'link' => esc_url($doc['link']),
'id' => $doc['id']
];
}
public function getAddons(Request $request)
{
$canAutoInstallToolkit = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
$toolkitPluginFile = 'fluent-toolkit/fluent-toolkit.php';
$toolkitLoaded = defined('FLUENT_TOOLKIT_VERSION');
$toolkitPluginExists = $this->isPluginInstalled($toolkitPluginFile);
$toolkitActionText = __('Get FluentHub from GitHub', 'fluent-crm');
if ($canAutoInstallToolkit) {
$toolkitActionText = $toolkitPluginExists ? __('Activate FluentHub', 'fluent-crm') : __('Install FluentHub', 'fluent-crm');
}
$addOns = [
'fluentform' => [
'title' => __('Fluent Forms', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluentform.png'),
'is_installed' => defined('FLUENTFORM'),
'learn_more_url' => 'https://wordpress.org/plugins/fluentform/',
'settings_url' => admin_url('admin.php?page=fluent_forms'),
'action_text' => $this->isPluginInstalled('fluent-form/fluent-form.php') ? __('Activate Fluent Forms', 'fluent-crm') : __('Install Fluent Forms', 'fluent-crm'),
'description' => __('Collect leads and build any type of forms, accept payments, connect with your CRM with the Fastest Contact Form Builder Plugin for WordPress', 'fluent-crm')
],
'fluentsmtp' => [
'title' => __('Fluent SMTP', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-smtp.svg'),
'is_installed' => defined('FLUENTMAIL'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-smtp/',
'settings_url' => admin_url('options-general.php?page=fluent-mail#/'),
'action_text' => $this->isPluginInstalled('fluent-smtp/fluent-smtp.php') ? __('Activate Fluent SMTP', 'fluent-crm') : __('Install Fluent SMTP', 'fluent-crm'),
'description' => __('The Ultimate SMTP and SES Plugin for WordPress. Connect with any SMTP, SendGrid, Mailgun, SES, Sendinblue, PepiPost, Google, Microsoft and more.', 'fluent-crm')
],
'fluent-support' => [
'title' => __('Fluent Support', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-support.svg'),
'is_installed' => defined('FLUENT_SUPPORT_VERSION'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-support/',
'settings_url' => admin_url('admin.php?page=fluent-support#/'),
'action_text' => $this->isPluginInstalled('fluent-support/fluent-support.php') ? __('Activate Fluent Support', 'fluent-crm') : __('Install Fluent Support', 'fluent-crm'),
'description' => __('WordPress Helpdesk and Customer Support Ticket Plugin. Provide awesome support and manage customer queries right from your WordPress dashboard.', 'fluent-crm')
],
'fluent-cart' => [
'title' => __('Fluent Cart', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-cart-dark.svg'),
'is_installed' => defined('FLUENTCART_VERSION'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-cart/',
'settings_url' => admin_url('admin.php?page=fluent-cart#/'),
'action_text' => $this->isPluginInstalled('fluent-cart/fluent-cart.php') ? __('Activate Fluent Cart', 'fluent-crm') : __('Install Fluent Cart', 'fluent-crm'),
'description' => __('WordPress eCommerce and Shopping Cart Plugin. Build an online store and manage products, orders, and customers right from your WordPress dashboard.', 'fluent-crm')
],
'fluent-boards' => [
'title' => __('Fluent Boards', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-boards.svg'),
'is_installed' => defined('FLUENT_BOARDS'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-boards/',
'settings_url' => admin_url('admin.php?page=fluent-boards#/'),
'action_text' => $this->isPluginInstalled('fluent-boards/fluent-boards.php') ? __('Activate Fluent Boards', 'fluent-crm') : __('Install Fluent Boards', 'fluent-crm'),
'description' => __('WordPress Project Management and Collaboration Plugin. Manage projects, tasks, and team collaboration right from your WordPress dashboard.', 'fluent-crm')
],
'fluent-community' => [
'title' => __('Fluent Community', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-community.svg'),
'is_installed' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-community/',
'settings_url' => admin_url('admin.php?page=fluent-community#/'),
'action_text' => $this->isPluginInstalled('fluent-community/fluent-community.php') ? __('Activate Fluent Community', 'fluent-crm') : __('Install Fluent Community', 'fluent-crm'),
'description' => __('WordPress Forum and Community Plugin. Build a thriving online community and discussion forum right from your WordPress dashboard.', 'fluent-crm')
],
'fluent-booking' => [
'title' => __('Fluent Booking', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-booking.svg'),
'is_installed' => defined('FLUENT_BOOKING_VERSION'),
'learn_more_url' => 'https://wordpress.org/plugins/fluent-booking/',
'settings_url' => admin_url('admin.php?page=fluent-booking#/'),
'action_text' => $this->isPluginInstalled('fluent-booking/fluent-booking.php') ? __('Activate Fluent Booking', 'fluent-crm') : __('Install Fluent Booking', 'fluent-crm'),
'description' => __('WordPress Appointment Booking Plugin. Manage appointments, bookings, and customer scheduling right from your WordPress dashboard.', 'fluent-crm')
],
'fluent-toolkit' => [
'title' => __('FluentHub', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-toolkit.svg'),
'is_installed' => $toolkitLoaded,
'learn_more_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
'settings_url' => admin_url('admin.php?page=fluent-toolkit'),
'action_text' => $toolkitActionText,
'install_route' => $canAutoInstallToolkit ? 'mcp/install-adapter' : '',
'install_url' => $canAutoInstallToolkit ? '' : 'https://github.com/WPManageNinja/fluent-toolkit',
'description' => __('FluentCRM ships AI agent tools, but they only become available once FluentHub is installed and active.', 'fluent-crm')
]
];
$data = [
'addons' => $addOns
];
if (in_array('experimental_features', $request->get('with', []))) {
$data['experimental_features'] = Helper::getExperimentalSettings();
}
return $data;
}
private function isPluginInstalled($plugin)
{
return file_exists(WP_PLUGIN_DIR . '/' . $plugin);
}
private function getDocsPerChunk($url, $chunkKey)
{
return fluentCrmGetFromCache($chunkKey, function () use ($url) {
$request = wp_remote_get($url);
if (is_wp_error($request)) {
return [];
}
$docs = json_decode(wp_remote_retrieve_body($request), true);
$formattedDocs = [];
foreach ($docs as $doc) {
if (empty($doc['title'])) {
continue;
}
$primaryCategory = Arr::get($doc, 'taxonomy_info.doc_category.0', ['value' => 'none', 'label' => 'Other']);
$formattedDocs[] = [
'title' => sanitize_text_field($doc['title']['rendered']),
'content' => links_add_target(Helper::sanitizeHtml($doc['content']['rendered'])),
'link' => esc_url($doc['link']),
'category' => wp_kses_post_deep($primaryCategory)
];
}
return $formattedDocs;
}, WEEK_IN_SECONDS);
}
}
@@ -0,0 +1,485 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Meta;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
class EmailPatternController extends Controller
{
private $objectType = 'email_pattern';
private $categoryObjectType = 'email_pattern_category';
public function index(Request $request)
{
$query = Meta::where('object_type', $this->objectType)
->orderBy('id', 'desc');
if ($search = $request->getSafe('search', 'sanitize_text_field')) {
$query->where('value', 'LIKE', '%' . $search . '%');
}
$patterns = $query->paginate();
$formattedPatterns = [];
foreach ($patterns as $pattern) {
$formattedPatterns[] = $this->formatPattern($pattern);
}
return $this->sendSuccess([
'patterns' => [
'data' => $formattedPatterns,
'total' => $patterns->total()
]
]);
}
public function show(Request $request, $id)
{
$pattern = Meta::where('object_type', $this->objectType)
->where('id', $id)
->firstOrFail();
return $this->sendSuccess([
'pattern' => $this->formatPattern($pattern)
]);
}
/**
* Return patterns in wp_block REST format for the editor middleware.
*/
public function indexWpFormat(Request $request)
{
$patterns = Meta::where('object_type', $this->objectType)
->orderBy('id', 'desc')
->get();
$categoryMap = $this->getCategoryMap();
$formatted = [];
foreach ($patterns as $pattern) {
$formatted[] = $this->formatAsWpBlock($pattern, $categoryMap);
}
return $formatted;
}
public function store(Request $request)
{
$this->validate($request->all(), [
'title' => 'required|string',
'content' => 'required|string',
]);
$title = sanitize_text_field($request->get('title'));
$content = wp_kses_post($request->get('content'));
$category = sanitize_text_field($request->get('category', ''));
$description = sanitize_text_field($request->get('description', ''));
$syncStatus = sanitize_text_field($request->get('sync_status', 'unsynced'));
$slug = 'fluentcrm/' . sanitize_title($title . '-' . uniqid());
$pattern = Meta::create([
'object_type' => $this->objectType,
'object_id' => get_current_user_id(),
'key' => $slug,
'value' => [
'title' => $title,
'content' => $content,
'category' => $category,
'description' => $description,
'sync_status' => $syncStatus,
],
]);
return $this->sendSuccess([
'message' => __('Pattern saved successfully', 'fluent-crm'),
'pattern' => $this->formatPattern($pattern),
]);
}
/**
* Store a pattern from wp_block format (called by editor middleware).
*/
public function storeWpFormat(Request $request)
{
$title = $request->get('title', '');
if (is_array($title)) {
$title = Arr::get($title, 'raw', '');
}
$title = sanitize_text_field($title);
$content = $request->get('content', '');
if (is_array($content)) {
$content = Arr::get($content, 'raw', '');
}
$content = wp_kses_post($content);
if (!$title && !$content) {
return $this->sendError([
'message' => __('Title or content is required', 'fluent-crm')
]);
}
if (!$title) {
$title = __('Untitled Pattern', 'fluent-crm');
}
$meta = $request->get('meta', []);
$syncStatus = Arr::get($meta, 'wp_pattern_sync_status', '');
$syncStatus = sanitize_text_field($syncStatus);
$categoryIds = (array) $request->get('wp_pattern_category', []);
$categoryName = $this->resolveCategoryName($categoryIds);
$slug = 'fluentcrm/' . sanitize_title($title . '-' . uniqid());
$pattern = Meta::create([
'object_type' => $this->objectType,
'object_id' => get_current_user_id(),
'key' => $slug,
'value' => [
'title' => $title,
'content' => $content,
'category' => $categoryName,
'description' => '',
'sync_status' => $syncStatus,
],
]);
$categoryMap = $this->getCategoryMap();
return $this->formatAsWpBlock($pattern, $categoryMap);
}
public function update(Request $request, $id)
{
$pattern = Meta::where('object_type', $this->objectType)
->where('id', $id)
->firstOrFail();
$value = $pattern->value;
if ($title = $request->get('title')) {
if (is_array($title)) {
$title = Arr::get($title, 'raw', '');
}
$value['title'] = sanitize_text_field($title);
}
if ($request->has('content')) {
$content = $request->get('content');
if (is_array($content)) {
$content = Arr::get($content, 'raw', '');
}
$value['content'] = wp_kses_post($content);
}
$value['category'] = sanitize_text_field($request->get('category', ''));
if ($request->has('wp_pattern_category')) {
$categoryIds = (array) $request->get('wp_pattern_category', []);
$value['category'] = $this->resolveCategoryName($categoryIds);
}
if ($request->has('description')) {
$value['description'] = sanitize_text_field($request->get('description'));
}
if ($request->exists('sync_status')) {
$value['sync_status'] = sanitize_text_field($request->get('sync_status'));
}
$meta = $request->get('meta', []);
if (is_array($meta) && isset($meta['wp_pattern_sync_status'])) {
$value['sync_status'] = sanitize_text_field($meta['wp_pattern_sync_status']);
}
if ($title = $request->get('title')) {
if (is_array($title)) {
$title = Arr::get($title, 'raw', '');
}
if ($title) {
$pattern->key = 'fluentcrm/' . sanitize_title($title . '-' . $pattern->id);
}
}
$pattern->value = $value;
$pattern->save();
return $this->sendSuccess([
'message' => __('Pattern updated successfully', 'fluent-crm'),
'pattern' => $this->formatPattern($pattern),
]);
}
public function delete(Request $request, $id)
{
Meta::where('object_type', $this->objectType)
->where('id', $id)
->firstOrFail()
->delete();
return $this->sendSuccess([
'message' => __('Pattern deleted successfully', 'fluent-crm'),
]);
}
public function handleBulkAction(Request $request)
{
$actionName = sanitize_text_field($request->get('action_name'));
if ($actionName !== 'delete_patterns') {
return $this->sendError([
'message' => __('Invalid action', 'fluent-crm')
]);
}
$query = Meta::where('object_type', $this->objectType);
if (filter_var($request->get('select_all'), FILTER_VALIDATE_BOOLEAN)) {
$search = $request->getSafe('search', 'sanitize_text_field', '');
if ($search !== '') {
$query->where('value', 'LIKE', '%' . $search . '%');
}
} else {
$patternIds = array_map('intval', (array) $request->get('pattern_ids', []));
if (empty($patternIds)) {
return $this->sendError([
'message' => __('No patterns selected', 'fluent-crm')
]);
}
$query->whereIn('id', $patternIds);
}
$count = $query->delete();
return $this->sendSuccess([
'message' => sprintf(__('%d pattern(s) deleted successfully', 'fluent-crm'), $count)
]);
}
/**
* CRUD for pattern categories (stored as fc_meta with separate object_type).
*/
public function getCategories()
{
// Collect unique category names from all patterns
$patterns = Meta::where('object_type', $this->objectType)->get();
$categories = [];
foreach ($patterns as $pattern) {
$cat = Arr::get($pattern->value, 'category', '');
if ($cat && !in_array($cat, $categories)) {
$categories[] = $cat;
}
}
sort($categories);
return $this->sendSuccess([
'categories' => $categories
]);
}
public function storeCategory(Request $request)
{
$name = sanitize_text_field($request->get('name', ''));
if (!$name) {
return $this->sendError(['message' => __('Category name is required', 'fluent-crm')]);
}
$slug = sanitize_title($name);
// Check for existing
$existing = Meta::where('object_type', $this->categoryObjectType)
->where('key', $slug)
->first();
if ($existing) {
return $this->formatCategoryAsWpTerm($existing);
}
$category = Meta::create([
'object_type' => $this->categoryObjectType,
'object_id' => 0,
'key' => $slug,
'value' => ['name' => $name],
]);
return $this->formatCategoryAsWpTerm($category);
}
public function deleteCategory(Request $request, $id)
{
Meta::where('object_type', $this->categoryObjectType)
->where('id', $id)
->firstOrFail()
->delete();
return $this->sendSuccess([
'message' => __('Category deleted successfully', 'fluent-crm'),
]);
}
/**
* Format a pattern Meta record as a wp_block REST response.
*/
private function formatAsWpBlock($meta, $categoryMap = [])
{
$value = $meta->value;
$title = Arr::get($value, 'title', '');
$content = Arr::get($value, 'content', '');
$syncStatus = Arr::get($value, 'sync_status', 'unsynced');
$category = Arr::get($value, 'category', '');
$categoryIds = [];
if ($category) {
$catSlug = sanitize_title($category);
if (isset($categoryMap[$catSlug])) {
$categoryIds[] = (int) $categoryMap[$catSlug];
}
}
return [
'id' => (int) $meta->id,
'date' => $meta->created_at ? $meta->created_at : gmdate('Y-m-d\TH:i:s'),
'date_gmt' => $meta->created_at ? $meta->created_at : gmdate('Y-m-d\TH:i:s'),
'modified' => $meta->updated_at ? $meta->updated_at : gmdate('Y-m-d\TH:i:s'),
'modified_gmt' => $meta->updated_at ? $meta->updated_at : gmdate('Y-m-d\TH:i:s'),
'slug' => $meta->key,
'status' => 'publish',
'type' => 'wp_block',
'link' => '',
'title' => ['raw' => $title],
'content' => ['raw' => $content, 'protected' => false],
'meta' => new \stdClass(),
'wp_pattern_sync_status' => $syncStatus ?: '',
'wp_pattern_category' => $categoryIds,
];
}
private function formatCategoryAsWpTerm($meta)
{
$value = $meta->value;
return [
'id' => (int) $meta->id,
'count' => 0,
'name' => Arr::get($value, 'name', $meta->key),
'slug' => $meta->key,
'parent' => 0,
];
}
private function formatPattern($meta)
{
$value = $meta->value;
return [
'id' => (int) $meta->id,
'slug' => $meta->key,
'title' => Arr::get($value, 'title', ''),
'content' => Arr::get($value, 'content', ''),
'category' => Arr::get($value, 'category', ''),
'description' => Arr::get($value, 'description', ''),
'sync_status' => Arr::get($value, 'sync_status', 'unsynced'),
'created_at' => $meta->created_at ? (string) $meta->created_at : '',
'updated_at' => $meta->updated_at ? (string) $meta->updated_at : '',
];
}
/**
* Build slug → id map for all pattern categories.
*/
private function getCategoryMap()
{
$categories = Meta::where('object_type', $this->categoryObjectType)->get();
$map = [];
foreach ($categories as $cat) {
$map[$cat->key] = $cat->id;
}
return $map;
}
/**
* Resolve category IDs back to a single category name.
*/
private function resolveCategoryName($categoryIds)
{
if (empty($categoryIds)) {
return '';
}
$categoryIds = array_map('intval', $categoryIds);
$category = Meta::where('object_type', $this->categoryObjectType)
->whereIn('id', $categoryIds)
->first();
if ($category) {
return Arr::get($category->value, 'name', $category->key);
}
return '';
}
/**
* Get patterns formatted for the block editor boot data.
*/
public static function getEditorPatterns()
{
$patterns = Meta::where('object_type', 'email_pattern')
->orderBy('id', 'desc')
->get();
$editorPatterns = [];
$categories = [];
$seenCategories = [];
foreach ($patterns as $pattern) {
$value = $pattern->value;
$title = Arr::get($value, 'title', '');
$content = Arr::get($value, 'content', '');
$category = Arr::get($value, 'category', '');
$description = Arr::get($value, 'description', '');
if (!$content) {
continue;
}
$patternCategories = [];
if ($category) {
$catSlug = sanitize_title($category);
$patternCategories[] = $catSlug;
if (!isset($seenCategories[$catSlug])) {
$seenCategories[$catSlug] = true;
$categories[] = [
'name' => $catSlug,
'label' => $category,
];
}
}
// Always include in the general fluentcrm-patterns category
$patternCategories[] = 'fluentcrm-patterns';
$editorPatterns[] = [
'name' => $pattern->key,
'title' => $title,
'content' => $content,
'description' => $description,
'categories' => $patternCategories,
'keywords' => ['fluentcrm', 'email'],
];
}
// Always add the root category
array_unshift($categories, [
'name' => 'fluentcrm-patterns',
'label' => __('My Patterns', 'fluent-crm'),
]);
return [
'patterns' => $editorPatterns,
'categories' => $categories,
];
}
}
@@ -0,0 +1,464 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Funnel;
use FluentCrm\App\Models\Lists;
use FluentCrm\App\Models\Tag;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
use FluentForm\App\Modules\Acl\Acl;
/**
* FormsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class FormsController extends Controller
{
/**
* Get all of the lists
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response|array
* @throws \WpFluent\Exception
*/
public function index(Request $request)
{
if (!defined('FLUENTFORM')) {
return [
'installed' => false,
'forms' => (object)[
'data' => [],
'total' => 0
]
];
}
// Now let's find the forms which are connected with Fluent Forms
$connectFeedForms = fluentCrmDb()->table('fluentform_form_meta')
->where('meta_key', 'fluentcrm_feeds')
->select(['form_id', 'id', 'value'])
->groupBy('form_id')
->get();
$formIds = [];
$connectedFormIds = [];
foreach ($connectFeedForms as $form) {
$formIds[] = $form->form_id;
$settings = json_decode($form->value, true);
$connectedFormIds[$form->form_id] = [
'feed_id' => $form->id,
'settings' => $settings
];
}
// Now let's get forms ids from funnel
$fluentFormFunnels = Funnel::where('trigger_name', 'fluentform_submission_inserted')
->get();
$connectedFunnelIds = [];
foreach ($fluentFormFunnels as $funnel) {
$formId = Arr::get($funnel->settings, 'form_id');
if ($formId) {
$connectedFunnelIds[$formId] = $funnel->id;
$formIds[] = $formId;
}
}
$formIds = array_unique($formIds);
$page = $request->get('page', 1);
$limit = $request->get('per_page', 10);
$offset = ($page - 1) * $limit;
$forms = [];
if ($formIds) {
$crmBaseUrl = fluentcrm_menu_url_base();
$search = sanitize_text_field($request->get('search', ''));
$allFormsQuery = fluentCrmDb()->table('fluentform_forms')
->whereIn('id', $formIds);
if ($search) {
$allFormsQuery->where('title', 'LIKE', '%' . $search . '%');
}
$allForms = $allFormsQuery->orderBy('id', 'DESC')
->limit($limit)
->offset($offset)
->get();
foreach ($allForms as $form) {
$funnelUrl = '';
$feedUrl = '';
$associateTags = [];
$associateList = '';
if (isset($connectedFunnelIds[$form->id])) {
$funnelUrl = $crmBaseUrl . 'funnel/' . $connectedFunnelIds[$form->id] . '/edit';
}
if (isset($connectedFormIds[$form->id])) {
$feedUrl = admin_url('admin.php?page=fluent_forms&form_id=' . $form->id . '&route=settings&sub_route=form_settings#/all-integrations/' . $connectedFormIds[$form->id]['feed_id'] . '/fluentcrm');
$tagIds = Arr::get($connectedFormIds[$form->id], 'settings.tag_ids');
if ($tagIds) {
$tags = Tag::whereIn('id', $tagIds)->get();
foreach ($tags as $tag) {
$associateTags[] = $tag->title;
}
}
$listId = Arr::get($connectedFormIds[$form->id], 'settings.list_id');
if ($listId && $list = Lists::find($listId)) {
$associateList = $list->title;
}
}
$forms[] = [
'id' => $form->id,
'title' => $form->title,
'status' => $form->status,
'created_at' => $form->created_at,
'funnel_url' => $funnelUrl,
'feed_url' => $feedUrl,
'associate_tags' => implode(', ', $associateTags),
'associate_lists' => $associateList,
'shortcode' => '[fluentform id="' . $form->id . '"]',
'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $form->id),
'preview_url' => site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $form->id)
];
}
}
$total = count($formIds);
return [
'installed' => true,
'forms' => [
'data' => $forms,
'page' => $page,
'per_page' => $limit,
'total' => $total,
'last_page' => ceil($total / $limit)
]
];
}
public function create(Request $request)
{
$form = $this->validate($request->all(), [
'template_id' => 'required',
'title' => 'required|unique:fluentform_forms',
'selected_tags' => 'required',
'selected_list' => 'required'
]);
$template = $this->getSelectedTemplate($form['template_id']);
$now = current_time('mysql');
$formData = [
'title' => $form['title'],
'status' => 'published',
'type' => 'form',
'created_by' => get_current_user_id(),
'created_at' => $now,
'updated_at' => $now,
'form_fields' => $template['form_fields']
];
$formId = fluentCrmDb()->table('fluentform_forms')->insertGetId($formData);
if ($template['custom_css']) {
fluentCrmDb()->table('fluentform_form_meta')
->insert([
'form_id' => $formId,
'meta_key' => '_custom_form_css',
'value' => $template['custom_css']
]);
}
$defaultSettings = (new \FluentForm\App\Modules\Form\Form(wpFluentForm()))->getFormsDefaultSettings();
if ($form['double_optin']) {
$defaultSettings['confirmation']['messageToShow'] = __('Please check your inbox to confirm your subscription', 'fluent-crm');
} else {
$defaultSettings['confirmation']['messageToShow'] = __('You are successfully subscribed to our email list', 'fluent-crm');
}
fluentCrmDb()->table('fluentform_form_meta')
->insert(array(
'form_id' => $formId,
'meta_key' => 'formSettings',
'value' => json_encode($defaultSettings)
));
$feedDefaults = [
'name' => __('FluentCRM Integration Feed', 'fluent-crm'),
'first_name' => '',
'last_name' => '',
'email' => 'email',
'other_fields' => [
[
'item_value' => '',
'label' => ''
]
],
'list_id' => $form['selected_list'],
'tag_ids' => $form['selected_tags'],
'skip_if_exists' => false,
'double_opt_in' => $form['double_optin'],
'conditionals' => [
'conditions' => [],
'status' => false,
'type' => 'all'
],
'enabled' => true,
'status' => true
];
if (is_array($template['map_fields'])) {
$feedDefaults = wp_parse_args($template['map_fields'], $feedDefaults);
}
$feedData = [
'meta_key' => 'fluentcrm_feeds',
'form_id' => $formId,
'value' => \json_encode($feedDefaults)
];
$createdFeedId = fluentCrmDb()->table('fluentform_form_meta')
->insertGetId($feedData);
do_action('fluentform/inserted_new_form', $formId, $formData);
do_action('fluentcrm_created_new_fluentform', $formId, $formData);
$feedUrl = admin_url('admin.php?page=fluent_forms&form_id=' . $formId . '&route=settings&sub_route=form_settings#/all-integrations/' . $createdFeedId . '/fluentcrm');
return [
'message' => __('Form has been created', 'fluent-crm'),
'created_form' => [
'id' => $formId,
'shortcode' => '[fluentform id="' . $formId . '"]',
'feed_url' => $feedUrl,
'edit_url' => admin_url('admin.php?page=fluent_forms&route=editor&form_id=' . $formId),
'preview_url' => site_url('?fluent_forms_pages=1&design_mode=1&preview_id=' . $formId)
]
];
}
public function getTemplates()
{
$templates = [
'inline_subscribe' => [
'label' => __('Inline Opt-in Form', 'fluent-crm'),
'image' => fluentCrmMix('images/forms/form_1.svg'),
'id' => 'inline_subscribe',
'form_fields' => '{"fields":[{"index":1,"element":"input_email","attributes":{"type":"email","name":"email","value":"","id":"","class":"extra_spaced","placeholder":"Email Address"},"settings":{"container_class":"","label":"","label_placement":"","help_message":"","admin_field_label":"Email Address","validation_rules":{"required":{"value":true,"message":"This field is required"},"email":{"value":true,"message":"This field must contain a valid email"}},"conditional_logics":{"type":"any","status":false,"conditions":[{"field":"","value":"","operator":""}]},"is_unique":"no","unique_validation_message":"Email address need to be unique."},"editor_options":{"title":"Email Address","icon_class":"ff-edit-email","template":"inputText"},"uniqElKey":"el_1601142291509"}],"submitButton":{"uniqElKey":"el_1524065200616","element":"button","attributes":{"type":"submit","class":""},"settings":{"align":"left","button_style":"default","container_class":"top_merged","help_message":"","background_color":"#409EFF","button_size":"md","color":"#ffffff","button_ui":{"type":"default","text":"Subscribe","img_url":""},"normal_styles":{"backgroundColor":"#409EFF","borderColor":"#409EFF","color":"#ffffff","borderRadius":"","minWidth":""},"hover_styles":{"backgroundColor":"#ffffff","borderColor":"#409EFF","color":"#409EFF","borderRadius":"","minWidth":""},"current_state":"normal_styles"},"editor_options":{"title":"Submit Button"}}}',
'custom_css' => $this->getFormCss('inline_subscribe'),
'map_fields' => [
'email' => 'email'
]
],
'simple_optin' => [
'label' => __('Simple Opt-in Form', 'fluent-crm'),
'image' => fluentCrm('url.assets') . 'images/forms/form_2.svg',
'id' => 'simple_optin',
'form_fields' => '{"fields":[{"index":1,"element":"input_email","attributes":{"type":"email","name":"email","value":"","id":"","class":"","placeholder":"Your Email Address"},"settings":{"container_class":"","label":"","label_placement":"","help_message":"","admin_field_label":"Email Address","validation_rules":{"required":{"value":true,"message":"This field is required"},"email":{"value":true,"message":"This field must contain a valid email"}},"conditional_logics":[],"is_unique":"no","unique_validation_message":"Email address need to be unique."},"editor_options":{"title":"Email Address","icon_class":"ff-edit-email","template":"inputText"},"uniqElKey":"el_16011431576720.7540920979222681"}],"submitButton":{"uniqElKey":"el_1524065200616","element":"button","attributes":{"type":"submit","class":""},"settings":{"align":"left","button_style":"default","container_class":"","help_message":"","background_color":"#409EFF","button_size":"md","color":"#ffffff","button_ui":{"type":"default","text":"Subscribe To Newsletter","img_url":""},"normal_styles":{"backgroundColor":"#409EFF","borderColor":"#409EFF","color":"#ffffff","borderRadius":"","minWidth":""},"hover_styles":{"backgroundColor":"#ffffff","borderColor":"#409EFF","color":"#409EFF","borderRadius":"","minWidth":""},"current_state":"normal_styles"},"editor_options":{"title":"Submit Button"}}}',
'custom_css' => '',
'map_fields' => [
'email' => 'email'
]
],
'with_name_subscribe' => [
'label' => __('Subscription Form', 'fluent-crm'),
'image' => fluentCrm('url.assets') . 'images/forms/form_3.svg',
'id' => 'with_name_subscribe',
'form_fields' => '{"fields":[{"index":0,"element":"input_name","attributes":{"name":"names","data-type":"name-element"},"settings":{"container_class":"","admin_field_label":"Name","conditional_logics":{"type":"any","status":false,"conditions":[{"field":"","value":"","operator":""}]},"label_placement":""},"fields":{"first_name":{"element":"input_text","attributes":{"type":"text","name":"first_name","value":"","id":"","class":"","placeholder":"First Name"},"settings":{"container_class":"","label":"First Name","help_message":"","visible":true,"validation_rules":{"required":{"value":false,"message":"This field is required"}},"conditional_logics":[]},"editor_options":{"template":"inputText"}},"middle_name":{"element":"input_text","attributes":{"type":"text","name":"middle_name","value":"","id":"","class":"","placeholder":"","required":false},"settings":{"container_class":"","label":"Middle Name","help_message":"","error_message":"","visible":false,"validation_rules":{"required":{"value":false,"message":"This field is required"}},"conditional_logics":[]},"editor_options":{"template":"inputText"}},"last_name":{"element":"input_text","attributes":{"type":"text","name":"last_name","value":"","id":"","class":"","placeholder":"Last Name","required":false},"settings":{"container_class":"","label":"Last Name","help_message":"","error_message":"","visible":true,"validation_rules":{"required":{"value":false,"message":"This field is required"}},"conditional_logics":[]},"editor_options":{"template":"inputText"}}},"editor_options":{"title":"Name Fields","element":"name-fields","icon_class":"ff-edit-name","template":"nameFields"},"uniqElKey":"el_1570866006692"},{"index":1,"element":"input_email","attributes":{"type":"email","name":"email","value":"","id":"","class":"","placeholder":"Email Address"},"settings":{"container_class":"","label":"Email","label_placement":"","help_message":"","admin_field_label":"","validation_rules":{"required":{"value":true,"message":"This field is required"},"email":{"value":true,"message":"This field must contain a valid email"}},"conditional_logics":{"type":"any","status":false,"conditions":[{"field":"","value":"","operator":""}]},"is_unique":"no","unique_validation_message":"Email address need to be unique."},"editor_options":{"title":"Email Address","icon_class":"ff-edit-email","template":"inputText"},"uniqElKey":"el_1570866012914"}],"submitButton":{"uniqElKey":"el_1524065200616","element":"button","attributes":{"type":"submit","class":""},"settings":{"align":"left","button_style":"default","container_class":"","help_message":"","background_color":"#409EFF","button_size":"md","color":"#ffffff","button_ui":{"type":"default","text":"Subscribe","img_url":""},"normal_styles":{"backgroundColor":"#409EFF","borderColor":"#409EFF","color":"#ffffff","borderRadius":"","minWidth":""},"hover_styles":{"backgroundColor":"#ffffff","borderColor":"#409EFF","color":"#409EFF","borderRadius":"","minWidth":""},"current_state":"normal_styles"},"editor_options":{"title":"Submit Button"}}}',
'custom_css' => '',
'map_fields' => [
'email' => 'email',
'first_name' => '{inputs.names.first_name}',
'last_name' => '{inputs.names.last_name}'
]
]
];
/**
* Define the form templates for FluentCRM Forms(Fluent Forms).
*
* This filter allows customization of the Fluent Forms templates used in FluentCRM.
*
* @param array {
* An array of form templates.
*
* @type array $inline_subscribe {
* Inline Opt-in Form template.
* @type string $label The label for the form.
* @type string $image The URL of the form image.
* @type string $id The ID of the form.
* @type string $form_fields The JSON string of form fields.
* @type string $custom_css The custom CSS for the form.
* @type array $map_fields The mapping of form fields.
* }
* @type array $simple_optin {
* Simple Opt-in Form template.
* @type string $label The label for the form.
* @type string $image The URL of the form image.
* @type string $id The ID of the form.
* @type string $form_fields The JSON string of form fields.
* @type string $custom_css The custom CSS for the form.
* @type array $map_fields The mapping of form fields.
* }
* @type array $with_name_subscribe {
* Subscription Form template.
* @type string $label The label for the form.
* @type string $image The URL of the form image.
* @type string $id The ID of the form.
* @type string $form_fields The JSON string of form fields.
* @type string $custom_css The custom CSS for the form.
* @type array $map_fields The mapping of form fields.
* }
* }
* @since 2.7.0
*
*/
return apply_filters('fluent_crm/ff_form_templates', [
'templates' => $templates
]);
}
private function getFormCss($name)
{
$css = '';
if ($name == 'inline_subscribe') {
$css = '.fluent_form_FF_ID {
position: relative;
}
.fluent_form_FF_ID .top_merged.ff_submit_btn_wrapper {
position: absolute;
top: 5px;
right: 5px;
}
.fluent_form_FF_ID .extra_spaced {
padding: 12px 15px !important;
}';
}
return $css;
}
private function getSelectedTemplate($templateId)
{
$templates = $this->getTemplates();
if (isset($templates['templates'][$templateId])) {
return $templates['templates'][$templateId];
}
$templatesArray = array_values($templates['templates']);
return $templatesArray[0];
}
public function getEntries(Request $request, $id)
{
if (!defined('FLUENTFORM')) {
return $this->sendError([
'message' => __('Fluent Forms is not installed', 'fluent-crm'),
'entries' => []
]);
}
if (!Acl::hasPermission('fluentform_entries_viewer', $id)) {
return $this->sendError([
'message' => __('You do not have permission to view these entries', 'fluent-crm'),
'entries' => []
]);
}
// Check if form exists
$form = fluentCrmDb()->table('fluentform_forms')
->where('id', $id)
->first();
if (!$form) {
return $this->sendError([
'message' => __('Form not found', 'fluent-crm'),
'entries' => []
]);
}
$page = $request->get('page', 1);
$limit = $request->get('per_page', 10);
$offset = ($page - 1) * $limit;
$search = sanitize_text_field($request->get('search', ''));
// Get total count
$totalQuery = fluentCrmDb()->table('fluentform_submissions')
->where('form_id', $id);
if ($search) {
$totalQuery->where(function ($query) use ($search) {
$query->where('response', 'LIKE', '%' . $search . '%')
->orWhere('status', 'LIKE', '%' . $search . '%');
});
}
// Get entries
$entriesQuery = fluentCrmDb()->table('fluentform_submissions')
->where('form_id', $id);
if ($search) {
$entriesQuery->where(function ($query) use ($search) {
$query->where('response', 'LIKE', '%' . $search . '%')
->orWhere('status', 'LIKE', '%' . $search . '%');
});
}
$total = $totalQuery->count();
$entries = $entriesQuery->orderBy('id', 'DESC')
->limit($limit)
->offset($offset)
->get();
// Format entries
$formattedEntries = [];
foreach ($entries as $entry) {
$response = json_decode($entry->response, true);
$formattedEntries[] = [
'id' => $entry->id,
'serial_number' => $entry->serial_number,
'status' => $entry->status,
'created_at' => $entry->created_at,
'response' => $response,
'user_id' => $entry->user_id,
'browser' => $entry->browser,
'device' => $entry->device,
'ip' => $entry->ip,
'entry_url' => admin_url('admin.php?page=fluent_forms&form_id=' . $id . '&route=entries#/entries/' . $entry->id)
];
}
return [
'entries' => [
'data' => $formattedEntries,
'page' => $page,
'per_page' => $limit,
'total' => $total,
'last_page' => ceil($total / $limit)
],
'form' => [
'id' => $form->id,
'title' => $form->title
]
];
}
public function getEntry(Request $request, $formId, $id)
{
$dataView = apply_filters('fluent_crm/dynamic_contact_item_view_fluentform', [
'content_html' => 'No data found'
], [
'__id' => $id
]);
return [
'entry' => $dataView
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,149 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Funnel;
use FluentCrm\App\Models\Label;
use FluentCrm\App\Models\TermRelation;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\Framework\Support\Arr;
/**
* FunnelLabelController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 2.9.25
*/
class GlobalLabelController extends Controller
{
public function getLabels()
{
$labels = Label::orderBy('position', 'ASC')->get();
return [
'labels' => $labels
];
}
public function create(Request $request)
{
$data = Arr::get($request->all(), 'label');
// sanitize the data
$labelData = [
'slug' => sanitize_text_field($data['slug']),
'title' => sanitize_text_field($data['title']),
];
$color = sanitize_hex_color($data['color']);
$labelData['settings'] = [
'color' => $color
];
$label = Label::create($labelData);
return [
'label' => $label,
'message' => __('Label has been created successfully', 'fluent-crm')
];
}
public function update(Request $request, $id)
{
$data = Arr::get($request->all(), 'label');
$label = Label::findOrFail($id);
// sanitize the data
$labelData = [
'slug' => sanitize_text_field($data['slug']),
'title' => sanitize_text_field($data['title']),
];
$color = sanitize_hex_color($data['color']);
$labelData['settings'] = [
'color' => $color
];
$label->update($labelData);
return [
'label' => $label,
'message' => __('Labels have been updated successfully', 'fluent-crm')
];
}
public function delete(Request $request, $id)
{
$label = Label::findOrFail($id);
if ($label) {
$label->delete();
}
return [
'message' => __('Label has been deleted successfully', 'fluent-crm')
];
}
public function deleteLabel(Request $request)
{
$funnelId = $request->getSafe('funnel_id', 'intval');
$labelSlug = $request->getSafe('label_slug');
$action = $request->getSafe('action');
if (!$labelSlug) {
return [
'message' => __('Please provide label slug', 'fluent-crm')
];
}
switch ($action) {
case 'delete_from_funnel':
$this->deleteLabelFromFunnel($funnelId, $labelSlug);
return [
'message' => __('Removed from funnel successfully', 'fluent-crm')
];
case 'delete_from_funnel_label':
$this->deleteLabelFromFunnelLabel($labelSlug);
return [
'message' => __('Label has been deleted successfully', 'fluent-crm')
];
default:
return [
'message' => __('Invalid Action', 'fluent-crm')
];
}
}
protected function deleteLabelFromFunnel($funnelId, $slug)
{
$funnel = Funnel::findOrFail($funnelId);
$label = Label::where('slug', $slug)->first();
if (!$label) {
return;
}
$funnel->detachLabels([$label->id]);
}
protected function deleteLabelFromFunnelLabel($slug)
{
$label = Label::where('slug', $slug)->first();
if (!$label) {
return;
}
TermRelation::where('term_id', $label->id)
->where('object_type', Funnel::class)
->delete();
$label->delete();
}
}
@@ -0,0 +1,386 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\App\Models\Subscriber;
/**
* ImporterController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class ImporterController extends Controller
{
public function getDrivers()
{
/**
* Determine the list of contact import providers for FluentCRM.
*
* This filter allows you to modify the list of available import providers
* in the FluentCRM plugin. By default, it includes CSV File and WordPress Users.
*
* @since 2.7.0
*
* @param array {
* An associative array of import providers.
*
* @type array csv {
* Details for the CSV File import provider.
*
* @type string $label The label for the provider.
* @type string $logo The URL to the provider's logo.
* @type bool $disabled Whether the provider is disabled.
* }
* @type array users {
* Details for the WordPress Users import provider.
*
* @type string $label The label for the provider.
* @type string $logo The URL to the provider's logo.
* @type bool $disabled Whether the provider is disabled.
* }
* }
*/
$drivers = apply_filters('fluent_crm/import_providers', [
'csv' => [
'label' => __('CSV File', 'fluent-crm'),
'logo' => fluentCrmMix('images/csv.svg'),
'disabled' => false
],
'users' => [
'label' => __('WordPress Users', 'fluent-crm'),
'logo' => fluentCrmMix('images/wordpress.svg'),
'disabled' => false
]
]);
if (defined('FLUENTCART_VERSION')) {
$drivers['fluent_cart'] = [
'label' => __('FluentCart', 'fluent-crm'),
'logo' => fluentCrmMix('images/fluent-cart-dark.svg'),
'disabled' => false
];
}
if ($proDrivers = $this->getProDrivers()) {
$drivers = array_merge($drivers, $proDrivers);
}
return [
'drivers' => $drivers
];
}
public function getDriver(Request $request, $driver)
{
if ($driver == 'users') {
return $this->processUserDriver($request);
}
/**
* Determine the import driver response (CSV).
*
* This filter allows modification of the import driver response based on the specified driver.
*
* @since 2.7.0
*
* @param bool The response to be filtered or not. Default false.
* @param object $request The request object containing import data.
*/
$response = apply_filters('fluent_crm/get_import_driver_' . $driver, false, $request);
if (!$response || is_wp_error($response)) {
$message = __('Sorry no driver found for this import', 'fluent-crm');
if (is_wp_error($response)) {
$message = $response->get_error_message();
}
return $this->sendError([
'message' => $message
]);
}
return $response;
}
public function importData(Request $request, $driver)
{
$config = $request->get('config', []);
$page = $request->getSafe('importing_page', 'intval', 1);
if ($driver == 'users') {
return $this->processUserImport($config, $page);
}
/**
* Determine the response after importing data using a specific driver (CSV).
*
* This filter allows you to modify the response after the import process
* using a specified driver.
*
* @since 2.7.0
*
* @param bool The response to be filtered or not. Default false.
* @param array $config The configuration array for the import process.
* @param int $page The current page number being processed.
*/
$response = apply_filters('fluent_crm/post_import_driver_' . $driver, false, $config, $page);
if (!$response || is_wp_error($response)) {
$message = __('Sorry no driver found for this import', 'fluent-crm');
if (is_wp_error($response)) {
$message = $response->get_error_message();
}
return $this->sendError([
'message' => $message
]);
}
return $response;
}
private function processUserDriver($request)
{
$summary = $request->get('summary');
if ($summary) {
$config = $request->get('config');
$userQuery = new \WP_User_Query([
'role__in' => Arr::get($config, 'roles'),
'number' => 5,
'fields' => ['ID', 'display_name', 'user_email'],
]);
$users = $userQuery->get_results();
$total = $userQuery->get_total();
$formattedUsers = [];
foreach ($users as $user) {
$formattedUsers[] = [
'name' => $user->display_name,
'email' => $user->user_email
];
}
return $this->send([
'import_info' => [
'subscribers' => $formattedUsers,
'total' => $total,
'has_list_config' => true,
'has_tag_config' => true,
'has_status_config' => true,
'has_update_config' => true,
'has_silent_config' => true
]]);
}
if (!function_exists('get_editable_roles')) {
require_once(ABSPATH . '/wp-admin/includes/user.php');
}
$roles = \get_editable_roles();
$formattedRoles = [];
foreach ($roles as $roleKey => $role) {
$formattedRoles[] = [
'id' => $roleKey,
'label' => $role['name']
];
}
$infoSvg = '<span class="fc-inline-help-icon" aria-hidden="true" style="display:inline-flex;vertical-align:middle">'
. '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">'
. '<path d="M8 14C4.6862 14 2 11.3138 2 8C2 4.6862 4.6862 2 8 2C11.3138 2 14 4.6862 14 8C14 11.3138 11.3138 14 8 14ZM7.4 7.4V11H8.6V7.4H7.4ZM7.4 5V6.2H8.6V5H7.4Z" fill="currentColor"/>'
. '</svg></span>';
return [
'config' => [
'roles' => []
],
'fields' => [
'roles' => [
'label' => __('Select User Roles', 'fluent-crm'),
'inline_help' => $infoSvg . ' ' . __('Please check the user roles that you want to import as contact', 'fluent-crm'),
'type' => 'checkbox-group',
'options' => $formattedRoles,
'has_all_selector' => true,
'all_selector_label' => __('All', 'fluent-crm')
]
],
'labels' => [
'step_2' => __('Next [Review Data]', 'fluent-crm'),
'step_3' => __('Import Users Now', 'fluent-crm')
]
];
}
private function processUsers($users, $inputs)
{
$subscribers = [];
foreach ($users as $user) {
$subscriber = Helper::getWPMapUserInfo($user);
$subscriber['source'] = 'wp_users';
if (isset($subscriber['email']) && $subscriber['email']) {
$subscribers[] = $subscriber;
}
}
$sendDoubleOptin = Arr::get($inputs, 'double_optin_email') == 'yes';
return Subscriber::import(
$subscribers,
Arr::get($inputs, 'tags', []),
Arr::get($inputs, 'lists', []),
Arr::get($inputs, 'update', ''),
Arr::get($inputs, 'status', ''),
$sendDoubleOptin
);
}
private function processUserImport($config, $page)
{
$inputs = Arr::only($config, [
'map', 'tags', 'lists', 'roles', 'update', 'status', 'double_optin_email', 'import_silently'
]);
/**
* Determine the number of subscribers to process per request while importing.
*
* This filter allows you to modify the number of subscribers that are processed in each request.
*
* @since 2.7.0
*
* @param int $limit The number of subscribers to process per request. Default is 100.
*/
$limit = apply_filters('fluent_crm/import_users_limit_per_request', 100);
$userQuery = new \WP_User_Query([
'role__in' => $inputs['roles'],
'number' => $limit,
'offset' => ($page - 1) * $limit
]);
if (Arr::get($inputs, 'import_silently') == 'yes') {
if (!defined('FLUENTCRM_DISABLE_TAG_LIST_EVENTS')) {
define('FLUENTCRM_DISABLE_TAG_LIST_EVENTS', true);
}
}
$total = $userQuery->get_total();
$users = $userQuery->get_results();
if ($users) {
$this->processUsers($users, $inputs);
}
$hasRecords = !!count($users);
return $this->sendSuccess([
'page_total' => ceil($total / $limit),
'record_total' => $total,
'has_more' => $hasRecords,
'current_page' => $page,
'next_page' => $page + 1
]);
}
private function getProDrivers()
{
$drivers = [];
if (defined('FLUENTCAMPAIGN')) {
return $drivers;
}
if (defined('LLMS_PLUGIN_FILE')) {
$drivers['lifterlms'] = [
'label' => __('LifterLMS', 'fluent-crm'),
'logo' => fluentCrmMix('images/lifterlms.png'),
'disabled' => true,
'disabled_message' => __('Import LifterLMS students by course and groups then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (defined('LEARNDASH_VERSION')) {
$drivers['learndash'] = [
'label' => __('LearnDash', 'fluent-crm'),
'logo' => fluentCrmMix('images/learndash.png'),
'disabled' => true,
'disabled_message' => __('Import LearnDash students by course and groups then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (defined('TUTOR_VERSION')) {
$drivers['tutorlms'] = [
'label' => __('TutorLMS', 'fluent-crm'),
'logo' => fluentCrmMix('images/tutorlms.jpg'),
'disabled' => true,
'disabled_message' => __('Import TutorLMS students by course then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (defined('PMPRO_VERSION')) {
$drivers['pmpro'] = [
'label' => __('Paid Membership Pro', 'fluent-crm'),
'logo' => fluentCrmMix('images/pmpro.png'),
'disabled' => true,
'disabled_message' => __('Import Paid Membership Pro members by membership levels then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (defined('WLM3_PLUGIN_VERSION')) {
$drivers['wishlist_member'] = [
'label' => __('Wishlist member', 'fluent-crm'),
'logo' => fluentCrmMix('images/wishlist_member.png'),
'disabled' => true,
'disabled_message' => __('Import Wishlist members by membership levels then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (class_exists('\Restrict_Content_Pro')) {
$drivers['rcp'] = [
'label' => __('Restrict Content Pro', 'fluent-crm'),
'logo' => fluentCrmMix('images/rcp.png'),
'disabled' => true,
'disabled_message' => __('Import Restrict Content Pro members by membership levels then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
if (defined('BP_REQUIRED_PHP_VERSION') && function_exists('\buddypress')) {
$pluginName = 'BuddyPress';
$logo = fluentCrmMix('images/buddypress.png');
if (defined('BP_PLATFORM_VERSION')) {
$pluginName = 'BuddyBoss';
$logo = fluentCrmMix('images/buddyboss.svg');
}
$drivers['buddypress'] = [
'label' => $pluginName,
'logo' => $logo,
'disabled' => true,
/* translators: %s: plugin name */
'disabled_message' => sprintf(__('Import %s members by member groups and member types then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm'), $pluginName)
];
}
if (defined('LP_PLUGIN_FILE')) {
$drivers['learnpress'] = [
'label' => __('LearnPress', 'fluent-crm'),
'logo' => fluentCrmMix('images/learnpress.png'),
'disabled' => true,
'disabled_message' => __('Import LearnPress students by course then segment by associate tags. This is a pro feature. Please upgrade to activate this feature', 'fluent-crm')
];
}
return $drivers;
}
}
@@ -0,0 +1,260 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Lists;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
/**
* ListsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class ListsController extends Controller
{
/**
* Get all of the lists
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
*/
public function index(Request $request)
{
$with = $request->get('with', []);
$order = [
'by' => $request->getSafe('sort_by', 'sanitize_sql_orderby', 'id'),
'order' => $request->getSafe('sort_order', 'sanitize_sql_orderby', 'DESC')
];
$paginatedLists = Lists::orderBy($order['by'], $order['order'])
->searchBy($request->getSafe('search'))
->paginate();
$lists = $paginatedLists->items();
if (!$request->get('exclude_counts')) {
foreach ($lists as $list) {
$list->totalCount = $list->totalCount();
$list->subscribersCount = $list->countByStatus('subscribed');
}
}
$data = [
'lists' => $lists,
'pagination' => [
'total' => $paginatedLists->total(),
]
];
if ($request->get('all_lists')) {
$allLists = Lists::get();
$formattedLists = [];
foreach ($allLists as $list) {
$formattedLists[] = [
'id' => strval($list->id),
'title' => $list->title,
'slug' => $list->slug,
'description' => $list->description
];
}
$data['all_lists'] = $formattedLists;
}
return $this->send($data);
}
/**
* Find a list.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @param int $id
* @return \WP_REST_Response
*/
public function find(Request $request, $id)
{
return $this->send(Lists::find($id));
}
/**
* Store a list.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
*/
public function create(Request $request)
{
$allData = $request->all();
if (empty($allData['slug'])) {
if ($allData['title']) {
$allData['slug'] = sanitize_text_field($allData['title']);
}
}
$data = $this->validate($allData, [
'title' => 'required',
'slug' => "required|unique:fc_lists,slug"
]);
$list = Lists::create([
'title' => sanitize_text_field($allData['title']),
'slug' => sanitize_title($data['slug'], 'display'),
'description' => sanitize_textarea_field(Arr::get($allData, 'description'))
]);
do_action('fluentcrm_list_created', $list->id);
do_action('fluent_crm/list_created', $list);
return $this->send([
'lists' => $list,
'item' => $list,
'message' => __('Successfully saved the list.', 'fluent-crm')
]);
}
/**
* Store a list.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @param $id int
* @return \WP_REST_Response
*/
public function update(Request $request, $id)
{
$allData = $this->validate($request->all(), [
'title' => 'required'
]);
if(!empty($allData['slug'])) {
$allData['slug'] = Helper::slugify($allData['title']);
}
if ($id == 0 && $request->get('update_by') == 'slug' && !empty($allData['slug'])) {
$list = Lists::where('slug', $allData['slug'])->first();
if (!$list) {
return $this->sendError([
'message' => __('List could not be found', 'fluent-crm')
]);
}
$id = $list->id;
} else {
$list = Lists::findOrFail($id);
if(empty($allData['slug'])) {
$allData['slug'] = $list->slug;
}
}
if (Lists::where('slug', $allData['slug'])->where('id', '!=', $id)->first()) {
return $this->sendError([
'message' => __('Provided slug already exists in another list', 'fluent-crm')
]);
}
$list = Lists::where('id', $id)->update([
'title' => sanitize_text_field($allData['title']),
'slug' => $allData['slug'],
'description' => sanitize_textarea_field(Arr::get($allData, 'description')),
]);
do_action('fluentcrm_list_updated', $id);
do_action('fluent_crm/list_updated', $list);
return $this->send([
'lists' => $list,
'message' => __('Successfully saved the list.', 'fluent-crm'),
]);
}
/**
* Bulk store lists.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
*/
public function storeBulk(Request $request)
{
$lists = $request->get('lists', []);
if (empty($lists)) {
$lists = $this->request->get('items', []);
}
$createdIds = [];
foreach ($lists as $list) {
if (empty($list['title'])) {
continue;
}
if (empty($list['slug'])) {
$list['slug'] = Helper::slugify($list['title']);
}
$list = Lists::updateOrCreate(
['slug' => sanitize_title($list['slug'], 'display')],
['title' => sanitize_text_field($list['title'])]
);
$createdIds[] = $list->id;
if($list->wasRecentlyCreated) {
do_action('fluentcrm_list_created', $list->id);
do_action('fluent_crm/list_created', $list);
} else {
do_action('fluentcrm_list_updated', $list->id);
do_action('fluent_crm/list_updated', $list);
}
}
return $this->sendSuccess([
'message' => __('Provided Lists have been successfully created', 'fluent-crm'),
'ids' => $createdIds
]);
}
/**
* Delete a list
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @param int $id
* @return \WP_REST_Response
*/
public function remove(Request $request, $id)
{
Lists::where('id', $id)->delete();
do_action('fluent_crm/list_deleted', $id);
do_action('fluentcrm_list_deleted', $id);
return $this->send([
'message' => __('Successfully removed the list.', 'fluent-crm')
]);
}
public function handleBulkAction(Request $request)
{
$listIds = array_map('intval', (array)$request->get('listIds', []));
$listIds = array_unique(array_filter($listIds));
foreach ($listIds as $listId) {
Lists::where('id', $listId)->delete();
do_action('fluent_crm/list_deleted', $listId);
do_action('fluentcrm_list_deleted', $listId);
}
return $this->sendSuccess([
'message' => __('Selected Lists have been removed permanently', 'fluent-crm'),
]);
}
}
@@ -0,0 +1,431 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Modules\MCP\AbilitiesRegistrar;
use FluentCrm\App\Modules\MCP\MCPInit;
use FluentCrm\Framework\Http\Request\Request;
/**
* Settings → MCP admin endpoints (MCP_PLAN.md § 13).
*
* Surfaces:
* - status: adapter detected? CRM count? enabled toggle?
* - install-adapter: one-click install of the WP MCP Adapter plugin
* - toggle: enable/disable the entire MCP module without uninstalling
* - config-snippet: pre-filled JSON for Claude Desktop / Claude Code /
* Cursor / generic clients
*/
class MCPSettingsController extends Controller
{
const ADAPTER_PLUGIN_FILE = 'mcp-adapter/mcp-adapter.php';
const TOOLKIT_PLUGIN_FILE = 'fluent-toolkit/fluent-toolkit.php';
/**
* Status block — what the Settings page lights up with on load.
*/
public function status()
{
if (!function_exists('is_plugin_active')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$adapterInstalled = $this->isAdapterPresent();
$toolkitInstalled = $this->isToolkitPresent();
$adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
$standaloneActive = is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable;
$toolkitActive = $this->isToolkitLoaded();
$toolkitAdapterActive = $toolkitActive && $this->isToolkitAdapterAvailable();
$adapterActive = $standaloneActive || $toolkitAdapterActive;
$adapterProvider = $standaloneActive ? 'plugin' : ($toolkitAdapterActive ? 'toolkit' : '');
$abilitiesAvailable = function_exists('wp_register_ability');
$canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
$toolsCount = $abilitiesAvailable ? $this->countAbilities() : 0;
$currentUser = wp_get_current_user();
// Detect a local dev environment heuristically. Self-signed/local
// certs trip Node's TLS validation in the npx proxy that Claude
// Desktop uses; if we know the user is on dev, we pre-bake the
// workaround into the generated snippet. The Vue page also exposes
// a manual toggle for edge cases.
$isLocalDev = self::detectLocalDevEnvironment();
return [
'adapter_installed' => $adapterInstalled || $toolkitInstalled,
'adapter_active' => $adapterActive,
'adapter_provider' => $adapterProvider,
'standalone_adapter_installed' => $adapterInstalled,
'toolkit_installed' => $toolkitInstalled,
'toolkit_active' => $toolkitActive,
'toolkit_adapter_available' => $toolkitAdapterActive,
'adapter_runtime_available' => $adapterRuntimeAvailable,
'adapter_version' => $this->detectAdapterVersion(),
'toolkit_version' => $this->detectToolkitVersion(),
'abilities_api_loaded' => $abilitiesAvailable,
'endpoint_url' => MCPInit::getEndpointUrl(),
'tools_count' => $toolsCount,
'mcp_enabled' => fluentcrm_get_option('mcp_enabled', 'yes') === 'yes',
'pro_active' => defined('FLUENTCAMPAIGN'),
'app_passwords_url' => admin_url('profile.php#application-passwords-section'),
'plugins_url' => admin_url('plugins.php'),
'can_auto_install_adapter' => $canAutoInstall,
'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
'current_user_login' => $currentUser ? $currentUser->user_login : '',
'is_local_dev' => $isLocalDev,
];
}
/**
* Toggle the kill-switch. Stored as a FluentCRM option so the lazy-register
* guard in app/Hooks/actions.php picks it up on the next request.
*/
public function toggle(Request $request)
{
$value = $request->get('mcp_enabled');
$enabled = is_string($value) ? ($value === 'yes' || $value === 'true' || $value === '1') : (bool) $value;
fluentcrm_update_option('mcp_enabled', $enabled ? 'yes' : 'no');
return [
'ok' => true,
'mcp_enabled' => $enabled,
'message' => $enabled
? __('MCP tools enabled. New requests will see the FluentCRM abilities.', 'fluent-crm')
: __('MCP tools disabled. The adapter will no longer report FluentCRM abilities.', 'fluent-crm'),
];
}
/**
* One-click adapter install. Free can only explain the missing dependency;
* Pro may opt in to the FluentHub background installer via hooks.
*/
public function installAdapter()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugins', 'fluent-crm'),
]);
}
$canAutoInstall = (bool) apply_filters('fluent_toolkit/can_auto_install', false);
if (!$canAutoInstall) {
return $this->sendError([
'message' => __('Please install FluentHub from GitHub, then reload this page to connect FluentCRM with AI agents.', 'fluent-crm'),
'toolkit_download_url' => 'https://github.com/WPManageNinja/fluent-toolkit',
]);
}
do_action('fluent_toolkit/do_auto_install');
wp_clean_plugins_cache();
if (!function_exists('is_plugin_active')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$toolkitInstalled = $this->isToolkitPresent();
$toolkitActive = $this->isToolkitLoaded();
$adapterRuntimeAvailable = $this->isAdapterRuntimeAvailable();
$toolkitAdapterAvailable = $toolkitActive && $this->isToolkitAdapterAvailable();
$isInstalled = $this->isAdapterPresent() || $toolkitInstalled;
$isActive = (is_plugin_active(self::ADAPTER_PLUGIN_FILE) && $adapterRuntimeAvailable) || $toolkitAdapterAvailable;
if ($isInstalled && $isActive) {
$message = __('FluentHub installed and activated. Reload the page to register FluentCRM MCP tools.', 'fluent-crm');
} elseif ($toolkitInstalled && $toolkitActive) {
$message = __('FluentHub is installed and active, but this version does not include the bundled MCP adapter yet. Please update FluentHub when the MCP-ready build is available, then reload this page.', 'fluent-crm');
} elseif ($toolkitInstalled) {
$message = __('FluentHub is installed but could not be activated automatically. Please activate FluentHub from the Plugins page, then reload this page.', 'fluent-crm');
} else {
$message = __('Could not install FluentHub automatically. Please install FluentHub manually, then reload this page.', 'fluent-crm');
}
return [
'is_installed' => $isInstalled,
'adapter_active' => $isActive,
'toolkit_active' => $toolkitActive,
'toolkit_adapter_available' => $toolkitAdapterAvailable,
'message' => $message,
];
}
/**
* Generate a copy-paste config snippet for the requested client.
*
* Every client uses WordPress Application Passwords — built into WP 5.6+,
* no extra plugin needed. Direct HTTP clients (Claude Code, Cursor,
* generic) carry credentials via Basic Auth header; the
* @automattic/mcp-wordpress-remote stdio bridge that Claude Desktop uses
* accepts the username/password directly via WP_API_USERNAME and
* WP_API_PASSWORD env vars and handles encoding itself.
*
* Placeholders used here are stable strings the Vue page substitutes via
* regex when the user fills the credentials inputs.
*/
public function getConfigSnippet(Request $request)
{
$client = sanitize_key((string) $request->get('client', 'claude-code'));
$endpoint = MCPInit::getEndpointUrl();
// Optional override from the Settings UI checkbox. When the user
// explicitly says "I'm on local dev" we add the TLS-bypass env var to
// Claude Desktop's snippet; when they say "no" we omit it even if
// auto-detection thinks otherwise.
$forceLocalDev = $request->get('local_dev');
if ($forceLocalDev === 'yes' || $forceLocalDev === '1' || $forceLocalDev === 'true') {
$isLocalDev = true;
} elseif ($forceLocalDev === 'no' || $forceLocalDev === '0' || $forceLocalDev === 'false') {
$isLocalDev = false;
} else {
$isLocalDev = self::detectLocalDevEnvironment();
}
// The Vue page replaces these tokens with the user's real values.
// Keep them stable + distinct so the regex stays simple.
$basicPlaceholder = '<base64(your-username:application-password)>';
$usernamePlaceholder = '<your-username>';
$passwordPlaceholder = '<your-application-password>';
$appPasswordsUrl = admin_url('profile.php#application-passwords-section');
switch ($client) {
case 'codex':
$snippet = sprintf(
"Settings → Connect to a custom MCP\n\nName: fluent-crm\nTransport: Streamable HTTP ← click this tab first\n\nURL: %s\n\nHeader:\n Key: Authorization\n Value: Basic %s\n\nClick Save.",
$endpoint,
$basicPlaceholder
);
$instructions = sprintf(
/* translators: %s: link to WP user profile application passwords section */
__('Open OpenAI Codex → Settings → Connect to a custom MCP. Click the "Streamable HTTP" tab. Generate a WordPress Application Password from %s, then paste username + app password into the inputs above — the Value field will auto-fill with the encoded Basic auth string.', 'fluent-crm'),
$appPasswordsUrl
);
break;
case 'cursor':
$snippet = wp_json_encode([
'mcpServers' => [
'fluent-crm' => [
'url' => $endpoint,
'type' => 'http',
'headers' => [
'Authorization' => 'Basic ' . $basicPlaceholder,
],
],
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$instructions = __('Cursor speaks HTTP MCP natively. Fill in your username and application password above and the snippet will be ready to paste into Cursor → Settings → MCP. Restart Cursor afterwards.', 'fluent-crm');
break;
case 'generic':
$snippet = sprintf(
"URL: %s\nAuth: Authorization: Basic %s\n\n# Quick test (curl handles the base64 for you)\ncurl -s -u '%s:%s' \\\n -X POST %s \\\n -H 'Content-Type: application/json' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'",
$endpoint,
$basicPlaceholder,
$usernamePlaceholder,
$passwordPlaceholder,
$endpoint
);
$instructions = __('Use the URL + Basic Auth header with any HTTP MCP client. The endpoint speaks the standard MCP protocol — initialize, tools/list, tools/call — over JSON-RPC.', 'fluent-crm');
break;
case 'claude-desktop':
// Claude Desktop cannot speak HTTP MCP directly yet — it
// routes through @automattic/mcp-wordpress-remote, which
// accepts WP_API_USERNAME / WP_API_PASSWORD plain (proxy
// does the encoding). No JWT plugin needed.
$env = [
'WP_API_URL' => $endpoint,
'WP_API_USERNAME' => $usernamePlaceholder,
'WP_API_PASSWORD' => $passwordPlaceholder,
'OAUTH_ENABLED' => 'false',
];
if ($isLocalDev) {
// Self-signed certs trip Node's bundled CA store. Trust
// the connection wholesale for local dev — the proxy
// only ever talks to one URL the user explicitly chose,
// so the practical risk is bounded.
$env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
}
$snippet = wp_json_encode([
'mcpServers' => [
'fluent-crm' => [
'command' => 'npx',
'args' => ['-y', '@automattic/mcp-wordpress-remote@latest'],
'env' => $env,
],
],
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
$localDevNote = $isLocalDev
? ' ' . __('Local dev mode is on, so NODE_TLS_REJECT_UNAUTHORIZED is included — the npx proxy needs it to talk to self-signed Valet/MAMP/Local SSL.', 'fluent-crm')
: '';
$instructions = __('Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\\Claude\\claude_desktop_config.json (Windows), then restart Claude Desktop.', 'fluent-crm') . $localDevNote;
break;
case 'claude-code':
default:
$snippet = sprintf(
"claude mcp add \\\n --transport http \\\n fluent-crm %s \\\n --header \"Authorization: Basic %s\"",
$endpoint,
$basicPlaceholder
);
$instructions = __('Fill in your username and application password above, then paste the command into your terminal. Run `claude` and the FluentCRM tools will appear under MCP servers.', 'fluent-crm');
$client = 'claude-code';
break;
}
return [
'client' => $client,
'snippet' => $snippet,
'instructions' => $instructions,
'endpoint' => $endpoint,
'app_passwords_url' => $appPasswordsUrl,
'is_local_dev' => $isLocalDev,
];
}
/**
* Heuristic check for "we're running on a local development install."
*
* Tested in order:
* 1. Hostname ends in a dev TLD (.test, .lab, .local, .localhost)
* 2. Hostname is literally `localhost`
* 3. Host resolves to a private/loopback IP range
*
* Filterable via `fluent_crm/mcp_is_local_dev` so operators can override
* detection on edge cases (a public-facing site on `.local`, an internal
* tool that needs the dev-mode behavior anyway, etc.).
*
* @return bool
*/
private static function detectLocalDevEnvironment()
{
$host = wp_parse_url(home_url(), PHP_URL_HOST);
$host = strtolower((string) $host);
$isDev = false;
$devTlds = ['.test', '.lab', '.local', '.localhost', '.docker', '.dev'];
foreach ($devTlds as $tld) {
$len = strlen($tld);
if ($len > 0 && substr($host, -$len) === $tld) {
$isDev = true;
break;
}
}
if (!$isDev && ($host === 'localhost' || $host === '127.0.0.1' || $host === '::1')) {
$isDev = true;
}
if (!$isDev && filter_var($host, FILTER_VALIDATE_IP)) {
// Private IP ranges per RFC 1918.
$isPrivate = !filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
if ($isPrivate) {
$isDev = true;
}
}
/**
* Override the local-dev detection. Useful when the heuristic gets
* it wrong (e.g. a public site on a `.local` mDNS hostname).
*
* @since 2.10.0
*
* @param bool $isDev Whether the install looks like local dev.
* @param string $host The detected hostname.
*/
return (bool) apply_filters('fluent_crm/mcp_is_local_dev', $isDev, $host);
}
// ---------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------
private function isAdapterPresent()
{
return $this->isPluginPresent(self::ADAPTER_PLUGIN_FILE);
}
private function isToolkitPresent()
{
return $this->isToolkitLoaded() || $this->isPluginPresent(self::TOOLKIT_PLUGIN_FILE);
}
private function detectAdapterVersion()
{
return $this->detectPluginVersion(self::ADAPTER_PLUGIN_FILE);
}
private function detectToolkitVersion()
{
if ($this->isToolkitLoaded()) {
return (string) FLUENT_TOOLKIT_VERSION;
}
return $this->detectPluginVersion(self::TOOLKIT_PLUGIN_FILE);
}
private function isToolkitLoaded()
{
return defined('FLUENT_TOOLKIT_VERSION');
}
private function isPluginPresent($pluginFile)
{
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
return isset($plugins[$pluginFile]);
}
private function detectPluginVersion($pluginFile)
{
if (!function_exists('get_plugins')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$plugins = get_plugins();
if (!isset($plugins[$pluginFile])) {
return null;
}
return $plugins[$pluginFile]['Version'] ?? null;
}
private function isToolkitAdapterAvailable()
{
if (!$this->isToolkitLoaded()) {
return false;
}
if (class_exists('\FluentToolkit\Mcp\AdapterBootstrap') && method_exists('\FluentToolkit\Mcp\AdapterBootstrap', 'available')) {
return (bool) \FluentToolkit\Mcp\AdapterBootstrap::available();
}
return $this->isAdapterRuntimeAvailable();
}
private function isAdapterRuntimeAvailable()
{
return defined('WP_MCP_VERSION')
&& class_exists('\WP\MCP\Core\McpAdapter')
&& function_exists('wp_register_ability');
}
private function countAbilities()
{
$count = count(AbilitiesRegistrar::getDefinitions());
// Pro tools are pushed onto the names list via the
// `fluent_crm/mcp_ability_names` filter in MCPInit.
$names = apply_filters('fluent_crm/mcp_ability_names', array_keys(AbilitiesRegistrar::getDefinitions()));
if (is_array($names)) {
$count = count(array_unique($names));
}
return $count;
}
}
@@ -0,0 +1,208 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Services\CrmMigrator\ActiveCampaignMigrator;
use FluentCrm\App\Services\CrmMigrator\ConvertKitMigrator;
use FluentCrm\App\Services\CrmMigrator\DripMigrator;
use FluentCrm\App\Services\CrmMigrator\MailChimpMigrator;
use FluentCrm\App\Services\CrmMigrator\MailerLiteMigrator;
use FluentCrm\Framework\Http\Request\Request;
/**
* MigratorController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class MigratorController extends Controller
{
public function getDrivers(Request $request)
{
return [
'drivers' => $this->getMigrators()
];
}
public function verifyCredential(Request $request)
{
$driver = $request->get('driver');
$driverClassName = $this->getDriverClass($driver);
if (!$driverClassName) {
return $this->sendError([
'message' => __('Sorry no driver found for the selected CRM', 'fluent-crm')
]);
}
$credential = $request->get('credential', []);
$driverClass = new $driverClassName;
$result = $driverClass->verifyCredentials($credential);
if (is_wp_error($result)) {
return $this->sendError([
'message' => $result->get_error_message(),
], 422);
}
return [
'message' => __('Your provided API key is valid', 'fluent-crm')
];
}
public function getListTagMappings(Request $request)
{
$driver = $request->get('driver');
$driverClassName = $this->getDriverClass($driver);
if (!$driverClassName) {
return $this->sendError([
'message' => __('Sorry no driver found for the selected CRM', 'fluent-crm')
]);
}
$credential = $request->get('credential', []);
$result = (new $driverClassName)->getListTagMappings($request->all());
if (is_wp_error($result)) {
return $this->sendError([
'message' => $result->get_error_message(),
], 422);
}
return [
'options' => $result
];
}
public function getImportSummary(Request $request)
{
$driver = $request->get('driver');
$driverClassName = $this->getDriverClass($driver);
if (!$driverClassName) {
return $this->sendError([
'message' => __('Sorry no driver found for the selected CRM', 'fluent-crm')
]);
}
$credential = $request->get('credential', []);
$mapSettings = $request->get('map_settings', []);
$summary = (new $driverClassName)->getSummary($request->all());
if (is_wp_error($summary)) {
return $this->sendError([
'message' => $summary->get_error_message(),
], 422);
}
return [
'import_summary' => $summary
];
}
public function handleImport(Request $request)
{
if (!defined('FLUENTCRM_DOING_BULK_IMPORT')) {
define('FLUENTCRM_DOING_BULK_IMPORT', true);
}
$driver = $request->get('driver');
$driverClassName = $this->getDriverClass($driver);
if (!$driverClassName) {
return $this->sendError([
'message' => __('Sorry no driver found for the selected CRM', 'fluent-crm')
]);
}
$summary = (new $driverClassName)->runImport($request->all());
if (is_wp_error($summary)) {
return $this->sendError([
'message' => $summary->get_error_message(),
], 422);
}
return [
'import_info' => $summary
];
}
private function getDriverClass($driver)
{
if ($driver == 'mailchimp') {
return MailChimpMigrator::class;
} else if ($driver == 'ConvertKit') {
return ConvertKitMigrator::class;
} else if ($driver == 'MailerLite') {
return MailerLiteMigrator::class;
} else if ($driver == 'Drip') {
return DripMigrator::class;
} else if ($driver == 'ActiveCampaign') {
return ActiveCampaignMigrator::class;
}
/**
* Filter the migrator driver class.
*
* This filter allows you to modify the migrator driver class.
*
* @since 2.7.0
*
* @param mixed $class The current migrator driver class. Default null.
* @param string $driver The driver name.
*/
return apply_filters('fluent_crm/migrator_driver_class', null, $driver);
}
private function getMigrators()
{
/**
* Filter the list of available SaaS migrators.
*
* This filter allows modification of the list of available SaaS migrators
* by adding, removing, or modifying the migrators.
*
* @since 2.7.0
*
* @param array $migrators {
* An associative array of migrators.
*
* @type array $mailchimp {
* Information about the MailChimp migrator.
* }
* @type array $ConvertKit {
* Information about the ConvertKit migrator.
* }
* @type array $MailerLite {
* Information about the MailerLite migrator.
* }
* @type array $Drip {
* Information about the Drip migrator.
* }
* @type array $ActiveCampaign {
* Information about the ActiveCampaign migrator.
* }
* }
*/
return apply_filters('fluent_crm/saas_migrators', [
'mailchimp' => (new MailChimpMigrator())->getInfo(),
'ConvertKit' => (new ConvertKitMigrator())->getInfo(),
'MailerLite' => (new MailerLiteMigrator())->getInfo(),
'Drip' => (new DripMigrator())->getInfo(),
'ActiveCampaign' => (new ActiveCampaignMigrator())->getInfo()
]);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Services\Helper;
/**
* PurchaseHistoryController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class PurchaseHistoryController extends Controller
{
public function historyProviders()
{
return $this->sendSuccess([
'providers' => Helper::getPurchaseHistoryProviders()
]);
}
public function getOrders()
{
$provider = $this->request->getSafe('provider');
$subscriberId = $this->request->getSafe('id', 'intval');
$subscriber = Subscriber::findOrFail($subscriberId);
/**
* Determine the purchase history data for a specific provider in FluentCRM.
*
* The dynamic portion of the hook name, `$provider`, refers to the purchase history provider.
*
* @since 1.0.0
*
* @param array {
* The purchase history data.
*
* @type array $orders List of orders.
* @type int $total Total number of orders.
* }
* @param object $subscriber The subscriber object.
*/
$data = apply_filters('fluent_crm/purchase_history_'.$provider, [
'orders' => [],
'total' => 0
], $subscriber);
return $this->sendSuccess([
'orders' => $data
]);
}
}
@@ -0,0 +1,597 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Campaign;
use FluentCrm\App\Models\CampaignEmail;
use FluentCrm\App\Models\Funnel;
use FluentCrm\App\Models\FunnelSubscriber;
use FluentCrm\App\Models\Lists;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\Tag;
use FluentCrm\App\Hooks\Handlers\Scheduler;
use FluentCrm\App\Services\Reporting;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\App\Models\CampaignUrlMetric;
use FluentCrm\Framework\Support\Arr;
/**
* ReportingController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class ReportingController extends Controller
{
public function getContactGrowth(Request $request, Reporting $reporting)
{
list($from, $to) = $request->get('date_range') ?: ['', ''];
$tagId = intval($request->get('tag_id', 0));
$listId = intval($request->get('list_id', 0));
$compareType = sanitize_text_field($request->get('compare_type', ''));
$compareRange = $request->get('compare_range', []);
$currentStats = $reporting->getSubscribersGrowth($from, $to, $tagId, $listId);
$currentFrom = $from ?: gmdate('Y-m-d', strtotime('-30 days'));
$currentTo = $to ?: gmdate('Y-m-d', strtotime('+1 day'));
$dataSets = [
[
'label' => __('Current Range', 'fluent-crm'),
'data' => $currentStats,
'range' => [$currentFrom, $currentTo],
'backgroundColor' => '#335CFF',
'borderColor' => '#335CFF',
'fill' => true,
],
];
if ($compareType && $compareType !== 'no_comparison') {
$compRange = $this->resolveCompareRange($compareType, $compareRange, $currentFrom, $currentTo);
if ($compRange) {
$compareStats = $reporting->getSubscribersGrowth($compRange[0], $compRange[1], $tagId, $listId);
$dataSets[] = [
'label' => __('Compare Range', 'fluent-crm'),
'data' => $compareStats,
'range' => $compRange,
'backgroundColor' => '#1FC16B',
'borderColor' => '#1FC16B',
'fill' => true,
];
}
}
return $this->sendSuccess([
'data_sets' => $dataSets,
'current_range' => [$currentFrom, $currentTo],
]);
}
/**
* Calculate comparison date range based on type.
*
* @param string $type
* @param array $compareRange
* @param string $from
* @param string $to
* @return array|false
*/
private function resolveCompareRange($type, $compareRange, $from, $to)
{
$fromTs = strtotime($from);
$toTs = strtotime($to);
$diffDays = (int)(($toTs - $fromTs) / 86400);
switch ($type) {
case 'previous_period':
return [
gmdate('Y-m-d', $fromTs - ($diffDays + 1) * 86400),
gmdate('Y-m-d', $fromTs - 86400),
];
case 'previous_month':
$newFrom = gmdate('Y-m-d', strtotime($from . ' -1 month'));
return [$newFrom, gmdate('Y-m-d', strtotime($newFrom) + $diffDays * 86400)];
case 'previous_quarter':
$newFrom = gmdate('Y-m-d', strtotime($from . ' -3 months'));
return [$newFrom, gmdate('Y-m-d', strtotime($newFrom) + $diffDays * 86400)];
case 'previous_year':
$newFrom = gmdate('Y-m-d', strtotime($from . ' -12 months'));
return [$newFrom, gmdate('Y-m-d', strtotime($newFrom) + $diffDays * 86400)];
case 'custom':
if (is_array($compareRange) && count(array_filter($compareRange)) >= 2) {
return [
sanitize_text_field($compareRange[0]),
sanitize_text_field($compareRange[1]),
];
}
return false;
default:
return false;
}
}
public function getEmailSentStats(Request $request, Reporting $reporting)
{
list($from, $to) = $request->get('date_range') ?: ['', ''];
$campaignId = intval($request->get('campaign_id', 0));
return $this->sendSuccess([
'stats' => $reporting->getEmailStats($from, $to, 'sent', $campaignId)
]);
}
public function getEmailOpenStats(Request $request, Reporting $reporting)
{
list($from, $to) = $request->get('date_range') ?: ['', ''];
$campaignId = intval($request->get('campaign_id', 0));
return $this->sendSuccess([
'stats' => $reporting->getEmailOpenStats($from, $to, $campaignId)
]);
}
public function getEmailClickStats(Request $request, Reporting $reporting)
{
list($from, $to) = $request->get('date_range') ?: ['', ''];
$campaignId = intval($request->get('campaign_id', 0));
return $this->sendSuccess([
'stats' => $reporting->getEmailClickStats($from, $to, $campaignId)
]);
}
public function getEmailUnsubStats(Request $request, Reporting $reporting)
{
list($from, $to) = $request->get('date_range') ?: ['', ''];
return $this->sendSuccess([
'stats' => $reporting->getUnsubscribeStats($from, $to)
]);
}
public function getEmailPerformance(Request $request, Reporting $reporting)
{
$dateRange = Arr::get($request->all(), 'date_range', []);
if (!empty($dateRange[0]) && !empty($dateRange[1])) {
$from = sanitize_text_field($dateRange[0]);
$to = sanitize_text_field($dateRange[1]);
} else {
$days = intval(Arr::get($request->all(), 'days'));
if ($days > 0) {
$from = '-' . $days . ' days';
} elseif ($request->exists('days')) {
// days=0 means "All Time"
$from = '2000-01-01';
} else {
$from = null; // default: -30 days
}
$to = null;
}
return $this->sendSuccess([
'stats' => $reporting->getEmailPerformance($from, $to)
]);
}
public function getEmails(Request $request)
{
$status = sanitize_text_field($request->get('status', ''));
$search = sanitize_text_field($request->get('search', ''));
$selectedTypes = array_values(array_filter(array_map('sanitize_text_field', (array)$request->get('types', []))));
$types = CampaignEmail::expandEmailTypes($selectedTypes);
$emails = CampaignEmail::orderBy('scheduled_at', 'DESC')
->with('subscriber', 'campaign')
->when($search, function ($q) use ($search) {
return $this->applyEmailSearchFilter($q, $search);
})
->when($status, function ($q) use ($status) {
return $q->where('status', $status);
})
->when($types, function ($q) use ($types) {
return $q->whereIn('email_type', $types);
})
->paginate();
$statuses = null;
$emailTypes = null;
if ($request->get('page') == 1 && !$search) {
$statuses = CampaignEmail::select('status')
->selectRaw('count(id) as total')
->when($types, function ($q) use ($types) {
return $q->whereIn('email_type', $types);
})
->groupBy('status')
->get()
->keyBy('status')
->map(function ($status) {
return $status->total;
});
$typeCounts = CampaignEmail::select('email_type')
->selectRaw('count(id) as total')
->whereNotNull('email_type')
->when($status, function ($q) use ($status) {
return $q->where('status', $status);
})
->groupBy('email_type')
->get()
->reduce(function ($carry, $emailType) {
$canonicalType = CampaignEmail::normalizeEmailType($emailType->email_type);
if (!isset($carry[$canonicalType])) {
$carry[$canonicalType] = [
'id' => $canonicalType,
'label' => CampaignEmail::resolveEmailTypeLabel($canonicalType),
'count' => 0,
];
}
$carry[$canonicalType]['count'] += (int)$emailType->total;
return $carry;
}, []);
$orderedTypes = [];
foreach (array_keys(CampaignEmail::getEmailTypeLabels()) as $canonicalType) {
if (!empty($typeCounts[$canonicalType])) {
$orderedTypes[] = $typeCounts[$canonicalType];
}
}
$emailTypes = array_values($orderedTypes);
}
return [
'emails' => $emails,
'statuses' => $statuses,
'types' => $emailTypes
];
}
/**
* Apply the search filter to email activity queries.
*
* Search is scoped to fields the table actually exposes so users can find
* rows by subject, source campaign title, recipient email, or related
* contact email without triggering extra broad scans on large datasets.
*
* @param \FluentCrm\Framework\Database\Orm\Builder $query
* @param string $search
* @return \FluentCrm\Framework\Database\Orm\Builder
*/
private function applyEmailSearchFilter($query, $search)
{
global $wpdb;
$escapedSearch = $wpdb->esc_like($search);
$containsLike = '%' . $escapedSearch . '%';
$emailLike = strpos($search, '@') !== false ? $escapedSearch . '%' : $containsLike;
return $query->where(function ($subQuery) use ($containsLike, $emailLike) {
$subQuery->where('email_subject', 'LIKE', $containsLike)
->orWhere('email_address', 'LIKE', $emailLike)
->orWhereHas('campaign', function ($campaignQuery) use ($containsLike) {
$campaignQuery->where('title', 'LIKE', $containsLike);
})
->orWhereHas('subscriber', function ($subscriberQuery) use ($emailLike) {
$subscriberQuery->where('email', 'LIKE', $emailLike);
});
});
}
public function deleteEmails(Request $request)
{
$emailIds = $request->get('email_ids');
CampaignEmail::whereIn('id', $emailIds)
->delete();
return [
'message' => __('Selected emails have been deleted', 'fluent-crm')
];
}
public function getContactsByStatus()
{
$statuses = fluentCrmDb()->table('fc_subscribers')
->select(fluentCrmDb()->raw('status, COUNT(id) as count'))
->groupBy('status')
->get();
$defaultOrder = [
'subscribed',
'unsubscribed',
'pending',
'bounced',
'complained',
'spammed',
'transactional'
];
$defaultStats = array_fill_keys($defaultOrder, 0);
$total = 0;
foreach ($statuses as $row) {
$count = (int)$row->count;
$status = sanitize_text_field($row->status);
$total += $count;
if (array_key_exists($status, $defaultStats)) {
$defaultStats[$status] = $count;
}
}
$result = [];
foreach ($defaultOrder as $status) {
$result[] = [
'status' => $status,
'count' => $defaultStats[$status],
];
}
return $this->sendSuccess([
'stats' => $result,
'total' => $total,
]);
}
public function getContactsByTags(Request $request)
{
$limit = intval($request->get('per_page', 20));
$tags = Tag::select(['fc_tags.id', 'fc_tags.title'])
->selectRaw('COUNT(subscriber_id) as contact_count')
->leftJoin('fc_subscriber_pivot', function ($join) {
$join->on('fc_tags.id', '=', 'fc_subscriber_pivot.object_id')
->where('fc_subscriber_pivot.object_type', '=', 'FluentCrm\App\Models\Tag');
})
->groupBy('fc_tags.id', 'fc_tags.title')
->orderByDesc('contact_count')
->paginate($limit);
return $this->sendSuccess([
'tags' => $tags,
]);
}
public function getContactsByLists(Request $request)
{
$limit = intval($request->get('per_page', 20));
$lists = Lists::select(['fc_lists.id', 'fc_lists.title'])
->selectRaw('COUNT(subscriber_id) as contact_count')
->leftJoin('fc_subscriber_pivot', function ($join) {
$join->on('fc_lists.id', '=', 'fc_subscriber_pivot.object_id')
->where('fc_subscriber_pivot.object_type', '=', 'FluentCrm\App\Models\Lists');
})
->groupBy('fc_lists.id', 'fc_lists.title')
->orderByDesc('contact_count')
->paginate($limit);
return $this->sendSuccess([
'lists' => $lists,
]);
}
public function getContactsByCountry()
{
$countries = fluentCrmDb()->table('fc_subscribers')
->select(fluentCrmDb()->raw('UPPER(TRIM(country)) as country_code, COUNT(id) as contact_count'))
->whereNotNull('country')
->whereRaw("TRIM(country) != ''")
->groupBy(fluentCrmDb()->raw('UPPER(TRIM(country))'))
->orderByDesc('contact_count')
->get();
$result = [];
foreach ($countries as $row) {
$result[] = [
'country_code' => $row->country_code,
'contact_count' => (int) $row->contact_count,
];
}
return $this->sendSuccess([
'countries' => $result,
]);
}
public function getCampaignsList(Request $request)
{
$limit = intval($request->get('per_page', 15));
$campaigns = Campaign::where('status', 'archived')
->orderBy('updated_at', 'DESC')
->paginate($limit);
foreach ($campaigns as $campaign) {
$campaign->stats = $campaign->stats();
}
return $this->sendSuccess([
'campaigns' => $campaigns,
]);
}
public function getAutomationReports(Request $request)
{
$limit = intval($request->get('per_page', 15));
$funnels = Funnel::where('status', 'published')
->orderBy('created_at', 'DESC')
->paginate($limit);
$totalSubscribers = 0;
$totalCompleted = 0;
$totalInProgress = 0;
foreach ($funnels as $funnel) {
$funnel->total_subscribers = FunnelSubscriber::where('funnel_id', $funnel->id)
->distinct()
->count('subscriber_id');
$funnel->completed_count = FunnelSubscriber::where('funnel_id', $funnel->id)
->where('status', 'completed')
->count();
$funnel->in_progress_count = FunnelSubscriber::where('funnel_id', $funnel->id)
->where('status', 'active')
->count();
// Last run time
$lastRun = FunnelSubscriber::where('funnel_id', $funnel->id)
->whereNotNull('last_executed_time')
->orderByDesc('last_executed_time')
->first();
$funnel->last_run_at = $lastRun ? $lastRun->last_executed_time : null;
// Recent 3 subscribers who entered
$recentEntries = FunnelSubscriber::where('funnel_id', $funnel->id)
->with(['subscriber' => function ($q) {
$q->select(['id', 'first_name', 'last_name', 'email', 'avatar']);
}])
->orderByDesc('created_at')
->limit(3)
->get();
$funnel->recent_subscribers = $recentEntries->map(function ($entry) {
if (!$entry->subscriber) {
return null;
}
return [
'id' => $entry->subscriber->id,
'name' => trim($entry->subscriber->first_name . ' ' . $entry->subscriber->last_name),
'email' => $entry->subscriber->email,
'avatar' => $entry->subscriber->avatar,
'entered_at' => $entry->created_at,
];
})->filter()->values();
$totalSubscribers += $funnel->total_subscribers;
$totalCompleted += $funnel->completed_count;
$totalInProgress += $funnel->in_progress_count;
}
// Top 5 automations by total subscribers (most triggered)
$topAutomations = Funnel::where('status', 'published')
->get()
->map(function ($funnel) {
$funnel->trigger_count = FunnelSubscriber::where('funnel_id', $funnel->id)
->count();
return $funnel;
})
->sortByDesc('trigger_count')
->take(5)
->values()
->map(function ($funnel) {
return [
'id' => $funnel->id,
'title' => $funnel->title,
'trigger_name' => $funnel->trigger_name,
'trigger_count' => $funnel->trigger_count,
];
});
$overview = [
'total' => Funnel::where('status', 'published')->count(),
'subscribers' => $totalSubscribers,
'completed' => $totalCompleted,
'in_progress' => $totalInProgress,
];
return $this->sendSuccess([
'automations' => $funnels,
'overview' => $overview,
'top_automations' => $topAutomations,
]);
}
public function getAutomationStepReport(Request $request, Reporting $reporting, $id)
{
$id = intval($id);
$funnel = Funnel::findOrFail($id);
$stats = $reporting->funnelStat($funnel->id);
return $this->sendSuccess([
'funnel' => $funnel,
'stats' => $stats,
]);
}
public function getCampaignOptions(Request $request)
{
global $wpdb;
$search = sanitize_text_field($request->get('search', ''));
$limit = intval($request->get('per_page', 50));
$query = Campaign::select(['id', 'title'])
->where('status', 'archived');
if ($search) {
$query->where('title', 'LIKE', '%' . $wpdb->esc_like($search) . '%');
}
$options = $query->orderBy('updated_at', 'DESC')
->limit($limit)
->get();
return $this->sendSuccess([
'options' => $options,
]);
}
public function getAdvancedReportProviders()
{
return [
/**
* Determine the advanced report providers for FluentCRM.
*
* This filter allows you to modify the list of advanced report providers.
*
* @since 1.0.0
*
* @param array An array of advanced report providers.
*/
'providers' => apply_filters('fluent_crm/advanced_report_providers', [])
];
}
public function getRecentTags(Request $request)
{
$limit = intval($request->get('per_page', 5));
$tags = Tag::select(['fc_tags.id', 'fc_tags.title', 'fc_tags.created_at'])
->selectRaw('COUNT(subscriber_id) as contact_count')
->leftJoin('fc_subscriber_pivot', function ($join) {
$join->on('fc_tags.id', '=', 'fc_subscriber_pivot.object_id')
->where('fc_subscriber_pivot.object_type', '=', 'FluentCrm\App\Models\Tag');
})
->groupBy('fc_tags.id', 'fc_tags.title', 'fc_tags.created_at')
->orderByDesc('fc_tags.created_at')
->limit($limit)
->get();
return $this->sendSuccess([
'tags' => $tags,
]);
}
public function ping()
{
// Browser-driven cron fallback: while an admin has any CRM page open,
// the app pings this endpoint ~every 50s. If Action Scheduler (and the
// WP-Cron fallback) have stalled, take over the every-minute email task
// here. No-ops in a single option read when scheduling is healthy, and
// is fully locked/throttled internally — safe across tabs and users.
Scheduler::maybeProcessFromBrowserPing();
return [
'message' => 'pong'
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,382 @@
<?php
/**
* Setup wizard class
*
* Intial Setup Wizard for FluentCRM
*
*/
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\Framework\Support\Arr;
/**
* SetupController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class SetupController extends Controller
{
public function CompleteWizard(Request $request)
{
$installFluentForm = $request->get('install_fluentform', 'no');
if ($installFluentForm == 'yes' && !defined('FLUENTFORM')) {
$this->installFluentForm();
}
if ($request->get('install_fluentcart', 'no') === 'yes' && !defined('FLUENTCART_VERSION')) {
$this->installFluentCart();
}
$optinEmail = $request->get('optin_email', 'no');
if ($optinEmail && is_email($optinEmail)) {
$this->shareEmail($optinEmail);
}
$shareEssential = $request->get('share_essentials', 'no');
if ($shareEssential == 'yes') {
fluentcrm_update_option('_fluentcrm_share_essential', $shareEssential);
}
return $this->sendSuccess([
'message' => __('Installation has been completed', 'fluent-crm')
]);
}
public function handleFluentFormInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentForm();
return [
'ff_config' => [
'is_installed' => defined('FLUENTFORM'),
'create_form_link' => admin_url('admin.php?page=fluent_forms#add=1')
],
'is_installed' => defined('FLUENTFORM'),
'message' => __('Fluent Forms has been installed and activated', 'fluent-crm')
];
}
public function handleFluentBoardsInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentBoards();
return [
'message' => __('Fluent Boards has been installed and activated', 'fluent-crm'),
'is_installed' => defined('FLUENT_BOARDS'),
];
}
public function handleFluentCommunityInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentCommunity();
return [
'message' => __('Fluent Community has been installed and activated', 'fluent-crm'),
'is_installed' => defined('FLUENT_COMMUNITY_PLUGIN_VERSION'),
];
}
public function handleFluentBookingInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentBooking();
return [
'message' => __('Fluent Booking has been installed and activated', 'fluent-crm'),
'is_installed' => defined('FLUENT_BOOKING_VERSION'),
];
}
public function handleFluentCartInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentCart();
return [
'message' => __('FluentCart has been installed and activated', 'fluent-crm'),
'is_installed' => defined('FLUENTCART_VERSION'),
];
}
public function handleFluentSmtpInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$this->installFluentSMTP();
return [
'is_installed' => defined('FLUENTMAIL'),
'config_url' => admin_url('options-general.php?page=fluent-mail#/'),
'message' => __('FluentSMTP plugin has been installed and activated successfully', 'fluent-crm')
];
}
public function handleFluentSupportInstall()
{
if (!current_user_can('install_plugins')) {
return $this->sendError([
'message' => __('Sorry! you do not have permission to install plugin', 'fluent-crm')
]);
}
$plugin_id = 'fluent-support';
$plugin = [
'name' => __('Fluent Support', 'fluent-crm'),
'repo-slug' => 'fluent-support',
'file' => 'fluent-support.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
return [
'is_installed' => defined('FLUENT_SUPPORT_VERSION'),
'message' => __('Fluent Support plugin has been installed and activated successfully', 'fluent-crm')
];
}
private function shareEmail($optinEmail)
{
$user = get_user_by('ID', get_current_user_id());
$data = [
'answers' => [
'website' => site_url(),
'email' => $optinEmail,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'name' => $user->display_name
],
'questions' => [
'website' => 'website',
'first_name' => 'first_name',
'last_name' => 'last_name',
'email' => 'email',
'name' => 'name'
],
'user' => [
'email' => $optinEmail
],
'fb_capture' => 1,
'form_id' => 54
];
$url = add_query_arg($data, 'https://wpmanageninja.com/');
wp_remote_post($url);
}
private function installFluentForm()
{
$plugin_id = 'fluentform';
$plugin = [
'name' => __('Fluent Forms', 'fluent-crm'),
'repo-slug' => 'fluentform',
'file' => 'fluentform.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function installFluentBoards()
{
$plugin_id = 'fluent-boards';
$plugin = [
'name' => __('Fluent Boards', 'fluent-crm'),
'repo-slug' => 'fluent-boards',
'file' => 'fluent-boards.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function installFluentCommunity()
{
$plugin_id = 'fluent-community';
$plugin = [
'name' => __('Fluent Community', 'fluent-crm'),
'repo-slug' => 'fluent-community',
'file' => 'fluent-community.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function installFluentBooking()
{
$plugin_id = 'fluent-booking';
$plugin = [
'name' => __('Fluent Booking', 'fluent-crm'),
'repo-slug' => 'fluent-booking',
'file' => 'fluent-booking.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function installFluentCart()
{
$plugin_id = 'fluent-cart';
$plugin = [
'name' => __('FluentCart', 'fluent-crm'),
'repo-slug' => 'fluent-cart',
'file' => 'fluent-cart.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function installFluentSMTP()
{
$plugin_id = 'fluent-smtp';
$plugin = [
'name' => __('FluentSMTP', 'fluent-crm'),
'repo-slug' => 'fluent-smtp',
'file' => 'fluent-smtp.php',
];
$this->backgroundInstaller($plugin, $plugin_id);
}
private function backgroundInstaller($plugin_to_install, $plugin_id)
{
if (!empty($plugin_to_install['repo-slug'])) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
WP_Filesystem();
$skin = new \Automatic_Upgrader_Skin();
$upgrader = new \WP_Upgrader($skin);
$installed_plugins = array_reduce(array_keys(\get_plugins()), array($this, 'associate_plugin_file'), array());
$plugin_slug = $plugin_to_install['repo-slug'];
$plugin_file = isset($plugin_to_install['file']) ? $plugin_to_install['file'] : $plugin_slug . '.php';
$installed = false;
$activate = false;
// See if the plugin is installed already.
if (isset($installed_plugins[$plugin_file])) {
$installed = true;
$activate = !is_plugin_active($installed_plugins[$plugin_file]);
}
// Install this thing!
if (!$installed) {
// Suppress feedback.
ob_start();
try {
$plugin_information = plugins_api(
'plugin_information',
array(
'slug' => $plugin_slug,
'fields' => array(
'short_description' => false,
'sections' => false,
'requires' => false,
'rating' => false,
'ratings' => false,
'downloaded' => false,
'last_updated' => false,
'added' => false,
'tags' => false,
'homepage' => false,
'donate_link' => false,
'author_profile' => false,
'author' => false,
),
)
);
if (is_wp_error($plugin_information)) {
throw new \Exception($plugin_information->get_error_message());
}
$package = $plugin_information->download_link;
$download = $upgrader->download_package($package);
if (is_wp_error($download)) {
throw new \Exception($download->get_error_message());
}
$working_dir = $upgrader->unpack_package($download, true);
if (is_wp_error($working_dir)) {
throw new \Exception($working_dir->get_error_message());
}
$result = $upgrader->install_package(
array(
'source' => $working_dir,
'destination' => WP_PLUGIN_DIR,
'clear_destination' => false,
'abort_if_destination_exists' => false,
'clear_working' => true,
'hook_extra' => array(
'type' => 'plugin',
'action' => 'install',
),
)
);
if (is_wp_error($result)) {
throw new \Exception($result->get_error_message());
}
$activate = true;
} catch (\Exception $e) {
}
// Discard feedback.
ob_end_clean();
}
wp_clean_plugins_cache();
// Activate this thing.
if ($activate) {
try {
$result = activate_plugin($installed ? $installed_plugins[$plugin_file] : $plugin_slug . '/' . $plugin_file);
if (is_wp_error($result)) {
throw new \Exception($result->get_error_message());
}
} catch (\Exception $e) {
}
}
}
}
private function associate_plugin_file($plugins, $key)
{
$path = explode('/', $key);
$filename = end($path);
$plugins[$filename] = $key;
return $plugins;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,308 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\SystemLog;
use FluentCrm\Framework\Http\Request\Request;
/**
* SystemLog Controller - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 2.8.40
*/
class SystemLogController extends Controller
{
/**
* Get all the System Logs
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return array || \WP_REST_Response
*/
public function index(Request $request)
{
$search = $this->getSearchTerm($request);
$logs = $this->getLogsQuery($search);
$logs = $logs->paginate($request->per_page ?: 20);
return [
'logs' => $logs
];
}
/**
* Stream system logs as CSV without loading all rows into memory.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return void
*/
public function export(Request $request)
{
$range = $this->getExportRange($request);
$startDate = $this->getExportStartDate($range);
$search = $this->getSearchTerm($request);
$chunkSize = $this->getExportChunkSize();
$lastId = PHP_INT_MAX;
$this->prepareCsvDownload($this->getExportFilename($range));
$output = fopen('php://output', 'w');
if (!$output) {
exit;
}
fwrite($output, "\xEF\xBB\xBF");
fputcsv($output, $this->getCsvHeaders(), ',', '"', '\\');
do {
$logs = $this->getLogsQuery($search, $startDate)
->where('id', '<', $lastId)
->select(['id', 'created_at', 'title', 'description'])
->limit($chunkSize)
->get();
$count = count($logs);
foreach ($logs as $log) {
$lastId = (int) $log->id;
fputcsv($output, $this->formatCsvLogRow($log), ',', '"', '\\');
}
fflush($output);
if (function_exists('flush')) {
flush();
}
if (connection_aborted()) {
break;
}
} while ($count === $chunkSize);
fclose($output);
exit;
}
public function deleteAll(Request $request)
{
SystemLog::where('id', '>', 0)->delete();
return [
'message' => __('All logs have been deleted', 'fluent-crm')
];
}
/**
* @param string $search
* @param string|null $startDate
* @return mixed
*/
private function getLogsQuery($search = '', $startDate = null)
{
global $wpdb;
$logs = SystemLog::orderBy('id', 'DESC');
if ($startDate) {
$logs = $logs->where('created_at', '>=', $startDate);
}
if ($search !== '') {
$searchLike = '%' . $wpdb->esc_like($search) . '%';
$logs = $logs->where(function ($query) use ($searchLike) {
$query->where('title', 'LIKE', $searchLike)
->orWhere('description', 'LIKE', $searchLike);
});
}
return $logs;
}
/**
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return string
*/
private function getSearchTerm(Request $request)
{
$search = $request->get('search', '');
if (!is_scalar($search)) {
return '';
}
return trim(sanitize_text_field($search));
}
/**
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return int|string
*/
private function getExportRange(Request $request)
{
$range = $request->get('range', 'all');
if (!is_scalar($range)) {
return 'all';
}
return $this->normalizeExportRange(sanitize_text_field($range));
}
/**
* @param mixed $range
* @return int|string
*/
private function normalizeExportRange($range)
{
$range = is_scalar($range) ? (string) $range : 'all';
if ($range === 'all') {
return 'all';
}
$range = intval($range);
$allowedRanges = [7, 15, 30];
return in_array($range, $allowedRanges, true) ? $range : 'all';
}
/**
* @param int|string $range
* @param int|null $currentTimestamp
* @return string|null
*/
private function getExportStartDate($range, $currentTimestamp = null)
{
$range = $this->normalizeExportRange($range);
if ($range === 'all') {
return null;
}
if (!$currentTimestamp) {
$currentTimestamp = current_time('timestamp');
}
return gmdate('Y-m-d H:i:s', $currentTimestamp - ($range * 86400));
}
/**
* @return array
*/
private function getCsvHeaders()
{
return ['ID', 'Date & Time', 'Title', 'Description'];
}
/**
* @param object $log
* @return array
*/
private function formatCsvLogRow($log)
{
return [
(int) $log->id,
$this->sanitizeCsvCell($log->created_at),
$this->sanitizeCsvCell($log->title),
$this->sanitizeCsvCell($this->plainText($log->description))
];
}
/**
* @param int|string $range
* @return string
*/
private function getExportFilename($range)
{
$range = $this->normalizeExportRange($range);
$rangePart = ($range === 'all') ? 'all' : 'last-' . $range . '-days';
return 'fluent-crm-system-logs-' . $rangePart . '-' . gmdate('Y-m-d-His') . '.csv';
}
/**
* Prevent spreadsheet formula execution while preserving visible values.
*
* @param mixed $value
* @return string
*/
private function sanitizeCsvCell($value)
{
if ($value === null) {
return '';
}
if ($value instanceof \DateTimeInterface) {
$value = $value->format('Y-m-d H:i:s');
} else {
$value = is_scalar($value) ? (string) $value : wp_json_encode($value);
}
if ($value !== '' && preg_match('/^[=+\-@\t\r]/', $value)) {
$value = "'" . $value;
}
return $value;
}
/**
* @param mixed $value
* @return string
*/
private function plainText($value)
{
if ($value === null) {
return '';
}
$value = is_scalar($value) ? (string) $value : wp_json_encode($value);
return trim(html_entity_decode(strip_tags($value), ENT_QUOTES, 'UTF-8'));
}
/**
* @return int
*/
private function getExportChunkSize()
{
$chunkSize = (int) apply_filters('fluent_crm/system_logs_export_chunk_size', 1000);
if ($chunkSize < 100) {
return 100;
}
if ($chunkSize > 5000) {
return 5000;
}
return $chunkSize;
}
/**
* @param string $filename
* @return void
*/
private function prepareCsvDownload($filename)
{
if (function_exists('set_time_limit')) {
// Shared hosts may still enforce web server timeouts; this only removes PHP's timer.
@set_time_limit(0);
}
while (ob_get_level()) {
if (!@ob_end_clean()) {
break;
}
}
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . sanitize_file_name($filename) . '"');
header('X-Content-Type-Options: nosniff');
}
}
@@ -0,0 +1,258 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Tag;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
/**
* TagsController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class TagsController extends Controller
{
/**
* Get all of the tags
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response | array
*/
public function index(Request $request)
{
$order = [
'by' => $request->getSafe('sort_by', 'sanitize_sql_orderby', 'id'),
'order' => $request->getSafe('sort_order', 'sanitize_sql_orderby', 'DESC')
];
$tags = Tag::orderBy($order['by'], $order['order'])
->searchBy($request->getSafe('search'))
->paginate();
if (!$request->get('exclude_counts')) {
foreach ($tags as $tag) {
$tag->subscribersCount = $tag->countByStatus('subscribed');
}
}
$data = [
'tags' => $tags
];
if ($request->get('all_tags')) {
$allTags = Tag::get();
$formattedTags = [];
foreach ($allTags as $tag) {
$formattedTags[] = [
'id' => strval($tag->id),
'title' => $tag->title,
'slug' => $tag->slug,
'description' => $tag->description
];
}
$data['all_tags'] = $formattedTags;
}
return $data;
}
/**
* Find a tag.
*/
public function find($id)
{
return $this->send([
'tag' => Tag::find($id)
]);
}
/**
* Store a tag.
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
*/
public function create(Request $request)
{
$allData = $request->all();
if (empty($allData['slug'])) {
$allData['slug'] = Helper::slugify($allData['title']);
} else {
$allData['slug'] = sanitize_text_field($allData['slug']);
}
$allData = $this->validate($allData, [
'title' => 'required',
'slug' => "required|unique:fc_tags,slug"
]);
$tag = Tag::create([
'title' => sanitize_text_field($allData['title']),
'slug' => $allData['slug'],
'description' => sanitize_textarea_field(Arr::get($allData, 'description'))
]);
do_action('fluentcrm_tag_created', $tag->id);
do_action('fluent_crm/tag_created', $tag);
return $this->sendSuccess([
'lists' => $tag,
'item' => $tag,
'message' => __('Successfully saved the tag.', 'fluent-crm')
]);
}
/**
* Store a tag.
* @param \FluentCrm\Framework\Http\Request\Request $request
* @param $id int Tag ID
* @return \WP_REST_Response
*/
public function store(Request $request, $id)
{
$allData = $this->validate($request->all(), [
'title' => 'required'
]);
if (empty($allData['slug'])) {
$allData['slug'] = Helper::slugify($allData['title']);
}
if ($id == 0 && $request->get('update_by') == 'slug' && !empty($allData['slug'])) {
$tag = Tag::where('slug', $allData['slug'])->first();
if (!$tag) {
return $this->sendError([
'message' => __('Tag could not be found', 'fluent-crm')
]);
}
$id = $tag->id;
} else {
$tag = Tag::findOrFail($id);
if (empty($allData['slug'])) {
$allData['slug'] = $tag->slug;
}
}
if (Tag::where('slug', $allData['slug'])->where('id', '!=', $id)->first()) {
return $this->sendError([
'message' => __('Provided slug already exists in another tag', 'fluent-crm')
]);
}
$tag = Tag::where('id', $id)->update([
'title' => sanitize_text_field($allData['title']),
'slug' => $allData['slug'],
'description' => sanitize_textarea_field(Arr::get($allData, 'description')),
]);
do_action('fluentcrm_tag_updated', $id);
do_action('fluent_crm/tag_updated', $tag);
return $this->sendSuccess([
'lists' => $tag,
'message' => __('Successfully saved the tag.', 'fluent-crm')
]);
}
/**
* Store a tag.
*/
public function storeBulk()
{
$tags = $this->request->get('tags', []);
if (!$tags) {
$tags = $this->request->get('items', []);
}
$createdIds = [];
foreach ($tags as $tag) {
if (empty($tag['title'])) {
continue;
}
if (empty($tag['slug'])) {
$tag['slug'] = Helper::slugify($tag['title']);
}
$tag = Tag::updateOrCreate(
['slug' => sanitize_title($tag['slug'], 'display')],
['title' => sanitize_text_field($tag['title'])]
);
$createdIds[] = $tag->id;
if ($tag->wasRecentlyCreated) {
do_action('fluentcrm_tag_created', $tag->id);
do_action('fluent_crm/tag_created', $tag);
} else {
do_action('fluentcrm_tag_updated', $tag->id);
do_action('fluent_crm/tag_updated', $tag);
}
}
return $this->sendSuccess([
'message' => __('Successfully saved the tags.', 'fluent-crm'),
'ids' => $createdIds
]);
}
/**
* Delete a tag by id
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @param $tagId
* @return \WP_REST_Response $object
*/
public function remove(Request $request, $tagId)
{
$tag = Tag::find($tagId);
if (!$tag) {
return $this->sendError([
'message' => __('Tag not found', 'fluent-crm')
], 404);
}
$tag->delete();
do_action('fluentcrm_tag_deleted', $tagId);
do_action('fluent_crm/tag_deleted', $tagId);
return $this->sendSuccess([
'message' => __('Successfully removed the tag.', 'fluent-crm')
]);
}
public function handleBulkAction(Request $request)
{
$tagIds = array_map('intval', (array)$request->get('tagIds', []));
$tagIds = array_unique(array_filter($tagIds));
if ($tagIds) {
foreach ($tagIds as $tagId) {
Tag::where('id', $tagId)->delete();
do_action('fluentcrm_tag_deleted', $tagId);
do_action('fluent_crm/tag_deleted', $tagId);
}
}
return $this->sendSuccess([
'message' => __('Selected Tags have been removed permanently', 'fluent-crm'),
]);
}
}
@@ -0,0 +1,703 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Template;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Sanitize;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
/**
* TemplateController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class TemplateController extends Controller
{
public function templates(Request $request)
{
$order = $request->getSafe('order', 'sanitize_sql_orderby', 'desc');
$orderBy = $request->getSafe('orderBy', 'sanitize_sql_orderby', 'ID');
$templatesQuery = Template::emailTemplates(
$request->get('types', ['publish', 'draft'])
);
if ($search = $request->getSafe('search')) {
$templatesQuery->where('post_title', 'LIKE', '%' . $search . '%');
}
// Order the query results and paginate
$templates = $templatesQuery
->orderBy($orderBy, $order)
->paginate();
foreach ($templates as $template) {
$template->design_template = get_post_meta($template->ID, '_design_template', true);
}
return $this->sendSuccess([
'templates' => $templates
]);
}
public function template(Request $request, $templateId = 0)
{
$template = Template::find($templateId);
if ($template) {
$editType = get_post_meta($template->ID, '_edit_type', true);
if (!$editType) {
$editType = 'html';
}
$designTemplate = get_post_meta($template->ID, '_design_template', true);
$templateConfig = get_post_meta($template->ID, '_template_config', true);
if(!$templateConfig || !is_array($templateConfig)) {
$templateConfig = [];
}
$footerSettings = get_post_meta($template->ID, '_footer_settings', true);
$normalizedSettings = $this->normalizeTemplateSettings([
'template_config' => $templateConfig,
'footer_settings' => $footerSettings
], $designTemplate);
$templateData = [
'post_title' => $template->post_title,
'post_content' => $template->post_content,
'post_excerpt' => $template->post_excerpt,
'email_subject' => get_post_meta($template->ID, '_email_subject', true),
'edit_type' => $editType,
'design_template' => $designTemplate,
'settings' => $normalizedSettings
];
/**
* Filter the template data before editing.
*
* @since 2.6.51
*
* @param array $templateData The data of the template being edited.
* @param object $template The template object.
*/
$templateData = apply_filters('fluent_crm/editing_template_data', $templateData, $template);
} else {
$defaultTemplate = Helper::getDefaultEmailTemplate();
$normalizedSettings = $this->normalizeTemplateSettings([
'template_config' => Helper::getTemplateConfig($defaultTemplate),
'footer_settings' => []
], $defaultTemplate);
$templateData = [
'post_title' => '',
'post_content' => '',
'post_excerpt' => '',
'email_subject' => '',
'edit_type' => 'html',
'design_template' => $defaultTemplate,
'settings' => $normalizedSettings
];
}
return $this->sendSuccess([
'template' => $templateData
]);
}
public function create(Request $request)
{
if($templateId = $request->get('template_id')) {
return $this->update($request, $templateId);
}
$templateData = Helper::parseArrayOrJson($this->request->get('template'));
$designTemplate = Arr::get($templateData, 'design_template');
if (!$designTemplate) {
$designTemplate = Helper::getDefaultEmailTemplate();
$templateData['design_template'] = $designTemplate;
}
$templateData['settings'] = $this->normalizeTemplateSettings(Arr::get($templateData, 'settings', []), $designTemplate);
$postData = Arr::only($templateData, [
'post_title',
'post_content',
'post_excerpt'
]);
if(empty($postData['post_title'])) {
$postData['post_title'] = 'Email Template @ '.current_time('mysql');
}
if (empty($templateData['email_subject'])) {
$templateData['email_subject'] = $postData['post_title'];
}
if(empty($postData['post_excerpt'])) {
$postData['post_excerpt'] = '';
}
$postData['post_modified'] = current_time('mysql');
$postData['post_modified_gmt'] = gmdate('Y-m-d H:i:s');
$postData['post_date'] = current_time('mysql');
$postData['post_date_gmt'] = gmdate('Y-m-d H:i:s');
$postData['post_type'] = fluentcrmTemplateCPTSlug();
$templateId = wp_insert_post($postData);
update_post_meta($templateId, '_email_subject', Arr::get($templateData, 'email_subject'));
update_post_meta($templateId, '_edit_type', Arr::get($templateData, 'edit_type'));
update_post_meta($templateId, '_template_config', Arr::get($templateData, 'settings.template_config', []));
update_post_meta($templateId, '_footer_settings', Arr::get($templateData, 'settings.footer_settings', []));
update_post_meta($templateId, '_design_template', $designTemplate);
do_action('fluent_crm/email_template_created', $templateId, $templateData);
return $this->sendSuccess([
'message' => __('Template successfully created', 'fluent-crm'),
'template_id' => $templateId
]);
}
public function duplicate($templateId)
{
$template = Template::findOrFail($templateId);
$postData = [
'post_title' => __('[Duplicate] ', 'fluent-crm') . $template['post_title'],
'post_content' => $template['post_content'],
'post_excerpt' => $template['post_excerpt'],
'post_modified' => current_time('mysql'),
'post_modified_gmt' => gmdate('Y-m-d H:i:s'),
'post_date' => current_time('mysql'),
'post_date_gmt' => gmdate('Y-m-d H:i:s'),
'post_type' => fluentcrmTemplateCPTSlug(),
];
$newTemplateId = wp_insert_post($postData);
// Meta fields to copy over
$metaKeys = [
'_email_subject',
'_edit_type',
'_template_config',
'_design_template',
'_footer_settings'
];
// Update post meta in a loop
$this->copyMetaFields($templateId, $newTemplateId, $metaKeys);
do_action('fluent_crm/email_template_duplicated', $newTemplateId, $template);
return $this->sendSuccess([
'message' => __('Template successfully duplicated', 'fluent-crm'),
'template_id' => $newTemplateId
]);
}
/**
* Helper method to copy meta fields from one post to another
*/
protected function copyMetaFields($oldPostId, $newPostId, $metaKeys)
{
foreach ($metaKeys as $metaKey) {
update_post_meta($newPostId, $metaKey, get_post_meta($oldPostId, $metaKey, true));
}
}
public function update(Request $request, $id)
{
$oldTemplate = Template::findOrFail($id);
$templateData = Helper::parseArrayOrJson($this->request->get('template'));
$designTemplate = Arr::get($templateData, 'design_template');
if (!$designTemplate) {
$designTemplate = get_post_meta($id, '_design_template', true) ?: Helper::getDefaultEmailTemplate();
$templateData['design_template'] = $designTemplate;
}
$templateData['settings'] = $this->normalizeTemplateSettings(Arr::get($templateData, 'settings', []), $designTemplate);
$footerSettings = Arr::get($templateData, 'settings.footer_settings');
if($footerSettings) {
if (($footerSettings['custom_footer'] == 'yes') && !Helper::hasComplianceText($footerSettings['footer_content'])) {
return $this->sendError([
'message' => __('##crm.manage_subscription_url## or ##crm.unsubscribe_url## string is required for compliance. Please include unsubscription or manage subscription link', 'fluent-crm')
]);
}
}
if(empty($templateData['post_title'])) {
$templateData['post_title'] = 'Email template created at '.gmdate('Y-m-d H:i');
}
if(empty($templateData['email_subject'])) {
$templateData['email_subject'] = 'Email template created at '.gmdate('Y-m-d H:i');
}
$postData = Arr::only($templateData, [
'post_title',
'post_content',
'post_excerpt'
]);
$postData['post_modified'] = current_time('mysql');
$postData['post_modified_gmt'] = gmdate('Y-m-d H:i:s');
Template::where('ID', $id)->update($postData);
update_post_meta($id, '_email_subject', Arr::get($templateData, 'email_subject'));
update_post_meta($id, '_edit_type', Arr::get($templateData, 'edit_type'));
update_post_meta($id, '_design_template', Arr::get($templateData, 'design_template'));
update_post_meta($id, '_template_config', Arr::get($templateData, 'settings.template_config', []));
update_post_meta($id, '_footer_settings', Arr::get($templateData, 'settings.footer_settings', []));
$template = Template::findOrFail($id);
do_action('fluent_crm/email_template_updated', $templateData, $template);
return $this->sendSuccess([
'message' => __('Template successfully updated', 'fluent-crm'),
'template_id' => $id
]);
}
public function handleBulkAction(Request $request)
{
$actionName = sanitize_text_field($request->get('action_name', ''));
$templateIds = array_map('intval', (array)$request->get('template_ids', []));
$templateIds = array_unique(array_filter($templateIds));
$selectAllTemplates = filter_var($request->get('select_all'), FILTER_VALIDATE_BOOLEAN);
if ($selectAllTemplates) {
$templateIds = Template::pluck('id')->toArray();
}
$templateIds = array_filter($templateIds);
if ($actionName == 'change_template_status') {
$newStatus = sanitize_text_field($request->get('status', ''));
if (!$newStatus) {
return $this->sendError([
'message' => __('Please select status', 'fluent-crm')
]);
}
$templates = Template::whereIn('ID', $templateIds)->get();
foreach ($templates as $template) {
$oldStatus = $template->post_status;
if ($oldStatus != $newStatus) {
$template->post_status = $newStatus;
$template->save();
}
}
return [
'message' => __('Status has been changed for the selected templates', 'fluent-crm')
];
} else if ($actionName == 'delete_templates') {
$templates = Template::whereIn('id', $templateIds)->get();
foreach ($templates as $template) {
wp_delete_post($template->ID, true);
}
return $this->sendSuccess([
'message' => __('Selected Templates have been deleted permanently', 'fluent-crm'),
]);
}
return [
'message' => __('invalid bulk action', 'fluent-crm')
];
}
public function delete(Request $request, $id)
{
$template = Template::findOrFail($id);
wp_delete_post($template->ID, true);
return $this->sendSuccess([
'message' => __('The template has been deleted successfully.', 'fluent-crm')
]);
}
public function render()
{
$rendered = Template::findOrFail(
$this->request->get('ID')
)->render();
return $this->sendSuccess($rendered);
}
public function allTemplates()
{
return $this->sendSuccess([
'templates' => Template::emailTemplates(['publish'])->orderBy('ID', 'desc')->get(),
'smartcodes' => $this->smartCodes()
]);
}
public function getSmartCodes()
{
return $this->sendSuccess([
'smartcodes' => $this->smartCodes()
]);
}
protected function smartCodes()
{
return Helper::getGlobalSmartCodes();
}
public function setGlobalStyle(Request $request)
{
$settings = $request->get('config', []);
foreach ($settings as $settingKey => $setting) {
$settings[$settingKey] = sanitize_text_field($setting);
}
fluentcrm_update_option('global_email_style_config', $settings);
return [
'message' => __('Global style settings have been updated', 'fluent-crm')
];
}
/**
* Fetches built-in templates from cached locally
* cached for 24 hours, then refreshed
* @return
*/
public function getBuiltInTemplates()
{
$templates = fluentCrmPersistentCache('email_remote_templates', function () {
return $this->loadRemoteTemplates();
}, 60 * 60 * 24); // 24 hours
// Return a success response with the formatted templates
return $this->sendSuccess([
'templates' => $templates
]);
}
/**
* Downloads a single built-in template file and returns it without saving
* it as a local email template.
*
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \FluentCrm\Framework\Http\Response\Response
*/
public function getBuiltInTemplate(Request $request)
{
$fileUrl = esc_url_raw($request->get('file', ''));
if (!$fileUrl || !$this->isAllowedRemoteTemplateUrl($fileUrl)) {
return $this->sendError([
'message' => __('Invalid template source URL', 'fluent-crm')
]);
}
$response = wp_remote_get($fileUrl, [
'sslverify' => true,
'timeout' => 20,
'redirection' => 0,
'limit_response_size' => 1024 * 1024
]);
if (is_wp_error($response)) {
return $this->sendError([
'message' => __('Unable to download the selected template. Please try again.', 'fluent-crm')
]);
}
$responseCode = wp_remote_retrieve_response_code($response);
if ($responseCode < 200 || $responseCode >= 300) {
return $this->sendError([
'message' => __('Unable to download the selected template. Please try again.', 'fluent-crm')
]);
}
$templateData = Helper::parseArrayOrJson(wp_remote_retrieve_body($response));
if (Arr::get($templateData, 'is_fc_template') !== 'yes') {
return $this->sendError([
'message' => __('The selected file is not a valid FluentCRM template.', 'fluent-crm')
]);
}
$template = $this->formatRemoteTemplateData($templateData);
$hasVisualBuilderDesign = $template['design_template'] === 'visual_builder' && !empty($template['_visual_builder_design']);
if (!$template['post_content'] && !$hasVisualBuilderDesign) {
return $this->sendError([
'message' => __('The selected template does not have any email content.', 'fluent-crm')
]);
}
return $this->sendSuccess([
'message' => __('Template has been inserted', 'fluent-crm'),
'template' => $template
]);
}
/**
* Restricts direct template downloads to trusted FluentCRM template hosts.
*
* @param string $url
* @return bool
*/
protected function isAllowedRemoteTemplateUrl($url)
{
$parsedUrl = wp_parse_url($url);
if (empty($parsedUrl['scheme']) || empty($parsedUrl['host']) || $parsedUrl['scheme'] !== 'https') {
return false;
}
$allowedHosts = [
'fluentcrm.com',
'www.fluentcrm.com',
'wpmanageninja.com',
'www.wpmanageninja.com'
];
if (defined('FC_TEMPLATE_API_DOMAIN')) {
$configuredHost = wp_parse_url(FC_TEMPLATE_API_DOMAIN, PHP_URL_HOST);
if ($configuredHost) {
$allowedHosts[] = strtolower($configuredHost);
}
}
return in_array(strtolower($parsedUrl['host']), array_unique($allowedHosts), true);
}
/**
* Normalizes remote JSON to the local template shape without creating a WP post.
*
* @param array $templateData
* @return array
*/
protected function formatRemoteTemplateData($templateData)
{
$designTemplate = sanitize_text_field(Arr::get($templateData, 'design_template'));
if (!$designTemplate) {
$designTemplate = Helper::getDefaultEmailTemplate();
}
$normalizedSettings = $this->normalizeTemplateSettings(Arr::get($templateData, 'settings', []), $designTemplate);
return [
'post_title' => sanitize_text_field(Arr::get($templateData, 'post_title', '')),
'post_content' => Arr::get($templateData, 'post_content', ''),
'post_excerpt' => sanitize_textarea_field(Arr::get($templateData, 'post_excerpt', '')),
'email_subject' => sanitize_text_field(Arr::get($templateData, 'email_subject', '')),
'edit_type' => sanitize_text_field(Arr::get($templateData, 'edit_type', 'html')),
'design_template' => $designTemplate,
'settings' => $normalizedSettings,
'_visual_builder_design' => Arr::get($templateData, '_visual_builder_design')
];
}
/**
* Normalize template settings with legacy footer disable compatibility.
*
* @param array $settings
* @return array
*/
protected function normalizeTemplateSettings($settings, $designTemplate = '')
{
$templateConfig = Arr::get($settings, 'template_config', []);
if (!is_array($templateConfig)) {
$templateConfig = [];
}
if (!$this->templateSupportsContentPadding($designTemplate)) {
unset($templateConfig['content_padding']);
} elseif (!isset($templateConfig['content_padding'])) {
$templateConfig['content_padding'] = 20;
}
$footerSettings = Arr::get($settings, 'footer_settings', []);
if (!is_array($footerSettings)) {
$footerSettings = [];
}
$hasExplicitDisableFooter = array_key_exists('disable_footer', $footerSettings);
$hasExplicitCustomFooter = array_key_exists('custom_footer', $footerSettings);
$disableFooter = Arr::get($footerSettings, 'disable_footer');
if ($disableFooter !== 'yes' && $disableFooter !== 'no') {
$legacyDisable = Arr::get($templateConfig, 'disable_footer');
$disableFooter = ($legacyDisable === 'yes' || $legacyDisable === 'no') ? $legacyDisable : 'no';
}
$customFooter = Arr::get($footerSettings, 'custom_footer');
if ($customFooter !== 'yes' && $customFooter !== 'no') {
$legacyFooterContent = Arr::get($footerSettings, 'footer_content', '');
$customFooter = (is_string($legacyFooterContent) && trim(wp_strip_all_tags($legacyFooterContent)))
? 'yes'
: 'no';
}
$footerSettings = wp_parse_args($footerSettings, [
'custom_footer' => 'no',
'footer_content' => '',
'disable_footer' => 'no',
'font_size' => 13,
'font_color' => '#202020',
'background_color' => 'transparent',
'footer_padding' => 20
]);
// Footer content is user-editable from a raw text mode; sanitize before persistence.
$footerSettings['footer_content'] = Sanitize::sanitizeFooterHtml(Arr::get($footerSettings, 'footer_content', ''));
$footerSettings['disable_footer'] = $disableFooter;
$footerSettings['custom_footer'] = $customFooter;
// Legacy imported templates may carry disable_footer in template_config without
// explicit footer settings. Treat those as Global Footer instead of hidden footer.
$isLegacyImportedDisabled = (
!$hasExplicitDisableFooter &&
!$hasExplicitCustomFooter &&
$footerSettings['disable_footer'] === 'yes' &&
Arr::get($templateConfig, 'disable_footer') === 'yes' &&
$footerSettings['custom_footer'] !== 'yes' &&
!trim(wp_strip_all_tags(Arr::get($footerSettings, 'footer_content', '')))
);
if ($isLegacyImportedDisabled) {
$footerSettings['disable_footer'] = 'no';
$footerSettings['custom_footer'] = 'no';
}
// Keep legacy key in sync during transition to avoid regressions in old readers.
$templateConfig['disable_footer'] = $footerSettings['disable_footer'];
return [
'template_config' => $templateConfig,
'footer_settings' => $footerSettings
];
}
/**
* Raw classic editor templates only support font family and footer flags.
*
* @param string $designTemplate
* @return bool
*/
protected function templateSupportsContentPadding($designTemplate)
{
if ($designTemplate === 'raw_classic') {
return false;
}
$templates = Helper::getEmailDesignTemplates();
$template = Arr::get($templates, $designTemplate, []);
return Arr::get($template, 'template_type') !== 'classic_editor';
}
/**
* Fetches and formats email templates from a remote FluentCRM API endpoint.
* This method makes an HTTP request to retrieve email templates from FluentCRM's public API.
* It processes the response and formats the templates into a standardized structure.
* @throws \WP_Error Logs error message if the API request fails
* @return array
* @access public
*/
public function loadRemoteTemplates()
{
$restBase = defined('FC_TEMPLATE_API_DOMAIN') ? FC_TEMPLATE_API_DOMAIN : 'https://fluentcrm.com';
$restApi = $restBase.'/wp-json/wp/v2/email-templates?per_page=50';
// Make a GET request to retrieve CRM templates
$response = wp_remote_get($restApi, [
'sslverify' => false,
]);
// Check if the request resulted in an error
if (is_wp_error($response)) {
// Handle error
error_log($response->get_error_message());
return [];
}
// Decode the JSON response from the request
$templateLists = json_decode(wp_remote_retrieve_body($response), true);
if (!is_array($templateLists)) {
return [];
}
$formattedTemplates = [];
foreach ($templateLists as $template) {
if (!$template['template_json']) {
// Skip if no template json
continue;
}
$mediaURL = '';
if ($template['featured_media'] != 0) {
$mediaURL = $this->getMediaURL($template['featured_media'], $restApi);
}
$formattedTemplates[] = [
'id' => $template['id'],
'title' => $template['title']['rendered'],
'content' => $template['template_json'],
'short_description' => $template['short_description'],
'link' => $template['link'],
'media_url' => $mediaURL,
'status' => $template['status'],
'cover_image' => $template['cover_image'],
];
}
return $formattedTemplates;
}
/**
* Retrieves the full source URL of a media item.
*
* @param int $mediaID Media item ID.
* @param string $restAPI The base URL of the REST API.
*
* @return string Full source URL of the media item.
*/
public function getMediaURL($mediaID, $restAPI) {
$request = wp_remote_get($restAPI.'media/'.$mediaID, [
'sslverify' => false,
]);
// Check for request errors
if (is_wp_error($request)) {
return '';
}
$image = json_decode($request['body'], true);
$img = Arr::get($image, 'source_url');
return $img;
}
}
@@ -0,0 +1,131 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Sanitize;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Http\Request\Request;
/**
* UsersController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class UsersController extends Controller
{
/**
* Get all the users.
* @param \FluentCrm\Framework\Http\Request\Request $request
* @return \WP_REST_Response
*/
public function index(Request $request)
{
$roles = $request->getSafe('roles', 'sanitize_text_field', []);
$limit = $request->limit ?: 5;
$fields = $request->fields ?: ['ID', 'display_name', 'user_email'];
$userQuery = new \WP_User_Query([
'role__in' => $roles,
'number' => $limit,
'fields' => $fields,
]);
$users = $userQuery->get_results();
$total = $userQuery->get_total();
return $this->send([
'users' => $users,
'total' => $total
]);
}
public function import(Request $request)
{
$inputs = $request->only([
'map', 'tags', 'lists', 'roles', 'update', 'new_status', 'double_optin_email', 'import_silently'
]);
/**
* Filter the number of subscribers to process per request while importing users in FluentCRM.
*
* This filter allows you to modify the number of subscribers that are processed
* in a single request when processing subscribers in FluentCRM.
*
* @param int $limit The number of subscribers to process per request. Default is 100.
*/
$limit = apply_filters('fluent_crm/process_subscribers_per_request', 100);
$page = absint($request->get('page', 1));
$userQuery = new \WP_User_Query([
'role__in' => Arr::get($inputs, 'roles', []),
'number' => $limit,
'offset' => ($page - 1) * $limit
]);
if (Arr::get($inputs, 'import_silently') == 'yes') {
if(!defined('FLUENTCRM_DISABLE_TAG_LIST_EVENTS')) {
define('FLUENTCRM_DISABLE_TAG_LIST_EVENTS', true);
}
}
$total = $userQuery->get_total();
$users = $userQuery->get_results();
if($users) {
$this->processUsers($users, $inputs);
}
$hasRecords = !!count($users);
return $this->sendSuccess([
'message' => __('Processing', 'fluent-crm'),
'page_total' => ceil($total / $limit),
'record_total' => $total,
'has_more' => $hasRecords,
'current_page' => $page,
'next_page' => $page + 1
]);
}
private function processUsers($users, $inputs)
{
$subscribers = [];
foreach ($users as $user) {
$subscriber = Helper::getWPMapUserInfo($user);
$subscriber['source'] = 'wp_users';
if ($subscriber['email']) {
$subscribers[] = Sanitize::contact($subscriber);
}
}
$sendDoubleOptin = Arr::get($inputs, 'double_optin_email') == 'yes';
return Subscriber::import(
$subscribers,
Arr::get($inputs, 'tags', []),
Arr::get($inputs, 'lists', []),
Arr::get($inputs, 'update'),
Arr::get($inputs, 'new_status'),
$sendDoubleOptin
);
}
public function roles()
{
if (!function_exists('get_editable_roles')) {
require_once(ABSPATH . '/wp-admin/includes/user.php');
}
$roles = \get_editable_roles();
return [
'roles' => $roles
];
}
}
@@ -0,0 +1,87 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Services\ExternalIntegrations\MailComplaince\Webhook;
use FluentCrm\Framework\Http\Request\Request;
/**
* WebhookBounceController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class WebhookBounceController extends Controller
{
private $validServices = ['mailgun', 'pepipost', 'postmark', 'sendgrid', 'sparkpost', 'elasticemail', 'postalserver', 'smtp2go', 'brevo', 'tosend'];
public function handleBounce(Request $request, $serviceName, $securityCode)
{
if (!in_array($serviceName, $this->validServices)) {
/**
* Filter the bounce handling response for a specific service.
*
* The dynamic portion of the hook name, `$serviceName`, refers to the name of the email service. This is a custom bounce handler.
*
* @since 2.5.95
*
* @param array {
* The response data.
*
* @type int $success Indicates if the bounce handling was successful (0 or 1).
* @type string $message The message associated with the bounce handling.
* @type string $service The name of the email service.
* @type string $result The result of the bounce handling.
* @type int $time The timestamp when the bounce was handled.
* }
* @param object $request The request object.
* @param string $securityCode The security code for the request.
*/
return apply_filters('fluent_crm_handle_bounce_' . $serviceName, [
'success' => 0,
'message' => '',
'service' => $serviceName,
'result' => '',
'time' => time()
], $request, $securityCode);
}
if (!hash_equals($this->getSecurityCode(), $securityCode)) {
return $this->getError();
}
$result = (new Webhook())->handle($serviceName, $request);
return [
'success' => 1,
'message' => 'recorded',
'service' => $serviceName,
'result' => $result,
'time' => time()
];
}
private function getSecurityCode()
{
$code = fluentcrm_get_option('_fc_bounce_key');
if (!$code) {
$code = 'fcrm_' . substr(md5(wp_generate_uuid4()), 0, 14);
fluentcrm_update_option('_fc_bounce_key', $code);
}
return $code;
}
private function getError()
{
return [
'status' => false,
'message' => __('Invalid Data or Security Code', 'fluent-crm')
];
}
}
@@ -0,0 +1,113 @@
<?php
namespace FluentCrm\App\Http\Controllers;
use FluentCrm\App\Models\Company;
use FluentCrm\App\Services\Helper;
use FluentCrm\Framework\Http\Request\Request;
use FluentCrm\Framework\Support\Str;
use FluentCrm\App\Models\Webhook;
use FluentCrm\App\Models\Lists;
use FluentCrm\App\Models\Tag;
/**
* WebhookController - REST API Handler Class
*
* REST API Handler
*
* @package FluentCrm\App\Http
*
* @version 1.0.0
*/
class WebhookController extends Controller
{
public function index(Request $request, Webhook $webhook)
{
$fields = $webhook->getFields();
$search = $request->getSafe('search');
$webhooks = $webhook->latest()->get()->toArray();
if (!empty($search)) {
$search = strtolower($search);
$webhooks = array_map(function ($row) use ($search) {
$value = isset($row['value']) && is_array($row['value']) ? $row['value'] : [];
$name = strtolower((string)($value['name'] ?? ''));
if ($name !== '' && Str::contains($name, $search)) {
return $row;
}
return null;
}, $webhooks);
}
$rows = [];
foreach ($webhooks as $row) {
if ($row) {
$rows[] = $row;
}
}
$response = [
'webhooks' => $rows,
'fields' => $fields['fields'],
'custom_fields' => $fields['custom_fields'],
'lists' => Lists::get(),
'tags' => Tag::get()
];
if (Helper::isCompanyEnabled()) {
$response['companies'] = Company::get();
}
return $response;
}
public function create(Request $request, Webhook $webhook)
{
$data = $request->all();
$validatedData = $this->validate($data, [
'name' => 'required',
'status' => 'required'
]);
$webhook = $webhook->store($validatedData);
return [
'id' => $webhook->id,
'webhook' => $webhook->value,
'webhooks' => $webhook->latest()->get(),
'message' => __('Successfully created the WebHook', 'fluent-crm')
];
}
public function update(Request $request, Webhook $webhook, $id)
{
$existingWebhook = $webhook->find($id);
if (!$existingWebhook) {
return $this->sendError([
'message' => __('Webhook not found', 'fluent-crm')
], 404);
}
$existingWebhook->saveChanges($request->all());
return [
'webhooks' => $webhook->latest()->get(),
'message' => __('Successfully updated the webhook', 'fluent-crm')
];
}
public function delete(Webhook $webhook, $id)
{
$webhook->where('id', $id)->delete();
return [
'webhooks' => $webhook->latest()->get(),
'message' => __('Successfully deleted the webhook', 'fluent-crm')
];
}
}