PluginProbe ʕ •ᴥ•ʔ
Hostinger Reach – AI-Powered Email Marketing for WordPress / 1.7.1
Hostinger Reach – AI-Powered Email Marketing for WordPress v1.7.1
1.7.2 1.7.1 1.7.0 1.6.0 1.5.9 1.5.8 1.5.7 1.5.6 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 trunk 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6
hostinger-reach / vendor / hostinger / hostinger-wp-amplitude / src / AmplitudeManager.php
hostinger-reach / vendor / hostinger / hostinger-wp-amplitude / src Last commit date
ActionDispatcher.php 1 month ago AmplitudeLoader.php 1 month ago AmplitudeManager.php 1 month ago Rest.php 1 month ago
AmplitudeManager.php
328 lines
1 <?php
2
3 namespace Hostinger\Amplitude;
4
5 use Hostinger\WpHelper\Config;
6 use Hostinger\WpHelper\Requests\Client;
7 use Hostinger\WpHelper\Utils as Helper;
8
9 class AmplitudeManager
10 {
11 public const AMPLITUDE_ENDPOINT = '/v3/wordpress/plugin/trigger-event';
12 public const CACHE_THREE_HOURS = 10800;
13 public const CACHE_ONE_HOUR = 3600;
14 public const ERROR_LOG_THROTTLE_SECONDS = 300;
15 private const LOGIN_DATA = 'hostinger_login_data';
16 private const OPTION_PREFIX = 'amplitude_event_';
17 private const OPTION_COUNT_PREFIX = 'amplitude_count_';
18 private const CLEANUP_PREFIX = 'amplitude_cleanup_';
19 private const ERROR_LOG_PREFIX = 'amplitude_error_';
20
21 private Config $configHandler;
22 private Client $client;
23 private Helper $helper;
24
25 public function __construct(
26 Helper $helper,
27 Config $configHandler,
28 Client $client
29 ) {
30 $this->helper = $helper;
31 $this->configHandler = $configHandler;
32 $this->client = $client;
33 }
34
35 public function sendRequest(string $endpoint, array $params, array $headers = []): array
36 {
37 try {
38 if (! $this->helper->checkTransientEligibility('check_transients', self::CACHE_THREE_HOURS)) {
39 return [];
40 }
41
42 if (! $this->shouldSendAmplitudeEvent($params)) {
43 return [];
44 }
45
46 $params = $this->addImpersonationData($params);
47 $params = $this->addDomainAndDirectory($params);
48
49 $headers = $this->extractCorrelationIdHeader($headers);
50
51 $response = $this->client->post($endpoint, [ 'params' => $params ], $headers);
52
53 if (is_wp_error($response)) {
54 $this->logErrorThrottled(
55 'Hostinger WP Amplitude package: ' . $response->get_error_message(),
56 'wp_error_' . $response->get_error_code()
57 );
58 return [];
59 }
60
61 return $response;
62 } catch (\Exception $exception) {
63 $this->logErrorThrottled(
64 'Error sending request: ' . $exception->getMessage(),
65 'exception_' . get_class($exception)
66 );
67
68 return [ 'status' => 'error', 'message' => 'An error occurred while sending the request.' ];
69 }
70 }
71
72 public function addDomainAndDirectory(array $params): array
73 {
74 if ($siteUrl = get_site_url()) {
75 $params['siteurl'] = $siteUrl;
76
77 $websiteDir = $this->getSubdirectoryName($siteUrl);
78 $params['directory'] = $websiteDir;
79 }
80
81 return $params;
82 }
83
84 public function getSubdirectoryName(string $siteUrl): string
85 {
86 $sitePath = parse_url($siteUrl, PHP_URL_PATH) ?? '';
87
88 return trim($sitePath, '/') ? : '';
89 }
90
91 public function addImpersonationData(array $params): array
92 {
93 $login_data = get_transient(self::LOGIN_DATA);
94
95 if ($login_data === false) {
96 return $params;
97 }
98
99 if (! empty($login_data['acting_client_id'])) {
100 $params['is_impersonated'] = true;
101 $params['impersonated_client_id'] = sanitize_text_field($login_data['acting_client_id']);
102 }
103
104 if (! empty($login_data['client_id'])) {
105 $params['client_id'] = sanitize_text_field($login_data['client_id']);
106 }
107
108 return $params;
109 }
110
111 public static function getSingleAmplitudeEvents(): array
112 {
113 return apply_filters('hostinger_once_per_day_events', []);
114 }
115
116 public static function getLimitedPerDayEvents(): array
117 {
118 return apply_filters('hostinger_limited_per_day_events', []);
119 }
120
121 public function shouldSendAmplitudeEvent(array $params): bool
122 {
123 if (empty($params['action'])) {
124 return false;
125 }
126
127 $eventAction = sanitize_text_field($params['action']);
128 $oneTimePerDay = self::getSingleAmplitudeEvents();
129 $limitedPerDay = self::getLimitedPerDayEvents();
130 $today = wp_date('Y-m-d');
131
132 if (in_array($eventAction, $oneTimePerDay, true)) {
133 return $this->checkOncePerDayLimit($eventAction, $today);
134 }
135
136 if (isset($limitedPerDay[ $eventAction ])) {
137 $maxCount = (int) $limitedPerDay[ $eventAction ];
138
139 return $this->checkCountLimit($eventAction, $today, $maxCount);
140 }
141
142 return true;
143 }
144
145 private function checkOncePerDayLimit(string $eventAction, string $today): bool
146 {
147 $optionKey = self::OPTION_PREFIX . $eventAction . '_' . $today;
148
149 $existingValue = $this->getOptionDirect($optionKey);
150
151 if ($existingValue !== null) {
152 return false;
153 }
154
155 $added = $this->addOptionAtomic($optionKey, time());
156
157 if ($added === false) {
158 $existingValue = $this->getOptionDirect($optionKey);
159
160 if ($existingValue !== null) {
161 return false;
162 }
163
164 $this->logErrorThrottled(
165 'Amplitude rate limiter storage error for: ' . $optionKey . '. Blocking event to prevent flooding.',
166 'rate_limiter_' . $eventAction
167 );
168
169 return false;
170 }
171
172 $this->scheduleCleanup($eventAction, $today, self::OPTION_PREFIX);
173
174 return true;
175 }
176
177 private function checkCountLimit(string $eventAction, string $today, int $maxCount): bool
178 {
179 $optionKey = self::OPTION_COUNT_PREFIX . $eventAction . '_' . $today;
180
181 $currentCount = $this->getOptionDirect($optionKey);
182
183 if ($currentCount !== null && (int) $currentCount >= $maxCount) {
184 return false;
185 }
186
187 $newCount = $this->incrementCounter($optionKey);
188
189 if ($newCount === false) {
190 $this->logErrorThrottled(
191 'Amplitude counter increment error for: ' . $optionKey . '. Blocking event to prevent flooding.',
192 'counter_increment_' . $eventAction
193 );
194
195 return false;
196 }
197
198 if ($newCount > $maxCount) {
199 return false;
200 }
201
202 $this->scheduleCleanup($eventAction, $today, self::OPTION_COUNT_PREFIX);
203
204 return true;
205 }
206
207 private function incrementCounter(string $optionKey): int|false
208 {
209 global $wpdb;
210
211 $result = $wpdb->query(
212 $wpdb->prepare(
213 "INSERT INTO {$wpdb->options} (option_name, option_value, autoload)
214 VALUES (%s, 1, 'no')
215 ON DUPLICATE KEY UPDATE option_value = option_value + 1",
216 $optionKey
217 )
218 );
219
220 if ($result === false) {
221 return false;
222 }
223
224 $newValue = $this->getOptionDirect($optionKey);
225
226 return $newValue !== null ? (int) $newValue : false;
227 }
228
229 private function getOptionDirect(string $optionKey): ?string
230 {
231 global $wpdb;
232
233 $value = $wpdb->get_var(
234 $wpdb->prepare(
235 "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s LIMIT 1",
236 $optionKey
237 )
238 );
239
240 return $value;
241 }
242
243 private function addOptionAtomic(string $optionKey, int $value): bool
244 {
245 global $wpdb;
246
247 $result = $wpdb->query(
248 $wpdb->prepare(
249 "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
250 $optionKey,
251 $value
252 )
253 );
254
255 if ($result === false) {
256 return false;
257 }
258
259 return $result > 0;
260 }
261
262 private function scheduleCleanup(string $eventAction, string $today, string $optionPrefix): void
263 {
264 $cleanupKey = self::CLEANUP_PREFIX . $optionPrefix . $eventAction;
265
266 if (get_transient($cleanupKey)) {
267 return;
268 }
269
270 set_transient($cleanupKey, true, self::CACHE_ONE_HOUR);
271
272 $this->cleanupOldEventOptions($eventAction, $today, $optionPrefix);
273 }
274
275 private function cleanupOldEventOptions(string $eventAction, string $today, string $optionPrefix): void
276 {
277 global $wpdb;
278
279 $todayOption = $optionPrefix . $eventAction . '_' . $today;
280
281 $result = $wpdb->query(
282 $wpdb->prepare(
283 "DELETE FROM {$wpdb->options}
284 WHERE option_name LIKE %s
285 AND option_name != %s",
286 $wpdb->esc_like($optionPrefix . $eventAction . '_') . '%',
287 $todayOption
288 )
289 );
290
291 if ($result === false) {
292 $this->logErrorThrottled(
293 'Failed to cleanup old amplitude event options for: ' . $eventAction,
294 'cleanup_' . $eventAction
295 );
296 }
297 }
298
299 public function extractCorrelationIdHeader(array $headers = []): array
300 {
301 if (empty($headers)) {
302 return [];
303 }
304
305 foreach ($headers as $key => $value) {
306 if (strtolower($key) === 'x-correlation-id' || strtolower($key) === 'x_correlation_id') {
307 $idValue = is_array($value) && ! empty($value) ? $value[0] : $value;
308
309 return [ 'X-Correlation-ID' => sanitize_text_field((string) $idValue) ];
310 }
311 }
312
313 return [];
314 }
315
316 private function logErrorThrottled(string $message, string $errorKey): void
317 {
318 $throttleKey = self::ERROR_LOG_PREFIX . md5($errorKey);
319
320 if (get_transient($throttleKey)) {
321 return;
322 }
323
324 set_transient($throttleKey, true, self::ERROR_LOG_THROTTLE_SECONDS);
325 $this->helper->errorLog($message);
326 }
327 }
328