request->get('filter_type', 'simple'); $with = ['tags', 'lists']; if (Helper::isCompanyEnabled()) { $with[] = 'company'; $with[] = 'companies'; } if ($filterType == 'advanced') { $queryArgs = [ 'with' => $with, 'filter_type' => 'advanced', 'filters_groups_raw' => Helper::parseArrayOrJson($this->request->get('advanced_filters')), 'search' => trim(sanitize_text_field($this->request->get('search', ''))), 'sort_by' => sanitize_sql_orderby($this->request->get('sort_by', 'id')), 'sort_type' => sanitize_sql_orderby($this->request->get('sort_type', 'DESC')), 'has_commerce' => $this->request->get('has_commerce'), 'custom_fields' => $this->request->get('custom_fields') == 'true', 'company_ids' => $this->request->get('company_ids', []), ]; } else { $queryArgs = [ 'with' => $with, 'filter_type' => 'simple', 'search' => trim(sanitize_text_field($this->request->get('search', ''))), 'sort_by' => sanitize_sql_orderby($this->request->get('sort_by', 'id')), 'sort_type' => sanitize_sql_orderby($this->request->get('sort_type', 'DESC')), 'has_commerce' => $this->request->get('has_commerce'), 'custom_fields' => $this->request->get('custom_fields') == 'true', 'tags' => $this->request->get('tags', []), 'statuses' => $this->request->get('statuses', []), 'sms_statuses' => $this->request->get('sms_statuses', []), 'lists' => $this->request->get('lists', []), 'company_ids' => $this->request->get('company_ids', []), ]; } $subscribers = (new ContactsQuery($queryArgs))->paginate(); return $this->sendSuccess([ 'subscribers' => $subscribers, 'custom' => $this->request->get('custom_fields') ]); } /** * Find a subscriber by id * * @return \WP_REST_Response $object */ public function show() { $with = $this->request->get('with', []); $contactId = $this->request->get('id'); $defaultWith = ['tags', 'lists']; if (Helper::isCompanyEnabled()) { $defaultWith[] = 'companies'; } $subscriber = false; if ($contactId) { $subscriber = Subscriber::with($defaultWith)->find($contactId); } else if ($byEmail = $this->request->get('get_by_email')) { $subscriber = Subscriber::with($defaultWith)->where('email', sanitize_email($byEmail))->first(); } if (!$subscriber) { return $this->sendError([ 'message' => __('Subscriber not found', 'fluent-crm') ]); } if (in_array('commerce_stat', $with)) { $subscriber->commerce_stat = []; /** * Determine the commerce provider for FluentCRM. * * This filter allows you to modify the commerce provider used in FluentCRM. * * @param string The current commerce provider. * @since 2.5.1 * */ $commerceProvider = apply_filters('fluentcrm_commerce_provider', ''); if ($commerceProvider) { /** * Determine the purchase statistics for a specific subscriber and commerce provider. * * This filter allows modification of the purchase statistics for a given subscriber * based on the specified commerce provider. * * @param array The current purchase statistics for the subscriber. * @param int $subscriber_id The ID of the subscriber. * * @return array Modified purchase statistics for the subscriber. * @since 2.7.0 * */ $subscriber->commerce_stat = apply_filters('fluent_crm/contact_purchase_stat_' . $commerceProvider, [], $subscriber->id); } } if ($wpUser = $subscriber->getWpUser()) { $subscriber->user_edit_url = get_edit_user_link($wpUser->ID); $subscriber->user_roles = array_values($wpUser->roles); } if (in_array('stats', $with)) { $subscriber->stats = $subscriber->stats(); } if (in_array('subscriber.custom_values', $with)) { $subscriber->custom_values = (object)$subscriber->custom_fields(); } if ($subscriber->date_of_birth == '0000-00-00' || empty($subscriber->date_of_birth)) { $subscriber->date_of_birth = ''; } if ($subscriber->status == 'unsubscribed') { $subscriber->unsubscribe_reason = $subscriber->unsubscribeReason(); $subscriber->unsubscribe_date = $subscriber->unsubscribeReasonDate(); } else if ($subscriber->status == 'bounced' || $subscriber->status == 'complained') { $subscriber->unsubscribe_reason = $subscriber->unsubscribeReason('reason'); $subscriber->unsubscribe_date = $subscriber->unsubscribeReasonDate('reason'); } $data = [ 'subscriber' => $subscriber ]; if (in_array('custom_fields', $with)) { $data['custom_fields'] = fluentcrm_get_option('contact_custom_fields', []); } return $this->sendSuccess($data); } public function updateProperty() { $column = $this->request->getSafe('property', 'sanitize_text_field'); $value = $this->request->getSafe('value', 'sanitize_text_field'); $subscriberIds = $this->request->get('subscribers'); if (!is_array($subscriberIds)) { $subscriberIds = [$subscriberIds]; // say, this is single value, convert to array } $subscriberIds = array_map('intval', $subscriberIds); $subscriberIds = array_unique(array_filter($subscriberIds)); $validColumns = ['status', 'contact_type', 'avatar', 'company_id', 'sms_status']; $subscriberStatuses = fluentcrm_subscriber_statuses(); $leadStatuses = fluentcrm_contact_types(); $smsStatuses = apply_filters('fluentcrm_sms_statuses', [ 'sms_subscribed', 'sms_unsubscribed', 'sms_pending', 'sms_bounced' ]); $this->validate([ 'column' => $column, 'subscriber_ids' => $subscriberIds ], [ 'column' => 'required', 'subscriber_ids' => 'required' ]); if (!in_array($column, $validColumns)) { return $this->sendError([ 'message' => __('Column is not valid', 'fluent-crm') ]); } if ($column == 'status' && !in_array($value, $subscriberStatuses)) { return $this->sendError([ 'message' => __('Value is not valid', 'fluent-crm') ]); } else if ($column == 'contact_type' && !isset($leadStatuses[$value])) { return $this->sendError([ 'message' => __('Value is not valid', 'fluent-crm') ]); } else if ($column == 'company_id') { Company::findOrFail($value); // just a check } else if ($column == 'sms_status' && !in_array($value, $smsStatuses)) { return $this->sendError([ 'message' => __('Value is not valid', 'fluent-crm') ]); } $subscribers = Subscriber::whereIn('id', $subscriberIds)->get(); foreach ($subscribers as $subscriber) { $oldValue = $subscriber->{$column}; if ($oldValue != $value) { $subscriber->{$column} = $value; $subscriber->save(); if (in_array($column, ['status', 'contact_type', 'sms_status'])) { do_action('fluentcrm_subscriber_' . $column . '_to_' . $value, $subscriber, $oldValue); if ($column == 'status') { /** * Contact's Status has been changed * * @param Subscriber $subscriber Subscriber Model. * @param string $oldStatus Old Status. * @since 1.0 * */ do_action('fluent_crm/subscriber_status_changed', $subscriber, $oldValue, $value); } else if ($column == 'sms_status') { /** * Contact's SMS Status has been changed * * @param Subscriber $subscriber Subscriber Model. * @param string $oldStatus Old SMS Status. * @since 3.0.0 * */ do_action('fluent_crm/subscriber_sms_status_changed', $subscriber, $oldValue, $value); } } if ($column == 'avatar') { do_action('fluent_crm/subscriber_avatar_update', $subscriber, $oldValue); } } } return $this->sendSuccess([ 'message' => __('Subscribers successfully updated', 'fluent-crm') ]); } public function deleteSubscriber(Request $request, $id) { $subscriber = Subscriber::findOrFail($id); Helper::deleteContacts([$subscriber->id]); return $this->sendSuccess([ 'message' => __('Selected Subscriber has been deleted successfully', 'fluent-crm') ]); } public function deleteSubscribers(Request $request) { $subscriberIds = $request->get('subscribers'); $this->validate( ['subscriber_ids' => $subscriberIds], ['subscriber_ids' => 'required'] ); Helper::deleteContacts($subscriberIds); return $this->sendSuccess([ 'message' => __('Selected Subscribers have been deleted', 'fluent-crm') ]); } /** * Tag a subscriber with Tags or Lists. */ public function tagger() { $model = $this->resolveModel(); $subscribers = $this->request->get('subscribers'); $attachments = $this->attachments($model, 'attach'); $detachments = $this->attachments($model, 'detach'); $type = $this->request->get('type'); foreach ($subscribers as $subscriberId) { $subscriber = Subscriber::find($subscriberId); if ($attachments) { if ($type == 'tags') { $subscriber->attachTags($attachments); } else { $subscriber->attachLists($attachments); } } if ($detachments) { if ($type == 'tags') { $subscriber->detachTags($detachments); } else { $subscriber->detachLists($detachments); } } } return $this->sendSuccess([ 'message' => __('Successfully updated the ', 'fluent-crm') . _n('subscriber', 'subscribers', count($subscribers), 'fluent-crm') . '.', 'subscribers' => Subscriber::with('tags', 'lists')->whereIn('id', $subscribers)->get() ]); } /** * Store a subscriber. * * @return array */ public function store(Request $request) { $forceUpdate = $request->get('__force_update') == 'yes'; if (!$forceUpdate) { $data = $this->validate($request->all(), [ 'email' => 'required|email|unique:fc_subscribers', 'status' => 'required' ], [ 'email.unique' => __('Provided email already assigned to another subscriber.', 'fluent-crm') ]); } else { $data = $this->validate($request->all(), [ 'email' => 'required|email', 'status' => 'required' ]); } unset($data['__force_update']); $data = Sanitize::contact($data); $user = get_user_by('email', $data['email']); if ($user) { $data['user_id'] = $user->ID; } else { $data['user_id'] = ''; } if ($this->isNew()) { $data['created_at'] = current_time('mysql'); $contact = FluentCrmApi('contacts')->createOrUpdate($data, false, false); /** * new Contact has been created * * @param Subscriber $contact Subscriber Model. * @param array $data Original raw subscriber. * @since 3.30.2 */ // do_action('fluentcrm_contact_created', $contact); // @deprecated since 2.8.0. Use fluent_crm/contact_created instead // do_action('fluent_crm/contact_created', $contact); // no need these action hooks because those are already in the updateOrCreate method in Subscriber model $double_optin = filter_var($request->get('double_optin'), FILTER_VALIDATE_BOOLEAN); if ($double_optin) { $contact->sendDoubleOptinEmail(); } return [ 'message' => __('Successfully added the subscriber.', 'fluent-crm'), 'contact' => $contact, 'action_type' => 'created' ]; } else if ($forceUpdate) { $contact = FluentCrmApi('contacts')->createOrUpdate($data, false, false); if ($contact && $contact->status == 'pending') { $contact->sendDoubleOptinEmail(); } return $this->sendSuccess([ 'message' => __('Contact has been successfully updated.', 'fluent-crm'), 'contact' => $contact, 'action_type' => 'updated' ]); } return $this->sendError([ 'message' => __('Sorry, contact already exists', 'fluent-crm') ], 422); } public function bulkAddUpdate(Request $request) { $contacts = Helper::parseArrayOrJson($request->get('contacts')); $invalids = []; $created = []; $updated = []; $double_optin = filter_var($request->get('double_optin'), FILTER_VALIDATE_BOOLEAN); $forceUpdate = filter_var($request->get('force_update'), FILTER_VALIDATE_BOOLEAN); foreach ($contacts as $contact) { $contactData = Sanitize::contact($contact); if (empty($contactData['email']) || !is_email($contactData['email'])) { $invalids[] = $contactData; continue; } $contactData['tags'] = Arr::get($contact, 'tags', []); $contactData['lists'] = Arr::get($contact, 'lists', []); $createdContact = FluentCrmApi('contacts')->createOrUpdate($contactData, $forceUpdate, false); if (!$createdContact) { $invalids[] = $contactData; continue; } if ($createdContact->status == 'pending' && $double_optin) { $createdContact->sendDoubleOptinEmail(); } if ($contact->wasRecentlyCreated) { $created[] = [ 'id' => $createdContact->id, 'email' => $createdContact->email, 'status' => $createdContact->status, ]; } else { $updated[] = [ 'id' => $createdContact->id, 'email' => $createdContact->email, 'status' => $createdContact->status, ]; } } return [ 'message' => __('Successfully added/updated the subscribers.', 'fluent-crm'), 'created' => $created, 'updated' => $updated, 'invalids' => $invalids ]; } public function updateSubscriber(Request $request, $id) { $subscriber = Subscriber::findOrFail($id); $originalData = Helper::parseArrayOrJson($request->get('subscriber')); if (!$originalData) { $originalData = $request->all(); } $data = []; if (isset($originalData['email'])) { $data = $this->validate($originalData, [ 'email' => 'required|email|unique:fc_subscribers,email,' . $id, ], [ 'email.unique' => __('Provided email already assigned to another subscriber.', 'fluent-crm') ]); } else { $data = $originalData; } if (isset($data['email'])) { // Maybe update user id $user = get_user_by('email', $data['email']); /** * Determine whether to update the WordPress user email on change. * * This filter allows you to control whether the WordPress user email should be updated * when there is a change in the FluentCRM subscriber email. * * @param bool Whether to update the WordPress user email on change. Default false. * @since 2.9.25 * */ if (!$user && apply_filters('fluentcrm_update_wp_user_email_on_change', false)) { $user = get_user_by('ID', $data['user_id']); } $data['user_id'] = $user ? $user->ID : NULL; } if (!empty($data['user_id'])) { $data['user_id'] = (int)$data['user_id']; } if (isset($data['date_of_birth']) && empty($data['date_of_birth'])) { $data['date_of_birth'] = NULL; } $validData = Sanitize::contact($data); unset($validData['created_at']); unset($validData['last_activity']); $customValues = Arr::get($originalData, 'custom_values', []); $oldEmail = $subscriber->email; $oldSubscriber = clone $subscriber; $subscriber->fill($validData); $dirtyFields = $subscriber->getDirty(); if ($dirtyFields) { $subscriber->save(); } if ($customValues) { $originalCustomFields = $subscriber->custom_fields(); $subscriber->syncCustomFieldValues($customValues, true); $dirtyCustomValues = $oldSubscriber->custom_fields(); do_action('fluent_crm/contact_updated_with_changes', $subscriber, $dirtyCustomValues, $originalCustomFields, ['source' => 'web', 'type' => 'custom_fields_only']); } if ($tags = Arr::get($originalData, 'attach_tags', [])) { $subscriber->attachTags($tags); } if ($lists = Arr::get($originalData, 'attach_lists', [])) { $subscriber->attachLists($lists); } if ($detachTags = Arr::get($originalData, 'detach_tags', [])) { $subscriber->detachTags($detachTags); } if ($detachLists = Arr::get($originalData, 'detach_lists', [])) { $subscriber->detachLists($detachLists); } if ($dirtyFields) { if (isset($dirtyFields['email'])) { /** * Contact's Email address has been updated * * @param Subscriber $subscriber Subscriber Model. * @param string $oldEmail Old Email Address. * @since 1.0 * */ do_action('fluent_crm/contact_email_changed', $subscriber, $oldEmail); } do_action('fluentcrm_contact_updated', $subscriber, $dirtyFields); do_action('fluent_crm/contact_updated', $subscriber, $dirtyFields); do_action('fluent_crm/contact_updated_with_changes', $subscriber, $dirtyFields, $oldSubscriber, ['source' => 'web', 'type' => 'all_fields']); } return $this->sendSuccess([ 'message' => __('Subscriber successfully updated', 'fluent-crm'), 'contact' => $subscriber, 'isDirty' => !!$dirtyFields, 'values' => $customValues ], 200); } /** * Resolve the appropriate model e.g. Tag or, Lists * * @return string */ private function resolveModel() { $type = $this->request->type; return 'FluentCrm\App\Models\\' . ($type === 'tags' ? 'Tag' : 'Lists'); } /** * Get the attachment options e.g. attach or, detach * * @param \FluentCrm\App\Models\Model $model * @param string $type * @return array */ private function attachments($model, $type = 'attach') { $attachments = $this->request->get($type, []); $findBy = sanitize_text_field($this->request->get('find_by', 'slug')); if ($attachments) { $items = $model::select('id')->whereIn($findBy, $attachments)->get(); if (!$items->isEmpty()) { return array_map(function ($item) { return $item['id']; }, $items->toArray()); } } return []; } /** * Handles if subscriber already exists. * * @return bool */ private function isNew() { $subscriber = Subscriber::where( 'email', $this->request->getSafe('email', 'sanitize_email', '') )->first(); if ($subscriber) { return false; } return true; } public function emails(Request $request, $subscriberId) { $filter = sanitize_text_field(Arr::get($request->get(), 'filter')); $emailsQuery = CampaignEmail::where('subscriber_id', $subscriberId) ->orderBy('id', 'DESC'); // Apply filter if present if ($filter == 'open') { $emailsQuery->where('is_open', '1'); } elseif ($filter == 'click') { $emailsQuery->whereNotNull('click_counter'); } elseif ($filter == 'unopened') { $emailsQuery->where('is_open', '==', 0); } $emails = $emailsQuery->paginate(); $tab = Arr::get($request->all(), 'tab', 'crm'); if (defined('FLUENTMAIL_PLUGIN_FILE') && $tab == 'fluentsmtp') { $getLogsByCurrentUser = Subscriber::where('id', $subscriberId)->pluck('email')->first(); $emails = []; if (!empty($getLogsByCurrentUser)) { $emails = fluentMailDb()->table(FLUENT_MAIL_DB_PREFIX . 'email_logs') ->where('to', 'LIKE', '%' . $getLogsByCurrentUser . '%') ->orderBy('id', 'DESC') ->paginate(); // Format the email log results $emails['data'] = $this->formatResult($emails['data']); } } /** * Determine and retrieve emails for a subscriber. * * This filter allows modifying the list of emails associated with a subscriber. * * @param array { * Array containing the email data. * * @type array $emails List of email records or paginated results. * } * @param int $subscriberId The ID of the subscriber. * * @return array Filtered email data for the subscriber. * @since 1.0.0 * */ return apply_filters('fluentcrm_contact_emails', [ 'emails' => $emails ], $subscriberId); } protected function formatResult($result) { $result = is_array($result) ? $result : func_get_args(); foreach ($result as $key => $row) { $result[$key] = array_map('maybe_unserialize', (array)$row); $result[$key]['id'] = (int)$result[$key]['id']; $result[$key]['retries'] = (int)$result[$key]['retries']; $result[$key]['from'] = htmlspecialchars($result[$key]['from']); $result[$key]['subject'] = wp_kses_post(wp_unslash($result[$key]['subject'])); } return $result; } public function deleteEmails(Request $request, $subscriberId) { $emailIds = array_map('intval', $request->get('email_ids')); CampaignEmail::where('subscriber_id', $subscriberId) ->whereIn('id', $emailIds) ->delete(); return [ 'message' => __('Selected emails have been deleted', 'fluent-crm') ]; } public function getNotes() { $subscriberId = $this->request->get('id'); $search = $this->request->get('search'); $includeId = intval($this->request->get('include_id', 0)); $notes = SubscriberNote::where('subscriber_id', $subscriberId); 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 = SubscriberNote::where('id', $includeId) ->where('subscriber_id', $subscriberId) ->first(); if ($includedNote) { $includedNote->added_by = $includedNote->createdBy(); $response['included_note'] = $includedNote; } } } return $this->sendSuccess($response); } public function addNote(Request $request, $id) { $subscriber = Subscriber::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'); } /** * Parse the Subscriber's Note Description. * * @param string $note ['description'] The subscriber's note description. * @param object $subscriber The subscriber object. * @since 2.8.44 * */ $note['description'] = apply_filters('fluent_crm/parse_campaign_email_text', $note['description'], $subscriber); $note['subscriber_id'] = $id; $note = Sanitize::contactNote($note); $subscriberNote = SubscriberNote::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/note_added', $subscriberNote, $subscriber, $note); return $this->sendSuccess([ 'note' => $subscriberNote, 'message' => __('Note successfully added', 'fluent-crm') ]); } public function updateNote(Request $request, $id, $noteId) { $subscriber = Subscriber::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']); } /** * Parse the campaign email text for Subscriber's Note Description. * * This filter allows you to modify the campaign email text before it is processed for a Subscriber's Note Description. * * @param string $note ['description'] Subscriber's Note Description from parsed campaign email text. * @param object $subscriber The subscriber object data. * @since 2.8.44 * */ $note['description'] = apply_filters('fluent_crm/parse_campaign_email_text', $note['description'], $subscriber); $note = Sanitize::contactNote($note); $subsciberNote = SubscriberNote::find($noteId); $subsciberNote->fill($note); $subsciberNote->save(); /** * Subscriber's Note Updated * * @param SubscriberNote $subscriberNote Note Model. * @param Subscriber $subscriber Contact Model. * @param array $note Contact Note Data Array. * @since 1.0 */ do_action('fluent_crm/note_updated', $subsciberNote, $subscriber, $note); return $this->sendSuccess([ 'note' => $subsciberNote, 'message' => __('Note successfully updated', 'fluent-crm') ]); } public function deleteNote($id, $noteId) { $subscriber = Subscriber::findOrFail($id); SubscriberNote::where('id', $noteId)->delete(); /** * Subscriber's Note Delete * * @param SubscriberNote $subscriberNote Note Model. * @param Subscriber $subscriber Contact Model. * @since 1.0 */ do_action('fluent_crm/note_delete', $noteId, $subscriber); return $this->sendSuccess([ 'message' => __('Note successfully deleted', 'fluent-crm') ]); } public function bulkDeleteNotes(Request $request, $id) { $subscriber = Subscriber::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 subscriber so users cannot delete notes belonging to other contacts. $deletableNoteIds = SubscriberNote::where('subscriber_id', $subscriber->id) ->whereIn('id', $noteIds) ->pluck('id') ->toArray(); $deletedCount = 0; if ($deletableNoteIds) { $deletedCount = SubscriberNote::whereIn('id', $deletableNoteIds)->delete(); foreach ($deletableNoteIds as $deletedNoteId) { do_action('fluent_crm/note_delete', $deletedNoteId, $subscriber); } } return $this->sendSuccess([ 'message' => sprintf( /* translators: %d: number of deleted notes */ _n('%d note deleted', '%d notes deleted', $deletedCount, 'fluent-crm'), $deletedCount ) ]); } public function getFormSubmissions() { $provider = $this->request->get('provider'); $subscriberId = intval($this->request->get('id')); $subscriber = Subscriber::findOrFail($subscriberId); /** * Filter the form submissions data for a specific provider. * * The dynamic portion of the hook name, `$provider`, refers to the form provider. * * @param array { * An array of form submissions data. * * @type array $data The form submissions data. * @type int $total The total number of form submissions. * } * @param object $subscriber The subscriber object. * @since 2.5.1 * */ $data = apply_filters('fluentcrm_get_form_submissions_' . $provider, [ 'data' => [], 'total' => 0 ], $subscriber); return $this->sendSuccess([ 'submissions' => $data ]); } public function getSupportTickets() { $provider = $this->request->get('provider'); $subscriberId = intval($this->request->get('id')); $subscriber = Subscriber::where('id', $subscriberId)->first(); /** * Determine the support tickets data for a specific provider and subscriber. * * The dynamic portion of the hook name, `$provider`, refers to the support ticket provider. * * @param array { * An array of support tickets data. * * @type array $data The support tickets data. * @type int $total The total number of support tickets. * } * @param object $subscriber The subscriber object. * @since 2.5.1 * */ $data = apply_filters('fluentcrm-get_support_tickets_' . $provider, [ 'data' => [], 'total' => 0 ], $subscriber); $data['columns_config'] = [ 'id' => [ 'label' => __('ID', 'fluent-crm'), 'width' => '100px' ], 'status' => [ 'label' => 'Status', 'width' => '120px' ], 'Submitted at' => [ 'label' => 'Submitted at', 'width' => '150px' ], 'action' => [ 'label' => 'Action', 'width' => '150px' ] ]; return $this->sendSuccess([ 'tickets' => $data ]); } public function sendDoubleOptinEmail(Request $request, $id) { $subscriber = Subscriber::findOrFail($id); if ($subscriber->status == 'subscribed') { return $this->sendError([ 'message' => __('Contact Already Subscribed', 'fluent-crm') ]); } $subscriber->sendDoubleOptinEmail(); return $this->sendSuccess([ 'message' => __('Double OptIn email has been sent', 'fluent-crm') ]); } public function getTemplateMock(Request $request, $id) { $emailMock = CustomEmailCampaign::getMock(); $emailMock['title'] = __('Custom Email to Contact', 'fluent-crm'); return [ 'email_mock' => $emailMock ]; } public function sendCustomEmail(Request $request, $contactId) { $contact = Subscriber::findOrFail($contactId); $validStatuses = ['subscribed', 'transactional']; if (!in_array($contact->status, $validStatuses)) { return $this->sendError([ 'message' => __('Subscriber\'s status need to be subscribed.', 'fluent-crm') ]); } add_action('wp_mail_failed', function ($wpError) { Helper::debugLog( 'Custom Email failed', $wpError->get_error_message(), 'error' ); }, 10, 1); $newCampaign = $request->get('campaign'); unset($newCampaign['id']); $newCampaign = Sanitize::campaign($newCampaign); $campaign = CustomEmailCampaign::create($newCampaign); $campaign->subscribe([$contactId], [ 'status' => 'scheduled', 'scheduled_at' => current_time('mysql') ]); do_action('fluentcrm_process_contact_jobs', $contact); return [ 'message' => __('Custom Email has been successfully sent', 'fluent-crm') ]; } public function getExternalView(Request $request, $subscriberId) { $subscriber = Subscriber::findOrFail($subscriberId); $sectionId = $request->get('section_provider'); /** * Filter the profile section content for a specific section ID. * * The dynamic portion of the hook name, `$sectionId`, refers to the ID of the profile section. * * @param array { * An array of profile section data. * * @type string $heading The heading of the profile section. * @type string $content_html The HTML content of the profile section. * } * @param object $subscriber The subscriber object. * @since 2.5.1 * */ return apply_filters('fluencrm_profile_section_' . $sectionId, [ 'heading' => '', 'content_html' => '' ], $subscriber); } public function saveExternalViewData(Request $request, $subscriberId) { $subscriber = Subscriber::findOrFail($subscriberId); $sectionId = $request->get('section_provider'); /** * Filter the data being saved for a specific profile section. * * This filter allows modifying the data before saving it for a profile section * identified by the `$sectionId`. * * @param mixed The data to be saved for the profile section. Defaults to an empty string. * @param array The input data received from the request. Defaults to an empty array. * @param object $subscriber The subscriber object for which the profile section is being updated. * * @return mixed Filtered data to be saved for the profile section. * @since 2.8.44 * */ $response = apply_filters('fluencrm_profile_section_save_' . $sectionId, '', $request->get('data', []), $subscriber); if (!$response) { return $this->sendError([ 'message' => __('Handled could not be found.', 'fluent-crm') ]); } return $response; } public function handleBulkActions(Request $request) { $actionName = sanitize_text_field($request->get('action_name', '')); $doingAllBulk = $request->get('is_all') == 'yes'; if ($doingAllBulk) { $contactQuery = $request->get('contact_query', []); $filterType = Arr::get($contactQuery, 'filter_type', 'simple'); $with = []; if ($filterType == 'advanced') { $rawGroup = json_decode(Arr::get($contactQuery, 'advanced_filters', ''), true); if (!$rawGroup || !is_array($rawGroup)) { return $this->sendError([ 'message' => __('Invalid Advanced Filters', 'fluent-crm') ]); } $queryArgs = [ 'with' => $with, 'filter_type' => 'advanced', 'filters_groups_raw' => $rawGroup, 'search' => trim(Arr::get($contactQuery, 'search', '')), 'sort_by' => 'id', 'sort_type' => 'ASC', 'has_commerce' => false, 'custom_fields' => false, 'company_ids' => Arr::get($contactQuery, 'company_ids', []), ]; } else { $queryArgs = [ 'with' => $with, 'filter_type' => 'simple', 'search' => trim(Arr::get($contactQuery, 'search', '')), 'sort_by' => 'id', 'sort_type' => 'ASC', 'has_commerce' => false, 'custom_fields' => false, 'tags' => Arr::get($contactQuery, 'tags', []), 'statuses' => Arr::get($contactQuery, 'statuses', []), 'lists' => Arr::get($contactQuery, 'lists', []), 'company_ids' => Arr::get($contactQuery, 'company_ids', []), ]; } $subscribersModel = (new ContactsQuery($queryArgs))->getModel(); $lastId = $request->get('last_id', 0); $bulkActionLimit = (int)apply_filters('fluent_crm/contact_bulk_action_limit', 400, $request); if ($bulkActionLimit < 1) { $bulkActionLimit = 1; } $subscribersModel = $subscribersModel->select(['id']) ->limit($bulkActionLimit) ->where('id', '>', $lastId) ->get(); if ($subscribersModel->isEmpty()) { return [ 'is_completed' => true, 'completed_contacts' => 0, 'message' => __('All contacts have been processed', 'fluent-crm') ]; } $subscriberIds = $subscribersModel->pluck('id')->toArray(); } else { $subscriberIds = array_map('intval', $request->get('subscriber_ids', [])); $subscriberIds = array_filter($subscriberIds); } $lastContactId = end($subscriberIds); if (!$subscriberIds) { return $this->sendError([ 'message' => __('Subscribers selection is required', 'fluent-crm') ]); } if ($actionName == 'delete_contacts') { Helper::deleteContacts($subscriberIds); return $this->sendSuccess([ 'completed_contacts' => count($subscriberIds), 'last_contact_id' => $lastContactId, 'message' => __('Selected Contacts have been deleted permanently', 'fluent-crm'), ]); } elseif ($actionName == 'send_double_optin') { Helper::sendDoubleOptin($subscriberIds); return $this->sendSuccess([ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('Double optin sent to selected contacts', 'fluent-crm'), ]); } elseif ($actionName == 'add_to_email_sequence') { if (!defined('FLUENTCAMPAIGN')) { return $this->sendError([ 'message' => __('This action requires FluentCRM Pro', 'fluent-crm') ]); } $sequenceId = (int)$request->get('new_status', ''); if (!$sequenceId) { return $this->sendError([ 'message' => __('Invalid Email Sequence ID', 'fluent-crm') ]); } $sequence = \FluentCampaign\App\Models\Sequence::findOrFail($sequenceId); $validSubscribers = Subscriber::whereIn('id', $subscriberIds) ->whereDoesntHave('sequences', function ($q) use ($sequenceId) { $q->where('fc_campaigns.id', $sequenceId); }) ->where('status', 'subscribed') ->get(); if ($validSubscribers->isEmpty()) { if ($doingAllBulk) { return $this->sendSuccess([ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('No valid active subscribers found for this chunk', 'fluent-crm') ]); } return $this->sendError([ 'message' => __('No valid active subscribers found for this sequence', 'fluent-crm') ]); } $sequence->subscribe($validSubscribers); return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), /* translators: 1. Number of subscribers */ 'message' => sprintf(__('%d subscribers have been attached to the selected email sequence', 'fluent-crm'), count($validSubscribers)) ]; } elseif ($actionName == 'add_to_company') { $companyId = (int)$request->get('new_status', ''); if (!$companyId) { return $this->sendError([ 'message' => __('Invalid Company ID', 'fluent-crm') ]); } $company = Company::findOrFail($companyId); $validSubscribers = Subscriber::whereIn('id', $subscriberIds) ->whereDoesntHave('companies', function ($q) use ($companyId) { $q->where('fc_companies.id', $companyId); }) ->get(); if ($validSubscribers->isEmpty()) { if ($doingAllBulk) { return $this->sendSuccess([ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('No valid active subscribers found for this chunk', 'fluent-crm') ]); } return $this->sendError([ 'message' => __('No valid active subscribers found for this company', 'fluent-crm') ]); } foreach ($validSubscribers as $contact) { if ($contact) { $contact->attachCompanies([$company->id]); if (!$contact->company_id) { $contact->company_id = $company->id; $contact->save(); } } } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), /* translators: %d is the number of subscribers */ 'message' => sprintf(__('%d subscribers have been attached to the selected company', 'fluent-crm'), count($validSubscribers)) ]; } elseif ($actionName == 'remove_from_company') { $companyId = (int)$request->get('new_status', ''); if (!$companyId) { return $this->sendError([ 'message' => __('Invalid Company ID', 'fluent-crm') ]); } $company = Company::findOrFail($companyId); $validSubscribers = Subscriber::whereIn('id', $subscriberIds) ->whereHas('companies', function ($q) use ($companyId) { $q->where('fc_companies.id', $companyId); }) ->get(); if ($validSubscribers->isEmpty()) { if ($doingAllBulk) { return $this->sendSuccess([ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('No valid active subscribers found for this chunk', 'fluent-crm') ]); } return $this->sendError([ 'message' => __('No valid active subscribers found for this company', 'fluent-crm') ]); } foreach ($validSubscribers as $contact) { if ($contact) { $contact->detachCompanies([$company->id]); if ($contact->company_id == $company->id) { $contact->company_id = null; $contact->save(); } } } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), /* translators: %d is the number of subscribers */ 'message' => sprintf(__('%d subscribers have been detached from the selected company', 'fluent-crm'), count($validSubscribers)) ]; } elseif ($actionName == 'change_contact_status') { $newStatus = sanitize_text_field($request->get('new_status', '')); if (!$newStatus) { return $this->sendError([ 'message' => __('Please select status', 'fluent-crm') ]); } $subscribers = Subscriber::whereIn('id', $subscriberIds)->get(); foreach ($subscribers as $subscriber) { $oldStatus = $subscriber->status; if ($oldStatus != $newStatus) { $subscriber->updateStatus($newStatus); } } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('Status has been changed for the selected subscribers', 'fluent-crm') ]; } elseif ($actionName == 'add_to_automation') { if (!defined('FLUENTCAMPAIGN')) { return $this->sendError([ 'message' => __('This action requires FluentCRM Pro', 'fluent-crm') ]); } $automationId = (int)$request->get('new_status', ''); if (!$automationId) { return $this->sendError([ 'message' => __('Invalid Automation Funnel ID', 'fluent-crm') ]); } $automation = Funnel::findOrFail($automationId); $validSubscribers = Subscriber::whereIn('id', $subscriberIds) ->whereDoesntHave('funnels', function ($q) use ($automationId) { $q->where('fc_funnels.id', $automationId); }) ->get(); if ($validSubscribers->isEmpty()) { if ($doingAllBulk) { return $this->sendSuccess([ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('No valid active subscribers found for this chunk', 'fluent-crm') ]); } return $this->sendError([ 'message' => __('No valid active subscribers found for this funnel', 'fluent-crm') ]); } foreach ($validSubscribers as $subscriber) { (new FunnelProcessor())->startFunnelSequence($automation, [], [ 'source_trigger_name' => 'fcrm_manual_attach' ], $subscriber); } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), /* translators: %d is the number of subscribers */ 'message' => sprintf(__('%d subscribers have been attached to the selected automation funnel', 'fluent-crm'), count($validSubscribers)), 'subscribers' => $validSubscribers ]; } else if ($actionName == 'change_contact_type') { $subscribers = Subscriber::whereIn('id', $subscriberIds)->get(); $newType = sanitize_text_field($request->get('new_status', '')); if (!$newType) { return $this->sendError([ 'message' => __('Please select new type', 'fluent-crm') ]); } foreach ($subscribers as $subscriber) { $oldType = $subscriber->contact_type; if ($oldType != $newType) { $subscriber->contact_type = $newType; $subscriber->save(); do_action('fluent_crm/subscriber_contact_type_to_' . $newType, $subscriber, $oldType); } } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('Contact Type has been updated for the selected subscribers', 'fluent-crm') ]; } else if ($actionName == 'update_custom_fields') { $customField = $request->get('custom_field'); $customFieldKey = Arr::get($customField, 'key'); $customFieldValue = Arr::get($customField, 'value'); $subscribers = Subscriber::whereIn('id', $subscriberIds)->get(); if (empty($customFieldKey)) { return $this->sendError([ 'message' => __('Please provide a valid custom field key', 'fluent-crm') ]); } foreach ($subscribers as $subscriber) { $existField = SubscriberMeta::where('key', $customFieldKey) ->where('subscriber_id', $subscriber->id) ->first(); // check if exists if ($existField) { if ($existField->value == $customFieldValue) { continue; } $existField->fill(['value' => $customFieldValue])->save(); } else { $customFieldMeta = new SubscriberMeta(); $customFieldMeta->fill([ 'subscriber_id' => $subscriber->id, 'object_type' => 'custom_field', 'key' => $customFieldKey, 'value' => $customFieldValue, 'created_by' => get_current_user_id() ]); $customFieldMeta->save(); } } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('Custom Fields has been updated for the selected subscribers', 'fluent-crm') ]; } $validActions = [ 'add_to_tags' => 'attachTags', 'add_to_lists' => 'attachLists', 'remove_from_tags' => 'detachTags', 'remove_from_lists' => 'detachLists' ]; if (!isset($validActions[$actionName])) { $response = $this->sendError([ 'message' => __('Selected Action is not valid', 'fluent-crm') ]); /** * Filter the result of a bulk action performed on FluentCRM contacts. * * The dynamic portion of the hook name, `$actionName`, refers to the specific bulk action being performed. * * @param mixed $response The initial response for the bulk action. Can be modified by the filter. * @param array $subscriberIds An array of subscriber IDs targeted by the bulk action. * @param array $request ->all() The full request data as an associative array. * * @return mixed Filtered response for the bulk action. * @since 2.9.0 * */ $result = apply_filters('fluent_crm/contact_bulk_action_' . $actionName, $response, $subscriberIds, $request->all()); if (is_array($result)) { $result['last_contact_id'] = $lastContactId; $result['completed_contacts'] = count($subscriberIds); } return $result; } $options = $request->get('action_options', []); $options = array_map(function ($id) { return intval($id); }, $options); $options = array_filter($options); if (!$options) { return $this->sendError([ 'message' => __('Please provide bulk options', 'fluent-crm') ]); } $method = $validActions[$actionName]; $subscribers = Subscriber::whereIn('id', $subscriberIds)->get(); foreach ($subscribers as $subscriber) { $subscriber->{$method}($options); } return [ 'last_contact_id' => $lastContactId, 'completed_contacts' => count($subscriberIds), 'message' => __('Selected bulk action has been successfully completed', 'fluent-crm') ]; } public function getPrevNextIds(Request $request) { $filterType = $this->request->get('filter_type'); $currentId = (int)$this->request->get('current_id'); if (!$filterType || !$currentId) { return $this->sendError([ 'message' => __('filter_type and current_id are required', 'fluent-crm') ]); } $sortType = sanitize_sql_orderby($this->request->get('sort_type', 'DESC')); $prevSortType = ($sortType == 'DESC') ? 'ASC' : 'DESC'; if ($filterType == 'advanced') { $queryArgs = [ 'filter_type' => 'advanced', 'filters_groups_raw' => Helper::parseArrayOrJson($this->request->get('advanced_filters')), 'search' => trim(sanitize_text_field($this->request->get('search', ''))), 'sort_by' => 'id', 'sort_type' => $sortType, 'with' => [] ]; } else { $queryArgs = [ 'filter_type' => 'simple', 'search' => trim(sanitize_text_field($this->request->get('search', ''))), 'sort_by' => 'id', 'sort_type' => $sortType, 'tags' => $this->request->get('tags', []), 'statuses' => $this->request->get('statuses', []), 'lists' => $this->request->get('lists', []), 'with' => [] ]; } $prevQueryArgs = $queryArgs; $prevQueryArgs['sort_type'] = ($sortType == 'DESC') ? 'ASC' : 'DESC'; $prevItems = (new ContactsQuery($prevQueryArgs)) ->getModel() ->select(['id']) ->limit(10) ->where('id', ($sortType == 'DESC') ? '>' : '<', $currentId) ->get(); $nextItems = (new ContactsQuery($queryArgs)) ->getModel() ->select(['id']) ->limit(10) ->where('id', ($sortType == 'DESC') ? '<' : '>', $currentId) ->get(); $formattedNext = []; foreach ($nextItems as $nextItem) { $formattedNext[] = $nextItem->id; } $formattedPrev = []; foreach ($prevItems as $prevItem) { $formattedPrev[] = $prevItem->id; } return [ 'navigation' => [ 'next' => $formattedNext, 'prev' => $formattedPrev ], 'has_next' => count($formattedNext) == 10, 'has_prev' => count($formattedPrev) == 10 ]; } public function searchContacts(Request $request) { $search = trim($request->getSafe('search', 'sanitize_text_field', '')); $limit = absint($request->get('limit', 20)); if (!$limit) { $limit = 20; } $offset = absint($request->get('offset', 0)); $loadDefault = $request->get('load_default'); $loadDefault = in_array($loadDefault, [true, 1, '1', 'true', 'yes'], true); $contacts = []; if ($search) { $subscribers = Subscriber::searchBy($search)->offset($offset)->limit($limit)->get(); foreach ($subscribers as $subscriber) { $contacts[$subscriber->id] = [ 'first_name' => $subscriber->first_name, 'last_name' => $subscriber->last_name, 'full_name' => $subscriber->full_name, 'email' => $subscriber->email, 'id' => (string)$subscriber->id, 'photo' => $subscriber->photo ]; } } else if ($loadDefault) { $subscribers = Subscriber::orderBy('id', 'DESC')->offset($offset)->limit($limit)->get(); foreach ($subscribers as $subscriber) { $contacts[$subscriber->id] = [ 'first_name' => $subscriber->first_name, 'last_name' => $subscriber->last_name, 'full_name' => $subscriber->full_name, 'email' => $subscriber->email, 'id' => (string)$subscriber->id, 'photo' => $subscriber->photo ]; } } $values = (array)$request->get('values', []); if ($values) { $pushedIds = array_keys($contacts); $includedIds = array_diff($values, $pushedIds); if ($includedIds) { $subscribers = Subscriber::whereIn('id', $includedIds)->get(); foreach ($subscribers as $subscriber) { $contacts[$subscriber->id] = [ 'first_name' => $subscriber->first_name, 'last_name' => $subscriber->last_name, 'full_name' => $subscriber->full_name, 'email' => $subscriber->email, 'id' => (string)$subscriber->id, 'photo' => $subscriber->photo ]; } } } return [ 'contacts' => (object)$contacts ]; } public function getInfoWidgets(Request $request, $subscriber) { if (is_numeric($subscriber)) { $subscriber = Subscriber::findOrFail($subscriber); } if ($byWidget = $request->get('by_widget')) { /** * Filter the subscriber info widget. * * This filter allows modification of the subscriber info widget based on the widget type. * * @param array The array of widgets. * @param object $subscriber The subscriber object data. * @since 2.8.40 * */ $widgets = apply_filters('fluent_crm/subscriber_info_widget_' . $byWidget, [], $subscriber); $widgets = array_values($widgets); if (isset($widgets[0])) { $widget = $widgets[0]; } else { $widget = [ 'content' => 'No content found' ]; } return [ 'widget' => $widget ]; } $commerce = (new PurchaseHistory())->getCommerceStatWidget($subscriber); /** * Filter the top widgets for a subscriber. * * This filter allows modification of the top widgets displayed for a subscriber. * * @param array The array of top widgets. * @param object $subscriber The subscriber object. * @since 2.8.0 * */ $topWidgets = array_filter(apply_filters('fluent_crm/subscriber_top_widgets', array_filter([$commerce]), $subscriber)); /** * Filter the subscriber info widgets. * * This filter allows modification of the subscriber info widgets. * * @param array An array of existing widgets. * @param object $subscriber The subscriber object. * @since 2.8.0 * */ $otherWidgets = apply_filters('fluent_crm/subscriber_info_widgets', [], $subscriber); return [ 'widgets' => [ 'top_widgets' => $topWidgets, 'other_widgets' => $otherWidgets, 'widgets_count' => count($topWidgets) + count($otherWidgets) ] ]; } public function getTrackingEvents(Request $request, $subscriberId) { if (!Helper::isExperimentalEnabled('event_tracking')) { return $this->sendError([ 'message' => __('Event Tracker is not enabled', 'fluent-crm'), 'error_code' => 'not_enabled' ]); } $subscriber = Subscriber::findOrFail($subscriberId); $events = EventTracker::where('subscriber_id', $subscriber->id) ->orderBy('id', 'DESC') ->paginate(); return [ 'events' => $events ]; } public function trackEvent(Request $request) { $data = $request->all(); $this->validate($data, [ 'event_key' => 'required', 'title' => 'required' ]); $isUnique = $request->get('repeatable', true); $result = FluentCrmApi('event_tracker')->track($data, $isUnique); if (is_wp_error($result)) { return $this->sendError([ 'message' => $result->get_error_message(), 'error_code' => $result->get_error_code() ]); } return $this->sendSuccess([ 'message' => __('Event has been tracked', 'fluent-crm'), 'id' => $result->id ]); } public function getUrlMetrics(Request $request, $id) { $sort_by = sanitize_sql_orderby($this->request->get('sort_by', 'id')); $sort_type = sanitize_sql_orderby($this->request->get('sort_type', 'DESC')); $subscriber = Subscriber::findOrFail($id); $urlActivityQuery = CampaignUrlMetric::with('url_stores') ->where('subscriber_id', $subscriber->id) ->where('type', 'click'); // Apply custom sorting if provided if (!empty($sort_by) && !empty($sort_type)) { $urlActivityQuery->orderBy($sort_by, $sort_type); } else { $urlActivityQuery->orderBy('id', 'DESC'); } $urlActivity = $urlActivityQuery->paginate(); $urlMetrics = $urlActivity->toArray(); if (!empty($urlMetrics['data'])) { $urlMetrics['data'] = $this->formatUrlActivityData($urlMetrics['data']); } return [ 'urlMetrics' => $urlMetrics ]; } public function formatUrlActivityData($data) { $result = []; foreach ($data as $item) { $result[] = [ 'url' => $item['url_stores']['url'], 'count' => $item['counter'] ]; } return $result; } public function getDynamicItemView(Request $request, $subscriberId) { $subscriber = Subscriber::findOrFail($subscriberId); $provider = (string)$request->get('provider'); $params = $request->get('params', []); /** * Filter the dynamic item view for a specific provider. * * The dynamic portion of the hook name, `$provider`, refers to the specific provider for which the view is being fetched. * * @param array { * An array containing the dynamic item view data. * * 'type' => (string) The type of the dynamic item. * 'title' => (string) The title of the dynamic item. * 'content_html' => (string) The HTML content of the dynamic item. * 'footer_content' => (string) The footer content associated with the dynamic item. * * } * @param object $subscriber The subscriber object. * @param array $params Additional parameters passed in the request. * @since 3.0.0 * */ $data = apply_filters('fluent_crm/dynamic_contact_item_view_' . $provider, [ 'type' => 'html', 'title' => 'no title', 'content_html' => 'sorry, no content found', 'footer_content' => '' ], $params, $subscriber); return [ 'data_view' => $data ]; } }