PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
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/file.php +139 -168 10.10.2 → 11.1.0 View file →
@@ -1,8 +1,12 @@
1 1 <?php
2 +defined('ABSPATH') || die('Restricted Access');
2 3
3 4 use AcyMailing\Helpers\ImageHelper;
4 5
6 +/**
7 + * Function to return the number of bytes of a val like 2M
8 + */
5 9 function acym_bytes(string $val): int
6 10 {
7 11 $val = trim($val);
8 12 if (empty($val)) {
@@ -23,8 +27,9 @@
23 27
24 28 return (int)$val;
25 29 }
26 30
31 +//Create a dir and an index.html file inside
27 32 function acym_createDir(string $dir, bool $report = true, bool $secured = false): bool
28 33 {
29 34 if (is_dir($dir)) {
30 35 return true;
@@ -31,8 +36,9 @@
31 36 }
32 37
33 38 $indexhtml = '<html><body bgcolor="#FFFFFF"></body></html>';
34 39
40 + //Create the directory with an index file inside
35 41 try {
36 42 $status = acym_createFolder($dir);
37 43 } catch (Exception $e) {
38 44 $status = false;
@@ -71,11 +77,18 @@
71 77
72 78 return $status;
73 79 }
74 80
81 +function acym_isForbiddenFileExtension(string $filename): bool
82 +{
83 + // Always blocked regardless of the "allowed files" configuration, for security reasons
84 + return (bool)preg_match('#\.(php.?|.?htm.?|pl|py|jsp|asp|sh|cgi)#Ui', $filename);
85 +}
86 +
75 87 function acym_importFile(array $file, string $uploadPath, bool $onlyPict): ?string
76 88 {
77 - acym_checkToken();
89 + // Check the token... no import without token!
90 + wp_verify_nonce(acym_getVar('cmd', '_wpnonce'), 'acymnonce') || die('Invalid Token');
78 91
79 92 $config = acym_config();
80 93 $additionalMsg = '';
81 94
@@ -102,11 +115,11 @@
102 115 }
103 116
104 117 acym_createDir($uploadPath, true);
105 118
106 - if (!is_writable($uploadPath)) {
107 - @chmod($uploadPath, '0755');
108 - if (!is_writable($uploadPath)) {
119 + if (!acym_isWritable($uploadPath)) {
120 + acym_chmod($uploadPath, 0755);
121 + if (!acym_isWritable($uploadPath)) {
109 122 acym_display(acym_translationSprintf('ACYM_WRITABLE_FOLDER', $uploadPath), 'error');
110 123
111 124 return null;
112 125 }
@@ -117,14 +130,15 @@
117 130 } else {
118 131 $allowedExtensions = explode(',', $config->get('allowed_files'));
119 132 }
120 133
134 + // We don't allow to upload anything else than a picture extension
121 135 if (!preg_match('#\.('.implode('|', $allowedExtensions).')$#Ui', $file['name'], $extension)) {
122 136 $ext = substr($file['name'], strrpos($file['name'], '.') + 1);
123 137 acym_display(
124 138 acym_translationSprintf(
125 139 'ACYM_ACCEPTED_TYPE',
126 - acym_escape($ext),
140 + esc_html($ext),
127 141 implode(', ', $allowedExtensions)
128 142 ),
129 143 'error'
130 144 );
@@ -131,9 +145,11 @@
131 145
132 146 return null;
133 147 }
134 148
135 - if (preg_match('#\.(php.?|.?htm.?|pl|py|jsp|asp|sh|cgi)#Ui', $file['name'])) {
149 + // We will never allow some files to be uploaded...
150 + // This should never happen... only if there is an hack tentative so no need to translate this error message
151 + if (acym_isForbiddenFileExtension($file['name'])) {
136 152 acym_display(
137 153 'This extension name is blocked by the system regardless your configuration for security reasons',
138 154 'error'
139 155 );
@@ -140,8 +156,9 @@
140 156
141 157 return null;
142 158 }
143 159
160 + // We remove all dots or space from the file name to avoid the double extension security issue and the fact some mail clients don't like spaces
144 161 $file['name'] = str_replace(
145 162 ['-', '.', ' ', '\''],
146 163 '_',
147 164 substr($file['name'], 0, strrpos($file['name'], '.'))
@@ -147,8 +164,9 @@
147 164 substr($file['name'], 0, strrpos($file['name'], '.'))
148 165 ).'.'.$extension[1];
149 166
150 167 if ($onlyPict) {
168 + // Extra security: we make sure we only have a valid picture format
151 169 $imageSize = @getimagesize($file['tmp_name']);
152 170 if (empty($imageSize)) {
153 171 acym_display(acym_translation('ACYM_INVALID_IMAGE'), 'error');
154 172
@@ -173,20 +191,18 @@
173 191 }
174 192 }
175 193
176 194 if (!acym_uploadFile($file['tmp_name'], rtrim($uploadPath, DS).DS.$file['name'])) {
177 - if (!move_uploaded_file($file['tmp_name'], rtrim($uploadPath, DS).DS.$file['name'])) {
178 - acym_display(
179 - acym_translationSprintf(
180 - 'ACYM_FAIL_UPLOAD',
181 - '<b><i>'.acym_escape($file['tmp_name']).'</i></b>',
182 - '<b><i>'.acym_escape(rtrim($uploadPath, DS).DS.$file['name']).'</i></b>'
183 - ),
184 - 'error'
185 - );
195 + acym_display(
196 + acym_translationSprintf(
197 + 'ACYM_FAIL_UPLOAD',
198 + '<b><i>'.esc_html($file['tmp_name']).'</i></b>',
199 + '<b><i>'.esc_html(rtrim($uploadPath, DS).DS.$file['name']).'</i></b>'
200 + ),
201 + 'error'
202 + );
186 203
187 - return null;
188 - }
204 + return null;
189 205 }
190 206
191 207 if ($onlyPict && $imageSize[0] > 1000) {
192 208 $imageHelper = new ImageHelper();
@@ -206,31 +222,48 @@
206 222
207 223 return $file['name'];
208 224 }
209 225
210 -function acym_inputFile(string $name, string $value = '', string $class = '', string $attributes = '', string $downloadUrl = ''): string
211 -{
226 +function acym_inputFile(
227 + string $name,
228 + string $value = '',
229 + string $class = '',
230 + array $attributes = [],
231 + string $downloadUrl = ''
232 +): void {
212 233 $hasValue = !empty($value);
213 - $actionStyle = $hasValue ? '' : ' style="display: none;"';
214 234 $noFileText = acym_translation('ACYM_NO_FILE_CHOSEN');
215 235 $baseName = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name;
236 + ?>
216 237
217 - $downloadAttr = !empty($downloadUrl) ? ' data-download-url="'.acym_escapeUrl($downloadUrl).'"' : '';
238 + <div class="cell <?php echo esc_attr($class); ?> grid-x acym__input__file__container">
239 + <input type="hidden" name="<?php echo esc_attr($baseName); ?>" value="<?php echo esc_attr($value); ?>" class="acym__input__file__clear">
240 + <input <?php
241 + foreach ($attributes as $oneAttribute => $oneValue) {
242 + if (is_array($oneValue) || is_object($oneValue)) {
243 + $oneValue = json_encode($oneValue);
244 + } elseif ($oneValue === true) {
245 + $oneValue = $oneAttribute;
246 + }
218 247
219 - $return = '</label><div class="cell '.acym_escape($class).' grid-x acym__input__file__container">';
220 - $return .= '<input type="hidden" name="'.acym_escape($baseName).'" value="'.acym_escape($value).'" class="acym__input__file__clear">';
221 - $return .= '<input '.$attributes.' style="display: none" type="file" name="'.acym_escape($name).'">';
222 - $return .= '<button type="button" class="acym__button__file button button-secondary cell shrink">'.acym_translation('ACYM_CHOOSE_FILE').'</button>';
223 - $return .= '<span class="cell shrink margin-left-2 margin-right-2 acym__input__file__name" data-no-file="'.acym_escape($noFileText).'">';
224 - $return .= acym_escape($hasValue ? $value : $noFileText);
225 - $return .= '</span>';
226 - $return .= '<i class="acym__input__file__download cell shrink acym__color__blue cursor-pointer acymicon-download margin-right-1"'.$actionStyle.$downloadAttr.
227 - ' title="'.acym_escape(acym_translation('ACYM_DOWNLOAD')).'" aria-label="'.acym_escape(acym_translation('ACYM_DOWNLOAD')).'"></i>';
228 - $return .= '<i class="acymicon-close acym__color__red acym__input__file__delete cursor-pointer cell shrink margin-left-1"'.$actionStyle.
229 - ' title="'.acym_escape(acym_translation('ACYM_DELETE')).'" aria-label="'.acym_escape(acym_translation('ACYM_DELETE')).'"></i>';
230 - $return .= '</div>';
231 -
232 - return $return;
248 + echo ' '.esc_attr($oneAttribute).'="'.esc_attr($oneValue).'"';
249 + }
250 + ?> style="display: none" type="file" name="<?php echo esc_attr($name); ?>">
251 + <button type="button" class="acym__button__file button button-secondary cell shrink">
252 + <?php echo esc_html(acym_translation('ACYM_CHOOSE_FILE')); ?>
253 + </button>
254 + <span class="cell shrink margin-left-2 margin-right-2 acym__input__file__name" data-no-file="<?php echo esc_attr($noFileText); ?>">
255 + <?php echo esc_html($hasValue ? $value : $noFileText); ?>
256 + </span>
257 + <i class="acym__input__file__download cell shrink acym__color__blue cursor-pointer acymicon-download margin-right-1"
258 + <?php echo $hasValue ? '' : ' style="display: none;"'; ?>
259 + <?php echo !empty($downloadUrl) ? ' data-download-url="'.esc_url($downloadUrl).'"' : ''; ?>
260 + title="<?php echo esc_attr(acym_translation('ACYM_DOWNLOAD')); ?>" aria-label="<?php echo esc_attr(acym_translation('ACYM_DOWNLOAD')); ?>"></i>
261 + <i class="acymicon-close acym__color__red acym__input__file__delete cursor-pointer cell shrink margin-left-1"
262 + <?php echo $hasValue ? '' : ' style="display: none;"'; ?>
263 + title="<?php echo esc_attr(acym_translation('ACYM_DELETE')); ?>" aria-label="<?php echo esc_attr(acym_translation('ACYM_DELETE')); ?>"></i>
264 + </div>
265 + <?php
233 266 }
234 267
235 268 function acym_getFilesFolder(bool $ignoreVariables = false): string
236 269 {
@@ -241,17 +274,22 @@
241 274 }
242 275 $uploadFolder = trim($uploadFolder, '/');
243 276
244 277 $uploadFolder = str_replace('{userid}', acym_currentUserId(), $uploadFolder);
245 - $uploadFolder = str_replace('{year}', date('Y'), $uploadFolder);
246 - $uploadFolder = str_replace('{month}', date('m'), $uploadFolder);
247 - $uploadFolder = str_replace('{day}', date('d'), $uploadFolder);
278 + $uploadFolder = str_replace('{year}', gmdate('Y'), $uploadFolder);
279 + $uploadFolder = str_replace('{month}', gmdate('m'), $uploadFolder);
280 + $uploadFolder = str_replace('{day}', gmdate('d'), $uploadFolder);
248 281
249 282 return acym_replaceGroupTags($uploadFolder);
250 283 }
251 284
285 +/**
286 + * Find all sub folders...
287 + * Return an array of all folders ordered properly
288 + */
252 289 function acym_generateArborescence(array $folders): array
253 290 {
291 + //Recursive algorythm to catch subfolders
254 292 $folderList = [];
255 293 foreach ($folders as $folder) {
256 294 $folderPath = acym_cleanPath(ACYM_ROOT.trim(str_replace('/', DS, trim($folder)), DS));
257 295 if (!file_exists($folderPath)) {
@@ -277,58 +315,8 @@
277 315
278 316 return trim(preg_replace($regex, '', $file));
279 317 }
280 318
281 -function acym_deleteFolder(string $path, bool $report = true): bool
282 -{
283 - $path = acym_cleanPath($path);
284 - if (!is_dir($path)) {
285 - if ($report) acym_enqueueMessage(acym_translationSprintf('ACYM_IS_NOT_A_FOLDER', $path), 'error');
286 -
287 - return false;
288 - }
289 -
290 - $files = acym_getFiles($path, '.', false, false, [], []);
291 - if (!empty($files)) {
292 - foreach ($files as $oneFile) {
293 - if (!acym_deleteFile($path.DS.$oneFile, $report)) {
294 - return false;
295 - }
296 - }
297 - }
298 -
299 - $folders = acym_getFolders($path, '.', false, false, []);
300 - if (!empty($folders)) {
301 - foreach ($folders as $oneFolder) {
302 - if (!acym_deleteFolder($path.DS.$oneFolder, $report)) {
303 - return false;
304 - }
305 - }
306 - }
307 -
308 - if (@rmdir($path)) {
309 - return true;
310 - } else {
311 - if ($report) acym_enqueueMessage(acym_translationSprintf('ACYM_COULD_NOT_DELETE_FOLDER', $path), 'error');
312 -
313 - return false;
314 - }
315 -}
316 -
317 -function acym_createFolder(string $path = '', int $mode = 0755): bool
318 -{
319 - $path = acym_cleanPath($path);
320 - if (file_exists($path)) {
321 - return true;
322 - }
323 -
324 - $origmask = @umask(0);
325 - $ret = @mkdir($path, $mode, true);
326 - @umask($origmask);
327 -
328 - return $ret;
329 -}
330 -
331 319 function acym_getFolders(
332 320 string $path,
333 321 string $filter = '.',
334 322 bool $recurse = false,
@@ -540,29 +528,11 @@
540 528
541 529 return $dirs;
542 530 }
543 531
544 -function acym_deleteFile(string $file, bool $report = true): bool
545 -{
546 - $file = acym_cleanPath($file);
547 - if (!is_file($file)) {
548 - if ($report) acym_enqueueMessage(acym_translationSprintf('ACYM_IS_NOT_A_FILE', $file), 'error');
549 -
550 - return false;
551 - }
552 -
553 - @chmod($file, 0777);
554 -
555 - if (!@unlink($file)) {
556 - $filename = basename($file);
557 - if ($report) acym_enqueueMessage(acym_translationSprintf('ACYM_FAILED_DELETE', $filename), 'error');
558 -
559 - return false;
560 - }
561 -
562 - return true;
563 -}
564 -
532 +/**
533 + * @param mixed $buffer
534 + */
565 535 function acym_writeFile(string $file, $buffer, int $flags = 0): bool
566 536 {
567 537 if (!file_exists(dirname($file)) && !acym_createFolder(dirname($file))) {
568 538 return false;
@@ -572,52 +542,8 @@
572 542
573 543 return is_int(file_put_contents($file, $buffer, $flags));
574 544 }
575 545
576 -function acym_moveFile(string $src, string $dest, string $path = ''): bool
577 -{
578 - if (!empty($path)) {
579 - $src = acym_cleanPath($path.'/'.$src);
580 - $dest = acym_cleanPath($path.'/'.$dest);
581 - }
582 -
583 - if (!is_readable($src)) {
584 - acym_enqueueMessage(acym_translationSprintf('ACYM_COULD_NOT_FIND_FILE_SOURCE_PERMISSION', $src), 'error');
585 -
586 - return false;
587 - }
588 -
589 - if (!@rename($src, $dest)) {
590 - acym_enqueueMessage(acym_translation('ACYM_COULD_NOT_MOVE_FILE'), 'error');
591 -
592 - return false;
593 - }
594 -
595 - return true;
596 -}
597 -
598 -function acym_uploadFile(string $src, string $dest): bool
599 -{
600 - $dest = acym_cleanPath($dest);
601 -
602 - $baseDir = dirname($dest);
603 - if (!file_exists($baseDir)) {
604 - acym_createFolder($baseDir);
605 - }
606 -
607 - if (is_writeable($baseDir) && move_uploaded_file($src, $dest)) {
608 - if (@chmod($dest, octdec('0644'))) {
609 - return true;
610 - } else {
611 - acym_enqueueMessage(acym_translation('ACYM_FILE_REJECTED_SAFETY_REASON'), 'error');
612 - }
613 - } else {
614 - acym_enqueueMessage(acym_translationSprintf('ACYM_COULD_NOT_UPLOAD_FILE_PERMISSION', $baseDir), 'error');
615 - }
616 -
617 - return false;
618 -}
619 -
620 546 function acym_copyFile(string $src, string $dest, string $path = ''): bool
621 547 {
622 548 if (!empty($path)) {
623 549 $src = acym_cleanPath($path.DS.$src);
@@ -711,24 +637,23 @@
711 637
712 638 return acym_writeFile($name.'.zip', $buffer);
713 639 }
714 640
715 -function acym_loaderLogo(bool $wrap = true): string
641 +function acym_loaderLogo(bool $withContainer = true): void
716 642 {
717 - $loader = '';
718 - if ($wrap) {
719 - $loader .= '<div class="cell shrink acym_loader_logo">';
643 + if ($withContainer) {
644 + echo '<div class="cell shrink acym_loader_logo">';
720 645 }
721 -
722 - $loader .= '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 1024 1024">
723 - <path class="acym__svg__loader" fill="#a6a9ab" d="M553.074 174.168c-12.201 7.319-26.84 10.573-40.668 10.573s-27.655-3.253-40.668-10.573l-242.376-139.081-229.362 132.575v732.011l254.576 143.963v-430.26l219.602 124.442c11.388 6.507 24.401 9.76 37.415 9.76 0 0 0 0 0 0s0 0 0 0c12.201 0 25.214-3.253 36.6-9.76l221.23-124.442v430.26l254.576-144.775v-732.011l-229.362-131.762-241.563 139.081zM491.261 701.215l-217.164-122.815-61.001-34.161v430.26l-173.243-97.601v-662.063l422.94 245.629c0 0 0 0 0 0 8.947 4.881 17.895 8.135 27.655 10.573v230.178zM983.334 876.086l-173.243 98.416v-431.073l-61.001 34.161-217.164 122.815v-229.362c9.76-2.441 18.707-5.694 27.655-10.573l423.753-246.444v662.063zM539.246 425.493c-17.080 9.76-38.227 9.76-55.307 0 0 0 0 0 0 0l-422.94-245.629 168.362-97.601 222.043 127.696c18.707 10.573 39.853 16.267 61.001 16.267s42.294-5.694 61.001-16.267l222.043-127.696 168.362 97.601-424.566 245.629z"></path>
724 - </svg>';
725 -
726 - if ($wrap) {
727 - $loader .= '</div>';
646 + ?>
647 + <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 1024 1024">
648 + <path class="acym__svg__loader"
649 + fill="#a6a9ab"
650 + d="M553.074 174.168c-12.201 7.319-26.84 10.573-40.668 10.573s-27.655-3.253-40.668-10.573l-242.376-139.081-229.362 132.575v732.011l254.576 143.963v-430.26l219.602 124.442c11.388 6.507 24.401 9.76 37.415 9.76 0 0 0 0 0 0s0 0 0 0c12.201 0 25.214-3.253 36.6-9.76l221.23-124.442v430.26l254.576-144.775v-732.011l-229.362-131.762-241.563 139.081zM491.261 701.215l-217.164-122.815-61.001-34.161v430.26l-173.243-97.601v-662.063l422.94 245.629c0 0 0 0 0 0 8.947 4.881 17.895 8.135 27.655 10.573v230.178zM983.334 876.086l-173.243 98.416v-431.073l-61.001 34.161-217.164 122.815v-229.362c9.76-2.441 18.707-5.694 27.655-10.573l423.753-246.444v662.063zM539.246 425.493c-17.080 9.76-38.227 9.76-55.307 0 0 0 0 0 0 0l-422.94-245.629 168.362-97.601 222.043 127.696c18.707 10.573 39.853 16.267 61.001 16.267s42.294-5.694 61.001-16.267l222.043-127.696 168.362 97.601-424.566 245.629z"></path>
651 + </svg>
652 + <?php
653 + if ($withContainer) {
654 + echo '</div>';
728 655 }
729 -
730 - return $loader;
731 656 }
732 657
733 658 function acym_fileNameValid(string $filename): bool
734 659 {
@@ -738,8 +663,54 @@
738 663
739 664 return true;
740 665 }
741 666
742 -function acym_getImageFileExtensions(): array
667 +function acym_isPathWithinDirectory(string $baseDir, string $path): bool
743 668 {
744 - return ['jpg', 'jpeg', 'png', 'gif', 'ico', 'bmp', 'svg'];
669 + $baseDir = rtrim($baseDir, DS).DS;
670 +
671 + if (strpos($path, $baseDir) !== 0) return false;
672 +
673 + return preg_match('#(^|[/\\\\])\.\.([/\\\\]|$)#', substr($path, strlen($baseDir))) !== 1;
674 +}
675 +
676 +function acym_getImageFileExtensions(bool $allowSvg = false): array
677 +{
678 + $imageFormats = ['jpg', 'jpeg', 'png', 'gif', 'ico', 'bmp'];
679 + if ($allowSvg) {
680 + $imageFormats[] = 'svg';
681 + }
682 +
683 + return $imageFormats;
684 +}
685 +
686 +function acym_isSvgFileSafe(string $path): bool
687 +{
688 + if (!is_file($path)) {
689 + return false;
690 + }
691 +
692 + $content = @file_get_contents($path);
693 + if ($content === false || trim($content) === '') {
694 + return false;
695 + }
696 +
697 + // Decode HTML entities so obfuscated payloads (e.g. "javascript&#58;") are caught, and drop NULL bytes.
698 + $content = str_replace("\0", '', html_entity_decode($content, ENT_QUOTES | ENT_HTML5));
699 +
700 + $dangerousPatterns = [
701 + '#<\s*script#i',
702 + '#<\s*(foreignObject|iframe|embed|object|audio|video|handler|listener)#i',
703 + '#\son[a-z]+\s*=#i',
704 + '#javascript\s*:#i',
705 + '#data\s*:\s*text/html#i',
706 + '#<!\s*(DOCTYPE|ENTITY)#i',
707 + ];
708 +
709 + foreach ($dangerousPatterns as $pattern) {
710 + if (preg_match($pattern, $content)) {
711 + return false;
712 + }
713 + }
714 +
715 + return true;
745 716 }