PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.0.5
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.0.5
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
acymailing / back / Helpers / ExportHelper.php

ExportHelper.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 11.0.5, at back/Helpers/ExportHelper.php

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