| 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 |
|