PluginProbe ʕ •ᴥ•ʔ
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress / trunk
Paid Membership Plugin, Ecommerce, User Registration Form, Login Form, User Profile & Restrict Content – ProfilePress vtrunk
4.16.19 4.16.18 4.16.17 4.16.16 trunk 1.0 1.0.1 1.0.2 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5a 1.1.6 1.1.7 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.4 1.4.1 1.4.2 1.5 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.7 1.7.1 1.7.2 1.8 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.1.9 2.2.10 2.2.11 2.2.12 2.2.13 2.2.14 2.2.15 2.2.16 2.2.2 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 3.0 3.1 3.1.1 3.1.10 3.1.11 3.1.12 3.1.13 3.1.14 3.1.15 3.1.16 3.1.17 3.1.18 3.1.19 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.10 3.2.11 3.2.12 3.2.13 3.2.14 3.2.15 3.2.16 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.10.0 4.10.1 4.10.2 4.10.3 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.13.3 4.13.4 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.15.0 4.15.1 4.15.10 4.15.11 4.15.12 4.15.13 4.15.14 4.15.15 4.15.16 4.15.17 4.15.18 4.15.19 4.15.2 4.15.20 4.15.20.1 4.15.21 4.15.22 4.15.23 4.15.24 4.15.25 4.15.3 4.15.4 4.15.5 4.15.6 4.15.7 4.15.8 4.15.9 4.16.0 4.16.1 4.16.10 4.16.11 4.16.12 4.16.13 4.16.14 4.16.15 4.16.2 4.16.3 4.16.4 4.16.5 4.16.6 4.16.7 4.16.8 4.16.9 4.2.0 4.3.0 4.3.1 4.3.2 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.7.0 4.8.0 4.9.0
wp-user-avatar / src / Membership / DigitalProducts / DownloadHandler.php
wp-user-avatar / src / Membership / DigitalProducts Last commit date
DownloadHandler.php 5 months ago DownloadService.php 3 years ago Init.php 3 years ago UploadHandler.php 11 hours ago index.php 3 years ago
DownloadHandler.php
504 lines
1 <?php
2
3 namespace ProfilePress\Core\Membership\DigitalProducts;
4
5 use ProfilePress\Core\Membership\Models\Customer\CustomerFactory;
6 use ProfilePress\Core\Membership\Models\Order\OrderFactory;
7
8 class DownloadHandler
9 {
10 public function __construct()
11 {
12 add_action('init', [$this, 'process_download'], 100);
13 }
14
15 /**
16 * //[0=> 'order ID', 1=> 'plan ID', 2 => 'file index']
17 * @return array|false
18 */
19 private function get_download_url_parts()
20 {
21 $parts = parse_url(add_query_arg([]));
22 wp_parse_str($parts['query'], $query_args);
23 $url = add_query_arg($query_args, site_url());
24
25 $valid_token = DownloadService::init()->validate_url_token($url);
26
27 if ( ! $valid_token) return false;
28
29 //[0=> 'order ID', 1=> 'plan ID', 2 => 'file index']
30 return explode(':', rawurldecode($_GET['ppress_file']));
31 }
32
33 /**
34 * Used to process a signed URL for processing downloads
35 *
36 * @return array|false
37 */
38 private function process_signed_download_url()
39 {
40 $order_parts = $this->get_download_url_parts();
41
42 if ( ! $order_parts) return false;
43
44 $order_id = isset($order_parts[0]) ? (int)$order_parts[0] : null;
45 $plan_id = isset($order_parts[1]) ? (int)$order_parts[1] : null;
46 $file_index = isset($order_parts[2]) ? (int)$order_parts[2] : null;
47
48 $downloads = ppress_get_plan($plan_id)->get_downloads();
49
50 $order = OrderFactory::fromId($order_id);
51
52 $file_url = '';
53
54 if (isset($downloads['files']) && is_array($downloads['files'])) {
55 $file_url = key(array_slice($downloads['files'], intval($file_index), 1));
56 }
57
58 $has_access = false;
59
60 if ($this->is_admin_initiated_downloads($order_id, $file_url)) {
61 $has_access = true;
62 } else {
63
64 // Check to make sure not at download limit
65 if (DownloadService::init()->is_file_at_download_limit($plan_id, $order_id, $file_url)) {
66 wp_die(apply_filters('ppress_download_limit_reached_text', __('Sorry but you have hit your download limit for this file.', 'wp-user-avatar')), __('Error', 'wp-user-avatar'), array('response' => 403));
67 }
68
69 if (
70 apply_filters('ppress_file_downloads_subscription_check', true) &&
71 ! CustomerFactory::fromId($order->customer_id)->has_active_subscription($plan_id)
72 ) {
73 wp_die(
74 sprintf(
75 __('You must have an active subscription to %s in order to download this file.', 'wp-user-avatar'),
76 ppress_get_plan($plan_id)->get_name()
77 ),
78 __('Access Denied', 'wp-user-avatar')
79 );
80 }
81 }
82
83 if ($plan_id == $order->plan_id && $order->is_completed()) {
84
85 $get_downloads = ppress_get_plan($plan_id)->get_downloads();
86
87 if (
88 isset($get_downloads['files']) &&
89 ! is_null($file_index) &&
90 ! empty(array_slice($get_downloads['files'], intval($file_index), 1))
91 ) {
92 $has_access = true;
93 }
94 }
95
96 return [
97 'has_access' => $has_access,
98 'order_id' => $order_id,
99 'plan_id' => $plan_id,
100 'file_index' => $file_index,
101 'requested_file' => $file_url
102 ];
103 }
104
105 private function get_download_content_type($file_path)
106 {
107 $file_extension = strtolower(substr(strrchr($file_path, '.'), 1));
108 $ctype = 'application/force-download';
109
110 foreach (get_allowed_mime_types() as $mime => $type) {
111 $mimes = explode('|', $mime);
112 if (in_array($file_extension, $mimes, true)) {
113 $ctype = $type;
114 break;
115 }
116 }
117
118 return $ctype;
119 }
120
121 /**
122 * Determines if a file should be allowed to be downloaded by making sure it's within the wp-content directory.
123 *
124 * @param $requested_file
125 *
126 * @return boolean
127 */
128 private function local_file_location_is_allowed($requested_file)
129 {
130 $file_details = parse_url($requested_file);
131
132 $should_allow = true;
133
134 // If the file is an absolute path, make sure it's in the wp-content directory, to prevent store owners from accidentally allowing privileged files from being downloaded.
135 if (( ! isset($file_details['scheme']) || ! in_array($file_details['scheme'], ['http', 'https'])) && isset($file_details['path'])) {
136
137 /** This is an absolute path */
138 $requested_file = wp_normalize_path(realpath($requested_file));
139 $normalized_abspath = wp_normalize_path(ABSPATH);
140 $normalized_content_dir = wp_normalize_path(WP_CONTENT_DIR);
141
142 if (0 !== strpos($requested_file, $normalized_abspath) || false === strpos($requested_file, $normalized_content_dir)) {
143 // If the file is not within the WP_CONTENT_DIR, it should not be able to be downloaded.
144 $should_allow = false;
145 }
146
147 }
148
149 return $should_allow;
150 }
151
152 /**
153 * Determine if the file being requested is hosted locally or not
154 *
155 * @param string $requested_file The file being requested
156 *
157 * @return bool If the file is hosted locally or not
158 */
159 private function is_local_file($requested_file)
160 {
161 $site_url = preg_replace('#^https?://#', '', site_url());
162 $requested_file = preg_replace('#^(https?|file)://#', '', $requested_file);
163
164 $is_local_url = strpos($requested_file, $site_url) === 0;
165 $is_local_path = strpos($requested_file, '/') === 0;
166
167 return ($is_local_url || $is_local_path);
168 }
169
170 /**
171 * Reads file in chunks so big downloads are possible without changing PHP.INI
172 *
173 * See https://github.com/bcit-ci/CodeIgniter/wiki/Download-helper-for-large-files
174 *
175 * @param string $file The file
176 * @param boolean $retbytes Return the bytes of file
177 *
178 * @return bool|string If string, $status || $cnt
179 */
180 private function readfile_chunked($file, $retbytes = true)
181 {
182 while (ob_get_level() > 0) {
183 ob_end_clean();
184 }
185
186 ob_start();
187
188 // If output buffers exist, make sure they are closed.
189 if (ob_get_length()) ob_clean();
190
191 $chunksize = 1024 * 1024;
192 $cnt = 0;
193 $handle = @fopen($file, 'r');
194
195 if ($size = @filesize($file)) {
196 header("Content-Length: " . $size);
197 }
198
199 if (false === $handle) {
200 return false;
201 }
202
203 if (isset($_SERVER['HTTP_RANGE'])) {
204 list($size_unit, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
205 if ('bytes' === $size_unit) {
206 if (strpos(',', $range)) {
207 list($range) = explode(',', $range, 1);
208 }
209 } else {
210 header('HTTP/1.1 416 Requested Range Not Satisfiable');
211 exit;
212 }
213 } else {
214 $range = '';
215 }
216
217 if (empty($range)) {
218 $seek_start = null;
219 $seek_end = null;
220 } else {
221 list($seek_start, $seek_end) = explode('-', $range, 2);
222 }
223
224 $seek_end = (empty($seek_end)) ? ($size - 1) : min(abs(intval($seek_end)), ($size - 1));
225 $seek_start = (empty($seek_start) || $seek_end < abs(intval($seek_start))) ? 0 : max(abs(intval($seek_start)), 0);
226
227 // Only send partial content header if downloading a piece of the file (IE workaround)
228 if ($seek_start > 0 || $seek_end < ($size - 1)) {
229 header(sprintf('%s 206 Partial Content', ppress_var($_SERVER, 'SERVER_PROTOCOL', 'HTTP/1.1', true)));
230 header('Content-Range: bytes ' . $seek_start . '-' . $seek_end . '/' . $size);
231 header('Content-Length: ' . ($seek_end - $seek_start + 1));
232 } else {
233 header("Content-Length: $size");
234 }
235
236 header('Accept-Ranges: bytes');
237
238 ppress_set_time_limit(false);
239
240 fseek($handle, $seek_start);
241
242 while ( ! @feof($handle)) {
243 $buffer = @fread($handle, $chunksize);
244 echo $buffer;
245 ob_flush();
246
247 if (ob_get_length()) {
248 ob_flush();
249 flush();
250 }
251
252 if ($retbytes) {
253 $cnt += strlen($buffer);
254 }
255
256 if (connection_status() != 0) {
257 @fclose($handle);
258 exit;
259 }
260 }
261
262 ob_flush();
263
264 $status = @fclose($handle);
265
266 if ($retbytes && $status) return $cnt;
267
268 return $status;
269 }
270
271 /**
272 * Deliver the download file
273 *
274 * If enabled, the file is symlinked to better support large file downloads
275 *
276 * @param string $file
277 * @param bool $redirect True if we should perform a header redirect
278 *
279 * @return void
280 */
281 private function deliver_download($file = '', $redirect = false)
282 {
283 if ($redirect) {
284 header('Location: ' . $file);
285 } else {
286 $this->readfile_chunked($file);
287 }
288 }
289
290 /**
291 *
292 * Important so admins can always download files
293 *
294 * @param $order_id
295 * @param $file_url
296 *
297 * @return bool
298 */
299 private function is_admin_initiated_downloads($order_id, $file_url = '')
300 {
301 if (is_user_logged_in() && current_user_can('manage_options')) {
302
303 $order = OrderFactory::fromId($order_id);
304
305 $user_id = $order->get_customer()->get_user_id();
306
307 // order was made by admin, log the download
308 if ($user_id > 0 && get_current_user_id() === $user_id) {
309 if ( ! empty($file_url)) {
310 DownloadService::init()->add_download_log([
311 'plan_id' => $order->get_plan_id(),
312 'file_url' => $file_url,
313 'order_id' => $order_id
314 ]);
315 }
316 }
317
318 return true;
319 }
320
321 return false;
322 }
323
324 public function process_download()
325 {
326 if (empty($_GET['ppress_file']) || empty($_GET['ttl']) || empty($_GET['token'])) return;
327
328 if ('true' === ppress_get_file_downloads_setting('access_restriction')) {
329 if ( ! is_user_logged_in()) {
330 wp_die(__('You must be logged in to download files.', 'wp-user-avatar') . ' <a href="' . esc_url(wp_login_url(ppress_get_current_url_query_string())) . '">' . __('Login', 'wp-user-avatar') . '</a>', __('Log in to Download Files', 'wp-user-avatar'), 403);
331 }
332
333 $order_parts = $this->get_download_url_parts();
334
335 $order = OrderFactory::fromId($order_parts[0]);
336
337 if ($order->get_customer()->get_user_id() !== get_current_user_id()) {
338 wp_die(__('You are not allowed to access this file', 'wp-user-avatar'), __('File download error', 'wp-user-avatar'), 403);
339 }
340 }
341
342 $args = $this->process_signed_download_url();
343
344 if ( ! $args['has_access']) {
345 $error_message = __('You do not have permission to download this file', 'wp-user-avatar');
346 wp_die(apply_filters('ppress_deny_download_message', $error_message, __('Order Verification Failed', 'wp-user-avatar')), __('Error', 'wp-user-avatar'), ['response' => 403]);
347 }
348
349 $method = $raw_method = ppress_get_file_downloads_setting('download_method', 'direct', true);
350
351 $requested_file = $args['requested_file'];
352
353 $file_details = parse_url($requested_file);
354 $schemes = ['http', 'https']; // Direct URL schemes
355
356 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN' && isset($file_details['scheme']) && ! in_array($file_details['scheme'], stream_get_wrappers())) {
357 wp_die(__('Error 103: Error downloading file. Please contact support.', 'wp-user-avatar'), __('File download error', 'wp-user-avatar'), 501);
358 }
359
360 if (
361 ( ! isset($file_details['scheme']) || ! in_array($file_details['scheme'], $schemes)) &&
362 isset($file_details['path']) && file_exists($requested_file)
363 ) {
364 /**
365 * Download method is set to Redirect in settings but an absolute path was provided
366 * We need to switch to a direct download in order for the file to download properly
367 */
368 $method = 'direct';
369 }
370
371 $file_is_in_allowed_location = $this->local_file_location_is_allowed($requested_file);
372
373 if (false === $file_is_in_allowed_location) {
374 wp_die(__('Sorry, this file could not be downloaded.', 'wp-user-avatar'), __('Error Downloading File', 'wp-user-avatar'), 403);
375 }
376
377 if ( ! $this->is_admin_initiated_downloads($args['order_id'])) {
378 DownloadService::init()->add_download_log([
379 'plan_id' => $args['plan_id'],
380 'file_url' => $requested_file,
381 'order_id' => $args['order_id']
382 ]);
383 }
384
385 ppress_set_time_limit(false);
386
387 if (function_exists('apache_setenv')) {
388 @apache_setenv('no-gzip', 1);
389 }
390 @ini_set('zlib.output_compression', 'Off');
391 @session_write_close();
392
393 nocache_headers();
394 header('Robots: none');
395 header('X-Robots-Tag: noindex, nofollow', true);
396 header('Content-Type: ' . $this->get_download_content_type($requested_file));
397 header('Content-Description: File Transfer');
398 header('Content-Disposition: attachment; filename="' . basename($requested_file) . '"');
399 header('Content-Transfer-Encoding: binary');
400
401 // If the file isn't locally hosted, process the redirect
402 if (filter_var($requested_file, FILTER_VALIDATE_URL) && ! $this->is_local_file($requested_file)) {
403 $this->deliver_download($requested_file, true);
404 exit;
405 }
406
407 if ($method == 'redirect') {
408 // Redirect straight to the file
409 $this->deliver_download($requested_file, true);
410 exit;
411 }
412
413 $direct = false;
414 $file_path = $requested_file;
415
416 if (( ! isset($file_details['scheme']) || ! in_array($file_details['scheme'], $schemes)) && isset($file_details['path']) && file_exists($requested_file)) {
417 /** This is an absolute path */
418 $direct = true;
419 } elseif (defined('UPLOADS') && strpos($requested_file, UPLOADS) !== false) {
420 /**
421 * This is a local file given by URL so we need to figure out the path
422 * UPLOADS is always relative to ABSPATH
423 * site_url() is the URL to where WordPress is installed
424 */
425 $file_path = str_replace(site_url(), '', $requested_file);
426 $file_path = realpath(ABSPATH . $file_path);
427 $direct = true;
428 } elseif (strpos($requested_file, content_url()) !== false) {
429 /** This is a local file given by URL so we need to figure out the path */
430 $file_path = str_replace(content_url(), WP_CONTENT_DIR, $requested_file);
431 $file_path = realpath($file_path);
432 $direct = true;
433 } elseif (strpos($requested_file, set_url_scheme(content_url(), 'https')) !== false) {
434 /** This is a local file given by an HTTPS URL so we need to figure out the path */
435 $file_path = str_replace(set_url_scheme(content_url(), 'https'), WP_CONTENT_DIR, $requested_file);
436 $file_path = realpath($file_path);
437 $direct = true;
438 }
439
440 // Set the file size header
441 header("Content-Length: " . @filesize($file_path));
442
443 $server_software = getenv('SERVER_SOFTWARE');
444
445 if ($raw_method == 'xsendfile') {
446
447 if (
448 function_exists('apache_get_modules') && in_array('mod_xsendfile', apache_get_modules(), true) ||
449 stristr($server_software, 'apache')
450 ) {
451 // https://tn123.org/mod_xsendfile/
452 header('X-Sendfile: ' . $file_path);
453 exit;
454 }
455
456 if (stristr($server_software, 'litespeed')) {
457 // We need a path relative to the domain
458 $file_path = trim(str_ireplace(realpath($_SERVER['DOCUMENT_ROOT']), '', $file_path), '/');
459 // https://www.litespeedtech.com/support/wiki/doku.php/litespeed_wiki:config:internal-redirect
460 header("X-LiteSpeed-Location: /$file_path");
461 exit;
462 }
463
464 if (stristr($server_software, 'lighttpd')) {
465 // apparently, X-Sendfile also work as X-LIGHTTPD-send-file is an alias that has been deprecated though still work
466 // see https://redmine.lighttpd.net/projects/lighttpd/wiki/Docs_ModFastCGI#X-Sendfile
467 header("X-LIGHTTPD-send-file: $file_path");
468 exit;
469 }
470
471 if ($direct && (stristr($server_software, 'nginx') || stristr($server_software, 'cherokee'))) {
472 // We need a path relative to the domain
473 $file_path = trim(str_ireplace(realpath($_SERVER['DOCUMENT_ROOT']), '', $file_path), '/');
474 // https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/
475 // https://cherokee-project.com/doc/other_goodies.html#x-sendfile
476 header("X-Accel-Redirect: /$file_path");
477 exit;
478 }
479
480 ppress_log_error('X-Sendfile file download method is not working.');
481 }
482
483 if ($direct) {
484 $this->deliver_download($file_path);
485 } else {
486
487 // The file supplied does not have a discoverable absolute path
488 $this->deliver_download($requested_file, true);
489 }
490
491 exit;
492 }
493
494 public static function get_instance()
495 {
496 static $instance = null;
497
498 if (is_null($instance)) {
499 $instance = new self();
500 }
501
502 return $instance;
503 }
504 }