Initial commit
This commit is contained in:
@@ -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 = '–';
|
||||
} 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')
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user