PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.1
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.1
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Helpers/global/security.php +0 -558 11.0.1 → 11.1.1 View file →
@@ -1,558 +1,0 @@
1 -<?php
2 -defined('ABSPATH') || die('Restricted Access');
3 -
4 -use AcyMailing\Helpers\UpdatemeHelper;
5 -
6 -/**
7 - * To secure text echoed in HTML attributes
8 - */
9 -function acym_escape(string $text): string
10 -{
11 - if (empty($text) && !is_numeric($text)) {
12 - return '';
13 - }
14 -
15 - if (!preg_match('#[&<>"\']#', $text)) {
16 - return $text;
17 - }
18 -
19 - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
20 -}
21 -
22 -/**
23 - * To secure text echoed in HTML content
24 - */
25 -function acym_escapeHtml(string $text): string
26 -{
27 - return esc_attr($text);
28 -}
29 -
30 -/**
31 - * To secure text echoed in HTML content, and whitelisting some HTML tags
32 - * $allowedHtmlTags:
33 - * [
34 - * 'a' => [
35 - * 'href' => true
36 - * ],
37 - * 'br' => []
38 - * ]
39 - */
40 -function acym_escapeHtmlWithAllowedTags(string $text, array $allowedHtmlTags = []): string
41 -{
42 - // Attributes whose value is an URL and must therefore not carry a dangerous scheme.
43 - $urlAttributes = ['href', 'src', 'xlink:href', 'action', 'formaction'];
44 -
45 - return preg_replace_callback(
46 - '#<\s*(/?)\s*([a-z][a-z0-9]*)\b([^>]*?)(/?)\s*>#i',
47 - static function (array $m) use ($allowedHtmlTags, $urlAttributes): string {
48 - [, $closing, $tag, $attrString, $selfClosing] = $m;
49 - $tag = strtolower($tag);
50 -
51 - if (!array_key_exists($tag, $allowedHtmlTags)) {
52 - return '';
53 - }
54 -
55 - if ($closing === '/') {
56 - return '</'.$tag.'>';
57 - }
58 -
59 - $keptAttrs = '';
60 - if (
61 - !empty($allowedHtmlTags[$tag])
62 - && preg_match_all(
63 - '#([a-z][a-z0-9:_-]*)\s*(?:=\s*("[^"]*"|\'[^\']*\'|[^\s>]+))?#i',
64 - $attrString,
65 - $attrMatches,
66 - PREG_SET_ORDER
67 - )
68 - ) {
69 - foreach ($attrMatches as $attr) {
70 - $name = strtolower($attr[1]);
71 - if (empty($allowedHtmlTags[$tag][$name])) {
72 - continue;
73 - }
74 -
75 - $value = isset($attr[2]) ? $attr[2] : '';
76 -
77 - // Drop URL attributes whose value uses a dangerous scheme.
78 - if ($value !== '' && in_array($name, $urlAttributes, true)) {
79 - // Normalize (unquote, decode entities, remove whitespace/control chars
80 - // that could hide the scheme) only to run the check.
81 - $normalized = strtolower(
82 - preg_replace('/[\s\x00-\x20]+/', '', html_entity_decode(trim($value, '"\''), ENT_QUOTES))
83 - );
84 - if (preg_match('#^(?:javascript|data|vbscript):#', $normalized)) {
85 - continue;
86 - }
87 - }
88 -
89 - $keptAttrs .= $value !== '' ? " {$name}={$value}" : " {$name}";
90 - }
91 - }
92 -
93 - $selfClose = $selfClosing === '/' ? ' /' : '';
94 -
95 - return '<'.$tag.$keptAttrs.$selfClose.'>';
96 - },
97 - $text
98 - );
99 -}
100 -
101 -/**
102 - * To secure URLs echoed in HTML attributes
103 - */
104 -function acym_escapeUrl(?string $url): string
105 -{
106 - if (empty($url)) {
107 - return '';
108 - }
109 -
110 - $url = str_replace(' ', '%20', ltrim($url));
111 - $url = preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\\x80-\\xff]|i', '', $url);
112 -
113 - if (empty($url)) {
114 - return '';
115 - }
116 -
117 - if (0 !== stripos($url, 'mailto:')) {
118 - $strip = ['%0d', '%0a', '%0D', '%0A'];
119 - $count = 1;
120 - while ($count) {
121 - $url = str_replace($strip, '', $url, $count);
122 - }
123 - }
124 -
125 - $url = str_replace(';//', '://', $url);
126 - if (strpos($url, ':') === false && !in_array($url[0], ['/', '#', '?'], true) && !preg_match('/^[a-z0-9-]+?\.php/i', $url)) {
127 - $url = 'https://'.$url;
128 - }
129 -
130 - $url = str_replace('&amp;', '&#038;', $url);
131 - $url = str_replace("'", '&#039;', $url);
132 -
133 - if (strpos($url, '[') !== false || strpos($url, ']') !== false) {
134 - $to_unset = [];
135 -
136 - if (strpos($url, '//') === 0) {
137 - $to_unset[] = 'scheme';
138 - $url = 'placeholder:'.$url;
139 - } elseif (strpos($url, '/') === 0) {
140 - $to_unset[] = 'scheme';
141 - $to_unset[] = 'host';
142 - $url = 'placeholder://placeholder'.$url;
143 - }
144 -
145 - $parsed = acym_parseUrl($url);
146 -
147 - if (!empty($parsed)) {
148 - foreach ($to_unset as $key) {
149 - unset($parsed[$key]);
150 - }
151 - }
152 -
153 - $front = '';
154 -
155 - if (isset($parsed['scheme'])) {
156 - $front .= $parsed['scheme'].'://';
157 - } elseif ('/' === $url[0]) {
158 - $front .= '//';
159 - }
160 -
161 - if (isset($parsed['user'])) {
162 - $front .= $parsed['user'];
163 - }
164 -
165 - if (isset($parsed['pass'])) {
166 - $front .= ':'.$parsed['pass'];
167 - }
168 -
169 - if (isset($parsed['user']) || isset($parsed['pass'])) {
170 - $front .= '@';
171 - }
172 -
173 - if (isset($parsed['host'])) {
174 - $front .= $parsed['host'];
175 - }
176 -
177 - if (isset($parsed['port'])) {
178 - $front .= ':'.$parsed['port'];
179 - }
180 -
181 - $end_dirty = str_replace($front, '', $url);
182 - $end_clean = str_replace(['[', ']'], ['%5B', '%5D'], $end_dirty);
183 - $url = str_replace($end_dirty, $end_clean, $url);
184 - }
185 -
186 - return $url;
187 -}
188 -
189 -function acym_arrayToInteger(array &$array): void
190 -{
191 - $array = @array_map('intval', $array);
192 -}
193 -
194 -function acym_getIP(bool $full = false): string
195 -{
196 - $config = acym_config();
197 - $ipCollection = $config->get('ip_collection', 'yes');
198 -
199 - if (!$full && $ipCollection === 'no') {
200 - return '';
201 - }
202 -
203 - $map = [
204 - 'HTTP_X_FORWARDED_IP',
205 - 'X_FORWARDED_FOR',
206 - 'HTTP_X_FORWARDED_FOR',
207 - 'HTTP_CLIENT_IP',
208 - 'HTTP_X_FORWARDED',
209 - 'HTTP_FORWARDED_FOR',
210 - 'HTTP_FORWARDED',
211 - 'REMOTE_ADDR',
212 - ];
213 -
214 - $ipAddress = '';
215 - foreach ($map as $oneAttribute) {
216 - $serverValue = acym_getVar('string', $oneAttribute, '', 'SERVER');
217 - if (empty($serverValue) || strlen($serverValue) < 7) {
218 - continue;
219 - }
220 -
221 - $ipAddress = $serverValue;
222 - break;
223 - }
224 -
225 - // Load balancers and CDNs may put multiple IPs comma separated
226 - if (strstr($ipAddress, ',') !== false) {
227 - $addresses = explode(',', $ipAddress);
228 - $ipAddress = trim(end($addresses));
229 - }
230 -
231 - // We strip tags here as the ip could be modified by something
232 - $ipAddress = acym_stripTags($ipAddress);
233 -
234 - if ($full || $ipCollection === 'yes') {
235 - return $ipAddress;
236 - }
237 -
238 - return acym_anonymizeIP($ipAddress);
239 -}
240 -
241 -function acym_anonymizeIP(string $ipAddress): string
242 -{
243 - if (empty($ipAddress)) {
244 - return '';
245 - }
246 -
247 - // IPv6
248 - if (strpos($ipAddress, ':') !== false) {
249 - $packed = inet_pton($ipAddress);
250 - if ($packed === false) {
251 - return '';
252 - }
253 -
254 - $anonymized = substr($packed, 0, 8).str_repeat("\x00", 8);
255 -
256 - return inet_ntop($anonymized);
257 - }
258 -
259 - // IPv4
260 - $parts = explode('.', $ipAddress);
261 - if (count($parts) !== 4) {
262 - return '';
263 - }
264 -
265 - $parts[3] = '0';
266 -
267 - return implode('.', $parts);
268 -}
269 -
270 -function acym_generateKey(int $length): string
271 -{
272 - $charactersPool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
273 - $randomString = '';
274 - $max = strlen($charactersPool) - 1;
275 - for ($i = 0; $i < $length; $i++) {
276 - $randomString .= $charactersPool[acym_rand(0, $max)];
277 - }
278 -
279 - return $randomString;
280 -}
281 -
282 -function acym_isRobot(): bool
283 -{
284 - if (empty($_SERVER)) {
285 - return false;
286 - }
287 -
288 - // Real calls are made using GET
289 - $requestMethod = acym_getVar('string', 'REQUEST_METHOD', '', 'SERVER');
290 - if (!empty($requestMethod) && $requestMethod === 'HEAD') {
291 - return true;
292 - }
293 -
294 - // SpamBayes checks the images in the sent emails, so the stats image too... Don't count it as opened!
295 - $userAgent = acym_getVar('string', 'HTTP_USER_AGENT', '', 'SERVER');
296 - if (!empty($userAgent) && strpos(strtolower($userAgent), 'spambayes') !== false) {
297 - return true;
298 - }
299 -
300 - // Avoid auto-confirming by the Barracuda firewall installed on some mail clients
301 - $remoteIp = acym_getVar('string', 'REMOTE_ADDR', '', 'SERVER');
302 - if (!empty($remoteIp) && version_compare($remoteIp, '64.235.144.0', '>=') && version_compare($remoteIp, '64.235.159.255', '<=')) {
303 - return true;
304 - }
305 -
306 - return false;
307 -}
308 -
309 -function acym_displayErrors(): void
310 -{
311 - // phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting -- intended error printing on export and debug mode.
312 - error_reporting(E_ALL);
313 - // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- Error display is toggled intentionally for debugging purposes.
314 - @ini_set('display_errors', 1);
315 -}
316 -
317 -function acym_checkRobots(): void
318 -{
319 - $userAgent = acym_getVar('string', 'HTTP_USER_AGENT', null, 'SERVER');
320 - if (!empty($userAgent) && preg_match('#(libwww-perl|python|googlebot)#i', $userAgent)) {
321 - die('Not allowed for robots. Please contact us if you are not a robot');
322 - }
323 -}
324 -
325 -function acym_noCache(): void
326 -{
327 - acym_header('Cache-Control: no-store, no-cache, must-revalidate');
328 - acym_header('Cache-Control: post-check=0, pre-check=0', false);
329 - acym_header('Pragma: no-cache');
330 - acym_header('Expires: Wed, 17 Sep 1975 21:32:10 GMT');
331 -}
332 -
333 -function acym_conditionsContainRestrictedFilter($data): bool
334 -{
335 - if (!is_array($data)) {
336 - return false;
337 - }
338 -
339 - foreach ($data as $key => $value) {
340 - if ($key === 'sqladvanced' && is_array($value) && (!empty($value['query']) || !empty($value['conn_host']))) {
341 - return true;
342 - }
343 - if (is_array($value) && acym_conditionsContainRestrictedFilter($value)) {
344 - return true;
345 - }
346 - }
347 -
348 - return false;
349 -}
350 -
351 -function acym_blockRestrictedFilterAjax($conditions): void
352 -{
353 - if (acym_conditionsContainRestrictedFilter($conditions) && !acym_hasAdminPermissions()) {
354 - acym_sendAjaxResponse(acym_translation('ACYM_ACCESS_DENIED'), [], false);
355 - }
356 -}
357 -
358 -function acym_isAllowed(string $controller, string $task = ''): bool
359 -{
360 - if ($controller === 'dashboard') {
361 - // Promotional pages and the home page hold no sensitive data, but they stay reserved to the back-end users
362 - if (in_array($task, ['acychecker', 'features', 'listing', 'upgrade'])) {
363 - return acym_hasBackofficeAccess();
364 - }
365 -
366 - return acym_isAllowed('configuration');
367 - }
368 -
369 - $controller = str_replace('front', '', $controller);
370 -
371 - $config = acym_config();
372 - $globalAccess = $config->get('acl_'.$controller, ACYM_ADMIN_GROUP);
373 - if ($globalAccess === 'all') {
374 - return true;
375 - }
376 -
377 - $globalAccess = explode(',', $globalAccess);
378 - $globalAccess[] = ACYM_ADMIN_GROUP;
379 -
380 - $userId = acym_currentUserId();
381 - if (empty($userId)) {
382 - return false;
383 - }
384 -
385 - $userGroups = acym_getGroupsByUser($userId);
386 - if (empty($userGroups)) {
387 - return false;
388 - }
389 -
390 - foreach ($userGroups as $oneGroup) {
391 - if (in_array($oneGroup, $globalAccess)) {
392 - return true;
393 - }
394 - }
395 -
396 - // Having access to campaigns gives access to some email actions
397 - if ($controller === 'mails' && in_array($task, ['autoSave', 'getTemplateAjax']) && acym_isAllowed('campaigns')) {
398 - return true;
399 - }
400 -
401 - // The language management is made in the configuration
402 - if ($controller === 'language' && acym_isAllowed('configuration')) {
403 - return true;
404 - }
405 -
406 - // When editing a campaign or a template, the user might need attachments
407 - if ($controller === 'file' && (acym_isAllowed('campaigns') || acym_isAllowed('mails'))) {
408 - return true;
409 - }
410 -
411 - // When editing a campaign, one can need to get a segment's number of affected users from the segment step
412 - if (in_array($task, ['countResultsTotal', 'countGlobalBySegmentId', 'countResults']) && acym_isAllowed('campaigns')) {
413 - return true;
414 - }
415 -
416 - // When editing an email, lists might be loaded with ajax
417 - if ($controller === 'lists' && $task === 'setAjaxListing' && (acym_isAllowed('campaigns') || acym_isAllowed('mails'))) {
418 - return true;
419 - }
420 -
421 - // The zones are used in the editor
422 - if (
423 - $controller === 'zones'
424 - && (
425 - acym_isAllowed('campaigns')
426 - || acym_isAllowed('mails')
427 - )
428 - ) {
429 - return true;
430 - }
431 -
432 - // Togglable icons are located in most features
433 - if (
434 - $controller === 'toggle'
435 - && (
436 - acym_isAllowed('campaigns')
437 - || acym_isAllowed('mails')
438 - || acym_isAllowed('automation')
439 - || acym_isAllowed('segments')
440 - || acym_isAllowed('scenarios')
441 - || acym_isAllowed('forms')
442 - || acym_isAllowed('users')
443 - || acym_isAllowed('lists')
444 - || acym_isAllowed('fields')
445 - || acym_isAllowed('bounces')
446 - )
447 - ) {
448 - return true;
449 - }
450 -
451 - // The add-ons are used in the editor and the filters
452 - if (
453 - $controller === 'dynamics'
454 - && (
455 - acym_isAllowed('campaigns')
456 - || acym_isAllowed('mails')
457 - || acym_isAllowed('automation')
458 - || acym_isAllowed('segments')
459 - || acym_isAllowed('scenarios')
460 - )
461 - ) {
462 - return true;
463 - }
464 -
465 - // The entity select is used in multiple features
466 - if (
467 - $task === 'loadEntityFront'
468 - && (
469 - acym_isAllowed('campaigns')
470 - || acym_isAllowed('mails')
471 - || acym_isAllowed('users')
472 - || acym_isAllowed('lists')
473 - )
474 - ) {
475 - return true;
476 - }
477 -
478 - return false;
479 -}
480 -
481 -function acym_raiseError(int $code, string $message): void
482 -{
483 - // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- Ending script here, no hook available.
484 - echo '<link type="text/css" rel="stylesheet" href="'.esc_url(ACYM_CSS.'back_global.min.css?v='.filemtime(ACYM_MEDIA.'css'.DS.'back_global.min.css')).'">';
485 - echo '<div id="acym_wrapper">';
486 - acym_display('Error '.$code.': '.$message, 'error', false);
487 - echo '</div>';
488 - http_response_code($code);
489 - exit;
490 -}
491 -
492 -/**
493 - * Check if the license is valid, make this check weekly
494 - */
495 -function acym_isLicenseValidWeekly(): bool
496 -{
497 - $config = acym_config();
498 - $expirationDate = $config->get('expirationdate', 0);
499 - // $expirationDate is empty when no call has been made yet on our server, or when it is a Starter license. Starter licenses don't have access to the cron
500 - if (empty($expirationDate) || (time() - 604800) > $config->get('lastlicensecheck', 0)) {
501 - UpdatemeHelper::getLicenseInfo();
502 - $config = acym_config(true);
503 - $expirationDate = $config->get('expirationdate', 0);
504 - }
505 -
506 - return $expirationDate >= time();
507 -}
508 -
509 -/**
510 - * Generate a time-limited autologin token for a subscriber.
511 - * Token format: hex(timestamp).hmac_signature
512 - * The token is bound to the subscriber ID and their secret key, and signed with the site's auth salt.
513 - */
514 -function acym_generateAutologinToken(int $subId, string $subKey): string
515 -{
516 - $timestamp = time();
517 - $payload = $subId.'|'.$timestamp;
518 - $secret = $subKey.acym_getSiteSalt();
519 - $signature = hash_hmac('sha256', $payload, $secret);
520 -
521 - return dechex($timestamp).'.'.$signature;
522 -}
523 -
524 -/**
525 - * Verify an autologin token. Returns true if the token is valid and not expired.
526 - *
527 - * @param int $subId The subscriber ID
528 - * @param string $token The token from the URL
529 - * @param string $storedKey The subscriber's stored secret key
530 - */
531 -function acym_verifyAutologinToken(int $subId, string $token, string $storedKey): bool
532 -{
533 - $parts = explode('.', $token, 2);
534 - if (count($parts) !== 2) {
535 - return false;
536 - }
537 -
538 - $timestamp = @hexdec($parts[0]);
539 - $signature = $parts[1];
540 -
541 - if (empty($timestamp) || $timestamp <= 0) {
542 - return false;
543 - }
544 -
545 - $config = acym_config();
546 - $maxAgeHours = intval($config->get('autologin_token_duration', 48));
547 - $maxAge = $maxAgeHours * 3600;
548 -
549 - if ((time() - $timestamp) > $maxAge) {
550 - return false;
551 - }
552 -
553 - $payload = $subId.'|'.$timestamp;
554 - $secret = $storedKey.acym_getSiteSalt();
555 - $expectedSignature = hash_hmac('sha256', $payload, $secret);
556 -
557 - return hash_equals($expectedSignature, $signature);
558 -}