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/ExportHelper.php +105 -48 10.0.1 → 11.1.0 View file →
@@ -7,33 +7,38 @@
7 7 use AcyMailing\Core\AcymObject;
8 8
9 9 class ExportHelper extends AcymObject
10 10 {
11 - var $eol = "\r\n";
12 - var $before = '"';
13 - var $after = '"';
11 + private string $eol = "\r\n";
12 + private string $before = '"';
13 + private string $after = '"';
14 14
15 - public function setDownloadHeaders($filename = 'export', $extension = 'csv')
15 + public function setDownloadHeaders(string $filename = 'export', string $extension = 'csv'): void
16 16 {
17 + // Fix for IE catching
17 18 acym_header('Pragma: public');
18 19 acym_header('Expires: 0');
19 20 acym_header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
20 21
22 + // force download dialog
21 23 acym_header('Content-Type: application/force-download');
22 24 acym_header('Content-Type: application/octet-stream');
23 25 acym_header('Content-Type: application/download');
24 26
27 + // Set file name and force the browser to display the save dialog
25 28 acym_header('Content-Disposition: attachment; filename='.$filename.'.'.$extension);
26 29 acym_header('Content-Transfer-Encoding: binary');
27 30 }
28 31
29 - public function exportTemplate($template)
32 + public function exportTemplate(object $template): void
30 33 {
34 + // 1 - Create a folder with the data to export
31 35 $name = preg_replace('#[^a-z0-9]#Uis', '_', $template->name);
32 36 $name = preg_replace('#_+#s', '_', $name);
33 37 $exportFolder = ACYM_ROOT.ACYM_MEDIA_FOLDER.'tmp'.DS.$name;
34 38 acym_createFolder($exportFolder);
35 39
40 + // 2 - Copy the images in the folder, and change the src in the <img> tags
36 41 $template->body = acym_absoluteURL($template->body);
37 42 $images = [];
38 43 preg_match_all('#<img[^>]* src="([^"]+)"#Uis', $template->body, $images);
39 44
@@ -49,15 +54,17 @@
49 54 [ACYM_LIVE, '/'],
50 55 [ACYM_ROOT, DS],
51 56 $oneImage
52 57 );
58 + // Still an http picture? Must be a picture out of the domain... so we just leave it that way
53 59 if (strpos($location, 'http') === 0) continue;
54 60
61 + // We can't find the image? We leave it that way
55 62 if (!file_exists($location)) continue;
56 63
57 64 $filename = basename($location);
58 65 while (file_exists($imagesFolder.DS.$filename)) {
59 - $filename = rand(0, 99).$filename;
66 + $filename = acym_rand(0, 99).$filename;
60 67 }
61 68
62 69 acym_copyFile($location, $imagesFolder.DS.$filename);
63 70 $replace[$oneImage] = 'images/'.$filename;
@@ -65,8 +72,9 @@
65 72
66 73 $template->body = str_replace(array_keys($replace), $replace, $template->body);
67 74 }
68 75
76 + // 3 - Copy the template's content
69 77 $structure = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
70 78 <html xmlns="http://www.w3.org/1999/xhtml">
71 79 <head>
72 80 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />';
@@ -80,9 +88,9 @@
80 88 'settings',
81 89 ];
82 90 foreach ($dataToCopy as $oneData) {
83 91 if (empty($template->$oneData)) continue;
84 - $structure .= "\n".'<meta name="'.$oneData.'" content="'.acym_escape($template->$oneData).'" />';
92 + $structure .= "\n".'<meta name="'.$oneData.'" content="'.esc_attr($template->$oneData).'" />';
85 93 }
86 94
87 95 $structure .= "\n".'<title>'.$template->name.'</title>
88 96 </head>
@@ -92,23 +100,27 @@
92 100 </html>';
93 101
94 102 acym_writeFile($exportFolder.DS.'template.html', $structure);
95 103
104 + // 4 - Copy the thumbnail
96 105 $thumbnail = acym_getMailThumbnail($template->thumbnail);
97 106 $thumbnail = str_replace(ACYM_LIVE, ACYM_ROOT, $thumbnail);
98 107 acym_copyFile($thumbnail, $exportFolder.DS.'thumbnail.png');
99 108
109 + // 5 - Handle custom stylesheet
100 110 if (!empty($template->stylesheet)) {
101 111 acym_createFolder($exportFolder.DS.'css');
102 112 acym_writeFile($exportFolder.DS.'css'.DS.'custom.css', $template->stylesheet);
103 113 }
104 114
115 + // 6 - Create the zip archive
105 116 $zipFiles = [];
106 117 $folders = acym_getFolders($exportFolder, '.', true, true);
107 - array_push($folders, $exportFolder);
118 + $folders[] = $exportFolder;
108 119 foreach ($folders as $folder) {
109 120 $files = acym_getFiles($folder, '.', false, true);
110 121 foreach ($files as $file) {
122 + // Avoid J3 issue on thumbnail and index.php path, last separator was not DS
111 123 $posSlash = strrpos($file, '/');
112 124 $posASlash = strrpos($file, '\\');
113 125 $pos = ($posSlash < $posASlash) ? $posASlash : $posSlash;
114 126 if (!empty($pos)) $file = substr_replace($file, DS, $pos, 1);
@@ -126,11 +138,14 @@
126 138 }
127 139
128 140 acym_createArchive($exportFolder, $zipFiles);
129 141
142 + // 7 - Remove the temporary folder
130 143 acym_deleteFolder($exportFolder);
131 144
145 + // 8 - Download the zip
132 146 $this->setDownloadHeaders($name, 'zip');
147 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Downloading an export file.
133 148 echo acym_fileGetContent($exportFolder.'.zip');
134 149 acym_deleteFile($exportFolder.'.zip');
135 150
136 151 exit;
@@ -135,16 +150,15 @@
135 150
136 151 exit;
137 152 }
138 153
139 - public function exportStatsFormattedCSV($mailName, $globalDonutsData, $globaline, $timeLinechart)
154 + public function exportStatsFormattedCSV(string $mailName, array $globalDonutsData, array $globaline, string $timeLinechart): void
140 155 {
141 - $nbExport = $this->getExportLimit();
142 156 acym_displayErrors();
143 157
144 - if ($timeLinechart == 'month') {
158 + if ($timeLinechart === 'month') {
145 159 $timeLinechart = acym_translation('ACYM_MONTHLY_STATS');
146 - } elseif ($timeLinechart == 'week') {
160 + } elseif ($timeLinechart === 'week') {
147 161 $timeLinechart = acym_translation('ACYM_WEEKLY_STATS');
148 162 } else {
149 163 $timeLinechart = acym_translation('ACYM_DAILY_STATS');
150 164 }
@@ -152,9 +166,8 @@
152 166 $separator = '","';
153 167
154 168 $csvLines = [];
155 169 $csvLines[] = $this->before.$mailName.$this->after;
156 -
157 170 $csvLines[] = $this->eol;
158 171
159 172 $globalDonutsTitle = [
160 173 acym_translation('ACYM_SUCCESSFULLY_SENT'),
@@ -165,33 +178,31 @@
165 178 ];
166 179
167 180 $csvLines[] = $this->before.implode($separator, $globalDonutsTitle).$this->after;
168 181 $csvLines[] = $this->before.implode($separator, $globalDonutsData).$this->after;
169 -
170 182 $csvLines[] = $this->eol;
171 183
172 184 $csvLines[] = $this->before.$timeLinechart.$separator.acym_translation('ACYM_OPEN').$separator.acym_translation('ACYM_CLICK').$this->after;
173 185
174 - $i = 0;
175 186 foreach ($globaline as $date => $value) {
176 - if ($i > $nbExport) break;
177 187 $csvLines[] = $this->before.$date.$separator.$value['open'].$separator.$value['click'].$this->after;
178 - $i++;
179 188 }
180 189
181 190 $this->finishStatsExport($csvLines);
182 191 }
183 192
184 - public function exportStatsFullCSV($query, $columns, $type = 'global')
193 + public function exportStatsFullCSV(string $query, array $columns, string $type = 'global'): void
185 194 {
186 - $mailsStats = acym_loadObjectList($query);
195 + $nbExport = $this->getExportLimit();
196 + $mailsStats = acym_loadObjectList($query.' LIMIT '.intval($nbExport));
187 197 $mailClass = new MailClass();
198 + // We don't want to decode the user's name
188 199 $mailClass->exceptKeysDecode = ['name'];
189 200 $mailsStats = $mailClass->decode($mailsStats);
190 - $nbExport = $this->getExportLimit();
191 201 acym_displayErrors();
192 202
193 - $separator = '","';
203 + $csvSeparator = $this->config->get('csv_separator', ',');
204 + $separator = '"'.$csvSeparator.'"';
194 205 $csvLines = [];
195 206
196 207 $csvLines[] = $this->before.implode($separator, $columns).$this->after;
197 208
@@ -196,19 +207,21 @@
196 207 $csvLines[] = $this->before.implode($separator, $columns).$this->after;
197 208
198 209 $valueNeedNumber = ['click'];
199 210
200 - $i = 0;
201 211 foreach ($mailsStats as $mailStat) {
202 - if ($i > $nbExport) break;
203 212 $oneLine = [];
204 213 foreach ($columns as $key => $trad) {
205 214 $key = strpos($key, '.') !== false ? explode('.', $key) : $key;
206 - if (is_array($key)) $key = $key[1];
215 + if (is_array($key)) {
216 + $key = $key[1];
217 + }
218 +
207 219 $line = in_array($key, $valueNeedNumber) && empty($mailStat->{$key}) ? 0 : $mailStat->{$key};
208 220 if (is_null($line)) {
209 221 $line = '';
210 222 }
223 +
211 224 if ($key === 'bounce_details' && !empty($line)) {
212 225 $line = @unserialize($line);
213 226 if ($line !== false) {
214 227 $formattedLine = '';
@@ -217,27 +230,33 @@
217 230 }
218 231 $line = rtrim($formattedLine, '; ');
219 232 }
220 233 }
234 +
235 + if (in_array($key, ['send_date', 'open_date', 'date_click']) && !empty($line)) {
236 + $line = acym_date(acym_getTimeFromUTCDate($line), 'Y-m-d H:i:s');
237 + }
238 +
221 239 $oneLine[] = htmlspecialchars($line, ENT_QUOTES, 'UTF-8');
222 240 }
241 + //We delete the double quote so it won't break the CSV
223 242 $csvLines[] = $this->before.implode($separator, $oneLine).$this->after;
224 - $i++;
225 243 }
226 244
227 245 $this->finishStatsExport($csvLines, $type);
228 246 }
229 247
230 - private function finishStatsExport($csvLines, $type = 'global')
248 + private function finishStatsExport(array $csvLines, string $type = 'global'): void
231 249 {
232 250 $final = implode($this->eol, $csvLines);
233 251
234 - if (ACYM_CMS === 'wordpress') @ob_get_clean();
235 - $filename = 'export_stats_'.$type.'_'.date('Y-m-d');
252 + if (ACYM_CMS === 'wordpress') {
253 + @ob_get_clean();
254 + }
255 + $filename = 'export_stats_'.$type.'_'.gmdate('Y-m-d');
236 256 $this->setDownloadHeaders($filename);
257 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
237 258 echo $final;
238 -
239 - return '';
240 259 }
241 260
242 261 private function getLists(): array
243 262 {
@@ -249,10 +268,18 @@
249 268
250 269 return $lists;
251 270 }
252 271
253 - public function exportCSV($query, $fieldsToExport, $customFieldsToExport, $specialFieldsToExport, $separator = ',', $charset = 'UTF-8', $exportFile = null, $flagToRemove = 0)
254 - {
272 + public function exportCSV(
273 + string $query,
274 + array $fieldsToExport,
275 + array $customFieldsToExport,
276 + array $specialFieldsToExport,
277 + string $separator = ',',
278 + string $charset = 'UTF-8',
279 + string $exportFile = '',
280 + int $flagToRemove = 0
281 + ): string {
255 282 $nbExport = $this->getExportLimit();
256 283 acym_displayErrors();
257 284 $encodingClass = new EncodingHelper();
258 285 $excelSecure = $this->config->get('export_excelsecurity', 0);
@@ -279,19 +306,23 @@
279 306 $allFieldsToExport[] = acym_translation('ACYM_UNSUBSCRIPTION_DATE').' '.acym_translation($oneList);
280 307 }
281 308 }
282 309
310 + // This first line shows the column headers ("name", "email", etc...)
283 311 $firstLine = $this->before.implode($separator, $allFieldsToExport).$this->after.$this->eol;
284 312
285 313 if (empty($exportFile)) {
286 314 @ob_get_clean();
287 - $filename = 'export_'.date('Y-m-d');
315 + $filename = 'export_'.gmdate('Y-m-d');
288 316 $this->setDownloadHeaders($filename);
317 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
289 318 echo $firstLine;
290 319 } else {
320 + // Make sure the folder exists
291 321 preg_match('#^(.+/)[^/]+$#', $exportFile, $folder);
292 322 if (!empty($folder[1]) && !file_exists($folder[1])) acym_createDir($folder[1]);
293 323
324 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Using buffer for big exports.
294 325 $fp = fopen($exportFile, 'w');
295 326 if (false === $fp) {
296 327 if ($flagToRemove !== 0) {
297 328 $automationHelper = new AutomationHelper();
@@ -300,8 +331,9 @@
300 331
301 332 return acym_translationSprintf('ACYM_FAIL_SAVE_FILE', $exportFile);
302 333 }
303 334
335 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
304 336 $error = fwrite($fp, $firstLine);
305 337 if (false === $error) {
306 338 if ($flagToRemove !== 0) {
307 339 $automationHelper = new AutomationHelper();
@@ -321,13 +353,15 @@
321 353 }
322 354 $start += $nbExport;
323 355
324 356 if ($users === false) {
325 - $errorLine = $this->eol.$this->eol.'Error: '.(isset($e) ? $e->getMessage() : acym_getDBError());
357 + $errorLine = $this->eol.$this->eol.'Error: '.esc_html(isset($e) ? $e->getMessage() : acym_getDBError());
326 358
327 359 if (empty($exportFile)) {
360 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
328 361 echo $errorLine;
329 362 } else {
363 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
330 364 $error = fwrite($fp, $errorLine);
331 365 if (false === $error) {
332 366 if ($flagToRemove !== 0) {
333 367 $automationHelper = new AutomationHelper();
@@ -338,8 +372,9 @@
338 372 }
339 373 }
340 374 }
341 375
376 + // There is no other user to export, end here
342 377 if (empty($users)) break;
343 378
344 379 foreach ($users as $userID => $oneUser) {
345 380 unset($oneUser->id);
@@ -398,8 +433,9 @@
398 433 if (is_null($oneData)) {
399 434 continue;
400 435 }
401 436
437 + // Not using empty() because 0 and '0' are valid values
402 438 if ($excelSecure == 1) {
403 439 $firstCharacter = substr($oneData, 0, 1);
404 440 if (in_array($firstCharacter, ['=', '+', '-', '@'])) {
405 441 $oneData = ' '.$oneData;
@@ -413,10 +449,12 @@
413 449 unset($data);
414 450
415 451 $oneLine = $this->before.$encodingClass->change($dataexport, 'UTF-8', $charset).$this->after.$this->eol;
416 452 if (empty($exportFile)) {
453 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
417 454 echo $oneLine;
418 455 } else {
456 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
419 457 $error = fwrite($fp, $oneLine);
420 458 if (false === $error) {
421 459 if ($flagToRemove !== 0) {
422 460 $automationHelper = new AutomationHelper();
@@ -435,17 +473,23 @@
435 473 $automationHelper = new AutomationHelper();
436 474 $automationHelper->removeFlag($flagToRemove);
437 475 }
438 476
439 - if (!empty($exportFile)) fclose($fp);
477 + if (!empty($exportFile)) {
478 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Using buffer for big exports.
479 + fclose($fp);
480 + }
440 481
441 482 return '';
442 483 }
443 484
444 - private function getExportLimit()
485 + private function getExportLimit(): int
445 486 {
487 + // Getting X users per batch based on the memory limit
446 488 $serverLimit = acym_bytes(ini_get('memory_limit'));
447 - if ($serverLimit > 150000000) {
489 + if ($serverLimit > 500000000) {
490 + return 250000;
491 + } elseif ($serverLimit > 150000000) {
448 492 return 50000;
449 493 } elseif ($serverLimit > 80000000) {
450 494 return 15000;
451 495 } else {
@@ -452,18 +496,20 @@
452 496 return 5000;
453 497 }
454 498 }
455 499
456 - public function getExportChangesFileName($year, $month, $extension = true)
500 + public function getExportChangesFileName(string $year, string $month, bool $extension = true): string
457 501 {
458 502 $filename = 'export_changes_'.$year.'_'.$month;
459 503
460 - if ($extension) $filename .= '.csv';
504 + if ($extension) {
505 + $filename .= '.csv';
506 + }
461 507
462 508 return $filename;
463 509 }
464 510
465 - public function getExportChangesFilePath()
511 + public function getExportChangesFilePath(): string
466 512 {
467 513 $filename = $this->getExportChangesFileName(acym_date('now', 'Y'), acym_date('now', 'm'));
468 514
469 515 return acym_getLogPath($filename);
@@ -468,9 +514,9 @@
468 514
469 515 return acym_getLogPath($filename);
470 516 }
471 517
472 - public function generateExportChangesFilePathConfigChanges()
518 + public function generateExportChangesFilePathConfigChanges(): string
473 519 {
474 520 $filename = $this->getExportChangesFileName(acym_date('now', 'Y'), acym_date('now', 'm'), false);
475 521
476 522 $i = 1;
@@ -484,11 +530,13 @@
484 530
485 531 return acym_getLogPath($newFilename);
486 532 }
487 533
488 - public function exportChanges($user, $fieldsToExport, $fieldName, $newValue, $oldValue)
534 + public function exportChanges(array $user, array $fieldsToExport, string $fieldName, $newValue, $oldValue): void
489 535 {
490 - if (empty($user) || empty($fieldName)) return;
536 + if (empty($user) || empty($fieldName)) {
537 + return;
538 + }
491 539
492 540 $exportFullPath = $this->getExportChangesFilePath();
493 541
494 542 $separator = '","';
@@ -537,17 +585,26 @@
537 585 FILE_APPEND
538 586 );
539 587 }
540 588
541 - public function cleanExportChangesFile()
589 + public function cleanExportChangesFile(): void
542 590 {
543 - $exportFolder = acym_getLogPath('');
591 + $exportFolder = acym_getLogPath();
592 + if (!file_exists($exportFolder)) {
593 + return;
594 + }
595 +
544 596 $files = scandir($exportFolder);
545 - if (empty($files)) return;
597 + if (empty($files)) {
598 + return;
599 + }
546 600
547 601 $filenameToSearch = $this->getExportChangesFileName(acym_date('2 month ago', 'Y'), acym_date('2 month ago', 'm'), false);
548 602 foreach ($files as $file) {
549 - if (strpos($file, $filenameToSearch) === false) continue;
550 - @unlink(acym_getLogPath($file));
603 + if (strpos($file, $filenameToSearch) === false) {
604 + continue;
605 + }
606 +
607 + acym_deleteFile(acym_getLogPath($file));
551 608 }
552 609 }
553 610 }