PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Router / Endpoints / ExportDownload.php
mailpoet / lib / Router / Endpoints Last commit date
Captcha.php 4 months ago CronDaemon.php 3 years ago ExportDownload.php 2 months ago FormPreview.php 2 years ago Subscription.php 5 days ago TemplateImage.php 2 months ago Track.php 2 months ago ViewInBrowser.php 1 month ago index.php 3 years ago
ExportDownload.php
254 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\Router\Endpoints;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Config\AccessControl;
9 use MailPoet\Config\Env;
10 use MailPoet\Newsletter\Statistics\Export\StatisticsExporter;
11 use MailPoet\Router\Router;
12 use MailPoet\Subscribers\ImportExport\Export\Export;
13 use MailPoet\Util\Helpers;
14 use MailPoet\Util\Security;
15 use MailPoet\WP\Functions as WPFunctions;
16
17 class ExportDownload {
18 public const ENDPOINT = 'export_download';
19 public const ACTION_SUBSCRIBER_EXPORT = 'subscriberExport';
20 public const ACTION_STATISTICS_EXPORT = 'statisticsExport';
21
22 private const DOWNLOAD_TOKEN_LENGTH = 32;
23 private const DOWNLOAD_TOKEN_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz0123456789';
24 private const EXPORT_DIRECTORY = 'exports';
25 private const CONTENT_TYPES = [
26 'csv' => 'text/csv; charset=utf-8',
27 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28 ];
29
30 /** @var string[] */
31 public $allowedActions = [
32 self::ACTION_SUBSCRIBER_EXPORT,
33 self::ACTION_STATISTICS_EXPORT,
34 ];
35
36 public $permissions = [
37 'actions' => [
38 self::ACTION_SUBSCRIBER_EXPORT => AccessControl::PERMISSION_MANAGE_SUBSCRIBERS,
39 self::ACTION_STATISTICS_EXPORT => AccessControl::PERMISSION_MANAGE_EMAILS,
40 ],
41 ];
42
43 /** @var WPFunctions */
44 private $wp;
45
46 public function __construct(
47 WPFunctions $wp
48 ) {
49 $this->wp = $wp;
50 }
51
52 public static function buildSubscriberExportUrl(string $token, string $format, ?string $baseUrl = null): string {
53 return self::buildExportUrl(self::ACTION_SUBSCRIBER_EXPORT, $token, $format, $baseUrl);
54 }
55
56 public static function buildStatisticsExportUrl(string $token, string $format, ?string $baseUrl = null): string {
57 return self::buildExportUrl(self::ACTION_STATISTICS_EXPORT, $token, $format, $baseUrl);
58 }
59
60 public static function getExportDirectory(): string {
61 return Env::$tempPath . '/' . self::EXPORT_DIRECTORY;
62 }
63
64 /**
65 * @return array{path: string, token: string}
66 */
67 public static function createExportFile(string $filePrefix, string $format): array {
68 $token = self::generateDownloadToken();
69 return [
70 'path' => self::getFilePathForToken($filePrefix, $token, $format),
71 'token' => $token,
72 ];
73 }
74
75 public static function generateDownloadToken(): string {
76 return Security::generateRandomString(self::DOWNLOAD_TOKEN_LENGTH);
77 }
78
79 public static function getFilePathForToken(string $filePrefix, string $token, string $format): string {
80 return self::getExportDirectory() . '/' . self::getFileNameForToken($filePrefix, $token, $format);
81 }
82
83 public static function ensureExportDirectory(?WPFunctions $wp = null): void {
84 $wp = $wp ?? WPFunctions::get();
85 $exportDirectory = self::getExportDirectory();
86 $wasJustCreated = !is_dir($exportDirectory);
87 if ($wasJustCreated && !$wp->wpMkdirP($exportDirectory)) {
88 throw new \RuntimeException('Could not create the export directory.');
89 }
90 if (!is_dir($exportDirectory)) {
91 throw new \RuntimeException('Could not create the export directory.');
92 }
93 if (!self::writeFile($exportDirectory . '/index.php', str_replace('\n', PHP_EOL, '<?php\n\n// Silence is golden'))) {
94 throw new \RuntimeException('Could not protect the export directory.');
95 }
96 $htaccessWritten = self::writeFile(
97 $exportDirectory . '/.htaccess',
98 implode(PHP_EOL, [
99 '<IfModule mod_authz_core.c>',
100 'Require all denied',
101 '</IfModule>',
102 '<IfModule !mod_authz_core.c>',
103 'Deny from all',
104 '</IfModule>',
105 '',
106 ])
107 );
108 if (!$htaccessWritten) {
109 throw new \RuntimeException('Could not protect the export directory.');
110 }
111 if ($wasJustCreated) {
112 self::purgeLegacyExportFiles();
113 }
114 }
115
116 private static function purgeLegacyExportFiles(): void {
117 $patterns = [
118 Env::$tempPath . '/' . Export::getFilePrefix() . '*.*',
119 Env::$tempPath . '/' . StatisticsExporter::FILE_PREFIX . '*.*',
120 ];
121 foreach ($patterns as $pattern) {
122 foreach (glob($pattern) ?: [] as $file) {
123 if (is_file($file)) {
124 unlink($file);
125 }
126 }
127 }
128 }
129
130 public function subscriberExport(array $data): void {
131 $this->downloadFile($data, Export::getFilePrefix());
132 }
133
134 public function statisticsExport(array $data): void {
135 $this->downloadFile($data, StatisticsExporter::FILE_PREFIX);
136 }
137
138 public function getDownloadFilePath(array $data, string $filePrefix): ?string {
139 if (empty($data['token']) || !is_string($data['token'])) {
140 return null;
141 }
142
143 $token = $data['token'];
144 if (
145 strlen($token) !== self::DOWNLOAD_TOKEN_LENGTH
146 || strspn($token, self::DOWNLOAD_TOKEN_CHARACTERS) !== self::DOWNLOAD_TOKEN_LENGTH
147 ) {
148 return null;
149 }
150
151 if (empty($data['format']) || !is_string($data['format'])) {
152 return null;
153 }
154
155 $extension = strtolower($data['format']);
156 if (!isset(self::CONTENT_TYPES[$extension])) {
157 return null;
158 }
159
160 $realExportPath = realpath(self::getExportDirectory());
161 $filePath = self::getFilePathForToken($filePrefix, $token, $extension);
162 $realFilePath = is_file($filePath) ? realpath($filePath) : false;
163 if (!is_string($realExportPath) || !is_string($realFilePath)) {
164 return null;
165 }
166
167 $exportPath = rtrim($realExportPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
168 if (strpos($realFilePath, $exportPath) !== 0) {
169 return null;
170 }
171
172 return $realFilePath;
173 }
174
175 private static function buildExportUrl(string $action, string $token, string $format, ?string $baseUrl): string {
176 $baseUrl = $baseUrl ?? WPFunctions::get()->homeUrl();
177 $params = [
178 Router::NAME => '',
179 'endpoint' => self::ENDPOINT,
180 'action' => Helpers::camelCaseToUnderscore($action),
181 'data' => Router::encodeRequestData([
182 'token' => $token,
183 'format' => strtolower($format),
184 ]),
185 ];
186 $separator = strpos($baseUrl, '?') === false ? '?' : '&';
187 return $baseUrl . $separator . http_build_query($params, '', '&');
188 }
189
190 private static function getFileNameForToken(string $filePrefix, string $token, string $format): string {
191 return sprintf(
192 '%s%s.%s',
193 $filePrefix,
194 hash_hmac('sha256', $token, self::getDownloadSecret()),
195 strtolower($format)
196 );
197 }
198
199 private static function getDownloadSecret(): string {
200 $secret = '';
201 $secretConstants = [
202 'AUTH_KEY',
203 'SECURE_AUTH_KEY',
204 'LOGGED_IN_KEY',
205 'NONCE_KEY',
206 'AUTH_SALT',
207 'SECURE_AUTH_SALT',
208 'LOGGED_IN_SALT',
209 'NONCE_SALT',
210 ];
211 foreach ($secretConstants as $constant) {
212 if (defined($constant)) {
213 $secret .= (string)constant($constant);
214 }
215 }
216 return $secret !== '' ? $secret : (string)Env::$path;
217 }
218
219 private static function writeFile(string $filePath, string $contents): bool {
220 // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- Reads protection files in MailPoet's export directory.
221 if (is_file($filePath) && file_get_contents($filePath) === $contents) {
222 return true;
223 }
224 // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_file_put_contents -- Writes protection files to MailPoet's export directory.
225 return file_put_contents($filePath, $contents) !== false;
226 }
227
228 private function downloadFile(array $data, string $filePrefix): void {
229 $filePath = $this->getDownloadFilePath($data, $filePrefix);
230 if (!$filePath) {
231 $this->wp->statusHeader(404);
232 exit;
233 }
234
235 $extension = strtolower((string)pathinfo($filePath, PATHINFO_EXTENSION));
236 if (!$this->wp->headersSent()) {
237 header('Content-Type: ' . self::CONTENT_TYPES[$extension]);
238 header('Content-Disposition: attachment; filename="' . rtrim($filePrefix, '_') . '.' . $extension . '"');
239 header('X-Content-Type-Options: nosniff');
240 header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
241 header('Pragma: no-cache');
242 header('Expires: 0');
243 $fileSize = filesize($filePath);
244 if ($fileSize !== false) {
245 header('Content-Length: ' . $fileSize);
246 }
247 }
248
249 // phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- Reads a validated local export file from Env::$tempPath.
250 readfile($filePath);
251 exit;
252 }
253 }
254