PluginProbe
Ebook Store / 6.25
Ebook Store v6.25
6.25 6.24 6.23 6.22 6.21 6.20 trunk
ebook-store / ebook_store.php

ebook_store.php in Ebook Store 6.25, at ebook_store.php

981 lines 38.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 Plugin Name: eBook store
4 Plugin URI: https://www.shopfiles.com/index.php/products/wordpress-ebook-store
5 Description: eBook Store is a unique and powerful standalone tool for selling ebooks with WordPress (also WooCommerce support is available), allowing you to display beautiful buy now forms for your ebook(s) and giving you the ability to offer encrypted, watermarked and QR code stamped ebooks to your buyers, a proven way to prevent piracy. With the built-in MailChimp integration you can directly subscribe your clients to a mailing list. It supports PDF, ePub, TXT, Mobi and Zip files. Now comes with WP Affiliates Manager integration so you can pay commissions to affiliates for promoting your ebook.
6 Author: Shopfiles Ltd
7 Text Domain: ebook-store
8 Domain Path: /languages
9 Author URI:https://www.shopfiles.com/index.php/products/wordpress-ebook-store
10 Version: 6.25
11 Requires at least: 5.8
12 Requires PHP: 7.4
13 License: GPLv2
14 */
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-bootstrap.php';
21
22 /**
23 * Refuse to run — with an explanation — rather than fatal on an old stack.
24 */
25 $ebook_store_environment = ebook_store_check_environment();
26 if ( $ebook_store_environment !== true ) {
27 add_action(
28 'admin_notices',
29 function () use ( $ebook_store_environment ) {
30 echo '<div class="notice notice-error"><p>' . esc_html( $ebook_store_environment ) . '</p></div>';
31 }
32 );
33
34 return;
35 }
36
37 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-license.php';
38 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-settings-ui.php';
39 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-gateway-ui.php';
40 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-paypal-rest.php';
41 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-health.php';
42 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-assets.php';
43 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-block.php';
44 require_once plugin_dir_path( __FILE__ ) . 'includes/ebook-store-chunked-upload.php';
45
46 /**
47 * Tell WooCommerce this plugin is safe with High-Performance Order Storage.
48 *
49 * The order lookups now go through wc_get_order() rather than reading wp_postmeta
50 * directly, so HPOS sites keep working.
51 */
52 add_action(
53 'before_woocommerce_init',
54 function () {
55 if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
56 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
57 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
58 }
59 }
60 );
61
62 /**
63 * Checks if Elementor is active.
64 *
65 * @return bool
66 */
67 function is_elementor_active() {
68 return did_action( 'elementor/loaded' );
69 }
70
71 /**
72 * Registers the Sell Ebook widget.
73 *
74 * @param \Elementor\Widgets_Manager $widgets_manager Elementor widgets manager.
75 */
76 function register_sell_ebook_widget( $widgets_manager ) {
77 require_once plugin_dir_path( __FILE__ ) . 'widgets/sell-ebook-widget.php';
78 $widgets_manager->register( new \Elementor_Sell_Ebook_Widget() );
79 }
80
81 /**
82 * Adds the "Ebook Store" category to Elementor.
83 *
84 * @param \Elementor\Elements_Manager $elements_manager Elementor elements manager.
85 */
86 function add_ebook_store_category( $elements_manager ) {
87 $elements_manager->add_category(
88 'ebook-store',
89 [
90 'title' => __( 'Ebook Store', 'ebook-store' ),
91 'icon' => 'fa fa-book',
92 ]
93 );
94 }
95
96 // Initialize the plugin after Elementor is loaded.
97 function init_ebook_store_extension() {
98 if ( is_elementor_active() ) {
99 add_action( 'elementor/widgets/register', 'register_sell_ebook_widget' );
100 add_action( 'elementor/elements/categories_registered', 'add_ebook_store_category' );
101 } else {
102 // Admin notice if Elementor is not active.
103 add_action( 'admin_notices', function() {
104 //echo '<div class="notice notice-warning"><p>' . esc_html__( 'Ebook Store Extension requires Elementor to be active.', 'ebook-store' ) . '</p></div>';
105 } );
106 }
107 }
108 add_action( 'plugins_loaded', 'init_ebook_store_extension' );
109
110
111
112 /**
113 * Load the plugin's translations.
114 *
115 * The path is derived from the actual folder name rather than hardcoded, so the
116 * translations survive being installed from a zip that unpacks to
117 * "ebook-store-main" or any other directory name.
118 *
119 * Runs on 'init' — loading earlier is what triggers WordPress 6.7's
120 * "_load_textdomain_just_in_time was called incorrectly" notice.
121 */
122 function ebookstoretextdomain() {
123 load_plugin_textdomain(
124 'ebook-store',
125 false,
126 dirname( plugin_basename( __FILE__ ) ) . '/languages'
127 );
128 }
129 add_action( 'init', 'ebookstoretextdomain' );
130
131 /*
132 * Note: this plugin deliberately does NOT call error_reporting() or ini_set().
133 * It used to force error_reporting(0) for the whole site whenever WP_DEBUG was
134 * off, which silenced every other plugin's errors and hid its own bugs.
135 * Error visibility belongs to the site owner's wp-config.php.
136 */
137
138 include_once( plugin_dir_path( __FILE__ ) . 'functions.php' );
139 include_once( plugin_dir_path( __FILE__ ) . 'class_qswpoptions.php' );
140 include_once( plugin_dir_path( __FILE__ ) . 'ebook_options.php' );
141
142 add_action('init', 'ebook_store_formContent');
143 add_action('init', 'ebook_store_check_ipn');
144 add_action('init', 'ebook_store_redirect_add_order');
145 add_action('template_redirect', 'ebook_store_render_public_order_verification_page');
146
147
148 /**
149 * The PayPal IPN fields the plugin stores against an order.
150 *
151 * The handler used to loop over the whole of $_REQUEST and write every key it
152 * found into post meta. Those values are later substituted into the thank-you
153 * page and the delivery email, so an attacker-chosen key/value pair became
154 * stored output. Only these known PayPal fields are persisted now.
155 *
156 * @return array
157 */
158 function ebook_store_get_ipn_allowed_fields() {
159 return apply_filters(
160 'ebook_store_ipn_allowed_fields',
161 array(
162 'txn_id', 'txn_type', 'payment_status', 'payment_type', 'payment_date',
163 'mc_gross', 'mc_fee', 'mc_currency', 'tax', 'quantity',
164 'payer_email', 'payer_id', 'payer_status',
165 'first_name', 'last_name', 'business', 'receiver_email', 'receiver_id',
166 'item_name', 'item_number', 'custom', 'invoice',
167 'address_name', 'address_street', 'address_city', 'address_state',
168 'address_zip', 'address_country', 'address_country_code', 'address_status',
169 'residence_country', 'protection_eligibility', 'verify_sign',
170 'notify_version', 'charset', 'ipn_track_id',
171 'md5_nonce', 'user_id',
172 )
173 );
174 }
175
176 /**
177 * Read one of the three routing values off an incoming notification.
178 *
179 * The buy button sets PayPal's notify_url to home_url() with task, ebook_key
180 * and md5_nonce in the QUERY STRING (see the form in functions.php). PayPal
181 * then POSTs the payment payload to that URL, so those three values arrive as
182 * GET while every payment field arrives as POST. PayPal also never echoes back
183 * arbitrary hidden inputs, so md5_nonce can only come from the query string.
184 *
185 * Routing values are looked up here; payment fields are still read from $_POST
186 * alone, so a crafted query string cannot supply or override an amount.
187 *
188 * @param string $key Field name.
189 * @return string
190 */
191 function ebook_store_ipn_routing_value( $key ) {
192 foreach ( array( $_GET, $_POST ) as $source ) {
193 if ( isset( $source[ $key ] ) && is_scalar( $source[ $key ] ) ) {
194 $value = trim( (string) wp_unslash( $source[ $key ] ) );
195 if ( $value !== '' ) {
196 return $value;
197 }
198 }
199 }
200
201 return '';
202 }
203
204 function ebook_store_check_ipn() {
205 $QSWPOptions = new QSWPOptions();
206 // 6.20 tightened this to $_POST['task'] to stop a query string overriding
207 // posted payment values. But `task` itself is a query-string argument on
208 // notify_url and PayPal never posts it, so no real notification matched and
209 // IPN stopped firing entirely. Route on either source; keep the payload POST-only.
210 //
211 // The method is checked up front rather than inside the listener: `task` is
212 // readable from the query string, so without this a plain GET to any URL with
213 // ?task=ipn would reach requirePostMethod() and die() with a bare 405 — which
214 // an <img src> on someone else's page could point at any page of the site.
215 $is_post = isset( $_SERVER['REQUEST_METHOD'] ) && strtoupper( $_SERVER['REQUEST_METHOD'] ) === 'POST';
216
217 if ( $is_post && ebook_store_ipn_routing_value( 'task' ) === 'ipn' ) {
218 $ebook_key = preg_replace( "/[^a-zA-Z0-9]+/", "", ebook_store_ipn_routing_value( 'ebook_key' ) );
219 // Refund / chargeback notifications reuse the original notify_url, so their
220 // ebook_key already has an order. They are handled after verification below.
221 $posted_status = ( isset( $_POST['payment_status'] ) && is_scalar( $_POST['payment_status'] ) ) ? sanitize_text_field( wp_unslash( $_POST['payment_status'] ) ) : '';
222 $is_refund_notice = in_array( $posted_status, ebook_store_ipn_refund_statuses(), true );
223 //if order with such ebook key / order key exists, drop order.
224 if ( ! $is_refund_notice && ( $ebook_key === '' || ebook_get_order('ebook_key', $ebook_key) ) ) {
225 return false;
226 }
227
228 global $ebook_email_delivery;
229 include_once 'payment_gateways/paypal/ipnlistener.php';
230 $listener = new IpnListener();
231 if (get_option('paypal_sandbox') > 0) {
232 $listener->use_sandbox = true;
233 }
234 try {
235 $listener->requirePostMethod();
236 $verified = $listener->processIpn();
237 } catch (Exception $e) {
238 error_log( 'Ebook Store IPN: ' . $e->getMessage() );
239
240 // A network blip talking to PayPal used to die with a 200, which tells
241 // PayPal the notification was delivered — so it never retried and the
242 // order was lost for good. Anything that is not an outright bad request
243 // gets a 500 so PayPal redelivers.
244 if ( ! headers_sent() && http_response_code() === 200 ) {
245 http_response_code( 500 );
246 }
247
248 die( esc_html( $e->getMessage() ) );
249 }
250
251 if ($verified) {
252 if ( isset( $_POST['payment_type'] ) && $_POST['payment_type'] === 'echeck' && get_option('ebook_store_allow_echeck') == 0) {
253 return false; //stop the process if echecks are not accepted.
254 }
255
256 $ipn = array();
257 foreach ( ebook_store_get_ipn_allowed_fields() as $field ) {
258 $ipn[ $field ] = isset( $_POST[ $field ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) : '';
259 }
260
261 // md5_nonce keys the saved checkout form. PayPal does not return the
262 // hidden input, so it has to come off notify_url's query string.
263 if ( $ipn['md5_nonce'] === '' ) {
264 $ipn['md5_nonce'] = sanitize_text_field( ebook_store_ipn_routing_value( 'md5_nonce' ) );
265 }
266
267 // A refund or chargeback for an order that already exists: record it
268 // and stop. Nothing is created or delivered.
269 if ( in_array( $ipn['payment_status'], ebook_store_ipn_refund_statuses(), true ) ) {
270 if ( ebook_store_ipn_receiver_is_us( $ipn ) ) {
271 ebook_store_handle_ipn_refund_notification( $ipn );
272 }
273 header( 'HTTP/1.1 200 OK' );
274 return false;
275 }
276
277 // Only a completed payment hands over the book. Pending, Failed,
278 // Refunded and Reversed notifications all used to create an order and
279 // deliver the file.
280 if ( ! in_array( $ipn['payment_status'], ebook_store_ipn_accepted_statuses(), true ) ) {
281 header( 'HTTP/1.1 200 OK' );
282 return false;
283 }
284
285 // The money has to have come to this store.
286 if ( ! ebook_store_ipn_receiver_is_us( $ipn ) ) {
287 ebook_store_notify_admin_of_ipn_problem(
288 __( 'A PayPal payment notification named a different PayPal account as the payee, so it was ignored. Nothing was delivered. If this was a genuine sale, check that the PayPal account on the settings page matches the account the payment was made to.', 'ebook-store' ),
289 $listener->getTextReport()
290 );
291 header( 'HTTP/1.1 200 OK' );
292 return false;
293 }
294
295 // PayPal redelivers a notification until it gets a 200, and a genuine
296 // signed notification can also be replayed by hand. Either way the
297 // same transaction must only ever produce one order.
298 if ( ebook_store_ipn_already_recorded( $ipn['txn_id'] ) ) {
299 header( 'HTTP/1.1 200 OK' );
300 return false;
301 }
302
303 $my_post = array(
304 'post_title' => trim( $ipn['first_name'] . ' ' . $ipn['last_name'] ),
305 'post_type' => 'ebook_order',
306 'post_status' => 'publish',
307 'post_author' => 1,
308 'post_category' => array(8,39));
309 $custom = explode( "|", $ipn['custom'] );
310 $custom[0] = isset( $custom[0] ) ? absint( $custom[0] ) : 0;
311 $custom[1] = isset( $custom[1] ) ? (string) $custom[1] : '';
312 $vat = esc_attr(get_option('vat_percent'));
313
314 // The base (pre-tax) amount the buy button signed. With VAT enabled the
315 // buyer paid price + tax, so subtract the tax back off to recover it.
316 $base_amount = (float) $ipn['mc_gross'];
317 if ($vat > 0) {
318 $base_amount = (float) $ipn['mc_gross'] - (float) $ipn['tax'];
319 }
320
321 // The fraud hash must match the buy button byte-for-byte. The button
322 // (functions.php) signs md5( NONCE_KEY . id . number_format($price,2,'.',',') ),
323 // i.e. WITH a thousands separator, so the hash input keeps it too.
324 $hash_gross = number_format( $base_amount, 2, '.', ',' );
325
326 // The value stored against the order must be machine-readable (NO
327 // thousands separator). Formatting it with ',' and then re-casting it
328 // to float truncated every amount of 1000+ to 1.00 — and, because that
329 // truncated value fed the hash below, it also made every PayPal order
330 // of 1000+ fail this amount check outright.
331 $mc_gross = number_format( $base_amount, 2, '.', '' );
332
333 $ebook = get_post_meta($custom[0], 'ebook', true);
334 $md5 = md5(NONCE_KEY . $custom[0] . $hash_gross);
335
336 // The `custom` field carries "<ebook_id>|<hmac of ebook_id + amount>".
337 // That binding is what stops a buyer from making a genuine 1-cent
338 // PayPal payment while asking for an expensive ebook: PayPal happily
339 // verifies the payment, so the amount must be checked here.
340 // This comparison used to be commented out and replaced by `if (1)`.
341 $is_donation = is_array( $ebook ) && isset( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] === 'donate';
342 $amount_is_authentic = hash_equals( $md5, $custom[1] );
343
344 // 6.24 added a third segment that binds the net price and the
345 // currency too. A button rendered before the upgrade only has two
346 // segments, so those keep using the original check above. The net
347 // price is what this side can recover exactly (mc_gross - tax); the
348 // gross would depend on how PayPal rounds a half cent of tax.
349 $custom[2] = isset( $custom[2] ) ? (string) $custom[2] : '';
350 $paid_currency = strtoupper( trim( (string) $ipn['mc_currency'] ) );
351
352 // The form charges in the ebook's own currency when it has a valid
353 // one, otherwise in the store's — the same rule the buy button uses.
354 $expected_currency = ebook_store_get_store_currency();
355 if ( is_array( $ebook ) && ! empty( $ebook['paypal_currency'] ) ) {
356 $ebook_currency = strtoupper( trim( (string) $ebook['paypal_currency'] ) );
357 if ( preg_match( '/^[A-Z]{3}$/', $ebook_currency ) ) {
358 $expected_currency = $ebook_currency;
359 }
360 }
361
362 if ( $custom[2] !== '' ) {
363 $amount_is_authentic = hash_equals(
364 ebook_store_paypal_amount_signature( $custom[0], $base_amount, $paid_currency ),
365 $custom[2]
366 );
367 }
368
369 $ipn_rejection = '';
370
371 // PayPal accepts money in whatever currency the form asked for, and
372 // the amount signature of a pre-6.24 button (and every donation) says
373 // nothing about currency — so a buyer could pay the sticker number in
374 // a far cheaper currency and pass every other check.
375 if ( $paid_currency !== '' && $paid_currency !== $expected_currency ) {
376 $ipn_rejection = sprintf(
377 /* translators: 1: currency paid in, 2: the currency the ebook is sold in. */
378 __( 'The payment arrived in %1$s but this ebook is sold in %2$s, so the amount could not be verified. Nothing was delivered.', 'ebook-store' ),
379 $paid_currency,
380 $expected_currency
381 );
382 }
383
384 // With VAT switched on the tax is part of the price, but it arrives in
385 // the notification, so removing the tax line from the posted form used
386 // to buy the book at its net price.
387 $vat_percent = ebook_store_get_vat_percent();
388 if ( $ipn_rejection === '' && ! $is_donation && $vat_percent > 0 ) {
389 // Compared in whole cents with a one-cent allowance: PayPal and
390 // PHP can round a half cent of tax differently, and PHP 8.4 no
391 // longer nudges such values before rounding.
392 $expected_tax = round( $base_amount * $vat_percent / 100, 2 );
393 if ( abs( (int) round( (float) $ipn['tax'] * 100 ) - (int) round( $expected_tax * 100 ) ) > 1 ) {
394 $ipn_rejection = sprintf(
395 /* translators: 1: tax reported by PayPal, 2: tax expected. */
396 __( 'The payment reported %1$s tax where %2$s was expected, so it could not be verified. Nothing was delivered.', 'ebook-store' ),
397 number_format( (float) $ipn['tax'], 2, '.', '' ),
398 number_format( $expected_tax, 2, '.', '' )
399 );
400 }
401 }
402
403 if ( $ipn_rejection !== '' ) {
404 // A 200 on purpose: this notification can never become acceptable,
405 // so asking PayPal to redeliver it would only repeat the failure.
406 ebook_store_notify_admin_of_ipn_problem( $ipn_rejection, $listener->getTextReport() );
407 error_log( 'Ebook Store IPN rejected: ' . $ipn_rejection );
408 header( 'HTTP/1.1 200 OK' );
409 return false;
410 }
411
412 if ( $amount_is_authentic || $is_donation ) {
413 $post_id = wp_insert_post( $my_post, true );
414 if ( is_wp_error( $post_id ) || ! $post_id ) {
415 return false;
416 }
417
418 $ipn['user_id'] = ebook_store_silent_registration( $ipn );
419
420 $order = array();
421 foreach ( $ipn as $k => $v ) {
422 update_post_meta($post_id, $k, $v);
423 $order[$k] = $v;
424 }
425 if ($vat > 0) {
426 $order['mc_gross'] = $order['mc_gross'] - $order['tax'];
427 }
428 $order['order_id'] = $post_id;
429 // Mailchimp and Brevo, like every other gateway, and only for a
430 // payment that passed the status, receiver and amount checks.
431 ebook_store_subscribe_buyer_to_marketing_lists( $ipn['payer_email'] );
432 $order['password'] = $ipn['payer_email'];
433 global $formData, $ebook_store_random_password;
434 $formData = ebook_store_get_form( $ipn['md5_nonce'] );
435 $formData = json_encode($formData);
436 $ebook_order = array();
437 $ebook_order['ebook_key'][0] = $ebook_key;
438 $ebook_order['ebook'][0] = $custom[0];
439 // ebook_download_link() reads md5_nonce and order_id too; without
440 // them the emailed link carried an empty nonce and no order id.
441 $ebook_order['md5_nonce'][0] = $ipn['md5_nonce'];
442 $ebook_order['order_id'] = $post_id;
443 $order['downloadlink'] = ebook_download_link($ebook_order);
444 $order['ebook'][0] = $custom[0];
445
446 $order['ebook_key_array'][0] = $ebook_key;
447 $order['ebook_key'][0] = $ebook_key;
448 // md5_nonce is already the scalar copied out of $ipn above. The old
449 // `$order['md5_nonce'][0] = ...` wrote into a string offset, which
450 // PHP warns about and which only ever replaced the first byte.
451 $order['md5_nonce'] = $ipn['md5_nonce'];
452
453 update_post_meta($post_id,'ebook_key',$ebook_key);
454 update_post_meta($post_id,'downloads',0);
455 update_post_meta($post_id,'mc_gross',$mc_gross);
456 update_post_meta($post_id,'formData',wp_slash($formData));
457 update_post_meta($post_id,'downloadlink',$order['downloadlink']);
458 update_post_meta($post_id,'ebook',$custom[0]);
459
460 // The shared resolver honours the "No password" and random-password
461 // settings and stores the result under the order's password meta.
462 $password_context = array(
463 'payer_email' => $ipn['payer_email'],
464 'password' => '',
465 'md5_nonce' => $ipn['md5_nonce'],
466 'ebook_key' => $ebook_key,
467 );
468 if (get_option('ebook_store_random_password') == 1) {
469 // wp_generate_password() uses a CSPRNG. The old
470 // substr(md5(microtime()),0,8) was guessable from the order
471 // timestamp.
472 $password_context['password'] = wp_generate_password( 10, false, false );
473 }
474 $order['password'] = (string) ebook_store_get_password_for_order( $post_id, $password_context );
475
476 global $attachment;
477 $attachment = ebook_attachment($custom[0],true); //important!!!
478 //$order['downloadlink_html'] = '<a href="'.ebook_download_link($ebook_order).'" target="_blank" rel="noopener">'.$_REQUEST['item_name'].'</a> ('.ebook_store_human_filesize(filesize($attachment[0]['file'])).')';
479
480 $ebookObj = new EbookStoreEbook(@$ebook_order['ebook'][0]);
481 // Plugin order: give link() the ebook_key credentials so the
482 // non-PDF format links use the download path this order is
483 // authorised for, not the WooCommerce-only endpoint.
484 $ebookObj->ebook_key = $ebook_key;
485 $ebookObj->md5_nonce = $ipn['md5_nonce'];
486 $ebookObj->order_id = $post_id;
487 $ebookObj->setLink['pdf'] = $order['downloadlink'];
488
489 $order['downloadlink_html'] = $ebookObj->format_links();
490 $order['ebook_bonus'] = (implode("<br />",ebook_download_links_bonus($order)) != '' ? implode("<br />",ebook_download_links_bonus($order)) : __('None','ebook-store'));
491
492 $email_template = ebook_store_get_effective_delivery_email_template( (int) $custom[0] );
493 $ebook_email_delivery = array(
494 'to' => $ipn['payer_email'],
495 'subject' => $email_template['subject'],
496 'text' => $email_template['text'],
497 'attachment' => $attachment,
498 'order' => $order,
499 'source' => 'paypal',
500 'related_order_type' => 'ebook_order',
501 );
502 $fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
503 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) { //$fileExt == 'pdf' && get_option('encrypt_pdf')
504 add_action( 'init', 'ebook_encrypt_pdf', 99 );
505 // error_log('ecnrypt pdf added to init');
506 }
507
508 add_action( 'init', 'ebook_email_delivery', 100);
509 ebook_store_ga4_track_purchase_for_order( $post_id );
510 if (get_option('ebook_store_wp_affiliate_integration') == 1) {
511 // Last, and isolated: a broken affiliate plugin used to fatal here
512 // before the order's key, link, password and email were written.
513 try {
514 do_action( 'ebook_store_payment_completed', $_POST, $post_id );
515 } catch ( \Throwable $e ) {
516 error_log( 'Ebook Store IPN: affiliate hook failed - ' . $e->getMessage() );
517 }
518 }
519 //error_log('ebook_email_delivery added to plugins_loaded');
520 //array('to' => $_REQUEST['payer_email'], 'subject' => get_option('email_delivery_subject'), 'text' => 'teeext', 'file' => $attachment[0]['file'])
521 } else {
522 // The payment was genuine but the amount does not match the ebook
523 // that was requested.
524 ebook_store_notify_admin_of_ipn_problem(
525 __( 'A PayPal payment was received, but the amount does not match the ebook that was requested. No order was created and nothing was delivered.', 'ebook-store' ),
526 $listener->getTextReport()
527 );
528 }
529 header("HTTP/1.1 200 OK");
530 } else {
531 // PayPal itself refused to confirm this notification.
532 ebook_store_notify_admin_of_ipn_problem(
533 __( 'A payment notification arrived that PayPal could not verify. It was ignored.', 'ebook-store' ),
534 $listener->getTextReport()
535 );
536 }
537 }
538 }
539
540 /**
541 * Is PayPal postback verification switched on?
542 *
543 * Treats "never configured" as ON. The option shipped defaulting to 0 and its
544 * label reads like a troubleshooting toggle, so most installs had the single
545 * control that authenticates a payment notification quietly disabled.
546 *
547 * @return bool
548 */
549 function ebook_store_paypal_verification_enabled() {
550 $value = get_option( 'paypal_verify_transactions', null );
551
552 // Only a missing option means "the owner never made a choice" — verify.
553 // The settings sanitizer stores an explicit '0' when the toggle is off.
554 if ( $value === null ) {
555 return true;
556 }
557
558 return (string) $value !== '0';
559 }
560
561 /**
562 * The PayPal accounts this store is willing to be paid into.
563 *
564 * @return array Lower-cased email addresses / merchant IDs.
565 */
566 function ebook_store_paypal_expected_receivers() {
567 $receivers = array( (string) get_option( 'paypal_account', '' ) );
568
569 // Stores that route some books to a second account.
570 $multiple = get_option( 'paypal_multiple_business', '' );
571
572 if ( is_string( $multiple ) && $multiple !== '' ) {
573 $receivers = array_merge( $receivers, preg_split( '/[\s,;]+/', $multiple ) );
574 } elseif ( is_array( $multiple ) ) {
575 $receivers = array_merge( $receivers, $multiple );
576 }
577
578 $receivers = array_filter( array_map( 'strtolower', array_map( 'trim', $receivers ) ) );
579
580 /**
581 * Filters the accounts an IPN may name as the payee.
582 *
583 * @param array $receivers Lower-cased addresses.
584 */
585 return array_values( array_unique( (array) apply_filters( 'ebook_store_ipn_expected_receivers', $receivers ) ) );
586 }
587
588 /**
589 * Was this payment actually made to this store?
590 *
591 * PayPal's postback only attests that PayPal sent the notification — not that
592 * the money came to you. Without this check an attacker can pay themselves the
593 * right amount on their own account and point notify_url at someone else's shop,
594 * which verifies cleanly and hands over the book.
595 *
596 * @param array $ipn Sanitised IPN fields.
597 * @return bool
598 */
599 function ebook_store_ipn_receiver_is_us( $ipn ) {
600 $expected = ebook_store_paypal_expected_receivers();
601
602 // Nothing configured to compare against — do not block the store's income.
603 if ( empty( $expected ) ) {
604 return true;
605 }
606
607 foreach ( array( 'receiver_email', 'business', 'receiver_id' ) as $field ) {
608 $value = isset( $ipn[ $field ] ) ? strtolower( trim( (string) $ipn[ $field ] ) ) : '';
609
610 if ( $value !== '' && in_array( $value, $expected, true ) ) {
611 return true;
612 }
613 }
614
615 return false;
616 }
617
618 /**
619 * Payment statuses that should hand over the book.
620 *
621 * @return array
622 */
623 function ebook_store_ipn_accepted_statuses() {
624 return (array) apply_filters( 'ebook_store_ipn_accepted_payment_statuses', array( 'Completed' ) );
625 }
626
627 /**
628 * Payment statuses PayPal sends when money goes back to the buyer, or when a
629 * chargeback is withdrawn again. They arrive on the ORIGINAL notify_url, with
630 * the original transaction id in parent_txn_id.
631 *
632 * @return array
633 */
634 function ebook_store_ipn_refund_statuses() {
635 return array( 'Refunded', 'Reversed', 'Canceled_Reversal' );
636 }
637
638 /**
639 * Update the order a Refunded / Reversed / Canceled_Reversal notification
640 * refers to. Nothing is created or delivered here.
641 *
642 * @param array $ipn Sanitised IPN fields.
643 * @return bool True when a matching order was found and updated.
644 */
645 function ebook_store_handle_ipn_refund_notification( $ipn ) {
646 $parent_txn_id = '';
647 if ( isset( $_POST['parent_txn_id'] ) && is_scalar( $_POST['parent_txn_id'] ) ) {
648 $parent_txn_id = sanitize_text_field( wp_unslash( $_POST['parent_txn_id'] ) );
649 }
650
651 if ( $parent_txn_id === '' || ! function_exists( 'ebook_store_get_order_id_by_meta' ) ) {
652 return false;
653 }
654
655 $order_id = ebook_store_get_order_id_by_meta( 'txn_id', $parent_txn_id );
656 if ( ! $order_id ) {
657 return false;
658 }
659
660 if ( $ipn['payment_status'] === 'Canceled_Reversal' ) {
661 // The chargeback was withdrawn: the order is paid again.
662 update_post_meta( $order_id, 'payment_status', 'Completed' );
663 update_post_meta( $order_id, 'refund_gateway_status', 'canceled_reversal' );
664 delete_post_meta( $order_id, 'refund_gateway_id' );
665 return true;
666 }
667
668 if ( function_exists( 'ebook_store_mark_order_refunded' ) ) {
669 // mc_gross is negative on a refund notification.
670 ebook_store_mark_order_refunded(
671 $order_id,
672 'paypal',
673 $ipn['txn_id'],
674 strtolower( $ipn['payment_status'] ),
675 number_format( abs( (float) $ipn['mc_gross'] ), 2, '.', '' ),
676 $ipn['mc_currency']
677 );
678 }
679
680 return true;
681 }
682
683 /**
684 * Has this PayPal transaction already been turned into an order?
685 *
686 * The only duplicate guard used to be the ebook_key from the query string, which
687 * is not part of what PayPal signs — so one genuine notification could be
688 * replayed with a fresh key over and over, each replay minting a new order and a
689 * new download link. txn_id is unique per PayPal transaction.
690 *
691 * @param string $txn_id Transaction id.
692 * @return bool
693 */
694 function ebook_store_ipn_already_recorded( $txn_id ) {
695 $txn_id = trim( (string) $txn_id );
696
697 if ( $txn_id === '' ) {
698 return false;
699 }
700
701 $existing = get_posts(
702 array(
703 'post_type' => 'ebook_order',
704 'post_status' => 'any',
705 'numberposts' => 1,
706 'fields' => 'ids',
707 'meta_key' => 'txn_id',
708 'meta_value' => $txn_id,
709 'no_found_rows' => true,
710 'suppress_filters' => false,
711 )
712 );
713
714 return ! empty( $existing );
715 }
716
717 /**
718 * Warn the store owner while payment verification is switched off.
719 */
720 function ebook_store_paypal_verification_notice() {
721 if ( ! current_user_can( 'manage_options' ) || ebook_store_paypal_verification_enabled() ) {
722 return;
723 }
724
725 if ( get_option( 'paypal_integration_enabled' ) != 1 ) {
726 return;
727 }
728
729 echo '<div class="notice notice-error"><p><strong>' .
730 esc_html__( 'Ebook Store: PayPal payment verification is switched off.', 'ebook-store' ) .
731 '</strong> ' .
732 esc_html__( 'Until you switch it back on, anyone can send your site a fake payment notification and be given your books for free. Turn on "Confirm every payment with PayPal" on the PayPal settings tab.', 'ebook-store' ) .
733 '</p></div>';
734 }
735 add_action( 'admin_notices', 'ebook_store_paypal_verification_notice' );
736
737 /**
738 * Email the site administrator about a rejected payment notification.
739 *
740 * Uses wp_mail() so SMTP plugins and the wp_mail_from filters apply. The old
741 * code called mail() directly with a message argument that PHP's operator
742 * precedence turned into a boolean, so the alerts arrived empty when they
743 * arrived at all.
744 *
745 * @param string $summary Human explanation.
746 * @param string $report Raw IPN report.
747 */
748 function ebook_store_notify_admin_of_ipn_problem( $summary, $report ) {
749 // This endpoint is anonymous, so without a ceiling a loop of junk POSTs turns
750 // into one admin email per request — enough to burn an SMTP quota and wreck
751 // the domain's sending reputation. A handful an hour is plenty to notice a
752 // real problem.
753 $limit = (int) apply_filters( 'ebook_store_ipn_alerts_per_hour', 5 );
754 $sent = (int) get_transient( 'ebook_store_ipn_alerts' );
755
756 if ( $sent >= $limit ) {
757 error_log( 'Ebook Store IPN: alert suppressed (hourly limit reached) — ' . $summary );
758 return;
759 }
760
761 set_transient( 'ebook_store_ipn_alerts', $sent + 1, HOUR_IN_SECONDS );
762
763 $body = $summary . "\n\n" . __( 'Details reported by PayPal:', 'ebook-store' ) . "\n\n" . $report;
764
765 if ( $sent + 1 >= $limit ) {
766 $body .= "\n\n" . __( 'Further alerts are paused for an hour to avoid flooding your inbox.', 'ebook-store' );
767 }
768
769 wp_mail(
770 get_option( 'admin_email' ),
771 __( 'Ebook Store: a payment notification was rejected', 'ebook-store' ),
772 $body
773 );
774 }
775
776 add_action( 'wp_loaded', 'ebook_store_add_to_cart');
777 add_action( 'init', 'ebook_create_post_type', 99);
778 register_activation_hook( __FILE__, 'ebook_store_activate' );
779
780 /**
781 * Tidy up scheduled work when the plugin is switched off.
782 *
783 * Without this the daily temp-upload sweep stayed in the cron table forever,
784 * firing an action nothing was listening for.
785 */
786 function ebook_store_deactivate() {
787 $timestamp = wp_next_scheduled( 'ebook_store_cleanup_uploads' );
788
789 while ( $timestamp ) {
790 wp_unschedule_event( $timestamp, 'ebook_store_cleanup_uploads' );
791 $timestamp = wp_next_scheduled( 'ebook_store_cleanup_uploads' );
792 }
793
794 wp_clear_scheduled_hook( 'ebook_store_cleanup_uploads' );
795
796 if ( function_exists( 'ebook_store_unschedule_license_check' ) ) {
797 ebook_store_unschedule_license_check();
798 }
799 }
800 register_deactivation_hook( __FILE__, 'ebook_store_deactivate' );
801
802 //register_deactivation_hook( __FILE__, 'ebook_store_activate' );
803
804 add_action( 'init', 'ebook_process_download', 100 );
805
806 add_action( 'init', 'ebook_process_download_woocomerce', 101 );
807
808 add_action( 'init', 'ebook_store_export_orders', 101 );
809
810 add_action( 'init', 'ebook_store_wp_super_cache_check',102);
811
812
813 // Registered unconditionally. Deciding at plugin-load time from $_GET made the
814 // set of registered hooks differ between requests, which breaks under REST and
815 // cron; the callback itself checks the column it was given.
816 add_action( 'manage_posts_custom_column', 'order_custom_columns' );
817
818 add_action('add_meta_boxes', 'ebook_add_custom_meta_boxes');
819 add_action('post_edit_form_tag', 'ebook_update_edit_form');
820
821 add_action( 'save_post_ebook', 'save_custom_meta_data' );
822 add_action( 'save_post', 'save_custom_meta_data_order' );
823
824 //add_action('init','ebook_store_post_type_view');
825
826 add_filter( 'the_content', 'ebook_store_post_type_view' );
827 add_filter( 'enter_title_here', 'custom_enter_title_author' );
828 add_filter( 'enter_title_here', 'custom_enter_title_publisher' );
829 add_filter("manage_edit-ebook_order_columns", "order_columns");
830 add_shortcode( 'ebook_store', 'ebook_store' );
831 add_shortcode( 'ebook_store_buy', 'ebook_store_buy' );
832
833 add_shortcode( 'ebook_thank_you', 'ebook_store' );
834 if (get_option('ebook_store_checkout_page') == 0) {
835 add_action( 'admin_notices', 'ebook_store_admin_notice' );
836 } else {
837 //wp_die(var_dump(get_option('ebook_store_checkout_page')));
838 }
839 if (!function_exists('imagecreatefrompng')) {
840 add_action( 'admin_notices', 'ebook_store_missing_gd' );
841 }
842 if (get_option('paypal_account') == '') {
843 add_action( 'admin_notices', 'ebook_store_admin_notice_paypal' );
844 }
845 if (get_option('ebook_store_woocommerce_integration') == 1) {
846 //add_action( 'admin_notices', 'ebook_store_admin_notice_autocompleteorders' );
847 }
848
849
850 add_action('init','ebook_store_offer_tutorial');
851
852
853
854 add_filter('post_updated_messages', 'ebook_store_set_messages' );
855 add_action( 'admin_head-post-new.php', 'ebook_admin_css' );
856 add_action( 'admin_head-post.php', 'ebook_admin_css' );
857 add_action( 'admin_head', 'ebook_store_render_dashboard_styles' );
858 add_action( 'admin_head', 'ebook_store_render_ebook_list_styles' );
859 add_action( 'all_admin_notices', 'ebook_store_render_ebook_list_summary' );
860 add_action( 'wp_dashboard_setup', 'ebook_store_register_dashboard_widget' );
861 add_filter( 'manage_edit-ebook_columns', 'ebook_store_set_columns' ) ;
862 add_action( 'manage_ebook_posts_custom_column', 'ebook_store_columns_output', 10, 2 );
863 add_filter('upload_mimes', 'ebook_mime_types');
864 add_action('admin_menu', 'ebook_store_register_my_custom_submenu_page');
865 add_action( 'admin_enqueue_scripts', 'ebook_store_import_books_admin_assets' );
866 add_action( 'wp_ajax_ebook_store_import_books_upload', 'ebook_store_import_books_ajax_upload' );
867 add_action( 'wp_ajax_ebook_store_import_books_process', 'ebook_store_import_books_ajax_process' );
868
869
870 add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), 'ebook_store_action_links' );
871
872
873 // Optional drop-in modules. Guarded by a directory check so the common case —
874 // no such directory — costs one stat() instead of a glob() on every request.
875 $ebook_store_modules_dir = ABSPATH . 'ebook_store_modules';
876 if ( is_dir( $ebook_store_modules_dir ) ) {
877 foreach ( glob( $ebook_store_modules_dir . '/*.php' ) as $ebook_store_module ) {
878 include_once $ebook_store_module;
879 }
880 }
881
882
883 add_filter("ebook_store_payment_gateway_parameters", "ebook_store_payment_gateway_parameters_wpam");
884 add_action("ebook_store_payment_completed", "ebook_store_payment_completed_wpam", 10, 2);
885
886 function ebook_store_endsWith( $str, $sub ) {
887 return ( substr( $str, strlen( $str ) - strlen( $sub ) ) === $sub );
888 }
889 add_action( 'init', 'ebook_store_my_taxonomies_product', 0 );
890 add_action( 'admin_init', 'ebook_store_redirect_add_new' );
891
892 /**
893 * Orders are created through a dedicated screen, not through the generic
894 * "Add New" post form. Send anyone who lands there to the right place.
895 *
896 * Runs on admin_init (it can never match on the front end) and uses a real HTTP
897 * redirect instead of wp_die() with a JavaScript location assignment.
898 */
899 function ebook_store_redirect_add_new() {
900 global $pagenow;
901
902 if ( $pagenow !== 'post-new.php' ) {
903 return;
904 }
905
906 if ( ! isset( $_GET['post_type'] ) || $_GET['post_type'] !== 'ebook_order' ) {
907 return;
908 }
909
910 wp_safe_redirect( admin_url( 'edit.php?post_type=ebook&page=ebook-store-add-order-page' ) );
911 exit;
912 }
913 //
914 add_action( 'woocommerce_process_product_meta_ebookstore', 'save_woocommerce_ebook_store_data' );
915 add_action( 'woocommerce_process_product_meta', 'save_woocommerce_ebook_store_data' );
916
917 //add_action( 'woocommerce_process_product_meta_variable_rental', 'save_woocommerce_ebook_store_data' );
918 add_action( 'woocommerce_order_details_after_order_table', 'ebook_store_woocommerce_order_details', 10, 1 );
919
920 //add_filter( 'woocommerce_payment_complete_order_status', 'ebook_store_woocommerce_email_delivery', 10, 2 );
921 add_filter( 'woocommerce_order_status_completed', 'ebook_store_woocommerce_email_delivery_completed', 10, 2 );
922 add_filter( 'woocommerce_order_status_pending', 'ebook_store_woocommerce_email_delivery_pending', 10, 2 );
923 add_filter( 'woocommerce_order_status_processing', 'ebook_store_woocommerce_email_delivery_processing', 10, 2 );
924 add_filter( 'woocommerce_order_status_on-hold', 'ebook_store_woocommerce_email_delivery_on_hold', 10, 2 );
925 add_filter( 'woocommerce_order_status_cancelled', 'ebook_store_woocommerce_email_delivery_cancelled', 10, 2 );
926 add_action( 'save_post_ebook', 'ebook_store_flush_dashboard_cache' );
927 add_action( 'save_post_ebook_order', 'ebook_store_flush_dashboard_cache' );
928 add_action( 'trashed_post', 'ebook_store_flush_dashboard_cache' );
929 add_action( 'untrashed_post', 'ebook_store_flush_dashboard_cache' );
930 add_action( 'delete_post', 'ebook_store_flush_dashboard_cache' );
931
932
933
934 add_filter( 'query_vars', 'ebook_store_add_query_vars_filter' );
935 add_action( 'woocommerce_product_data_panels', 'ebook_store_woocommerce_tab_content' );
936 add_action( 'woocommerce_product_after_variable_attributes', 'ebook_store_woocommerce_variation_fields', 10, 3 );
937 add_action( 'woocommerce_save_product_variation', 'ebook_store_save_woocommerce_variation_data', 10, 2 );
938
939
940 //add_filter( 'upload_dir', 'ebook_set_upload_dir' );
941
942 // Before VC Init
943 add_action( 'vc_before_init', 'ebook_store_vc_before_init_actions' );
944
945 // add_action('init', 'ebook_store_session_start', 1);
946 // add_action('wp_logout', 'ebook_store_session_end');
947 // add_action('wp_login', 'ebook_store_session_end');
948 add_action('ebook_store_file_formats_form','ebook_store_file_formats_form');
949 add_action( 'init', 'register_ebook_store_woocommerce_type' );
950
951 // add_filter( 'woocommerce_data_stores', 'woocommerce_data_stores_ebook_store' );
952 add_filter( 'woocommerce_product_data_tabs', 'woocommerce_custom_product_tabs_for_ebook_store' );
953
954 add_action( 'admin_footer', 'woocommerce_ebook_store_price_field' );
955
956
957
958
959
960
961 add_shortcode( 'ebook_store_row', 'ebook_store_row' );
962
963 add_shortcode( 'ebook_store_downloads', 'ebook_store_downloads' );
964
965 // No ebook_store_wpforms() callback ships in this build. Registering the
966 // shortcode anyway turned [ebook_store_wpforms] on any page into a fatal error.
967 if ( function_exists( 'ebook_store_wpforms' ) ) {
968 add_shortcode( 'ebook_store_wpforms', 'ebook_store_wpforms' );
969 }
970
971
972
973 //add zip file support for media uploads
974 function ebook_store_zip_upload_mimes($existing_mimes = array()) {
975 $existing_mimes['zip'] = 'application/zip';
976 $existing_mimes['gz'] = 'application/x-gzip';
977 return $existing_mimes;
978 }
979 add_filter('upload_mimes', 'ebook_store_zip_upload_mimes', 999, 1);
980
981