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 +62 -22 10.1.4 → 11.1.0 View file →
@@ -13,16 +13,19 @@
13 13 private string $after = '"';
14 14
15 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
@@ -27,13 +30,15 @@
27 30 }
28 31
29 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;
@@ -137,9 +152,8 @@
137 152 }
138 153
139 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 158 if ($timeLinechart === 'month') {
145 159 $timeLinechart = acym_translation('ACYM_MONTHLY_STATS');
@@ -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,18 +178,14 @@
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 }
@@ -182,16 +191,18 @@
182 191 }
183 192
184 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,12 +230,17 @@
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 }
@@ -233,10 +251,11 @@
233 251
234 252 if (ACYM_CMS === 'wordpress') {
235 253 @ob_get_clean();
236 254 }
237 - $filename = 'export_stats_'.$type.'_'.date('Y-m-d');
255 + $filename = 'export_stats_'.$type.'_'.gmdate('Y-m-d');
238 256 $this->setDownloadHeaders($filename);
257 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
239 258 echo $final;
240 259 }
241 260
242 261 private function getLists(): array
@@ -287,19 +306,23 @@
287 306 $allFieldsToExport[] = acym_translation('ACYM_UNSUBSCRIPTION_DATE').' '.acym_translation($oneList);
288 307 }
289 308 }
290 309
310 + // This first line shows the column headers ("name", "email", etc...)
291 311 $firstLine = $this->before.implode($separator, $allFieldsToExport).$this->after.$this->eol;
292 312
293 313 if (empty($exportFile)) {
294 314 @ob_get_clean();
295 - $filename = 'export_'.date('Y-m-d');
315 + $filename = 'export_'.gmdate('Y-m-d');
296 316 $this->setDownloadHeaders($filename);
317 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
297 318 echo $firstLine;
298 319 } else {
320 + // Make sure the folder exists
299 321 preg_match('#^(.+/)[^/]+$#', $exportFile, $folder);
300 322 if (!empty($folder[1]) && !file_exists($folder[1])) acym_createDir($folder[1]);
301 323
324 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Using buffer for big exports.
302 325 $fp = fopen($exportFile, 'w');
303 326 if (false === $fp) {
304 327 if ($flagToRemove !== 0) {
305 328 $automationHelper = new AutomationHelper();
@@ -308,8 +331,9 @@
308 331
309 332 return acym_translationSprintf('ACYM_FAIL_SAVE_FILE', $exportFile);
310 333 }
311 334
335 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
312 336 $error = fwrite($fp, $firstLine);
313 337 if (false === $error) {
314 338 if ($flagToRemove !== 0) {
315 339 $automationHelper = new AutomationHelper();
@@ -329,13 +353,15 @@
329 353 }
330 354 $start += $nbExport;
331 355
332 356 if ($users === false) {
333 - $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());
334 358
335 359 if (empty($exportFile)) {
360 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
336 361 echo $errorLine;
337 362 } else {
363 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
338 364 $error = fwrite($fp, $errorLine);
339 365 if (false === $error) {
340 366 if ($flagToRemove !== 0) {
341 367 $automationHelper = new AutomationHelper();
@@ -346,8 +372,9 @@
346 372 }
347 373 }
348 374 }
349 375
376 + // There is no other user to export, end here
350 377 if (empty($users)) break;
351 378
352 379 foreach ($users as $userID => $oneUser) {
353 380 unset($oneUser->id);
@@ -406,8 +433,9 @@
406 433 if (is_null($oneData)) {
407 434 continue;
408 435 }
409 436
437 + // Not using empty() because 0 and '0' are valid values
410 438 if ($excelSecure == 1) {
411 439 $firstCharacter = substr($oneData, 0, 1);
412 440 if (in_array($firstCharacter, ['=', '+', '-', '@'])) {
413 441 $oneData = ' '.$oneData;
@@ -421,10 +449,12 @@
421 449 unset($data);
422 450
423 451 $oneLine = $this->before.$encodingClass->change($dataexport, 'UTF-8', $charset).$this->after.$this->eol;
424 452 if (empty($exportFile)) {
453 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Exporting CSV data.
425 454 echo $oneLine;
426 455 } else {
456 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Using buffer for big exports.
427 457 $error = fwrite($fp, $oneLine);
428 458 if (false === $error) {
429 459 if ($flagToRemove !== 0) {
430 460 $automationHelper = new AutomationHelper();
@@ -443,9 +473,12 @@
443 473 $automationHelper = new AutomationHelper();
444 474 $automationHelper->removeFlag($flagToRemove);
445 475 }
446 476
447 - 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 + }
448 481
449 482 return '';
450 483 }
451 484
@@ -450,10 +483,13 @@
450 483 }
451 484
452 485 private function getExportLimit(): int
453 486 {
487 + // Getting X users per batch based on the memory limit
454 488 $serverLimit = acym_bytes(ini_get('memory_limit'));
455 - if ($serverLimit > 150000000) {
489 + if ($serverLimit > 500000000) {
490 + return 250000;
491 + } elseif ($serverLimit > 150000000) {
456 492 return 50000;
457 493 } elseif ($serverLimit > 80000000) {
458 494 return 15000;
459 495 } else {
@@ -552,8 +588,12 @@
552 588
553 589 public function cleanExportChangesFile(): void
554 590 {
555 591 $exportFolder = acym_getLogPath();
592 + if (!file_exists($exportFolder)) {
593 + return;
594 + }
595 +
556 596 $files = scandir($exportFolder);
557 597 if (empty($files)) {
558 598 return;
559 599 }
@@ -563,8 +603,8 @@
563 603 if (strpos($file, $filenameToSearch) === false) {
564 604 continue;
565 605 }
566 606
567 - @unlink(acym_getLogPath($file));
607 + acym_deleteFile(acym_getLogPath($file));
568 608 }
569 609 }
570 610 }