PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.3.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.3.1
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
fluent-support / app / Modules / MCP / Support / CustomerMetaEnricher.php

CustomerMetaEnricher.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.3.1, at app/Modules/MCP/Support/CustomerMetaEnricher.php

126 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App\Modules\MCP\Support;
4
5 use FluentSupport\App\Modules\PermissionManager;
6
7 /**
8 * Resolves an optional, integration-provided one-line meta string per customer
9 * for MCP list surfaces (e.g. "Paying · 8,016 sent/30d · domain verified").
10 *
11 * Fluent Support generates nothing here — it only exposes a batch filter that
12 * integrations (FluentCRM/FluentCart/etc.) may hook. The string can carry
13 * billing/CRM PII, so resolution is gated on the fst_sensitive_data capability,
14 * the same boundary AdminSensitivePolicy uses for customer data.
15 */
16 class CustomerMetaEnricher
17 {
18 const MAX_LEN = 120;
19
20 /**
21 * Build a [customer_id => meta line] map for the customers on one list page.
22 *
23 * Fires the filter ONCE per page (never per row) and returns [] for agents
24 * without fst_sensitive_data, so the integration round-trip is skipped and
25 * no PII leaks by default.
26 *
27 * @param iterable $tickets The page of Ticket models (customer eager-loaded).
28 * @param array $context Extra context passed to the filter (e.g. agent_id).
29 * @return array<int,string>
30 */
31 public static function resolve($tickets, array $context = [])
32 {
33 if (!PermissionManager::userCan('fst_sensitive_data')) {
34 return [];
35 }
36
37 $customers = [];
38 foreach ($tickets as $ticket) {
39 if ($ticket->customer_id && $ticket->relationLoaded('customer') && $ticket->customer) {
40 $customers[$ticket->customer_id] = $ticket->customer;
41 }
42 }
43
44 if (!$customers) {
45 return [];
46 }
47
48 $customerIds = array_keys($customers);
49
50 /**
51 * Filter: fluent_support/mcp_customer_list_meta
52 *
53 * Inject a short, plain-text status line per customer for MCP list
54 * surfaces, e.g. "Paying · 8,016 sent/30d · domain verified".
55 *
56 * CONTRACT — read before hooking:
57 * • BATCH ONLY. Fired ONCE per page with a deduplicated array of
58 * customer IDs. Do ALL work in a single query (WHERE customer_id
59 * IN (...)). DO NOT query per customer in a loop — that reintroduces
60 * N+1 and shows up in list latency. The loaded Customer models are
61 * passed so you can read email/user_id without an extra DB hit.
62 * • CHEAP. Runs on every list render. No remote HTTP calls. Cache
63 * anything expensive yourself (transient / object cache).
64 * • PLAIN STRING. The return value is sanitized to plain text: tags
65 * stripped, control chars removed, capped at 120 chars. HTML,
66 * markdown, arrays, objects and unknown keys are DISCARDED.
67 * • ADDITIVE. Merge into the incoming map; preserve existing keys.
68 *
69 * @param array<int,string> $lines Default []. Merge, don't overwrite.
70 * @param int[] $customerIds Deduped customer IDs on this page.
71 * @param array $customers [customer_id => loaded Customer model].
72 * @param array $context ['surface' => string, 'agent_id' => int|null].
73 * @return array<int,string> [customer_id => plain-text line].
74 */
75 $raw = apply_filters(
76 'fluent_support/mcp_customer_list_meta',
77 [],
78 $customerIds,
79 $customers,
80 $context
81 );
82
83 return self::sanitize($raw, $customerIds);
84 }
85
86 /**
87 * Coerce the untrusted filter output into a safe [int => plain string] map.
88 *
89 * @param mixed $raw
90 * @param int[] $allowedIds
91 * @return array<int,string>
92 */
93 private static function sanitize($raw, array $allowedIds)
94 {
95 if (!is_array($raw)) {
96 return [];
97 }
98
99 $allowed = array_flip(array_map('intval', $allowedIds));
100 $clean = [];
101
102 foreach ($raw as $id => $value) {
103 $id = (int) $id;
104
105 // Drop keys not on this page and non-scalar (array/object) values.
106 if (!isset($allowed[$id]) || !is_scalar($value)) {
107 continue;
108 }
109
110 $value = wp_strip_all_tags((string) $value, true);
111 $value = preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $value); // strip control chars
112 $value = trim(preg_replace('/\s+/u', ' ', $value));
113
114 if ($value === '') {
115 continue;
116 }
117
118 $clean[$id] = function_exists('mb_substr')
119 ? mb_substr($value, 0, self::MAX_LEN)
120 : substr($value, 0, self::MAX_LEN);
121 }
122
123 return $clean;
124 }
125 }
126