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

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

15,809 lines 615.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // error_reporting(E_ALL);
4
5 include_once('EbookStoreEbook.class.php');
6
7
8
9 function ebook_activate() {
10 // register taxonomies/post types here
11 flush_rewrite_rules();
12 }
13
14 function ebook_deactivate() {
15 flush_rewrite_rules();
16 }
17
18 function ebook_store_activate() {
19 ebook_create_post_type();
20 ebook_store_ensure_protected_dirs();
21 flush_rewrite_rules();
22 }
23
24 function ebook_create_post_type() {
25 $labels = array(
26 'name' => _x('Ebook Store', 'post type general name', 'ebook-store'),
27 'singular_name' => _x('Ebook', 'post type singular name', 'ebook-store'),
28 'add_new' => _x('Add New Ebook', 'ebook', 'ebook-store'),
29 'add_new_item' => __('Add New Ebook Item', 'ebook-store'),
30 'edit_item' => __('Edit Item', 'ebook-store'),
31 'all_items' => __('Ebooks', 'ebook-store'),
32 'new_item' => __('New Ebook Item', 'ebook-store'),
33 'view_item' => __('View Ebook Item', 'ebook-store'),
34 'search_items' => __('Search Ebook', 'ebook-store'),
35 'not_found' => __('Nothing found', 'ebook-store'),
36 'not_found_in_trash' => __('Nothing found in Trash', 'ebook-store'),
37 'parent_item_colon' => ''
38 );
39
40 $args = array(
41 'labels' => $labels,
42 'public' => true,
43 'publicly_queryable' => true,
44 'show_ui' => true,
45 'show_in_menu' => true,
46 'show_in_nav_menus' => true,
47 'query_var' => true,
48 'rewrite' => array('slug','ebook'),
49 'capability_type' => 'page',
50 'hierarchical' => true,
51 //'menu_position' => 5,
52 'has_archive' => true,
53 //'taxonomies' => array('category'),
54 'supports' => array('title','editor'),
55 'menu_icon' => 'dashicons-cart',
56 );
57
58 register_post_type( 'ebook' , $args );
59
60 $labels = array(
61 'name' => __('Authors', 'ebook-store'),
62 'singular_name' => __('Author', 'ebook-store'),
63 'add_new' => __('Add New', 'ebook-store'),
64 'add_new_item' => __('Add New Author', 'ebook-store'),
65 'edit_item' => __('Edit Author', 'ebook-store'),
66 'new_item' => __('New Author', 'ebook-store'),
67 'all_items' => __('Authors', 'ebook-store'),
68 'view_item' => __('View Author', 'ebook-store'),
69 'search_items' => __('Search Author', 'ebook-store'),
70 'not_found' => __('No authors found', 'ebook-store'),
71 'not_found_in_trash' => __('No authors found in Trash', 'ebook-store'),
72 'parent_item_colon' => '',
73 'menu_name' => __('Authors', 'ebook-store'),
74
75 );
76
77 $args = array(
78 'labels' => $labels,
79 'public' => true,
80 'publicly_queryable' => true,
81 'show_ui' => true,
82 'show_in_menu' => 'edit.php?post_type=ebook',
83 'query_var' => true,
84 'rewrite' => array( 'slug' => 'ebook_author' ),
85 'capability_type' => 'post',
86 'has_archive' => true,
87 'hierarchical' => false,
88 //'menu_position' => 5,
89 'supports' => array( 'title', 'thumbnail','comments' )
90 );
91
92 register_post_type( 'ebook_author', $args );
93 /////
94 $labels = array(
95 'name' => __('Publishers', 'ebook-store'),
96 'singular_name' => __('Publisher', 'ebook-store'),
97 'add_new' => __('Add New', 'ebook-store'),
98 'add_new_item' => __('Add New Publisher', 'ebook-store'),
99 'edit_item' => __('Edit Publisher', 'ebook-store'),
100 'new_item' => __('New Publisher', 'ebook-store'),
101 'all_items' => __('Publishers', 'ebook-store'),
102 'view_item' => __('View Publisher', 'ebook-store'),
103 'search_items' => __('Search Publisher', 'ebook-store'),
104 'not_found' => __('No Publishers found', 'ebook-store'),
105 'not_found_in_trash' => __('No Publishers found in Trash', 'ebook-store'),
106 'parent_item_colon' => '',
107 'menu_name' => __('Publishers', 'ebook-store'));
108
109 $args = array(
110 'labels' => $labels,
111 'public' => true,
112 'publicly_queryable' => true,
113 'show_ui' => true,
114 'show_in_menu' => 'edit.php?post_type=ebook',
115 'query_var' => true,
116 'rewrite' => array( 'slug' => 'publisher' ),
117 'capability_type' => 'post',
118 'has_archive' => true,
119 'hierarchical' => false,
120 //'menu_position' => 5,
121 'supports' => array( 'title', 'thumbnail','comments' )
122 );
123
124 register_post_type( 'ebook_publisher', $args );
125 ///////
126 $labels = array(
127 'name' => __('Orders', 'ebook-store'),
128 'singular_name' => __('Order', 'ebook-store'),
129 'add_new' => __('Add New', 'ebook-store'),
130 //'add_new_item' => 'Add New Order',
131 'edit_item' => __('Edit Order', 'ebook-store'),
132 'new_item' =>__( 'New Order', 'ebook-store'),
133 'all_items' => __('Orders', 'ebook-store'),
134 'view_item' => __('View Order Details', 'ebook-store'),
135 'search_items' => __('Search Order', 'ebook-store'),
136 'not_found' => __('No orders found', 'ebook-store'),
137 'not_found_in_trash' => __('No orders found in Trash', 'ebook-store'),
138 'parent_item_colon' => '',
139 'menu_name' => __('Orders', 'ebook-store'),
140 );
141
142 $args = array(
143 'labels' => $labels,
144 'public' => true,
145 'publicly_queryable' => false,
146 'show_ui' => true,
147 'show_in_menu' => 'edit.php?post_type=ebook',
148 'query_var' => true,
149 'rewrite' => array( 'slug' => 'order' ),
150 'capability_type' => 'post',
151 'has_archive' => false,
152 'hierarchical' => false,
153 //'menu_position' => 5,
154 'supports' => array('title','excerpt'),
155 // 'capabilities' => array(
156 // 'order' => true,
157 // 'read_ebook_order' => true,
158 // 'delete_ebook_order' => true,
159 // // 'delete_posts' => true,
160 // // 'edit_posts' => true,
161 // // 'edit_others_posts' => true,
162 // // 'publish_posts' => true,
163 // // 'read_private_posts' => false,
164 // // 'create_posts' => false,
165 // ),
166 );
167
168 register_post_type( 'ebook_order', $args );
169
170 $labels = array(
171 'name' => __('Email Log', 'ebook-store'),
172 'singular_name' => __('Email Entry', 'ebook-store'),
173 'add_new' => __('Add New', 'ebook-store'),
174 'edit_item' => __('View Email Entry', 'ebook-store'),
175 'new_item' => __('New Email Entry', 'ebook-store'),
176 'all_items' => __('Email Log', 'ebook-store'),
177 'view_item' => __('View Email Entry', 'ebook-store'),
178 'search_items' => __('Search Email Log', 'ebook-store'),
179 'not_found' => __('No email log entries found', 'ebook-store'),
180 'not_found_in_trash' => __('No email log entries found in Trash', 'ebook-store'),
181 'parent_item_colon' => '',
182 'menu_name' => __('Email Log', 'ebook-store'),
183 );
184
185 $args = array(
186 'labels' => $labels,
187 'public' => false,
188 'publicly_queryable' => false,
189 'show_ui' => true,
190 'show_in_menu' => 'edit.php?post_type=ebook',
191 'query_var' => false,
192 'rewrite' => false,
193 'capability_type' => 'page',
194 'has_archive' => false,
195 'hierarchical' => false,
196 'supports' => array('title'),
197 );
198
199 register_post_type( 'ebook_email_log', $args );
200 //wp_die(print_r($GLOBALS['wp_post_types'],true));
201 }
202
203 function ebook_add_custom_meta_boxes() {
204
205 // Define the custom attachment for posts
206 add_meta_box(
207 'ebook_wp_custom_attachment',
208 __('Ebook Details', 'ebook-store'),
209 'ebook_wp_custom_attachment',
210 'ebook',
211 'advanced'
212 );
213 add_meta_box(
214 'ebook_wp_embed_ebook',
215 __('Ebook Store - Click on Ebook to embed it', 'ebook-store'),
216 'ebook_wp_embed_ebook',
217 'post',
218 'advanced'
219 );
220 add_meta_box(
221 'ebook_wp_embed_ebook',
222 __('Ebook Store - Click Ebook to Embed', 'ebook-store'),
223 'ebook_wp_embed_ebook',
224 'page',
225 'advanced'
226 );
227 // Define the custom attachment for posts
228 // add_meta_box(
229 // 'ebook_code_box',
230 // 'Ebook Embed Code Box',
231 // 'ebook_code_box',
232 // 'ebook',
233 // 'side'
234 // );
235 add_meta_box(
236 'ebook_order_box',
237 'Order details',
238 'ebook_order_box',
239 'ebook_order',
240 'advanced'
241 );
242
243 add_meta_box(
244 'ebook_email_log_box',
245 __('Email details', 'ebook-store'),
246 'ebook_store_render_email_log_box',
247 'ebook_email_log',
248 'advanced'
249 );
250 }
251
252 function ebook_store_get_order_field_sections() {
253 static $sections = null;
254 if ($sections !== null) {
255 return $sections;
256 }
257
258 $sections = array(
259 'summary' => array(
260 'title' => __('Order summary', 'ebook-store'),
261 'description' => __('Key totals and product level identifiers that are most commonly updated.', 'ebook-store'),
262 'open' => true,
263 'fields' => array(
264 'item_name' => array(
265 'label' => __('Item name', 'ebook-store'),
266 'width' => 'full',
267 ),
268 'item_number' => array(
269 'label' => __('Item number / SKU', 'ebook-store'),
270 'width' => 'half',
271 ),
272 'quantity' => array(
273 'label' => __('Quantity', 'ebook-store'),
274 'width' => 'third',
275 ),
276 'mc_gross' => array(
277 'label' => __('Gross total', 'ebook-store'),
278 'width' => 'third',
279 ),
280 'payment_gross' => array(
281 'label' => __('Payment gross', 'ebook-store'),
282 'width' => 'third',
283 ),
284 'tax' => array(
285 'label' => __('Tax', 'ebook-store'),
286 'width' => 'third',
287 ),
288 'mc_currency' => array(
289 'label' => __('Currency', 'ebook-store'),
290 'width' => 'third',
291 ),
292 'payment_status' => array(
293 'label' => __('Payment status', 'ebook-store'),
294 'width' => 'third',
295 ),
296 'payment_date' => array(
297 'label' => __('Payment date', 'ebook-store'),
298 'width' => 'third',
299 ),
300 ),
301 ),
302 'customer' => array(
303 'title' => __('Customer', 'ebook-store'),
304 'description' => __('Buyer contact information, used for receipts and encryption.', 'ebook-store'),
305 'open' => true,
306 'fields' => array(
307 'first_name' => array(
308 'label' => __('First name', 'ebook-store'),
309 'width' => 'half',
310 ),
311 'last_name' => array(
312 'label' => __('Last name', 'ebook-store'),
313 'width' => 'half',
314 ),
315 'payer_email' => array(
316 'label' => __('Buyer email', 'ebook-store'),
317 'width' => 'full',
318 ),
319 'payer_id' => array(
320 'label' => __('Payer ID', 'ebook-store'),
321 'width' => 'half',
322 ),
323 'payer_status' => array(
324 'label' => __('Payer status', 'ebook-store'),
325 'width' => 'half',
326 ),
327 'residence_country' => array(
328 'label' => __('Country', 'ebook-store'),
329 'width' => 'third',
330 ),
331 'shipping' => array(
332 'label' => __('Shipping', 'ebook-store'),
333 'width' => 'third',
334 ),
335 'handling_amount' => array(
336 'label' => __('Handling', 'ebook-store'),
337 'width' => 'third',
338 ),
339 ),
340 ),
341 'transaction' => array(
342 'title' => __('Transaction details', 'ebook-store'),
343 'description' => __('Gateway responses and identifiers for this order.', 'ebook-store'),
344 'open' => false,
345 'fields' => array(
346 'txn_id' => array(
347 'label' => __('Transaction ID', 'ebook-store'),
348 'width' => 'full',
349 ),
350 'txn_type' => array(
351 'label' => __('Transaction type', 'ebook-store'),
352 'width' => 'half',
353 ),
354 'payment_type' => array(
355 'label' => __('Payment type', 'ebook-store'),
356 'width' => 'half',
357 ),
358 'payment_fee' => array(
359 'label' => __('Payment fee', 'ebook-store'),
360 'width' => 'third',
361 ),
362 'business' => array(
363 'label' => __('Business email', 'ebook-store'),
364 'width' => 'half',
365 ),
366 'receiver_email' => array(
367 'label' => __('Receiver email', 'ebook-store'),
368 'width' => 'half',
369 ),
370 'receiver_id' => array(
371 'label' => __('Receiver ID', 'ebook-store'),
372 'width' => 'half',
373 ),
374 'protection_eligibility' => array(
375 'label' => __('Protection eligibility', 'ebook-store'),
376 'width' => 'half',
377 ),
378 ),
379 ),
380 'advanced' => array(
381 'title' => __('Advanced metadata', 'ebook-store'),
382 'description' => __('Less frequently edited IPN fields. Collapse this section if you do not need to tweak them.', 'ebook-store'),
383 'open' => false,
384 'fields' => array(
385 'charset' => array(
386 'label' => __('Charset', 'ebook-store'),
387 'width' => 'third',
388 ),
389 'mc_fee' => array(
390 'label' => __('Gateway fee', 'ebook-store'),
391 'width' => 'third',
392 ),
393 'notify_version' => array(
394 'label' => __('Notify version', 'ebook-store'),
395 'width' => 'third',
396 ),
397 'custom' => array(
398 'label' => __('Custom payload', 'ebook-store'),
399 'type' => 'textarea',
400 'width' => 'full',
401 ),
402 'verify_sign' => array(
403 'label' => __('Verify signature', 'ebook-store'),
404 'width' => 'full',
405 ),
406 'test_ipn' => array(
407 'label' => __('Sandbox flag', 'ebook-store'),
408 'width' => 'third',
409 ),
410 'transaction_subject' => array(
411 'label' => __('Transaction subject', 'ebook-store'),
412 'type' => 'textarea',
413 'width' => 'full',
414 ),
415 'ipn_track_id' => array(
416 'label' => __('IPN Track ID', 'ebook-store'),
417 'width' => 'half',
418 ),
419 ),
420 ),
421 );
422
423 return $sections;
424 }
425
426 function ebook_store_get_order_field_definitions() {
427 static $definitions = null;
428 if ($definitions !== null) {
429 return $definitions;
430 }
431 $definitions = array();
432 foreach (ebook_store_get_order_field_sections() as $section) {
433 foreach ($section['fields'] as $key => $config) {
434 $definitions[$key] = $config;
435 }
436 }
437 return $definitions;
438 }
439
440 function ebook_store_render_order_field($field_key, $config, $value) {
441 $label = isset($config['label']) ? $config['label'] : $field_key;
442 $type = isset($config['type']) ? $config['type'] : 'text';
443 $width = isset($config['width']) ? $config['width'] : 'half';
444 $input_id = 'ebook-order-field-' . sanitize_key($field_key);
445 $description = isset($config['description']) ? $config['description'] : '';
446 $placeholder = isset($config['placeholder']) ? $config['placeholder'] : '';
447 $readonly = !empty($config['readonly']);
448 $value = ebook_store_prepare_order_display_value($value);
449
450 $classes = array('ebook-order-field', 'ebook-order-field--' . $width);
451 echo '<div class="' . esc_attr(implode(' ', $classes)) . '">';
452 echo '<label for="' . esc_attr($input_id) . '">' . esc_html($label) . '</label>';
453 if ($type === 'textarea') {
454 echo '<textarea id="' . esc_attr($input_id) . '" name="order[' . esc_attr($field_key) . ']" rows="3" ' . ($readonly ? 'readonly="readonly"' : '') . ' placeholder="' . esc_attr($placeholder) . '">' . esc_textarea($value) . '</textarea>';
455 } else {
456 echo '<input type="text" class="regular-text" id="' . esc_attr($input_id) . '" name="order[' . esc_attr($field_key) . ']" value="' . esc_attr($value) . '" placeholder="' . esc_attr($placeholder) . '" ' . ($readonly ? 'readonly="readonly"' : '') . ' />';
457 }
458 if ($description) {
459 echo '<p class="description">' . esc_html($description) . '</p>';
460 }
461 echo '</div>';
462 }
463
464 function ebook_store_sanitize_order_field_value($value, $config) {
465 if (is_array($value)) {
466 $value = '';
467 }
468 $type = isset($config['type']) ? $config['type'] : 'text';
469 if ($type === 'textarea') {
470 return sanitize_textarea_field($value);
471 }
472 return sanitize_text_field($value);
473 }
474
475 function ebook_store_order_money_display( $amount, $currency = '' ) {
476 $amount = (float) $amount;
477 $symbol = '';
478
479 if ( class_exists( 'Currencies' ) ) {
480 $currencies = new Currencies();
481 $symbol = $currencies->getSymbol( $currency );
482 }
483
484 if ( empty( $symbol ) ) {
485 $symbol = $currency ? $currency . ' ' : '$';
486 }
487
488 return $symbol . number_format_i18n( $amount, 2 );
489 }
490
491 function ebook_store_prepare_order_display_value( $value, $fallback = '' ) {
492 if ( is_null( $value ) ) {
493 return $fallback;
494 }
495
496 if ( is_bool( $value ) ) {
497 return $value ? '1' : '0';
498 }
499
500 if ( is_scalar( $value ) ) {
501 return (string) $value;
502 }
503
504 if ( is_array( $value ) || is_object( $value ) ) {
505 $encoded = wp_json_encode( $value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
506 return is_string( $encoded ) ? $encoded : $fallback;
507 }
508
509 return $fallback;
510 }
511
512 function ebook_store_order_status_class( $value ) {
513 $value = strtolower( trim( (string) $value ) );
514
515 if ( in_array( $value, array( 'completed', 'complete', 'paid', 'success', 'verified' ), true ) ) {
516 return 'is-success';
517 }
518
519 if ( in_array( $value, array( 'processing', 'pending', 'on-hold', 'echeck' ), true ) ) {
520 return 'is-warning';
521 }
522
523 if ( in_array( $value, array( 'failed', 'denied', 'cancelled', 'canceled', 'refunded', 'reversed' ), true ) ) {
524 return 'is-danger';
525 }
526
527 return 'is-neutral';
528 }
529
530 function ebook_store_render_order_badge( $label, $value, $class = '' ) {
531 $value = ebook_store_prepare_order_display_value( $value );
532
533 if ( $value === '' ) {
534 return '';
535 }
536
537 $classes = trim( 'ebook-order-badge ' . $class );
538
539 return '<span class="' . esc_attr( $classes ) . '"><span class="ebook-order-badge__label">' . esc_html( $label ) . '</span><strong>' . esc_html( $value ) . '</strong></span>';
540 }
541
542 function ebook_store_is_negative_order_status( $status ) {
543 $status = strtolower( trim( (string) $status ) );
544
545 return in_array( $status, array( 'failed', 'denied', 'cancelled', 'canceled', 'refunded', 'reversed' ), true );
546 }
547
548 function ebook_store_get_dashboard_cache_key() {
549 return 'ebook_store_dashboard_summary_v1';
550 }
551
552 function ebook_store_get_ebook_sales_index_cache_key() {
553 return 'ebook_store_ebook_sales_index_v1';
554 }
555
556 function ebook_store_get_ebook_catalog_summary_cache_key() {
557 return 'ebook_store_ebook_catalog_summary_v1';
558 }
559
560 function ebook_store_get_reports_cache_key( $ebook_id = 0 ) {
561 $ebook_id = absint( $ebook_id );
562
563 return 'ebook_store_reports_v1_' . $ebook_id;
564 }
565
566 function ebook_store_flush_dashboard_cache( $post_id = 0 ) {
567 if ( $post_id && ! in_array( get_post_type( $post_id ), array( 'ebook_order', 'ebook' ), true ) ) {
568 return;
569 }
570
571 delete_transient( ebook_store_get_dashboard_cache_key() );
572 delete_transient( ebook_store_get_ebook_sales_index_cache_key() );
573 delete_transient( ebook_store_get_ebook_catalog_summary_cache_key() );
574 delete_transient( ebook_store_get_reports_cache_key() );
575 }
576
577 function ebook_store_get_dashboard_summary() {
578 $cache_key = ebook_store_get_dashboard_cache_key();
579 $cached = get_transient( $cache_key );
580 if ( false !== $cached && is_array( $cached ) ) {
581 return $cached;
582 }
583
584 global $wpdb;
585
586 $rows = $wpdb->get_results(
587 "
588 SELECT
589 p.ID,
590 p.post_date_gmt,
591 status.meta_value AS payment_status,
592 gross.meta_value AS mc_gross,
593 fee.meta_value AS mc_fee,
594 currency.meta_value AS mc_currency,
595 ebook.meta_value AS ebook_id,
596 item.meta_value AS item_name,
597 quantity.meta_value AS quantity
598 FROM {$wpdb->posts} p
599 LEFT JOIN {$wpdb->postmeta} status ON (p.ID = status.post_id AND status.meta_key = 'payment_status')
600 LEFT JOIN {$wpdb->postmeta} gross ON (p.ID = gross.post_id AND gross.meta_key = 'mc_gross')
601 LEFT JOIN {$wpdb->postmeta} fee ON (p.ID = fee.post_id AND fee.meta_key = 'mc_fee')
602 LEFT JOIN {$wpdb->postmeta} currency ON (p.ID = currency.post_id AND currency.meta_key = 'mc_currency')
603 LEFT JOIN {$wpdb->postmeta} ebook ON (p.ID = ebook.post_id AND ebook.meta_key = 'ebook')
604 LEFT JOIN {$wpdb->postmeta} item ON (p.ID = item.post_id AND item.meta_key = 'item_name')
605 LEFT JOIN {$wpdb->postmeta} quantity ON (p.ID = quantity.post_id AND quantity.meta_key = 'quantity')
606 WHERE p.post_type = 'ebook_order'
607 AND p.post_status = 'publish'
608 ",
609 ARRAY_A
610 );
611
612 $now_gmt = current_time( 'timestamp', true );
613 $last_24h_cutoff = $now_gmt - DAY_IN_SECONDS;
614 $last_7d_cutoff = $now_gmt - ( 7 * DAY_IN_SECONDS );
615 $default_currency = ebook_store_get_store_currency();
616
617 $summary = array(
618 'total_sales' => 0.0,
619 'total_profit' => 0.0,
620 'last_24h_sales' => 0.0,
621 'last_7d_sales' => 0.0,
622 'total_orders' => 0,
623 'reportable_orders' => 0,
624 'paid_orders' => 0,
625 'free_orders' => 0,
626 'average_order' => 0.0,
627 'latest_order_gmt' => '',
628 'latest_order_id' => 0,
629 'latest_order_amount' => 0.0,
630 'currency' => $default_currency,
631 'mixed_currencies' => false,
632 'currency_count' => 0,
633 'top_item' => array(
634 'label' => __( 'No ebook sales yet', 'ebook-store' ),
635 'units' => 0,
636 'orders' => 0,
637 'revenue' => 0.0,
638 'ebook_id' => 0,
639 ),
640 );
641
642 $currencies_seen = array();
643 $item_totals = array();
644
645 foreach ( $rows as $row ) {
646 $summary['total_orders']++;
647
648 $status = isset( $row['payment_status'] ) ? (string) $row['payment_status'] : '';
649 if ( ebook_store_is_negative_order_status( $status ) ) {
650 continue;
651 }
652
653 $summary['reportable_orders']++;
654
655 $gross = isset( $row['mc_gross'] ) ? (float) $row['mc_gross'] : 0.0;
656 $fee = isset( $row['mc_fee'] ) ? (float) $row['mc_fee'] : 0.0;
657 $net = $gross - $fee;
658 $currency = trim( isset( $row['mc_currency'] ) ? (string) $row['mc_currency'] : '' );
659 $timestamp = ! empty( $row['post_date_gmt'] ) ? strtotime( $row['post_date_gmt'] . ' UTC' ) : false;
660 $quantity = isset( $row['quantity'] ) && is_numeric( $row['quantity'] ) && (float) $row['quantity'] > 0 ? (float) $row['quantity'] : 1.0;
661 $ebook_id = isset( $row['ebook_id'] ) ? absint( $row['ebook_id'] ) : 0;
662 $item_name = trim( isset( $row['item_name'] ) ? wp_strip_all_tags( (string) $row['item_name'] ) : '' );
663
664 if ( $currency !== '' ) {
665 $currencies_seen[ $currency ] = true;
666 if ( $summary['currency'] === '' || $summary['currency'] === $default_currency ) {
667 $summary['currency'] = $currency;
668 }
669 }
670
671 $summary['total_sales'] += $gross;
672 $summary['total_profit'] += $net;
673
674 if ( $gross > 0 ) {
675 $summary['paid_orders']++;
676 } else {
677 $summary['free_orders']++;
678 }
679
680 if ( $timestamp ) {
681 if ( $timestamp >= $last_24h_cutoff ) {
682 $summary['last_24h_sales'] += $gross;
683 }
684 if ( $timestamp >= $last_7d_cutoff ) {
685 $summary['last_7d_sales'] += $gross;
686 }
687 if ( $summary['latest_order_gmt'] === '' || strtotime( $summary['latest_order_gmt'] . ' UTC' ) < $timestamp ) {
688 $summary['latest_order_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
689 $summary['latest_order_id'] = (int) $row['ID'];
690 $summary['latest_order_amount'] = $gross;
691 }
692 }
693
694 $item_key = $ebook_id > 0 ? 'ebook:' . $ebook_id : 'item:' . md5( $item_name !== '' ? $item_name : __( 'Unknown ebook', 'ebook-store' ) );
695 if ( ! isset( $item_totals[ $item_key ] ) ) {
696 $item_totals[ $item_key ] = array(
697 'label' => $ebook_id > 0 ? get_the_title( $ebook_id ) : '',
698 'units' => 0.0,
699 'orders' => 0,
700 'revenue' => 0.0,
701 'ebook_id' => $ebook_id,
702 );
703
704 if ( $item_totals[ $item_key ]['label'] === '' ) {
705 $item_totals[ $item_key ]['label'] = $item_name !== '' ? $item_name : __( 'Unknown ebook', 'ebook-store' );
706 }
707 }
708
709 $item_totals[ $item_key ]['units'] += $quantity;
710 $item_totals[ $item_key ]['orders']++;
711 $item_totals[ $item_key ]['revenue'] += $gross;
712 }
713
714 $summary['currency_count'] = count( $currencies_seen );
715 $summary['mixed_currencies'] = $summary['currency_count'] > 1;
716
717 if ( $summary['paid_orders'] > 0 ) {
718 $summary['average_order'] = $summary['total_sales'] / $summary['paid_orders'];
719 }
720
721 if ( ! empty( $item_totals ) ) {
722 uasort(
723 $item_totals,
724 function( $left, $right ) {
725 if ( $left['units'] === $right['units'] ) {
726 if ( $left['revenue'] === $right['revenue'] ) {
727 return $right['orders'] <=> $left['orders'];
728 }
729 return $right['revenue'] <=> $left['revenue'];
730 }
731 return $right['units'] <=> $left['units'];
732 }
733 );
734
735 $summary['top_item'] = reset( $item_totals );
736 }
737
738 set_transient( $cache_key, $summary, 10 * MINUTE_IN_SECONDS );
739
740 return $summary;
741 }
742
743 function ebook_store_render_dashboard_stat_card( $label, $value, $tone = 'default', $meta = '' ) {
744 $classes = 'ebook-store-dashboard__stat ebook-store-dashboard__stat--' . sanitize_html_class( $tone );
745
746 echo '<div class="' . esc_attr( $classes ) . '">';
747 echo '<span class="ebook-store-dashboard__stat-label">' . esc_html( $label ) . '</span>';
748 echo '<strong class="ebook-store-dashboard__stat-value">' . esc_html( $value ) . '</strong>';
749 if ( $meta !== '' ) {
750 echo '<span class="ebook-store-dashboard__stat-meta">' . esc_html( $meta ) . '</span>';
751 }
752 echo '</div>';
753 }
754
755 function ebook_store_render_dashboard_widget() {
756 $summary = ebook_store_get_dashboard_summary();
757 $currency = $summary['currency'];
758 $top_item = $summary['top_item'];
759 $last_order_label = __( 'No recent orders yet', 'ebook-store' );
760
761 if ( ! empty( $summary['latest_order_gmt'] ) ) {
762 $latest_timestamp = strtotime( $summary['latest_order_gmt'] . ' UTC' );
763 if ( $latest_timestamp ) {
764 $last_order_label = sprintf(
765 /* translators: 1: relative time, 2: order ID */
766 __( '%1$s ago, order #%2$d', 'ebook-store' ),
767 human_time_diff( $latest_timestamp, current_time( 'timestamp', true ) ),
768 $summary['latest_order_id']
769 );
770 }
771 }
772
773 echo '<div class="ebook-store-dashboard">';
774 echo '<div class="ebook-store-dashboard__hero">';
775 echo '<div>';
776 echo '<span class="ebook-store-dashboard__eyebrow">' . esc_html__( 'Ebook Store', 'ebook-store' ) . '</span>';
777 echo '<h3 class="ebook-store-dashboard__title">' . esc_html__( 'Sales summary at a glance', 'ebook-store' ) . '</h3>';
778 echo '<p class="ebook-store-dashboard__summary">' . esc_html__( 'A fast snapshot of revenue, order flow, and your strongest-performing ebook directly on the WordPress dashboard.', 'ebook-store' ) . '</p>';
779 echo '</div>';
780 echo '<div class="ebook-store-dashboard__hero-meta">';
781 echo '<span>' . esc_html__( 'Latest order', 'ebook-store' ) . '</span>';
782 echo '<strong>' . esc_html( $last_order_label ) . '</strong>';
783 echo '</div>';
784 echo '</div>';
785
786 echo '<div class="ebook-store-dashboard__stats">';
787 ebook_store_render_dashboard_stat_card(
788 __( 'All-time sales', 'ebook-store' ),
789 ebook_store_order_money_display( $summary['total_sales'], $currency ),
790 'primary',
791 sprintf( __( '%d paid orders', 'ebook-store' ), $summary['paid_orders'] )
792 );
793 ebook_store_render_dashboard_stat_card(
794 __( 'Profit', 'ebook-store' ),
795 ebook_store_order_money_display( $summary['total_profit'], $currency ),
796 'success',
797 __( 'Gross minus gateway fees', 'ebook-store' )
798 );
799 ebook_store_render_dashboard_stat_card(
800 __( 'Last 7 days', 'ebook-store' ),
801 ebook_store_order_money_display( $summary['last_7d_sales'], $currency ),
802 'warning',
803 sprintf( __( '%d total reportable orders', 'ebook-store' ), $summary['reportable_orders'] )
804 );
805 ebook_store_render_dashboard_stat_card(
806 __( 'Last 24 hours', 'ebook-store' ),
807 ebook_store_order_money_display( $summary['last_24h_sales'], $currency ),
808 'default',
809 sprintf( __( '%d free orders', 'ebook-store' ), $summary['free_orders'] )
810 );
811 echo '</div>';
812
813 echo '<div class="ebook-store-dashboard__details">';
814 echo '<section class="ebook-store-dashboard__panel">';
815 echo '<div class="ebook-store-dashboard__panel-head">';
816 echo '<h4>' . esc_html__( 'Store pulse', 'ebook-store' ) . '</h4>';
817 echo '<p>' . esc_html__( 'Core order metrics for the current store history.', 'ebook-store' ) . '</p>';
818 echo '</div>';
819 echo '<div class="ebook-store-dashboard__metric-list">';
820 echo '<div><span>' . esc_html__( 'All orders', 'ebook-store' ) . '</span><strong>' . esc_html( number_format_i18n( $summary['total_orders'] ) ) . '</strong></div>';
821 echo '<div><span>' . esc_html__( 'Average paid order', 'ebook-store' ) . '</span><strong>' . esc_html( ebook_store_order_money_display( $summary['average_order'], $currency ) ) . '</strong></div>';
822 echo '<div><span>' . esc_html__( 'Latest order amount', 'ebook-store' ) . '</span><strong>' . esc_html( ebook_store_order_money_display( $summary['latest_order_amount'], $currency ) ) . '</strong></div>';
823 echo '</div>';
824 echo '</section>';
825
826 echo '<section class="ebook-store-dashboard__panel">';
827 echo '<div class="ebook-store-dashboard__panel-head">';
828 echo '<h4>' . esc_html__( 'Most sold item', 'ebook-store' ) . '</h4>';
829 echo '<p>' . esc_html__( 'Top performer based on total units across valid ebook orders.', 'ebook-store' ) . '</p>';
830 echo '</div>';
831 echo '<div class="ebook-store-dashboard__top-item">';
832 echo '<strong class="ebook-store-dashboard__top-title">' . esc_html( $top_item['label'] ) . '</strong>';
833 echo '<div class="ebook-store-dashboard__top-metrics">';
834 echo '<span>' . esc_html( sprintf( __( '%s units', 'ebook-store' ), number_format_i18n( $top_item['units'] ) ) ) . '</span>';
835 echo '<span>' . esc_html( sprintf( __( '%s orders', 'ebook-store' ), number_format_i18n( $top_item['orders'] ) ) ) . '</span>';
836 echo '<span>' . esc_html( ebook_store_order_money_display( $top_item['revenue'], $currency ) ) . '</span>';
837 echo '</div>';
838 if ( ! empty( $top_item['ebook_id'] ) ) {
839 echo '<a class="button button-secondary" href="' . esc_url( get_edit_post_link( $top_item['ebook_id'] ) ) . '">' . esc_html__( 'Open ebook', 'ebook-store' ) . '</a>';
840 }
841 echo '</div>';
842 echo '</section>';
843 echo '</div>';
844
845 echo '<div class="ebook-store-dashboard__footer">';
846 echo '<a class="button button-primary" href="' . esc_url( admin_url( 'edit.php?post_type=ebook_order' ) ) . '">' . esc_html__( 'View all orders', 'ebook-store' ) . '</a>';
847 echo '<a class="button button-secondary" href="' . esc_url( admin_url( 'edit.php?post_type=ebook&page=ebook-store-order_reports' ) ) . '">' . esc_html__( 'Open reports', 'ebook-store' ) . '</a>';
848 if ( $summary['mixed_currencies'] ) {
849 echo '<span class="ebook-store-dashboard__notice">' . esc_html( sprintf( __( 'Heads up: totals combine %d currencies. Review reports if you need a per-currency breakdown.', 'ebook-store' ), $summary['currency_count'] ) ) . '</span>';
850 }
851 echo '</div>';
852 echo '</div>';
853 }
854
855 function ebook_store_register_dashboard_widget() {
856 if ( ! current_user_can( 'edit_posts' ) ) {
857 return;
858 }
859
860 wp_add_dashboard_widget(
861 'ebook_store_dashboard_widget',
862 __( 'Ebook Store Summary', 'ebook-store' ),
863 'ebook_store_render_dashboard_widget'
864 );
865
866 global $wp_meta_boxes;
867 if ( isset( $wp_meta_boxes['dashboard']['normal']['core']['ebook_store_dashboard_widget'] ) ) {
868 $widget = array(
869 'ebook_store_dashboard_widget' => $wp_meta_boxes['dashboard']['normal']['core']['ebook_store_dashboard_widget'],
870 );
871 unset( $wp_meta_boxes['dashboard']['normal']['core']['ebook_store_dashboard_widget'] );
872 $wp_meta_boxes['dashboard']['normal']['core'] = $widget + $wp_meta_boxes['dashboard']['normal']['core'];
873 }
874 }
875
876 function ebook_store_render_dashboard_styles() {
877 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
878 if ( ! $screen || $screen->base !== 'dashboard' ) {
879 return;
880 }
881 ?>
882 <style>
883 #ebook_store_dashboard_widget .inside {
884 margin: 0;
885 padding: 0;
886 }
887 .ebook-store-dashboard {
888 padding: 18px;
889 background:
890 radial-gradient(circle at top right, rgba(21, 94, 117, 0.13), transparent 32%),
891 linear-gradient(180deg, #ffffff 0%, #f7fbfc 100%);
892 }
893 .ebook-store-dashboard__hero {
894 display: grid;
895 grid-template-columns: minmax(260px, 1.6fr) minmax(180px, 0.7fr);
896 gap: 18px;
897 padding: 20px;
898 border: 1px solid #d9e7ea;
899 border-radius: 22px;
900 background: linear-gradient(135deg, #0f766e 0%, #14532d 100%);
901 box-shadow: 0 18px 42px rgba(15, 23, 42, 0.12);
902 color: #f8fffd;
903 }
904 .ebook-store-dashboard__eyebrow {
905 display: inline-flex;
906 font-size: 11px;
907 font-weight: 700;
908 letter-spacing: 0.08em;
909 text-transform: uppercase;
910 opacity: 0.82;
911 }
912 .ebook-store-dashboard__title {
913 margin: 8px 0 10px;
914 font-size: 24px;
915 line-height: 1.18;
916 color: inherit;
917 }
918 .ebook-store-dashboard__summary {
919 margin: 0;
920 font-size: 14px;
921 line-height: 1.7;
922 max-width: 640px;
923 color: rgba(248, 255, 253, 0.88);
924 }
925 .ebook-store-dashboard__hero-meta {
926 align-self: start;
927 padding: 14px 16px;
928 border: 1px solid rgba(255, 255, 255, 0.16);
929 border-radius: 18px;
930 background: rgba(255, 255, 255, 0.09);
931 backdrop-filter: blur(8px);
932 min-width: 0;
933 }
934 .ebook-store-dashboard__hero-meta span {
935 display: block;
936 margin-bottom: 6px;
937 font-size: 11px;
938 font-weight: 700;
939 letter-spacing: 0.06em;
940 text-transform: uppercase;
941 opacity: 0.76;
942 }
943 .ebook-store-dashboard__hero-meta strong {
944 display: block;
945 font-size: 14px;
946 line-height: 1.5;
947 word-break: break-word;
948 }
949 .ebook-store-dashboard__stats {
950 display: grid;
951 grid-template-columns: repeat(4, minmax(0, 1fr));
952 gap: 14px;
953 margin-top: 16px;
954 }
955 .ebook-store-dashboard__stat {
956 display: grid;
957 gap: 8px;
958 padding: 16px;
959 border: 1px solid #dbe7ee;
960 border-radius: 18px;
961 background: #ffffff;
962 box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
963 }
964 .ebook-store-dashboard__stat--primary {
965 background: linear-gradient(180deg, #f2fbff 0%, #ffffff 100%);
966 }
967 .ebook-store-dashboard__stat--success {
968 background: linear-gradient(180deg, #f2fcf2 0%, #ffffff 100%);
969 border-color: #cbe7cb;
970 }
971 .ebook-store-dashboard__stat--warning {
972 background: linear-gradient(180deg, #fff9ee 0%, #ffffff 100%);
973 border-color: #edd9b0;
974 }
975 .ebook-store-dashboard__stat-label {
976 font-size: 11px;
977 font-weight: 700;
978 letter-spacing: 0.06em;
979 text-transform: uppercase;
980 color: #64748b;
981 }
982 .ebook-store-dashboard__stat-value {
983 font-size: 26px;
984 line-height: 1.15;
985 color: #17212b;
986 }
987 .ebook-store-dashboard__stat-meta {
988 font-size: 12px;
989 line-height: 1.5;
990 color: #5b6b78;
991 }
992 .ebook-store-dashboard__details {
993 display: grid;
994 grid-template-columns: minmax(280px, 1fr) minmax(280px, 1fr);
995 gap: 16px;
996 margin-top: 16px;
997 }
998 .ebook-store-dashboard__panel {
999 padding: 18px;
1000 border: 1px solid #dbe7ee;
1001 border-radius: 20px;
1002 background: #ffffff;
1003 box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05);
1004 min-width: 0;
1005 }
1006 .ebook-store-dashboard__panel-head h4 {
1007 margin: 0 0 4px;
1008 font-size: 16px;
1009 color: #17212b;
1010 }
1011 .ebook-store-dashboard__panel-head p {
1012 margin: 0;
1013 font-size: 13px;
1014 line-height: 1.6;
1015 color: #607080;
1016 }
1017 .ebook-store-dashboard__metric-list {
1018 display: grid;
1019 gap: 12px;
1020 margin-top: 14px;
1021 }
1022 .ebook-store-dashboard__metric-list div {
1023 display: flex;
1024 justify-content: space-between;
1025 gap: 16px;
1026 padding: 12px 0;
1027 border-bottom: 1px solid #edf2f7;
1028 }
1029 .ebook-store-dashboard__metric-list div:last-child {
1030 border-bottom: 0;
1031 padding-bottom: 0;
1032 }
1033 .ebook-store-dashboard__metric-list span {
1034 color: #607080;
1035 }
1036 .ebook-store-dashboard__metric-list strong,
1037 .ebook-store-dashboard__top-title {
1038 color: #17212b;
1039 }
1040 .ebook-store-dashboard__top-item {
1041 display: grid;
1042 gap: 12px;
1043 margin-top: 14px;
1044 }
1045 .ebook-store-dashboard__top-title {
1046 font-size: 18px;
1047 line-height: 1.35;
1048 word-break: break-word;
1049 }
1050 .ebook-store-dashboard__top-metrics {
1051 display: flex;
1052 flex-wrap: wrap;
1053 gap: 8px;
1054 }
1055 .ebook-store-dashboard__top-metrics span {
1056 display: inline-flex;
1057 align-items: center;
1058 padding: 7px 10px;
1059 border-radius: 999px;
1060 background: #f3f8fb;
1061 color: #28556f;
1062 font-size: 12px;
1063 font-weight: 600;
1064 }
1065 .ebook-store-dashboard__footer {
1066 display: flex;
1067 flex-wrap: wrap;
1068 align-items: center;
1069 gap: 10px;
1070 margin-top: 16px;
1071 }
1072 .ebook-store-dashboard__notice {
1073 font-size: 12px;
1074 line-height: 1.5;
1075 color: #8a5b12;
1076 }
1077 @media (max-width: 1360px) {
1078 .ebook-store-dashboard__hero,
1079 .ebook-store-dashboard__details {
1080 grid-template-columns: 1fr;
1081 }
1082 }
1083 @media (max-width: 1200px) {
1084 .ebook-store-dashboard__stats {
1085 grid-template-columns: repeat(2, minmax(0, 1fr));
1086 }
1087 }
1088 @media (max-width: 900px) {
1089 .ebook-store-dashboard__hero,
1090 .ebook-store-dashboard__details,
1091 .ebook-store-dashboard__stats {
1092 grid-template-columns: 1fr;
1093 }
1094 }
1095 </style>
1096 <?php
1097 }
1098
1099 function ebook_store_get_ebook_sales_index() {
1100 $cache_key = ebook_store_get_ebook_sales_index_cache_key();
1101 $cached = get_transient( $cache_key );
1102 if ( false !== $cached && is_array( $cached ) ) {
1103 return $cached;
1104 }
1105
1106 global $wpdb;
1107
1108 $rows = $wpdb->get_results(
1109 "
1110 SELECT
1111 p.ID,
1112 p.post_date_gmt,
1113 status.meta_value AS payment_status,
1114 gross.meta_value AS mc_gross,
1115 ebook.meta_value AS ebook_id
1116 FROM {$wpdb->posts} p
1117 LEFT JOIN {$wpdb->postmeta} status ON (p.ID = status.post_id AND status.meta_key = 'payment_status')
1118 LEFT JOIN {$wpdb->postmeta} gross ON (p.ID = gross.post_id AND gross.meta_key = 'mc_gross')
1119 LEFT JOIN {$wpdb->postmeta} ebook ON (p.ID = ebook.post_id AND ebook.meta_key = 'ebook')
1120 WHERE p.post_type = 'ebook_order'
1121 AND p.post_status = 'publish'
1122 ",
1123 ARRAY_A
1124 );
1125
1126 $now_gmt = current_time( 'timestamp', true );
1127 $last_7d_cutoff = $now_gmt - ( 7 * DAY_IN_SECONDS );
1128 $last_30d_cutoff = $now_gmt - ( 30 * DAY_IN_SECONDS );
1129 $index = array();
1130
1131 foreach ( $rows as $row ) {
1132 $ebook_id = isset( $row['ebook_id'] ) ? absint( $row['ebook_id'] ) : 0;
1133 if ( $ebook_id <= 0 ) {
1134 continue;
1135 }
1136
1137 $status = isset( $row['payment_status'] ) ? (string) $row['payment_status'] : '';
1138 if ( ebook_store_is_negative_order_status( $status ) ) {
1139 continue;
1140 }
1141
1142 if ( ! isset( $index[ $ebook_id ] ) ) {
1143 $index[ $ebook_id ] = array(
1144 'orders' => 0,
1145 'total' => 0.0,
1146 'last_7d' => 0.0,
1147 'last_30d' => 0.0,
1148 'latest_order_gmt' => '',
1149 );
1150 }
1151
1152 $gross = isset( $row['mc_gross'] ) ? (float) $row['mc_gross'] : 0.0;
1153 $timestamp = ! empty( $row['post_date_gmt'] ) ? strtotime( $row['post_date_gmt'] . ' UTC' ) : false;
1154
1155 $index[ $ebook_id ]['orders']++;
1156 $index[ $ebook_id ]['total'] += $gross;
1157
1158 if ( $timestamp ) {
1159 if ( $timestamp >= $last_7d_cutoff ) {
1160 $index[ $ebook_id ]['last_7d'] += $gross;
1161 }
1162 if ( $timestamp >= $last_30d_cutoff ) {
1163 $index[ $ebook_id ]['last_30d'] += $gross;
1164 }
1165 if ( $index[ $ebook_id ]['latest_order_gmt'] === '' || strtotime( $index[ $ebook_id ]['latest_order_gmt'] . ' UTC' ) < $timestamp ) {
1166 $index[ $ebook_id ]['latest_order_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
1167 }
1168 }
1169 }
1170
1171 set_transient( $cache_key, $index, 10 * MINUTE_IN_SECONDS );
1172
1173 return $index;
1174 }
1175
1176 function ebook_store_get_ebook_catalog_summary() {
1177 $cache_key = ebook_store_get_ebook_catalog_summary_cache_key();
1178 $cached = get_transient( $cache_key );
1179 if ( false !== $cached && is_array( $cached ) ) {
1180 return $cached;
1181 }
1182
1183 $counts = wp_count_posts( 'ebook' );
1184 $ebook_ids = get_posts(
1185 array(
1186 'post_type' => 'ebook',
1187 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ),
1188 'posts_per_page' => -1,
1189 'fields' => 'ids',
1190 'orderby' => 'date',
1191 'order' => 'DESC',
1192 'no_found_rows' => true,
1193 )
1194 );
1195
1196 $summary = array(
1197 'total_titles' => 0,
1198 'published_titles' => isset( $counts->publish ) ? (int) $counts->publish : 0,
1199 'draft_titles' => isset( $counts->draft ) ? (int) $counts->draft : 0,
1200 'free_titles' => 0,
1201 'paid_titles' => 0,
1202 );
1203
1204 foreach ( $ebook_ids as $ebook_id ) {
1205 $summary['total_titles']++;
1206 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
1207 $price = 0.0;
1208 if ( is_array( $ebook_meta ) && isset( $ebook_meta['ebook_price'] ) ) {
1209 $price = (float) $ebook_meta['ebook_price'];
1210 }
1211 if ( $price > 0 ) {
1212 $summary['paid_titles']++;
1213 } else {
1214 $summary['free_titles']++;
1215 }
1216 }
1217
1218 set_transient( $cache_key, $summary, 10 * MINUTE_IN_SECONDS );
1219
1220 return $summary;
1221 }
1222
1223 function ebook_store_render_ebook_list_summary() {
1224 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1225 if ( ! $screen || $screen->base !== 'edit' || $screen->post_type !== 'ebook' ) {
1226 return;
1227 }
1228
1229 $catalog = ebook_store_get_ebook_catalog_summary();
1230 $dashboard = ebook_store_get_dashboard_summary();
1231 $currency = $dashboard['currency'];
1232 $top_item = $dashboard['top_item'];
1233
1234 echo '<div class="ebook-catalog-admin">';
1235 echo '<section class="ebook-catalog-admin__hero">';
1236 echo '<div class="ebook-catalog-admin__hero-copy">';
1237 echo '<span class="ebook-catalog-admin__eyebrow">' . esc_html__( 'Ebook Store', 'ebook-store' ) . '</span>';
1238 echo '<h2 class="ebook-catalog-admin__title">' . esc_html__( 'Catalog overview', 'ebook-store' ) . '</h2>';
1239 echo '<p class="ebook-catalog-admin__summary">' . esc_html__( 'Manage covers, pricing, file formats, and sales performance from one cleaner inventory view. The list below is tuned for faster scanning and quicker publishing decisions.', 'ebook-store' ) . '</p>';
1240 echo '<div class="ebook-catalog-admin__actions">';
1241 echo '<a class="button button-primary" href="' . esc_url( admin_url( 'post-new.php?post_type=ebook' ) ) . '">' . esc_html__( 'Add new ebook', 'ebook-store' ) . '</a>';
1242 echo '<a class="button button-secondary" href="' . esc_url( admin_url( 'edit.php?post_type=ebook_order' ) ) . '">' . esc_html__( 'View orders', 'ebook-store' ) . '</a>';
1243 echo '<a class="button button-secondary" href="' . esc_url( admin_url( 'edit.php?post_type=ebook&page=ebook-store-order_reports' ) ) . '">' . esc_html__( 'Open reports', 'ebook-store' ) . '</a>';
1244 echo '<a class="button button-secondary" href="' . esc_url( admin_url( 'options-general.php?page=ebook_options.php' ) ) . '">' . esc_html__( 'Settings', 'ebook-store' ) . '</a>';
1245 echo '</div>';
1246 echo '</div>';
1247 echo '<div class="ebook-catalog-admin__hero-side">';
1248 echo '<span class="ebook-catalog-admin__hero-label">' . esc_html__( 'Best seller right now', 'ebook-store' ) . '</span>';
1249 echo '<strong class="ebook-catalog-admin__hero-value">' . esc_html( $top_item['label'] ) . '</strong>';
1250 echo '<span class="ebook-catalog-admin__hero-meta">' . esc_html( sprintf( __( '%1$s units sold, %2$s revenue', 'ebook-store' ), number_format_i18n( $top_item['units'] ), ebook_store_order_money_display( $top_item['revenue'], $currency ) ) ) . '</span>';
1251 echo '</div>';
1252 echo '</section>';
1253
1254 echo '<div class="ebook-catalog-admin__stats">';
1255 echo '<div class="ebook-catalog-admin__stat"><span>' . esc_html__( 'Published titles', 'ebook-store' ) . '</span><strong>' . esc_html( number_format_i18n( $catalog['published_titles'] ) ) . '</strong><small>' . esc_html__( 'Live in the catalog', 'ebook-store' ) . '</small></div>';
1256 echo '<div class="ebook-catalog-admin__stat"><span>' . esc_html__( 'Paid titles', 'ebook-store' ) . '</span><strong>' . esc_html( number_format_i18n( $catalog['paid_titles'] ) ) . '</strong><small>' . esc_html__( 'Pricing above zero', 'ebook-store' ) . '</small></div>';
1257 echo '<div class="ebook-catalog-admin__stat"><span>' . esc_html__( 'Free titles', 'ebook-store' ) . '</span><strong>' . esc_html( number_format_i18n( $catalog['free_titles'] ) ) . '</strong><small>' . esc_html__( 'Lead magnets or free downloads', 'ebook-store' ) . '</small></div>';
1258 echo '<div class="ebook-catalog-admin__stat ebook-catalog-admin__stat--accent"><span>' . esc_html__( 'All-time revenue', 'ebook-store' ) . '</span><strong>' . esc_html( ebook_store_order_money_display( $dashboard['total_sales'], $currency ) ) . '</strong><small>' . esc_html__( 'Across reportable ebook orders', 'ebook-store' ) . '</small></div>';
1259 echo '</div>';
1260 echo '</div>';
1261 }
1262
1263 function ebook_store_render_ebook_list_styles() {
1264 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1265 if ( ! $screen || $screen->base !== 'edit' || $screen->post_type !== 'ebook' ) {
1266 return;
1267 }
1268 ?>
1269 <style>
1270 .ebook-catalog-admin {
1271 margin: 18px 0 20px;
1272 }
1273 .ebook-catalog-admin__hero {
1274 display: grid;
1275 grid-template-columns: minmax(320px, 1.5fr) minmax(220px, 0.8fr);
1276 gap: 18px;
1277 padding: 22px;
1278 border: 1px solid #d8e4ee;
1279 border-radius: 24px;
1280 background:
1281 radial-gradient(circle at top right, rgba(14, 116, 144, 0.12), transparent 34%),
1282 linear-gradient(135deg, #ffffff 0%, #f6fbff 100%);
1283 box-shadow: 0 16px 38px rgba(15, 23, 42, 0.06);
1284 }
1285 .ebook-catalog-admin__eyebrow {
1286 display: inline-flex;
1287 font-size: 11px;
1288 font-weight: 700;
1289 letter-spacing: 0.08em;
1290 text-transform: uppercase;
1291 color: #0f5f8f;
1292 }
1293 .ebook-catalog-admin__title {
1294 margin: 8px 0 10px;
1295 font-size: 28px;
1296 line-height: 1.15;
1297 color: #17212b;
1298 }
1299 .ebook-catalog-admin__summary {
1300 margin: 0;
1301 max-width: 760px;
1302 font-size: 14px;
1303 line-height: 1.75;
1304 color: #5b6b78;
1305 }
1306 .ebook-catalog-admin__actions {
1307 display: flex;
1308 flex-wrap: wrap;
1309 gap: 10px;
1310 margin-top: 16px;
1311 }
1312 .ebook-catalog-admin__hero-side {
1313 align-self: start;
1314 display: grid;
1315 gap: 8px;
1316 padding: 18px;
1317 border: 1px solid #d7e7dd;
1318 border-radius: 20px;
1319 background: linear-gradient(180deg, #f0fbf2 0%, #ffffff 100%);
1320 min-width: 0;
1321 }
1322 .ebook-catalog-admin__hero-label {
1323 font-size: 11px;
1324 font-weight: 700;
1325 letter-spacing: 0.06em;
1326 text-transform: uppercase;
1327 color: #577161;
1328 }
1329 .ebook-catalog-admin__hero-value {
1330 font-size: 18px;
1331 line-height: 1.35;
1332 color: #17212b;
1333 word-break: break-word;
1334 }
1335 .ebook-catalog-admin__hero-meta {
1336 font-size: 13px;
1337 line-height: 1.6;
1338 color: #5f6f67;
1339 }
1340 .ebook-catalog-admin__stats {
1341 display: grid;
1342 grid-template-columns: repeat(4, minmax(0, 1fr));
1343 gap: 14px;
1344 margin-top: 14px;
1345 }
1346 .ebook-catalog-admin__stat {
1347 display: grid;
1348 gap: 7px;
1349 padding: 16px 18px;
1350 border: 1px solid #dde7f0;
1351 border-radius: 18px;
1352 background: #ffffff;
1353 box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
1354 }
1355 .ebook-catalog-admin__stat--accent {
1356 background: linear-gradient(180deg, #effaf6 0%, #ffffff 100%);
1357 border-color: #cae4d7;
1358 }
1359 .ebook-catalog-admin__stat span {
1360 font-size: 11px;
1361 font-weight: 700;
1362 letter-spacing: 0.06em;
1363 text-transform: uppercase;
1364 color: #64748b;
1365 }
1366 .ebook-catalog-admin__stat strong {
1367 font-size: 28px;
1368 line-height: 1.08;
1369 color: #17212b;
1370 }
1371 .ebook-catalog-admin__stat small {
1372 font-size: 12px;
1373 line-height: 1.55;
1374 color: #607080;
1375 }
1376 .edit-php.post-type-ebook .wp-header-end {
1377 margin-bottom: 0;
1378 }
1379 .edit-php.post-type-ebook .tablenav.top {
1380 margin-top: 8px;
1381 }
1382 .edit-php.post-type-ebook .wp-list-table.fixed {
1383 table-layout: auto;
1384 }
1385 .edit-php.post-type-ebook .column-cover { width: 132px; }
1386 .edit-php.post-type-ebook .column-title,
1387 .edit-php.post-type-ebook .column-title .row-title {
1388 white-space: nowrap;
1389 }
1390 .edit-php.post-type-ebook .column-title {
1391 width: auto;
1392 }
1393 .edit-php.post-type-ebook .column-price { width: 1%; white-space: nowrap; }
1394 .edit-php.post-type-ebook .column-formats { width: 1%; }
1395 .edit-php.post-type-ebook .column-sales { width: 1%; }
1396 .edit-php.post-type-ebook .column-embed_code {
1397 width: 320px;
1398 min-width: 320px;
1399 }
1400 .edit-php.post-type-ebook td.column-sales .ebook-list-sales {
1401 gap: 8px;
1402 }
1403 .edit-php.post-type-ebook #the-list td {
1404 vertical-align: top;
1405 }
1406 .ebook-list-cover {
1407 display: grid;
1408 gap: 8px;
1409 }
1410 .ebook-list-cover__frame {
1411 display: grid;
1412 place-items: center;
1413 width: 96px;
1414 min-height: 132px;
1415 padding: 6px;
1416 border: 1px solid #d7dfeb;
1417 border-radius: 18px;
1418 background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
1419 overflow: hidden;
1420 box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
1421 }
1422 .ebook-list-cover__frame img {
1423 display: block;
1424 max-width: 100%;
1425 height: auto;
1426 border-radius: 12px;
1427 }
1428 .ebook-list-cover__placeholder {
1429 padding: 18px 12px;
1430 font-size: 12px;
1431 line-height: 1.5;
1432 text-align: center;
1433 color: #789;
1434 }
1435 .ebook-list-status {
1436 display: inline-flex;
1437 align-items: center;
1438 justify-content: center;
1439 padding: 5px 9px;
1440 border-radius: 999px;
1441 background: #eef6ff;
1442 color: #28506c;
1443 font-size: 11px;
1444 font-weight: 700;
1445 text-transform: capitalize;
1446 }
1447 .ebook-list-price,
1448 .ebook-list-sales,
1449 .ebook-list-shortcuts {
1450 display: grid;
1451 gap: 10px;
1452 }
1453 .ebook-list-price__amount,
1454 .ebook-list-sales__amount {
1455 font-size: 22px;
1456 font-weight: 700;
1457 line-height: 1.15;
1458 color: #17212b;
1459 }
1460 .ebook-list-price__meta,
1461 .ebook-list-sales__meta {
1462 font-size: 12px;
1463 line-height: 1.6;
1464 color: #5b6b78;
1465 }
1466 .ebook-list-price__pills,
1467 .ebook-list-sales__pills,
1468 .ebook-list-formats {
1469 display: flex;
1470 flex-wrap: wrap;
1471 gap: 6px;
1472 }
1473 .edit-php.post-type-ebook td.column-sales .ebook-list-sales__pills {
1474 flex-direction: column;
1475 align-items: flex-start;
1476 }
1477 .edit-php.post-type-ebook td.column-sales .ebook-list-sales__pills .ebook-list-pill {
1478 width: 100%;
1479 justify-content: flex-start;
1480 }
1481 .ebook-list-pill {
1482 display: inline-flex;
1483 align-items: center;
1484 padding: 6px 9px;
1485 border-radius: 999px;
1486 background: #f2f7fb;
1487 color: #305066;
1488 font-size: 11px;
1489 font-weight: 700;
1490 line-height: 1.2;
1491 }
1492 .ebook-list-pill.is-green {
1493 background: #edf8f0;
1494 color: #27613b;
1495 }
1496 .ebook-list-pill.is-warm {
1497 background: #fff5e8;
1498 color: #8a5b12;
1499 }
1500 .ebook-list-formats__item {
1501 display: inline-flex;
1502 align-items: center;
1503 justify-content: center;
1504 width: 42px;
1505 height: 42px;
1506 padding: 6px;
1507 border: 1px solid #d8e2ec;
1508 border-radius: 14px;
1509 background: #ffffff;
1510 box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
1511 }
1512 .ebook-list-formats__item img {
1513 max-width: 100%;
1514 height: auto;
1515 }
1516 .ebook-format-test-link {
1517 margin-top: 4px;
1518 }
1519 .ebook-format-test {
1520 margin-top: 4px;
1521 }
1522 .ebook-format-test > summary {
1523 display: inline-block;
1524 list-style: none;
1525 cursor: pointer;
1526 }
1527 .ebook-format-test > summary::-webkit-details-marker { display: none; }
1528 .ebook-format-test__menu {
1529 margin-top: 6px;
1530 display: grid;
1531 gap: 4px;
1532 justify-items: start;
1533 }
1534 .ebook-format-test__prompt {
1535 font-size: 11px;
1536 font-weight: 700;
1537 letter-spacing: 0.04em;
1538 text-transform: uppercase;
1539 color: #64748b;
1540 }
1541 .ebook-list-shortcuts__field {
1542 display: grid;
1543 gap: 4px;
1544 }
1545 .ebook-list-shortcuts__label {
1546 font-size: 11px;
1547 font-weight: 700;
1548 letter-spacing: 0.06em;
1549 text-transform: uppercase;
1550 color: #64748b;
1551 }
1552 .ebook-list-shortcuts__control {
1553 display: flex;
1554 gap: 8px;
1555 align-items: center;
1556 }
1557 .ebook-list-shortcuts__control input {
1558 flex: 1 1 auto;
1559 min-width: 220px;
1560 border-color: #ccd8e4;
1561 border-radius: 12px;
1562 font-family: Consolas, Monaco, monospace;
1563 font-size: 12px;
1564 background: #fff;
1565 }
1566 .ebook-list-copy.is-copied {
1567 border-color: #16754b;
1568 color: #16754b;
1569 }
1570 .edit-php.post-type-ebook .row-title {
1571 font-size: 15px;
1572 font-weight: 700;
1573 }
1574 .edit-php.post-type-ebook .row-actions {
1575 margin-top: 6px;
1576 }
1577 @media (max-width: 1400px) {
1578 .ebook-catalog-admin__hero,
1579 .ebook-catalog-admin__stats {
1580 grid-template-columns: repeat(2, minmax(0, 1fr));
1581 }
1582 }
1583 @media (max-width: 900px) {
1584 .ebook-catalog-admin__hero,
1585 .ebook-catalog-admin__stats {
1586 grid-template-columns: 1fr;
1587 }
1588 .ebook-list-shortcuts__control {
1589 flex-direction: column;
1590 align-items: stretch;
1591 }
1592 }
1593 </style>
1594 <script>
1595 document.addEventListener("click", function(event){
1596 var button = event.target.closest(".ebook-list-copy");
1597 if (!button) {
1598 return;
1599 }
1600 var selector = button.getAttribute("data-copy-target");
1601 if (!selector) {
1602 return;
1603 }
1604 var input = document.querySelector(selector);
1605 if (!input) {
1606 return;
1607 }
1608 input.focus();
1609 input.select();
1610 try {
1611 document.execCommand("copy");
1612 var originalText = button.textContent;
1613 var successText = button.getAttribute("data-copied-text") || "Copied!";
1614 button.textContent = successText;
1615 button.classList.add("is-copied");
1616 setTimeout(function(){
1617 button.textContent = originalText;
1618 button.classList.remove("is-copied");
1619 }, 1800);
1620 } catch (error) {}
1621 });
1622 </script>
1623 <?php
1624 }
1625
1626 function ebook_store_render_order_admin_styles() {
1627 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
1628 if ( ! $screen ) {
1629 return;
1630 }
1631
1632 $is_order_list = ( $screen->base === 'edit' && $screen->post_type === 'ebook_order' );
1633 $is_order_edit = ( $screen->base === 'post' && $screen->post_type === 'ebook_order' );
1634
1635 if ( ! $is_order_list && ! $is_order_edit ) {
1636 return;
1637 }
1638 ?>
1639 <style>
1640 .ebook-order-admin .column-order_id { width: 7%; }
1641 .ebook-order-admin .column-title { width: 18%; }
1642 .ebook-order-admin .column-product { width: 18%; }
1643 .ebook-order-admin .column-amount { width: 12%; }
1644 .ebook-order-admin .column-paypal { width: 12%; }
1645 .ebook-order-admin .column-downloadlink { width: 15%; }
1646 .ebook-order-admin .column-formData { width: 18%; }
1647 .ebook-order-list {
1648 display: flex;
1649 flex-direction: column;
1650 gap: 8px;
1651 min-width: 0;
1652 }
1653 .ebook-order-list__title {
1654 font-weight: 700;
1655 color: #17212b;
1656 line-height: 1.35;
1657 }
1658 .ebook-order-list__meta,
1659 .ebook-order-list__subtle,
1660 .ebook-order-list__stack {
1661 font-size: 12px;
1662 line-height: 1.55;
1663 color: #52606d;
1664 }
1665 .ebook-order-list__stack {
1666 display: flex;
1667 flex-direction: column;
1668 gap: 2px;
1669 }
1670 .ebook-order-list__badges,
1671 .ebook-order-list__actions {
1672 display: flex;
1673 flex-wrap: wrap;
1674 gap: 6px;
1675 }
1676 .ebook-order-badge {
1677 display: inline-flex;
1678 flex-direction: column;
1679 gap: 2px;
1680 padding: 7px 9px;
1681 border: 1px solid #d7dfeb;
1682 border-radius: 10px;
1683 background: #ffffff;
1684 min-width: 76px;
1685 }
1686 .ebook-order-badge__label {
1687 font-size: 10px;
1688 font-weight: 700;
1689 line-height: 1.2;
1690 letter-spacing: 0.04em;
1691 text-transform: uppercase;
1692 color: #64748b;
1693 }
1694 .ebook-order-badge strong {
1695 font-size: 12px;
1696 line-height: 1.3;
1697 color: #17212b;
1698 }
1699 .ebook-order-badge.is-success {
1700 border-color: #b7e3c6;
1701 background: #edf9f1;
1702 }
1703 .ebook-order-badge.is-warning {
1704 border-color: #f5ddb4;
1705 background: #fff6e8;
1706 }
1707 .ebook-order-badge.is-danger {
1708 border-color: #efc1c1;
1709 background: #fff1f1;
1710 }
1711 .ebook-order-badge.is-neutral {
1712 border-color: #d7dfeb;
1713 background: #f8fbff;
1714 }
1715 .ebook-order-list__actions a {
1716 text-decoration: none;
1717 }
1718 .ebook-order-list__inline-code {
1719 display: inline-block;
1720 padding: 3px 7px;
1721 border-radius: 8px;
1722 background: #f1f5f9;
1723 color: #0f4c75;
1724 font-family: Consolas, Monaco, monospace;
1725 font-size: 11px;
1726 }
1727 .ebook-order-list__pairs {
1728 display: grid;
1729 gap: 4px;
1730 }
1731 .ebook-order-list__pair {
1732 display: grid;
1733 grid-template-columns: minmax(60px, 90px) 1fr;
1734 gap: 6px;
1735 font-size: 12px;
1736 line-height: 1.45;
1737 }
1738 .ebook-order-list__pair strong {
1739 color: #334155;
1740 }
1741 .ebook-order-list__empty {
1742 color: #8a94a6;
1743 font-style: italic;
1744 }
1745 .ebook-order-admin #the-list tr[type="post"] td {
1746 vertical-align: top;
1747 }
1748 .ebook-order-meta-box * { box-sizing: border-box; }
1749 .ebook-order-shell {
1750 display: grid;
1751 gap: 18px;
1752 }
1753 .ebook-order-hero {
1754 display: grid;
1755 grid-template-columns: minmax(260px, 1.2fr) minmax(320px, 1.4fr);
1756 gap: 18px;
1757 padding: 22px;
1758 border: 1px solid #d7dfeb;
1759 border-radius: 22px;
1760 background:
1761 radial-gradient(circle at top right, rgba(29, 124, 191, 0.10), transparent 35%),
1762 linear-gradient(135deg, #ffffff 0%, #f5f9fc 100%);
1763 box-shadow: 0 16px 34px rgba(15, 23, 42, 0.06);
1764 }
1765 .ebook-order-hero__eyebrow {
1766 display: inline-flex;
1767 font-size: 11px;
1768 font-weight: 700;
1769 letter-spacing: 0.08em;
1770 text-transform: uppercase;
1771 color: #135e96;
1772 }
1773 .ebook-order-hero__title {
1774 margin: 8px 0 10px;
1775 font-size: 24px;
1776 line-height: 1.2;
1777 color: #17212b;
1778 }
1779 .ebook-order-hero__summary {
1780 margin: 0;
1781 font-size: 14px;
1782 line-height: 1.7;
1783 color: #52606d;
1784 }
1785 .ebook-order-hero__meta {
1786 display: flex;
1787 flex-wrap: wrap;
1788 gap: 10px;
1789 margin-top: 16px;
1790 }
1791 .ebook-order-panel {
1792 border: 1px solid #d7dfeb;
1793 border-radius: 22px;
1794 background: #ffffff;
1795 box-shadow: 0 14px 32px rgba(15, 23, 42, 0.05);
1796 overflow: hidden;
1797 }
1798 .ebook-order-panel__header {
1799 display: flex;
1800 justify-content: space-between;
1801 gap: 16px;
1802 align-items: flex-start;
1803 padding: 18px 20px 14px;
1804 border-bottom: 1px solid #ecf1f6;
1805 }
1806 .ebook-order-panel__header h3 {
1807 margin: 0 0 6px;
1808 font-size: 18px;
1809 line-height: 1.3;
1810 color: #17212b;
1811 }
1812 .ebook-order-panel__header p {
1813 margin: 0;
1814 font-size: 13px;
1815 line-height: 1.6;
1816 color: #607080;
1817 }
1818 .ebook-order-panel__body {
1819 padding: 18px 20px 20px;
1820 }
1821 .ebook-order-quick-grid {
1822 display: grid;
1823 gap: 16px;
1824 grid-template-columns: repeat(2, minmax(0, 1fr));
1825 }
1826 .ebook-order-card {
1827 padding: 16px;
1828 border: 1px solid #e3ebf3;
1829 border-radius: 16px;
1830 background: #fbfdff;
1831 }
1832 .ebook-order-card h4 {
1833 margin: 0 0 10px;
1834 font-size: 14px;
1835 color: #17212b;
1836 }
1837 .ebook-order-card__meta {
1838 display: flex;
1839 flex-wrap: wrap;
1840 gap: 8px;
1841 margin-bottom: 12px;
1842 }
1843 .ebook-order-card__rows {
1844 display: grid;
1845 gap: 10px;
1846 }
1847 .ebook-order-card__row label {
1848 display: block;
1849 font-weight: 600;
1850 margin-bottom: 4px;
1851 color: #17212b;
1852 }
1853 .ebook-order-card__control {
1854 display: flex;
1855 gap: 8px;
1856 align-items: center;
1857 }
1858 .ebook-order-card__control input {
1859 flex: 1 1 auto;
1860 }
1861 .ebook-order-sections {
1862 display: grid;
1863 gap: 16px;
1864 }
1865 .ebook-order-section {
1866 border: 1px solid #d7dfeb;
1867 border-radius: 18px;
1868 background: #ffffff;
1869 overflow: hidden;
1870 }
1871 .ebook-order-section summary {
1872 cursor: pointer;
1873 padding: 16px 18px;
1874 font-weight: 700;
1875 font-size: 14px;
1876 color: #17212b;
1877 background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
1878 border-bottom: 1px solid #ecf1f6;
1879 list-style: none;
1880 }
1881 .ebook-order-section summary::-webkit-details-marker { display:none; }
1882 .ebook-order-section__description {
1883 margin: 0;
1884 padding: 14px 18px 0;
1885 color: #607080;
1886 font-size: 13px;
1887 line-height: 1.6;
1888 }
1889 .ebook-order-grid {
1890 display: grid;
1891 gap: 16px;
1892 grid-template-columns: repeat(2, minmax(0, 1fr));
1893 padding: 18px;
1894 }
1895 .ebook-order-field {
1896 min-width: 0;
1897 }
1898 .ebook-order-field--full { grid-column: 1 / -1; }
1899 .ebook-order-field--half,
1900 .ebook-order-field--third { grid-column: span 1; }
1901 .ebook-order-field label {
1902 font-weight: 700;
1903 margin-bottom: 6px;
1904 display: block;
1905 color: #17212b;
1906 }
1907 .ebook-order-field input[type="text"],
1908 .ebook-order-field textarea {
1909 width: 100%;
1910 border-color: #cbd6e2;
1911 border-radius: 12px;
1912 background: #ffffff;
1913 }
1914 .ebook-order-meta-box .description {
1915 margin-top: 6px;
1916 font-size: 12px;
1917 color: #607080;
1918 }
1919 @media (max-width: 960px) {
1920 .ebook-order-hero,
1921 .ebook-order-quick-grid,
1922 .ebook-order-grid {
1923 grid-template-columns: 1fr;
1924 }
1925 }
1926 </style>
1927 <script>
1928 document.documentElement.classList.add("ebook-order-admin");
1929 </script>
1930 <?php
1931 }
1932 add_action( 'admin_head', 'ebook_store_render_order_admin_styles' );
1933
1934 function ebook_order_box() {
1935 wp_nonce_field(plugin_basename(__FILE__), 'ebook_order_nonce');
1936 $post_id = get_the_ID();
1937 $sections = ebook_store_get_order_field_sections();
1938 $field_definitions = ebook_store_get_order_field_definitions();
1939
1940 $field_values = array();
1941 foreach ($field_definitions as $field_key => $config) {
1942 $field_values[$field_key] = get_post_meta($post_id, $field_key, true);
1943 }
1944
1945 $download_link = get_post_meta($post_id, 'downloadlink', true);
1946 $downloads = intval(get_post_meta($post_id, 'downloads', true));
1947 $ebook_id = intval(get_post_meta($post_id, 'ebook', true));
1948 $ebook_title = $ebook_id > 0 ? get_the_title($ebook_id) : '';
1949 $ebook_edit_link = $ebook_id > 0 ? get_edit_post_link($ebook_id) : '';
1950 $payment_status = isset( $field_values['payment_status'] ) ? (string) $field_values['payment_status'] : '';
1951 $payment_date = isset( $field_values['payment_date'] ) ? (string) $field_values['payment_date'] : '';
1952 $buyer_name = trim( ( isset( $field_values['first_name'] ) ? $field_values['first_name'] : '' ) . ' ' . ( isset( $field_values['last_name'] ) ? $field_values['last_name'] : '' ) );
1953 $buyer_email = isset( $field_values['payer_email'] ) ? (string) $field_values['payer_email'] : '';
1954 $country = isset( $field_values['residence_country'] ) ? (string) $field_values['residence_country'] : '';
1955 $payment_type = isset( $field_values['payment_type'] ) ? (string) $field_values['payment_type'] : '';
1956 $gateway = $payment_type !== '' ? $payment_type : __( 'PayPal', 'ebook-store' );
1957 $gross_total = isset( $field_values['mc_gross'] ) ? (string) $field_values['mc_gross'] : '';
1958 $currency = isset( $field_values['mc_currency'] ) ? (string) $field_values['mc_currency'] : '';
1959 $password_value = '';
1960 if (function_exists('ebook_store_get_password_for_order')) {
1961 $password_value = ebook_store_get_password_for_order($post_id, array(
1962 'payer_email' => isset($field_values['payer_email']) ? $field_values['payer_email'] : '',
1963 ));
1964 } else {
1965 $password_value = get_post_meta($post_id, 'password', true);
1966 }
1967 $password_value = trim((string)$password_value);
1968
1969 $download_input_id = 'ebook-order-download-link-' . $post_id;
1970 $password_input_id = 'ebook-order-password-' . $post_id;
1971
1972 echo '<div class="ebook-order-meta-box">';
1973 echo '<div class="ebook-order-shell">';
1974 echo '<section class="ebook-order-hero">';
1975 echo '<div class="ebook-order-hero__copy">';
1976 echo '<span class="ebook-order-hero__eyebrow">' . esc_html__( 'Ebook Order', 'ebook-store' ) . '</span>';
1977 echo '<h2 class="ebook-order-hero__title">' . esc_html( $ebook_title ? $ebook_title : __( 'Order without linked ebook', 'ebook-store' ) ) . '</h2>';
1978 $summary_bits = array();
1979 if ( $buyer_name !== '' ) {
1980 $summary_bits[] = sprintf( __( 'Buyer: %s', 'ebook-store' ), $buyer_name );
1981 }
1982 if ( $buyer_email !== '' ) {
1983 $summary_bits[] = sprintf( __( 'Email: %s', 'ebook-store' ), $buyer_email );
1984 }
1985 if ( $payment_date !== '' ) {
1986 $summary_bits[] = sprintf( __( 'Paid: %s', 'ebook-store' ), $payment_date );
1987 }
1988 echo '<p class="ebook-order-hero__summary">' . esc_html( implode( ' • ', $summary_bits ) ) . '</p>';
1989 echo '<div class="ebook-order-hero__meta">';
1990 echo ebook_store_render_order_badge( __( 'Order ID', 'ebook-store' ), '#' . $post_id );
1991 echo ebook_store_render_order_badge( __( 'Status', 'ebook-store' ), $payment_status !== '' ? $payment_status : __( 'Unknown', 'ebook-store' ), ebook_store_order_status_class( $payment_status ) );
1992 echo ebook_store_render_order_badge( __( 'Gateway', 'ebook-store' ), $gateway );
1993 if ( $gross_total !== '' ) {
1994 echo ebook_store_render_order_badge( __( 'Gross', 'ebook-store' ), ebook_store_order_money_display( $gross_total, $currency ) );
1995 }
1996 echo '</div>';
1997 echo '</div>';
1998
1999 echo '<div class="ebook-order-quick-grid">';
2000 echo '<div class="ebook-order-card">';
2001 echo '<h4>' . esc_html__( 'Order access', 'ebook-store' ) . '</h4>';
2002 echo '<div class="ebook-order-card__meta">';
2003 echo ebook_store_render_order_badge( __( 'Downloads', 'ebook-store' ), number_format_i18n( $downloads ) );
2004 echo ebook_store_render_order_badge( __( 'Country', 'ebook-store' ), $country !== '' ? $country : __( 'Not set', 'ebook-store' ) );
2005 echo '</div>';
2006 echo '<div class="ebook-order-card__rows">';
2007 if ($download_link) {
2008 echo '<div class="ebook-order-card__row">';
2009 echo '<label for="' . esc_attr($download_input_id) . '">' . esc_html__('Download link', 'ebook-store') . '</label>';
2010 echo '<div class="ebook-order-card__control">';
2011 echo '<input type="text" id="' . esc_attr($download_input_id) . '" class="regular-text" readonly value="' . esc_attr($download_link) . '" />';
2012 echo '<button type="button" class="button button-secondary ebook-order-copy-btn" data-copy-target="#' . esc_attr($download_input_id) . '" data-copied-text="' . esc_attr__('Copied!', 'ebook-store') . '">' . esc_html__('Copy', 'ebook-store') . '</button>';
2013 echo '<a href="' . esc_url($download_link) . '" class="button" target="_blank" rel="noopener">' . esc_html__('Open', 'ebook-store') . '</a>';
2014 echo '</div>';
2015 echo '</div>';
2016 }
2017 if ($password_value !== '') {
2018 echo '<div class="ebook-order-card__row">';
2019 echo '<label for="' . esc_attr($password_input_id) . '">' . esc_html__('Customer PDF password', 'ebook-store') . '</label>';
2020 echo '<div class="ebook-order-card__control">';
2021 echo '<input type="text" id="' . esc_attr($password_input_id) . '" class="regular-text" readonly value="' . esc_attr($password_value) . '" />';
2022 echo '<button type="button" class="button button-secondary ebook-order-copy-btn" data-copy-target="#' . esc_attr($password_input_id) . '" data-copied-text="' . esc_attr__('Copied!', 'ebook-store') . '">' . esc_html__('Copy', 'ebook-store') . '</button>';
2023 echo '</div>';
2024 echo '</div>';
2025 }
2026 echo '</div>';
2027 echo '</div>';
2028
2029 echo '<div class="ebook-order-card">';
2030 echo '<h4>' . esc_html__( 'Order context', 'ebook-store' ) . '</h4>';
2031 echo '<div class="ebook-order-card__meta">';
2032 if ( $ebook_title ) {
2033 echo ebook_store_render_order_badge( __( 'Product', 'ebook-store' ), $ebook_title );
2034 }
2035 if ( isset( $field_values['quantity'] ) && $field_values['quantity'] !== '' ) {
2036 echo ebook_store_render_order_badge( __( 'Qty', 'ebook-store' ), $field_values['quantity'] );
2037 }
2038 if ( isset( $field_values['txn_id'] ) && $field_values['txn_id'] !== '' ) {
2039 echo ebook_store_render_order_badge( __( 'Txn', 'ebook-store' ), $field_values['txn_id'] );
2040 }
2041 echo '</div>';
2042 if ($ebook_title) {
2043 $link_open = $ebook_edit_link ? '<a href="' . esc_url($ebook_edit_link) . '">' : '';
2044 $link_close = $ebook_edit_link ? '</a>' : '';
2045 echo '<p class="description"><strong>' . esc_html__('Linked ebook', 'ebook-store') . ':</strong> ' . $link_open . esc_html($ebook_title) . $link_close . '</p>';
2046 } else {
2047 echo '<p class="description">' . esc_html__('This order is not currently linked to an ebook.', 'ebook-store') . '</p>';
2048 }
2049 echo '<p class="description">' . esc_html__( 'All original order fields remain editable below, grouped by task instead of one long flat form.', 'ebook-store' ) . '</p>';
2050 echo '</div>';
2051 echo '</div>';
2052 echo '</section>';
2053
2054 echo '<div class="ebook-order-sections">';
2055 foreach ($sections as $section_id => $section) {
2056 $open_attr = !empty($section['open']) ? ' open' : '';
2057 echo '<details class="ebook-order-section"' . $open_attr . '>';
2058 echo '<summary>' . esc_html($section['title']) . '</summary>';
2059 if (!empty($section['description'])) {
2060 echo '<p class="ebook-order-section__description">' . esc_html($section['description']) . '</p>';
2061 }
2062 echo '<div class="ebook-order-grid">';
2063 foreach ($section['fields'] as $field_key => $config) {
2064 $value = isset($field_values[$field_key]) ? $field_values[$field_key] : '';
2065 ebook_store_render_order_field($field_key, $config, $value);
2066 }
2067 echo '</div>';
2068 echo '</details>';
2069 }
2070 echo '</div>'; // sections
2071 echo '</div>'; // meta box wrapper/shell
2072
2073 echo '<script>
2074 (function(){
2075 document.addEventListener("click", function(event){
2076 var target = event.target;
2077 if (!target.classList.contains("ebook-order-copy-btn")) {
2078 return;
2079 }
2080 var selector = target.getAttribute("data-copy-target");
2081 if (!selector) {
2082 return;
2083 }
2084 var input = document.querySelector(selector);
2085 if (!input) {
2086 return;
2087 }
2088 input.focus();
2089 input.select();
2090 try {
2091 document.execCommand("copy");
2092 var originalText = target.textContent;
2093 var successText = target.getAttribute("data-copied-text") || "' . esc_js(__('Copied!', 'ebook-store')) . '";
2094 target.textContent = successText;
2095 target.classList.add("is-copied");
2096 setTimeout(function(){
2097 target.textContent = originalText;
2098 target.classList.remove("is-copied");
2099 }, 2000);
2100 } catch(e) {}
2101 });
2102 })();
2103 </script>';
2104 }
2105
2106
2107 function save_custom_meta_data_order($id) {
2108
2109 /* --- security verification --- */
2110 if(!wp_verify_nonce(@$_POST['ebook_order_nonce'], plugin_basename(__FILE__))) {
2111 return $id;
2112 } // end if
2113
2114 if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
2115 return $id;
2116 } // end if
2117
2118 // 'edit_post' is the correct meta capability for these custom post types.
2119 // The old code checked 'edit_page' in BOTH branches, so authorisation for
2120 // ebooks and orders was decided by page permissions.
2121 if ( ! current_user_can( 'edit_post', $id ) ) {
2122 return $id;
2123 }
2124 /* - end security verification - */
2125 if (!isset($_POST['order']) || !is_array($_POST['order'])) {
2126 return $id;
2127 }
2128
2129 $field_definitions = ebook_store_get_order_field_definitions();
2130
2131 foreach ($_POST['order'] as $k => $v) {
2132 $config = isset($field_definitions[$k]) ? $field_definitions[$k] : array();
2133 $sanitized_value = ebook_store_sanitize_order_field_value($v, $config);
2134 update_post_meta($id, $k, $sanitized_value);
2135 }
2136 }
2137
2138 function ebook_code_box() {
2139 wp_reset_postdata();
2140 $post_id = get_the_ID();
2141 // absint() matters here: the value is echoed inside an HTML attribute, and on
2142 // PHP 8 a string like "1'><script>" compares > 0 as true.
2143 $requested_post = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : 0;
2144 if ( $requested_post > 0 ) {
2145 $post_id = $requested_post;
2146 }
2147 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
2148 $locale = ebook_store_get_locale_strings();
2149 $formats_locale = ebook_store_get_format_labels();
2150 $output = '<input id="ebook_code" onClick="this.select()" readonly type="text" size="29" value=\'[ebook_store ebook_id="' . $post_id . '"]\' /> ' . __('Copy and paste this code in your post or page where you want the order form to appear.', 'ebook-store');
2151 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
2152 }
2153 function ebook_wp_custom_attachment() {
2154 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
2155 $locale = ebook_store_get_locale_strings();
2156 $formats_locale = ebook_store_get_format_labels();
2157 $ebook_id = get_the_ID();
2158
2159 // 'fields' => 'ids' keeps this to one lean query instead of hydrating every
2160 // ebook (and its whole meta table) just to build an <option> list.
2161 $ebook_ids = get_posts( array( 'post_type' => 'ebook', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'orderby' => 'title', 'order' => 'ASC' ) );
2162 $ebook_meta = get_post_meta($ebook_id, 'ebook', true);
2163 if (!is_array($ebook_meta)) {
2164 $ebook_meta = array();
2165 }
2166
2167 if (!isset($ebook_meta['ebook_bonus']) || !is_array($ebook_meta['ebook_bonus'])) {
2168 $ebook_meta['ebook_bonus'] = array();
2169 }
2170
2171 $ebook_bonus_options = '<option value="">' . esc_html__('Please select', 'ebook-store') . '</option>';
2172 foreach ( $ebook_ids as $bonus_id ) {
2173 if ( (int) $bonus_id === (int) $ebook_id ) {
2174 continue;
2175 }
2176 // Stored values come from the form as strings while these ids are ints,
2177 // so a strict int comparison never matched and the current selection
2178 // was shown empty — and then lost on the next save.
2179 $selected = in_array( (string) $bonus_id, array_map( 'strval', (array) $ebook_meta['ebook_bonus'] ), true ) ? ' selected="selected"' : '';
2180 $title = get_the_title( $bonus_id );
2181 $title = $title !== '' ? $title : __( '(no title)', 'ebook-store' );
2182 $ebook_bonus_options .= '<option value="' . esc_attr( $bonus_id ) . '"' . $selected . '>' . esc_html( $title ) . '</option>';
2183 }
2184
2185 wp_enqueue_script('jquery-ui-datepicker');
2186 // The jQuery UI datepicker styling comes from core rather than a 2010-era
2187 // stylesheet on Google's CDN, which also leaked every admin's IP to Google.
2188 wp_enqueue_style( 'wp-jquery-ui-dialog' );
2189 wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
2190
2191 wp_enqueue_style( 'ebookstorestylesheet' );
2192
2193 $img = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
2194 $img = is_array($img) ? $img : array();
2195
2196 $img_side_photo = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_side_photo', true);
2197 $img_side_photo = is_array($img_side_photo) ? $img_side_photo : array();
2198
2199 $img_cover = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_cover', true);
2200 $img_cover = is_array($img_cover) ? $img_cover : array();
2201
2202 $preview = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_preview', true);
2203 $preview = is_array($preview) ? $preview : array();
2204
2205 wp_nonce_field(plugin_basename(__FILE__), 'ebook_wp_custom_attachment_nonce');
2206
2207 $selected_authors = isset($ebook_meta['ebook_author']) && is_array($ebook_meta['ebook_author']) ? $ebook_meta['ebook_author'] : array();
2208 $selected_publishers = isset($ebook_meta['ebook_publisher']) && is_array($ebook_meta['ebook_publisher']) ? $ebook_meta['ebook_publisher'] : array();
2209
2210 $ebook_authors = '';
2211 foreach ( get_posts( array( 'post_type' => 'ebook_author', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'orderby' => 'title', 'order' => 'ASC' ) ) as $author_id ) {
2212 $selected = in_array( $author_id, $selected_authors, true ) ? ' selected' : '';
2213 $ebook_authors .= '<option value="' . esc_attr( $author_id ) . '"' . $selected . '>' . esc_html( get_the_title( $author_id ) ) . '</option>';
2214 }
2215
2216 $ebook_publishers = '';
2217 foreach ( get_posts( array( 'post_type' => 'ebook_publisher', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'orderby' => 'title', 'order' => 'ASC' ) ) as $publisher_id ) {
2218 $selected = in_array( $publisher_id, $selected_publishers, true ) ? ' selected' : '';
2219 $ebook_publishers .= '<option value="' . esc_attr( $publisher_id ) . '"' . $selected . '>' . esc_html( get_the_title( $publisher_id ) ) . '</option>';
2220 }
2221
2222 $currencies = array(
2223 '' => '-- Optional --',
2224 'USD' => 'US Dollar',
2225 'EUR' => 'Euro',
2226 'ILS' => 'Israeli New Sheqel',
2227 'INR' => 'Indian Rupee',
2228 'GBP' => 'Pounds Sterling',
2229 'AUD' => 'Australian Dollar',
2230 'CAD' => 'Canadian Dollar',
2231 'JPY' => 'Japan Yen',
2232 'NZD' => 'New Zealand Dollar',
2233 'CHF' => 'Swiss Franc',
2234 'HKD' => 'Hong Kong Dollar',
2235 'SGD' => 'Singapore Dollar',
2236 'SEK' => 'Sweden Krona',
2237 'DKK' => 'Danish Krone',
2238 'PLN' => 'New Zloty',
2239 'NOK' => 'Norwegian Krone',
2240 'HUF' => 'Forint',
2241 'MXN' => 'Mexican Pesos',
2242 'CZK' => 'Czech Koruna',
2243 'BRL' => 'Brazilian Real',
2244 'TWD' => 'Taiwan New Dollar',
2245 'TRY' => 'Turkish Lira',
2246 'MYR' => 'Malaysian Ringgit',
2247 'ZAR' => 'South African Rands',
2248 'NGN' => 'Nigerian Naira',
2249 'RON' => 'Romanian leu',
2250 'THB' => 'Thai Baht'
2251 );
2252
2253 $selected_currency = isset($ebook_meta['paypal_currency']) ? $ebook_meta['paypal_currency'] : esc_attr(get_option('paypal_currency','USD'));
2254 $paypal_currency_options = '';
2255 foreach ($currencies as $currency => $name) {
2256 $is_selected = $currency === $selected_currency ? ' selected' : '';
2257 $paypal_currency_options .= '<option value="' . esc_attr($currency) . '"' . $is_selected . '>' . esc_html($name) . '</option>';
2258 }
2259 $form_templates = ebook_store_get_form_templates();
2260 $selected_form_template = isset($ebook_meta['form_template']) ? sanitize_key($ebook_meta['form_template']) : '';
2261 $selected_form_template = ebook_store_resolve_form_template($selected_form_template);
2262 if ($selected_form_template === '' || !isset($form_templates[$selected_form_template])) {
2263 $selected_form_template = ebook_store_resolve_form_template('');
2264 }
2265 $form_template_options = '';
2266 foreach ($form_templates as $template_key => $template_config) {
2267 $template_label = isset($template_config['label']) ? $template_config['label'] : $template_key;
2268 $form_template_options .= '<option value="' . esc_attr($template_key) . '"' . selected($selected_form_template, $template_key, false) . '>' . esc_html($template_label) . '</option>';
2269 }
2270
2271 $embed_shortcode = sprintf('[ebook_store ebook_id="%d"]', $ebook_id);
2272 $buy_shortcode = sprintf('[ebook_store_buy ebook_id="%d"]', $ebook_id);
2273 $max_upload_size = ini_get('upload_max_filesize') . 'B';
2274 $license_key = get_option('ebook_store_license_key');
2275 $upgrade_text = '';
2276 if ($license_key == '') {
2277 $upgrade_text = 'Supported file types in free version are: <b style="color:green;">PDF</b>, to use <b style="color:red;">EPUB</b>, <b style="color:red;">MOBI</b>, <b style="color:red;">MP3</b>, <b style="color:red;">TXT</b> and <b style="color:red;">ZIP</b> you can get the full version from here: <a target="_blank" href="https://www.shopfiles.com/index.php/products/wordpress-ebook-store">Upgrade Ebook Store</a>, no data will be lost upon upgrade.';
2278 }
2279
2280 $conversion_hint = '';
2281 if (empty($img['url'])) {
2282 $conversion_hint = sprintf(
2283 __('<strong>Need help converting files?</strong> Try <a target="_blank" href="%s">Calibre</a>, a free desktop tool for generating every major ebook format.', 'ebook-store'),
2284 'http://calibre-ebook.com/download'
2285 );
2286 }
2287
2288 $cover_size = '';
2289 if (!empty($img_cover['file']) && file_exists($img_cover['file'])) {
2290 $cover_size = humanFileSize(filesize($img_cover['file']));
2291 }
2292
2293 $side_size = '';
2294 if (!empty($img_side_photo['file']) && file_exists($img_side_photo['file'])) {
2295 $side_size = humanFileSize(filesize($img_side_photo['file']));
2296 }
2297
2298 $donate_or_download = isset($ebook_meta['donate_or_download']) ? $ebook_meta['donate_or_download'] : 'paid';
2299 $extendhtml = apply_filters('ebook_store_form_extend', get_the_ID());
2300 $sales_index = ebook_store_get_ebook_sales_index();
2301 $current_sales = isset($sales_index[$ebook_id]) ? $sales_index[$ebook_id] : array(
2302 'orders' => 0,
2303 'total' => 0.0,
2304 'last_7d' => 0.0,
2305 'last_30d' => 0.0,
2306 'latest_order_gmt' => '',
2307 );
2308 $current_price = isset($ebook_meta['ebook_price']) ? (float) $ebook_meta['ebook_price'] : 0.0;
2309 $current_currency = $selected_currency !== '' ? $selected_currency : esc_attr(get_option('paypal_currency','USD'));
2310 $current_status = get_post_status($ebook_id);
2311 $current_title = get_the_title($ebook_id);
2312 if ($current_title === '') {
2313 $current_title = __('Untitled ebook', 'ebook-store');
2314 }
2315 $current_permalink = $ebook_id ? get_permalink($ebook_id) : '';
2316 $current_latest_order = $current_sales['latest_order_gmt'] !== '' ? human_time_diff(strtotime($current_sales['latest_order_gmt'] . ' UTC'), current_time('timestamp', true)) : '';
2317 $delivery_mode_label = __('Paid download', 'ebook-store');
2318 if ($donate_or_download === 'free') {
2319 $delivery_mode_label = __('Free download', 'ebook-store');
2320 } elseif ($donate_or_download === 'donate') {
2321 $delivery_mode_label = __('Donate to download', 'ebook-store');
2322 }
2323 $template_label = isset($form_templates[$selected_form_template]['label']) ? $form_templates[$selected_form_template]['label'] : ucfirst($selected_form_template);
2324 $cover_preview_url = !empty($img_cover['url']) ? $img_cover['url'] : '';
2325 $cover_preview_note = $cover_preview_url ? basename($cover_preview_url) : __('No cover uploaded yet', 'ebook-store');
2326 $template_override_settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2327 $enable_custom_confirmation_page = !empty($template_override_settings['enable_custom_confirmation_page']);
2328 $enable_custom_delivery_email = !empty($template_override_settings['enable_custom_delivery_email']);
2329 $custom_confirmation_page_content = isset($template_override_settings['confirmation_page_content']) ? $template_override_settings['confirmation_page_content'] : '';
2330 $custom_delivery_email_subject = isset($template_override_settings['delivery_email_subject']) ? $template_override_settings['delivery_email_subject'] : '';
2331 $custom_delivery_email_text = isset($template_override_settings['delivery_email_text']) ? $template_override_settings['delivery_email_text'] : '';
2332
2333 ob_start();
2334 ?>
2335 <script>var ebook_store_license_key = '<?php echo esc_js($license_key); ?>'; </script>
2336 <div class="ebook-admin-panel ebook-admin-panel--workspace">
2337 <section class="ebook-editor-hero">
2338 <div class="ebook-editor-hero__cover">
2339 <div class="ebook-editor-hero__cover-frame <?php echo $cover_preview_url ? 'has-image' : 'is-empty'; ?>">
2340 <?php if ($cover_preview_url) : ?>
2341 <img src="<?php echo esc_url($cover_preview_url); ?>" alt="<?php echo esc_attr($current_title); ?>">
2342 <?php else : ?>
2343 <span class="ebook-editor-hero__cover-placeholder"><?php esc_html_e('Cover preview', 'ebook-store'); ?></span>
2344 <?php endif; ?>
2345 </div>
2346 <span class="ebook-editor-hero__cover-meta"><?php echo esc_html($cover_preview_note); ?></span>
2347 </div>
2348 <div class="ebook-editor-hero__content">
2349 <span class="ebook-editor-hero__eyebrow"><?php esc_html_e('Ebook editor', 'ebook-store'); ?></span>
2350 <h2 class="ebook-editor-hero__title"><?php echo esc_html($current_title); ?></h2>
2351 <p class="ebook-editor-hero__summary"><?php esc_html_e('Set up pricing, delivery, files, and storefront media in one cleaner product workflow. Everything below keeps the existing save behavior, but the layout is optimized for faster publishing.', 'ebook-store'); ?></p>
2352 <div class="ebook-editor-hero__badges">
2353 <span class="ebook-editor-badge"><?php esc_html_e('Status', 'ebook-store'); ?> <strong><?php echo esc_html($current_status ? $current_status : __('draft', 'ebook-store')); ?></strong></span>
2354 <span class="ebook-editor-badge"><?php esc_html_e('Mode', 'ebook-store'); ?> <strong><?php echo esc_html($delivery_mode_label); ?></strong></span>
2355 <span class="ebook-editor-badge"><?php esc_html_e('Template', 'ebook-store'); ?> <strong><?php echo esc_html($template_label); ?></strong></span>
2356 <span class="ebook-editor-badge"><?php esc_html_e('Price', 'ebook-store'); ?> <strong><?php echo esc_html(ebook_store_order_money_display($current_price, $current_currency)); ?></strong></span>
2357 <span class="ebook-editor-badge"><?php esc_html_e('Orders', 'ebook-store'); ?> <strong><?php echo esc_html(number_format_i18n($current_sales['orders'])); ?></strong></span>
2358 </div>
2359 </div>
2360 <div class="ebook-editor-hero__side">
2361 <div class="ebook-editor-hero__stat">
2362 <span><?php esc_html_e('Revenue', 'ebook-store'); ?></span>
2363 <strong><?php echo esc_html(ebook_store_order_money_display($current_sales['total'], $current_currency)); ?></strong>
2364 <small><?php echo esc_html(sprintf(__('Last 30 days: %s', 'ebook-store'), ebook_store_order_money_display($current_sales['last_30d'], $current_currency))); ?></small>
2365 </div>
2366 <div class="ebook-editor-hero__stat">
2367 <span><?php esc_html_e('Latest order', 'ebook-store'); ?></span>
2368 <strong><?php echo esc_html($current_latest_order ? sprintf(__('%s ago', 'ebook-store'), $current_latest_order) : __('No orders yet', 'ebook-store')); ?></strong>
2369 <small><?php printf(esc_html__('Upload max: %s', 'ebook-store'), esc_html($max_upload_size)); ?></small>
2370 </div>
2371 <?php if ($current_permalink) : ?>
2372 <a class="button button-secondary" href="<?php echo esc_url($current_permalink); ?>" target="_blank" rel="noopener"><?php esc_html_e('View ebook page', 'ebook-store'); ?></a>
2373 <?php endif; ?>
2374 </div>
2375 </section>
2376
2377 <div class="ebook-editor-layout">
2378 <div class="ebook-editor-layout__main">
2379 <section class="ebook-card ebook-card--summary">
2380 <div class="ebook-card__header">
2381 <h3><?php esc_html_e('Embed & launch tools', 'ebook-store'); ?></h3>
2382 <p><?php esc_html_e('Copy the order form or buy-button shortcode and jump straight into placement on your content pages.', 'ebook-store'); ?></p>
2383 </div>
2384 <div class="ebook-card__body">
2385 <div class="ebook-code-grid">
2386 <div class="ebook-code-block">
2387 <label for="ebook_code"><?php esc_html_e('Order form shortcode', 'ebook-store'); ?></label>
2388 <div class="ebook-code-field">
2389 <input id="ebook_code" onClick="this.select()" readonly type="text" class="regular-text code" value="<?php echo esc_attr($embed_shortcode); ?>" />
2390 <button type="button" class="button button-secondary ebook-code__copy" data-copy-target="#ebook_code"><?php esc_html_e('Copy', 'ebook-store'); ?></button>
2391 <span class="ebook-code__status" aria-live="polite"></span>
2392 </div>
2393 </div>
2394 <div class="ebook-code-block">
2395 <label for="ebook_code_buy"><?php esc_html_e('Buy button shortcode', 'ebook-store'); ?></label>
2396 <div class="ebook-code-field">
2397 <input id="ebook_code_buy" onClick="this.select()" readonly type="text" class="regular-text code" value="<?php echo esc_attr($buy_shortcode); ?>" />
2398 <button type="button" class="button button-secondary ebook-code__copy" data-copy-target="#ebook_code_buy"><?php esc_html_e('Copy', 'ebook-store'); ?></button>
2399 <span class="ebook-code__status" aria-live="polite"></span>
2400 </div>
2401 </div>
2402 </div>
2403 <ul class="ebook-inline-meta">
2404 <li><?php esc_html_e('Use the main shortcode when you want the full order form to render on a page or post.', 'ebook-store'); ?></li>
2405 <li><?php esc_html_e('Use the buy-button shortcode when you only need a compact call to action.', 'ebook-store'); ?></li>
2406 </ul>
2407 <?php if ($conversion_hint) : ?>
2408 <div class="ebook-inline-hint"><?php echo wp_kses_post($conversion_hint); ?></div>
2409 <?php endif; ?>
2410 <?php if ($upgrade_text) : ?>
2411 <div class="ebook-alert ebook-alert--upgrade"><?php echo wp_kses_post($upgrade_text); ?></div>
2412 <?php endif; ?>
2413 </div>
2414 </section>
2415
2416 <section class="ebook-card ebook-card--delivery">
2417 <div class="ebook-card__header">
2418 <h3><?php esc_html_e('Pricing & delivery', 'ebook-store'); ?></h3>
2419 <p><?php esc_html_e('Decide whether this title is paid, free, or donation-based, then set the selling price buyers should see.', 'ebook-store'); ?></p>
2420 </div>
2421 <div class="ebook-card__body">
2422 <div class="ebook-field-grid ebook-field-grid--tight">
2423 <div class="ebook-field">
2424 <label for="ebook_price"><?php esc_html_e('Price', 'ebook-store'); ?> <span class="required">*</span></label>
2425 <input id="ebook_price" name="ebook[ebook_price]" placeholder="0.00" min="0" type="number" step="any" value="<?php echo isset($ebook_meta['ebook_price']) ? esc_attr($ebook_meta['ebook_price']) : ''; ?>">
2426 <p class="description"><?php esc_html_e('For paid mode, zero is automatically adjusted to a minimum amount on save.', 'ebook-store'); ?></p>
2427 </div>
2428 <div class="ebook-field">
2429 <label for="ebook_paypal_currency"><?php esc_html_e('Currency', 'ebook-store'); ?></label>
2430 <select id="ebook_paypal_currency" name="ebook[paypal_currency]">
2431 <?php echo $paypal_currency_options; ?>
2432 </select>
2433 <p class="description"><?php esc_html_e('Leave this aligned with your store currency unless a title needs a specific override.', 'ebook-store'); ?></p>
2434 </div>
2435 <div class="ebook-field">
2436 <label for="ebook_form_template"><?php esc_html_e('Order form template', 'ebook-store'); ?></label>
2437 <select id="ebook_form_template" name="ebook[form_template]">
2438 <?php echo $form_template_options; ?>
2439 </select>
2440 <p class="description"><?php esc_html_e('This becomes the default frontend template for shortcode embeds of this ebook. Shortcode `template=\"...\"` still overrides it.', 'ebook-store'); ?></p>
2441 </div>
2442 </div>
2443 <div class="ebook-delivery-options">
2444 <label class="ebook-delivery-option">
2445 <input name="ebook[donate_or_download]" type="radio" value="paid" <?php checked($donate_or_download === 'paid' || $donate_or_download === ''); ?>>
2446 <div class="ebook-delivery-option__content">
2447 <span class="ebook-delivery-option__title"><?php esc_html_e('Paid download', 'ebook-store'); ?></span>
2448 <span class="ebook-delivery-option__description"><?php esc_html_e('Route buyers through checkout, confirm payment, and then release the file securely.', 'ebook-store'); ?></span>
2449 </div>
2450 </label>
2451 <label class="ebook-delivery-option goPro2">
2452 <input class="goPro2" name="ebook[donate_or_download]" type="radio" value="free" <?php checked($donate_or_download, 'free'); ?>>
2453 <div class="ebook-delivery-option__content">
2454 <span class="ebook-delivery-option__title"><?php esc_html_e('Free download', 'ebook-store'); ?></span>
2455 <span class="ebook-delivery-option__description"><?php esc_html_e('Skip payment entirely and hand off the file immediately after the visitor requests it.', 'ebook-store'); ?></span>
2456 </div>
2457 </label>
2458 <label class="ebook-delivery-option goPro2">
2459 <input class="goPro2" name="ebook[donate_or_download]" type="radio" value="donate" <?php checked($donate_or_download, 'donate'); ?>>
2460 <div class="ebook-delivery-option__content">
2461 <span class="ebook-delivery-option__title"><?php esc_html_e('Donate to download', 'ebook-store'); ?></span>
2462 <span class="ebook-delivery-option__description"><?php esc_html_e('Allow supporters to choose their own amount while still using the checkout flow.', 'ebook-store'); ?></span>
2463 </div>
2464 </label>
2465 </div>
2466 </div>
2467 </section>
2468
2469 <section class="ebook-card ebook-card--files">
2470 <div class="ebook-card__header">
2471 <h3><?php esc_html_e('Files & formats', 'ebook-store'); ?></h3>
2472 <p><?php esc_html_e('Upload the core PDF first, then add optional EPUB, MOBI, ZIP, audio, or video formats for broader device support.', 'ebook-store'); ?></p>
2473 </div>
2474 <div class="ebook-card__body ebook-card__body--flush">
2475 <?php do_action('ebook_store_file_formats_form', $ebook_id); ?>
2476 </div>
2477 </section>
2478 </div>
2479
2480 <aside class="ebook-editor-layout__sidebar">
2481 <section class="ebook-card ebook-card--media">
2482 <div class="ebook-card__header">
2483 <h3><?php esc_html_e('Media & preview', 'ebook-store'); ?></h3>
2484 <p><?php esc_html_e('Manage the storefront visuals buyers see first: cover, spine image, and sample preview file.', 'ebook-store'); ?></p>
2485 </div>
2486 <div class="ebook-card__body">
2487 <div class="ebook-media-grid">
2488 <div class="ebook-media-field">
2489 <span class="ebook-media-label"><?php esc_html_e('Cover image', 'ebook-store'); ?></span>
2490 <div class="ebook_store_image_wrapper">
2491 <div class="ebook_store_image_preview cover-image <?php echo !empty($img_cover['url']) ? 'has-image' : 'empty'; ?>">
2492 <?php if (!empty($img_cover['url'])) : ?>
2493 <img src="<?php echo esc_url($img_cover['url']); ?>" alt="<?php echo esc_attr(basename($img_cover['url'])); ?>">
2494 <div class="remove-image"><span class="dashicons dashicons-no-alt"></span></div>
2495 <?php if ($cover_size) : ?>
2496 <div class="image-size"><?php echo esc_html($cover_size); ?></div>
2497 <?php endif; ?>
2498 <?php endif; ?>
2499 </div>
2500 <div class="ebook-media-controls">
2501 <input name="ebook_wp_custom_attachment_cover" type="file" accept="image/*">
2502 <span class="image-upload-hint"><?php esc_html_e('Recommended size: 180 x 260px', 'ebook-store'); ?></span>
2503 <?php if (!empty($img_cover['url'])) : ?>
2504 <p class="ebook-media-current"><?php printf(esc_html__('Current file: %s', 'ebook-store'), '<a href="' . esc_url($img_cover['url']) . '" target="_blank">' . esc_html(basename($img_cover['url'])) . '</a>'); ?></p>
2505 <?php endif; ?>
2506 </div>
2507 </div>
2508 </div>
2509 <div class="ebook-media-field">
2510 <span class="ebook-media-label"><?php esc_html_e('Side image', 'ebook-store'); ?></span>
2511 <div class="ebook_store_image_wrapper">
2512 <div class="ebook_store_image_preview side-image <?php echo !empty($img_side_photo['url']) ? 'has-image' : 'empty'; ?>">
2513 <?php if (!empty($img_side_photo['url'])) : ?>
2514 <img src="<?php echo esc_url($img_side_photo['url']); ?>" alt="<?php echo esc_attr(basename($img_side_photo['url'])); ?>">
2515 <div class="remove-image"><span class="dashicons dashicons-no-alt"></span></div>
2516 <?php if ($side_size) : ?>
2517 <div class="image-size"><?php echo esc_html($side_size); ?></div>
2518 <?php endif; ?>
2519 <?php endif; ?>
2520 </div>
2521 <div class="ebook-media-controls">
2522 <input name="ebook_wp_custom_attachment_side_photo" type="file" accept="image/*">
2523 <span class="image-upload-hint"><?php esc_html_e('Recommended size: 20 x 260px', 'ebook-store'); ?></span>
2524 <?php if (!empty($img_side_photo['url'])) : ?>
2525 <p class="ebook-media-current"><?php printf(esc_html__('Current file: %s', 'ebook-store'), '<a href="' . esc_url($img_side_photo['url']) . '" target="_blank">' . esc_html(basename($img_side_photo['url'])) . '</a>'); ?></p>
2526 <?php endif; ?>
2527 </div>
2528 </div>
2529 </div>
2530 <div class="ebook-media-field">
2531 <label for="ebook_wp_custom_attachment_preview"><?php esc_html_e('Preview file', 'ebook-store'); ?></label>
2532 <?php if (!empty($preview['url'])) : ?>
2533 <p class="ebook-media-current">
2534 <?php
2535 printf(
2536 esc_html__('Uploaded: %s', 'ebook-store'),
2537 '<a href="' . esc_url($preview['url']) . '" target="_blank">' . esc_html(basename($preview['url'])) . '</a>'
2538 );
2539 ?>
2540 <label class="ebook-inline-delete" title="<?php esc_attr_e('Delete', 'ebook-store'); ?>">
2541 <input type="checkbox" name="ebook_store_delete_preview">
2542 <span class="dashicons dashicons-trash"></span>
2543 <span class="screen-reader-text"><?php esc_html_e('Delete preview file', 'ebook-store'); ?></span>
2544 </label>
2545 </p>
2546 <?php else : ?>
2547 <p class="ebook-media-current muted"><?php esc_html_e('No preview file uploaded yet.', 'ebook-store'); ?></p>
2548 <?php endif; ?>
2549 <input type="file" id="ebook_wp_custom_attachment_preview" name="ebook_wp_custom_attachment_preview" value="" size="25">
2550 </div>
2551 </div>
2552 </div>
2553 </section>
2554
2555 <section class="ebook-card ebook-card--additional">
2556 <div class="ebook-card__header">
2557 <h3><?php esc_html_e('Catalog metadata', 'ebook-store'); ?></h3>
2558 <p><?php esc_html_e('Add publication details and relationships so the catalog stays complete and easier to browse.', 'ebook-store'); ?></p>
2559 </div>
2560 <div class="ebook-card__body">
2561 <div class="ebook-field-grid">
2562 <div class="ebook-field">
2563 <label for="ebook_date"><?php esc_html_e('Publication date', 'ebook-store'); ?></label>
2564 <input id="ebook_date" name="ebook[ebook_date]" type="text" value="<?php echo isset($ebook_meta['ebook_date']) ? esc_attr($ebook_meta['ebook_date']) : ''; ?>">
2565 </div>
2566 <div class="ebook-field">
2567 <label for="ebook_pages"><?php esc_html_e('Pages', 'ebook-store'); ?></label>
2568 <input id="ebook_pages" name="ebook[ebook_pages]" type="number" value="<?php echo isset($ebook_meta['ebook_pages']) ? esc_attr($ebook_meta['ebook_pages']) : ''; ?>">
2569 </div>
2570 </div>
2571 <div class="ebook-field">
2572 <label for="ebook_author_select"><?php esc_html_e('Author', 'ebook-store'); ?> (<?php esc_html_e('optional', 'ebook-store'); ?>)</label>
2573 <select id="ebook_author_select" name="ebook[ebook_author][]" multiple class="ebook-multiselect" size="5">
2574 <option value="0"><?php esc_html_e('None', 'ebook-store'); ?></option>
2575 <?php echo $ebook_authors; ?>
2576 </select>
2577 </div>
2578 <div class="ebook-field">
2579 <label for="ebook_publisher_select"><?php esc_html_e('Publisher', 'ebook-store'); ?> (<?php esc_html_e('optional', 'ebook-store'); ?>)</label>
2580 <select id="ebook_publisher_select" name="ebook[ebook_publisher][]" multiple class="ebook-multiselect" size="5">
2581 <option value="0"><?php esc_html_e('None', 'ebook-store'); ?></option>
2582 <?php echo $ebook_publishers; ?>
2583 </select>
2584 </div>
2585 <div class="ebook-field goPro2">
2586 <label for="ebook_bonus_select"><?php esc_html_e('Bonus ebook', 'ebook-store'); ?></label>
2587 <select id="ebook_bonus_select" class="goPro2 ebook-multiselect" multiple size="6" name="ebook[ebook_bonus][]">
2588 <?php echo $ebook_bonus_options; ?>
2589 </select>
2590 <p class="description"><?php esc_html_e('Optional related downloads bundled with the main title.', 'ebook-store'); ?></p>
2591 </div>
2592 <?php if ($extendhtml != get_the_ID()) : ?>
2593 <div class="ebook-card__extension">
2594 <?php echo $extendhtml; ?>
2595 </div>
2596 <?php endif; ?>
2597 </div>
2598 </section>
2599 </aside>
2600 </div>
2601 <section class="ebook-card ebook-card--template-overrides">
2602 <div class="ebook-card__header">
2603 <h3><?php esc_html_e('Confirmation overrides', 'ebook-store'); ?></h3>
2604 <p><?php esc_html_e('By default this ebook uses the confirmation page and delivery email from Settings > Templates. Enable an override below only when this specific ebook needs different post-purchase content.', 'ebook-store'); ?></p>
2605 </div>
2606 <div class="ebook-card__body">
2607 <div class="ebook-template-override-grid">
2608 <div class="ebook-template-override-card">
2609 <div class="ebook-field">
2610 <span class="ebook-template-override-card__label"><?php esc_html_e('Enable custom confirmation page', 'ebook-store'); ?></span>
2611 <div class="ebook-template-override-radios" role="radiogroup" aria-label="<?php esc_attr_e('Enable custom confirmation page', 'ebook-store'); ?>">
2612 <label class="ebook-template-override-radio">
2613 <input type="radio" name="ebook_store_enable_custom_confirmation_page" value="0" <?php checked(!$enable_custom_confirmation_page); ?>>
2614 <span><?php esc_html_e('Use Settings > Templates default', 'ebook-store'); ?></span>
2615 </label>
2616 <label class="ebook-template-override-radio">
2617 <input type="radio" name="ebook_store_enable_custom_confirmation_page" value="1" <?php checked($enable_custom_confirmation_page); ?>>
2618 <span><?php esc_html_e('Enable for this ebook', 'ebook-store'); ?></span>
2619 </label>
2620 </div>
2621 <p class="description"><?php esc_html_e('If enabled, this content replaces the global Thank You page content for this ebook only.', 'ebook-store'); ?></p>
2622 </div>
2623 <div class="ebook-template-override-fields" data-override-fields="confirmation" <?php echo !$enable_custom_confirmation_page ? 'hidden' : ''; ?>>
2624 <div class="ebook-field">
2625 <label for="ebook_store_custom_confirmation_page_content_editor"><?php esc_html_e('Custom confirmation page content', 'ebook-store'); ?></label>
2626 <?php
2627 wp_editor(
2628 $custom_confirmation_page_content,
2629 'ebook_store_custom_confirmation_page_content_editor',
2630 array(
2631 'textarea_name' => 'ebook_store_custom_confirmation_page_content',
2632 'textarea_rows' => 10,
2633 )
2634 );
2635 ?>
2636 </div>
2637 </div>
2638 </div>
2639 <div class="ebook-template-override-card">
2640 <div class="ebook-field">
2641 <span class="ebook-template-override-card__label"><?php esc_html_e('Enable custom delivery email', 'ebook-store'); ?></span>
2642 <div class="ebook-template-override-radios" role="radiogroup" aria-label="<?php esc_attr_e('Enable custom delivery email', 'ebook-store'); ?>">
2643 <label class="ebook-template-override-radio">
2644 <input type="radio" name="ebook_store_enable_custom_delivery_email" value="0" <?php checked(!$enable_custom_delivery_email); ?>>
2645 <span><?php esc_html_e('Use Settings > Templates default', 'ebook-store'); ?></span>
2646 </label>
2647 <label class="ebook-template-override-radio">
2648 <input type="radio" name="ebook_store_enable_custom_delivery_email" value="1" <?php checked($enable_custom_delivery_email); ?>>
2649 <span><?php esc_html_e('Enable for this ebook', 'ebook-store'); ?></span>
2650 </label>
2651 </div>
2652 <p class="description"><?php esc_html_e('If enabled, this subject and message replace the global email delivery template for this ebook only.', 'ebook-store'); ?></p>
2653 </div>
2654 <div class="ebook-template-override-fields" data-override-fields="email" <?php echo !$enable_custom_delivery_email ? 'hidden' : ''; ?>>
2655 <div class="ebook-field">
2656 <label for="ebook_store_custom_delivery_email_subject"><?php esc_html_e('Custom delivery email subject', 'ebook-store'); ?></label>
2657 <input type="text" id="ebook_store_custom_delivery_email_subject" name="ebook_store_custom_delivery_email_subject" value="<?php echo esc_attr($custom_delivery_email_subject); ?>">
2658 </div>
2659 <div class="ebook-field">
2660 <label for="ebook_store_custom_delivery_email_text_editor"><?php esc_html_e('Custom delivery email content', 'ebook-store'); ?></label>
2661 <?php
2662 wp_editor(
2663 $custom_delivery_email_text,
2664 'ebook_store_custom_delivery_email_text_editor',
2665 array(
2666 'textarea_name' => 'ebook_store_custom_delivery_email_text',
2667 'textarea_rows' => 10,
2668 )
2669 );
2670 ?>
2671 </div>
2672 </div>
2673 </div>
2674 </div>
2675 </div>
2676 </section>
2677 </div>
2678 <script>
2679 jQuery(document).ready(function() {
2680 jQuery('#ebook_date').datepicker({
2681 dateFormat : 'dd-mm-yy'
2682 });
2683 jQuery('#wp-admin-bar-view').hide();
2684 jQuery('input[type=file]').show();
2685
2686 function ebookStoreToggleOverrideFields(groupName, targetSelector) {
2687 var enabled = jQuery('input[name="' + groupName + '"]:checked').val() === '1';
2688 var $target = jQuery(targetSelector);
2689 if (!$target.length) {
2690 return;
2691 }
2692
2693 $target.find('input, textarea, select, button').prop('disabled', !enabled);
2694 if (enabled) {
2695 $target.prop('hidden', false).stop(true, true).slideDown(160);
2696 } else {
2697 $target.stop(true, true).slideUp(160, function() {
2698 $target.prop('hidden', true);
2699 });
2700 }
2701 }
2702
2703 function ebookStoreRefreshOverrideRadioState(groupName) {
2704 var $inputs = jQuery('input[name="' + groupName + '"]');
2705 $inputs.each(function() {
2706 var $input = jQuery(this);
2707 $input.closest('.ebook-template-override-radio').toggleClass('is-selected', $input.is(':checked'));
2708 });
2709 }
2710
2711 ebookStoreToggleOverrideFields('ebook_store_enable_custom_confirmation_page', '[data-override-fields="confirmation"]');
2712 ebookStoreToggleOverrideFields('ebook_store_enable_custom_delivery_email', '[data-override-fields="email"]');
2713 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_confirmation_page');
2714 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_delivery_email');
2715
2716 jQuery('input[name="ebook_store_enable_custom_confirmation_page"]').on('change', function() {
2717 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_confirmation_page');
2718 ebookStoreToggleOverrideFields('ebook_store_enable_custom_confirmation_page', '[data-override-fields="confirmation"]');
2719 });
2720
2721 jQuery('input[name="ebook_store_enable_custom_delivery_email"]').on('change', function() {
2722 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_delivery_email');
2723 ebookStoreToggleOverrideFields('ebook_store_enable_custom_delivery_email', '[data-override-fields="email"]');
2724 });
2725 });
2726 </script>
2727 <?php
2728 $editor_output = ob_get_clean();
2729 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $editor_output ) : $editor_output;
2730 } // end ebook_wp_custom_attachment
2731
2732 function ebook_store_get_default_template_content_settings() {
2733 $QSWPOptions = new QSWPOptions();
2734
2735 return array(
2736 'confirmation_page_content' => get_option('thankyou_page', $QSWPOptions->thankyou_page),
2737 'delivery_email_subject' => get_option('email_delivery_subject', $QSWPOptions->email_delivery_subject),
2738 'delivery_email_text' => get_option('email_delivery_text', $QSWPOptions->email_delivery_text),
2739 );
2740 }
2741
2742 function ebook_store_get_ebook_template_override_settings($ebook_id) {
2743 $ebook_id = absint($ebook_id);
2744 $defaults = ebook_store_get_default_template_content_settings();
2745 $custom_confirmation_page_content = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_confirmation_page_content', true) : '';
2746 $custom_delivery_email_subject = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_delivery_email_subject', true) : '';
2747 $custom_delivery_email_text = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_delivery_email_text', true) : '';
2748
2749 return array(
2750 'enable_custom_confirmation_page' => $ebook_id > 0 && (string) get_post_meta($ebook_id, 'ebook_store_enable_custom_confirmation_page', true) === '1',
2751 'enable_custom_delivery_email' => $ebook_id > 0 && (string) get_post_meta($ebook_id, 'ebook_store_enable_custom_delivery_email', true) === '1',
2752 'confirmation_page_content' => $custom_confirmation_page_content !== '' ? $custom_confirmation_page_content : $defaults['confirmation_page_content'],
2753 'delivery_email_subject' => $custom_delivery_email_subject !== '' ? $custom_delivery_email_subject : $defaults['delivery_email_subject'],
2754 'delivery_email_text' => $custom_delivery_email_text !== '' ? $custom_delivery_email_text : $defaults['delivery_email_text'],
2755 'default_confirmation_page_content' => $defaults['confirmation_page_content'],
2756 'default_delivery_email_subject' => $defaults['delivery_email_subject'],
2757 'default_delivery_email_text' => $defaults['delivery_email_text'],
2758 );
2759 }
2760
2761 function ebook_store_get_effective_confirmation_page_content($ebook_id) {
2762 $settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2763
2764 if (!empty($settings['enable_custom_confirmation_page']) && trim((string) $settings['confirmation_page_content']) !== '') {
2765 return (string) $settings['confirmation_page_content'];
2766 }
2767
2768 return (string) $settings['default_confirmation_page_content'];
2769 }
2770
2771 function ebook_store_get_effective_delivery_email_template($ebook_id) {
2772 $settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2773
2774 if (!empty($settings['enable_custom_delivery_email'])) {
2775 return array(
2776 'subject' => trim((string) $settings['delivery_email_subject']) !== '' ? (string) $settings['delivery_email_subject'] : (string) $settings['default_delivery_email_subject'],
2777 'text' => trim((string) $settings['delivery_email_text']) !== '' ? (string) $settings['delivery_email_text'] : (string) $settings['default_delivery_email_text'],
2778 );
2779 }
2780
2781 return array(
2782 'subject' => (string) $settings['default_delivery_email_subject'],
2783 'text' => (string) $settings['default_delivery_email_text'],
2784 );
2785 }
2786
2787 function ebook_set_upload_dir( $upload ) {
2788 //wp_error(print_r($upload,true));
2789 // Override the year / month being based on the post publication date, if year/month organization is enabled
2790 if ( get_option( 'uploads_use_yearmonth_folders' ) ) {
2791 // Generate the yearly and monthly dirs
2792 $time = current_time( 'mysql' );
2793 $y = substr( $time, 0, 4 );
2794 $m = substr( $time, 5, 2 );
2795 $upload['subdir'] = "/$y/$m";
2796 }
2797
2798 $upload['subdir'] = '/ebooks' . $upload['subdir'];
2799 $upload['path'] = $upload['basedir'] . $upload['subdir'];
2800 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
2801 // The master files a customer paid for live here. Protection is written
2802 // unconditionally: it used to be REMOVED whenever the WooCommerce PDF reader
2803 // option was on, which published every paid ebook at a guessable URL.
2804 ebook_store_protect_uploads_dir( $upload['basedir'] . '/ebooks' );
2805
2806 return $upload;
2807 }
2808
2809 /**
2810 * Make an upload directory unreadable over HTTP.
2811 *
2812 * Writes both the modern mod_authz_core directive and the 2.2-era fallback, plus
2813 * an index.php so a server with directory indexes turned on still shows nothing.
2814 *
2815 * This is Apache/LiteSpeed only. On nginx the equivalent location block has to
2816 * be added to the server config, which is why the download handler streams files
2817 * through PHP rather than relying on the directory rules alone.
2818 *
2819 * @param string $dir Absolute path.
2820 */
2821 function ebook_store_protect_uploads_dir( $dir ) {
2822 if ( $dir === '' ) {
2823 return;
2824 }
2825
2826 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
2827 return;
2828 }
2829
2830 $htaccess = $dir . '/.htaccess';
2831
2832 if ( ! file_exists( $htaccess ) ) {
2833 @file_put_contents(
2834 $htaccess,
2835 "Options -Indexes\n"
2836 . "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n"
2837 . "<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n"
2838 );
2839 }
2840
2841 if ( ! file_exists( $dir . '/index.php' ) ) {
2842 @file_put_contents( $dir . '/index.php', "<?php\n// Silence is golden.\n" );
2843 }
2844
2845 // A previous version renamed the rules out of the way instead of deleting
2846 // them; clean that up so the directory is not left half protected.
2847 $backup = $dir . '/.htaccess-backup';
2848 if ( file_exists( $backup ) ) {
2849 @unlink( $backup );
2850 }
2851 }
2852
2853 /**
2854 * Ensure the protected upload directories exist and are locked down.
2855 *
2856 * Runs on activation and on upgrade, so protection no longer depends on somebody
2857 * happening to save an ebook.
2858 */
2859 function ebook_store_ensure_protected_dirs() {
2860 $uploads = wp_upload_dir();
2861
2862 if ( ! empty( $uploads['error'] ) ) {
2863 return;
2864 }
2865
2866 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebooks' );
2867 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook_store_data' );
2868 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook-store-cache' );
2869 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook-store-logs' );
2870 }
2871 function ebook_set_upload_dir_preview( $upload ) {
2872
2873 // Override the year / month being based on the post publication date, if year/month organization is enabled
2874 if ( get_option( 'uploads_use_yearmonth_folders' ) ) {
2875 // Generate the yearly and monthly dirs
2876 $time = current_time( 'mysql' );
2877 $y = substr( $time, 0, 4 );
2878 $m = substr( $time, 5, 2 );
2879 $upload['subdir'] = "/$y/$m";
2880 }
2881
2882 $upload['subdir'] = '/ebooks_misc' . $upload['subdir'];
2883 $upload['path'] = $upload['basedir'] . $upload['subdir'];
2884 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
2885 return $upload;
2886 }
2887 function ebook_store_destroy_image_resource($image) {
2888 if (is_resource($image)) {
2889 imagedestroy($image);
2890 return;
2891 }
2892
2893 if (class_exists('GdImage') && $image instanceof GdImage) {
2894 imagedestroy($image);
2895 }
2896 }
2897 function ebook_store_prepare_cover_for_upload($tmp_name) {
2898 if (!file_exists($tmp_name)) {
2899 return false;
2900 }
2901
2902 $image = @imagecreatefromjpeg($tmp_name);
2903 if (!$image) {
2904 $image = @imagecreatefrompng($tmp_name);
2905 }
2906 if (!$image) {
2907 $image = @imagecreatefromgif($tmp_name);
2908 }
2909
2910 $size = @getimagesize($tmp_name);
2911 if (!$image || !$size) {
2912 ebook_store_destroy_image_resource($image);
2913 return false;
2914 }
2915
2916 list($width, $height) = $size;
2917
2918 if ($width != 180 || $height != 260) {
2919 $image_p = imagecreatetruecolor(180, 260);
2920 imagecopyresampled($image_p, $image, 0, 0, 0, 0, 180, 260, $width, $height);
2921 imagejpeg($image_p, $tmp_name, 100);
2922 imagedestroy($image_p);
2923 }
2924
2925 ebook_store_destroy_image_resource($image);
2926 return true;
2927 }
2928 function ebook_store_generate_cover_from_pdf($post_id) {
2929 $existing_cover = get_post_meta($post_id, 'ebook_wp_custom_attachment_cover', true);
2930 if (is_array($existing_cover) && !empty($existing_cover['url'])) {
2931 return;
2932 }
2933 if (!class_exists('Imagick')) {
2934 return;
2935 }
2936
2937 $pdf_meta = get_post_meta($post_id, 'ebook_wp_custom_attachment_pdf', true);
2938 if (!is_array($pdf_meta) || empty($pdf_meta['file'])) {
2939 $pdf_meta = get_post_meta($post_id, 'ebook_wp_custom_attachment', true);
2940 }
2941 if (!is_array($pdf_meta) || empty($pdf_meta['file'])) {
2942 return;
2943 }
2944
2945 $pdf_path = $pdf_meta['file'];
2946 if (!file_exists($pdf_path)) {
2947 return;
2948 }
2949
2950 try {
2951 $imagick = new Imagick();
2952 $imagick->setResolution(150, 150);
2953 $imagick->readImage($pdf_path . '[0]');
2954 $imagick->setImageFormat('jpg');
2955 $imagick->setImageCompression(Imagick::COMPRESSION_JPEG);
2956 $imagick->setImageCompressionQuality(90);
2957 $image_blob = $imagick->getImageBlob();
2958 $imagick->clear();
2959 $imagick->destroy();
2960 } catch (Exception $e) {
2961 error_log('Ebook Store: Unable to auto-create cover image. ' . $e->getMessage());
2962 return;
2963 }
2964
2965 $temp_file = wp_tempnam('ebook-store-cover');
2966 if (!$temp_file) {
2967 return;
2968 }
2969 file_put_contents($temp_file, $image_blob);
2970
2971 ebook_store_prepare_cover_for_upload($temp_file);
2972
2973 $base_name = basename($pdf_path);
2974 $base_name = preg_replace('/\.[^.]+$/', '', $base_name);
2975 if ($base_name === '' || $base_name === null) {
2976 $base_name = 'ebook-cover';
2977 }
2978 $cover_filename = $base_name . '-autocover.jpg';
2979
2980 $upload = wp_upload_bits($cover_filename, null, file_get_contents($temp_file));
2981 @unlink($temp_file);
2982
2983 if(isset($upload['error']) && $upload['error'] != 0) {
2984 error_log('Ebook Store: Auto cover upload error. ' . $upload['error']);
2985 return;
2986 }
2987
2988 add_post_meta($post_id, 'ebook_wp_custom_attachment_cover', $upload, true);
2989 update_post_meta($post_id, 'ebook_wp_custom_attachment_cover', $upload);
2990 }
2991 /**
2992 * Is this upload one of the types the field accepts?
2993 *
2994 * Every upload branch in save_custom_meta_data() used to compute the allowed
2995 * types and then test `if(1)`, so the check never ran and the else-branch that
2996 * rejects the file was dead code.
2997 *
2998 * @param array $file One entry from $_FILES.
2999 * @param array $supported_types Allowed MIME types.
3000 * @return bool
3001 */
3002 function ebook_store_upload_type_is_allowed( $file, $supported_types ) {
3003 if ( empty( $file['name'] ) ) {
3004 return false;
3005 }
3006
3007 $supported_types = (array) $supported_types;
3008
3009 $ext = strtolower( (string) pathinfo( $file['name'], PATHINFO_EXTENSION ) );
3010
3011 if ( $ext === '' ) {
3012 return false;
3013 }
3014
3015 // Cover images and previews come through this same function, so the
3016 // content-aware branch below is scoped to the ebook formats it understands.
3017 $ebook_formats = array( 'pdf', 'epub', 'mobi', 'txt', 'zip', 'mp3', 'mp4' );
3018
3019 if ( in_array( $ext, $ebook_formats, true ) ) {
3020 // The extension must be one this site is allowed to upload — the same
3021 // gate the chunked uploader applies, so Pro formats stay gated on both
3022 // paths.
3023 if ( function_exists( 'ebook_store_get_allowed_upload_formats' )
3024 && ! in_array( $ext, (array) ebook_store_get_allowed_upload_formats(), true ) ) {
3025 return false;
3026 }
3027
3028 // Judge the contents rather than the declared MIME type.
3029 //
3030 // This used to lean on wp_check_filetype_and_ext(), which returns
3031 // ext => false, type => false for a genuine .epub: fileinfo reports
3032 // application/epub+zip while WordPress's map said otherwise, and the
3033 // mismatch made it refuse the file. Every valid ePub was rejected with
3034 // "that file type is not allowed here". The content check is both
3035 // friendlier to real books and stricter about disguised ones.
3036 if ( ! empty( $file['tmp_name'] ) && is_readable( $file['tmp_name'] ) && function_exists( 'ebook_store_validate_uploaded_ebook' ) ) {
3037 return ! is_wp_error( ebook_store_validate_uploaded_ebook( $file['tmp_name'], $ext ) );
3038 }
3039 }
3040
3041 // Images, previews, or no readable temp file to inspect: match the declared
3042 // type against the field's list, and still reject a claimed extension that
3043 // disagrees with the real contents.
3044 $checked = function_exists( 'wp_check_filetype_and_ext' ) && ! empty( $file['tmp_name'] ) && is_uploaded_file( $file['tmp_name'] )
3045 ? wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] )
3046 : wp_check_filetype( basename( $file['name'] ) );
3047
3048 $type = ! empty( $checked['type'] ) ? $checked['type'] : '';
3049
3050 if ( $type === '' || ! empty( $checked['proper_filename'] ) ) {
3051 return false;
3052 }
3053
3054 return in_array( $type, $supported_types, true );
3055 }
3056
3057 /**
3058 * Which ebook file formats may this site upload?
3059 *
3060 * PDF is the free format. The other formats are the flagship Pro feature, and
3061 * until now the only thing stopping a free install from using them was a CSS
3062 * class and a disabled attribute — the save handler accepted them regardless.
3063 *
3064 * @return array
3065 */
3066 function ebook_store_get_allowed_upload_formats() {
3067 $formats = array( 'pdf' );
3068
3069 if ( ebook_store_has_pro_license() ) {
3070 $formats = array_merge( $formats, array( 'mobi', 'txt', 'epub', 'zip', 'mp3', 'mp4' ) );
3071 }
3072
3073 return apply_filters( 'ebook_store_allowed_upload_formats', $formats );
3074 }
3075
3076 /**
3077 * Queue a message for the next admin screen this user loads.
3078 *
3079 * Used where a save silently changed what the owner submitted, so the change is
3080 * visible instead of being discovered later in the shop.
3081 *
3082 * @param string $message Already-translated text.
3083 * @param string $type info | success | warning | error.
3084 */
3085 function ebook_store_add_admin_notice( $message, $type = 'info' ) {
3086 $user_id = get_current_user_id();
3087 if ( $user_id < 1 || $message === '' ) {
3088 return;
3089 }
3090
3091 $key = 'ebook_store_notices_' . $user_id;
3092 $queued = get_transient( $key );
3093 $queued = is_array( $queued ) ? $queued : array();
3094
3095 // A save can raise the same message twice; show it once.
3096 foreach ( $queued as $existing ) {
3097 if ( isset( $existing['message'] ) && $existing['message'] === $message ) {
3098 return;
3099 }
3100 }
3101
3102 $queued[] = array( 'message' => (string) $message, 'type' => (string) $type );
3103 set_transient( $key, $queued, 5 * MINUTE_IN_SECONDS );
3104 }
3105
3106 /**
3107 * Print and clear whatever ebook_store_add_admin_notice() queued.
3108 */
3109 function ebook_store_render_queued_admin_notices() {
3110 $user_id = get_current_user_id();
3111 if ( $user_id < 1 ) {
3112 return;
3113 }
3114
3115 $key = 'ebook_store_notices_' . $user_id;
3116 $queued = get_transient( $key );
3117 if ( ! is_array( $queued ) || empty( $queued ) ) {
3118 return;
3119 }
3120
3121 delete_transient( $key );
3122
3123 $allowed = array( 'info', 'success', 'warning', 'error' );
3124 foreach ( $queued as $notice ) {
3125 $type = ( isset( $notice['type'] ) && in_array( $notice['type'], $allowed, true ) ) ? $notice['type'] : 'info';
3126 printf(
3127 '<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
3128 esc_attr( $type ),
3129 esc_html( isset( $notice['message'] ) ? $notice['message'] : '' )
3130 );
3131 }
3132 }
3133 add_action( 'admin_notices', 'ebook_store_render_queued_admin_notices' );
3134
3135 function save_custom_meta_data($id) {
3136 add_filter( 'upload_dir', 'ebook_set_upload_dir' );
3137 /* --- security verification --- */
3138 if(!wp_verify_nonce(@$_POST['ebook_wp_custom_attachment_nonce'], plugin_basename(__FILE__))) {
3139 return $id;
3140 } // end if
3141
3142 if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
3143 return $id;
3144 } // end if
3145
3146 // 'edit_post' is the correct meta capability for these custom post types.
3147 // The old code checked 'edit_page' in BOTH branches, so authorisation for
3148 // ebooks and orders was decided by page permissions.
3149 if ( ! current_user_can( 'edit_post', $id ) ) {
3150 return $id;
3151 }
3152 /* - end security verification - */
3153 if (strpos($_POST['content'], '[ebook_store') !== false) {
3154 wp_die("The embed code [ebook_store id=XX] is not meant to be in the ebook description, it must be in the post or page where you want the ebook order form to appear. Click Back, correct that and try again.");
3155 }
3156 // The price arrives as text. On PHP 8 a blank field ('') no longer compares
3157 // equal to 0, so a paid book saved without a price used to be stored as
3158 // {paid, ''} — which the storefront then read as a zero-priced, free book.
3159 // Anything that is not a number is treated as 0 on every PHP version. A
3160 // paid book gets its minimum price further down, once the delivery mode
3161 // is final.
3162 $submitted_price = isset( $_POST['ebook']['ebook_price'] ) ? str_replace( ',', '.', trim( (string) wp_unslash( $_POST['ebook']['ebook_price'] ) ) ) : '';
3163 $_POST['ebook']['ebook_price'] = is_numeric( $submitted_price ) ? $submitted_price : 0;
3164 if ( isset( $_POST['ebook']['form_template'] ) ) {
3165 $_POST['ebook']['form_template'] = ebook_store_resolve_form_template( $_POST['ebook']['form_template'] );
3166 } else {
3167 $_POST['ebook']['form_template'] = ebook_store_resolve_form_template( '' );
3168 }
3169 // "Free" and "Donate" delivery, and bonus books, are Pro features. They
3170 // were previously gated only by a CSS class, so a free install could set
3171 // them by posting the form directly.
3172 //
3173 // Only a CHANGE is refused. Re-saving a book that is ALREADY free or
3174 // donate must leave it alone: the gate used to rewrite it to paid on any
3175 // edit whenever the licence check said no — including the false negative
3176 // a slow or unreachable licence server produces — so an unrelated title
3177 // tweak silently put a price on a free book.
3178 if ( ! ebook_store_has_pro_license() ) {
3179 $stored_ebook = get_post_meta( $id, 'ebook', true );
3180 $stored_mode = ( is_array( $stored_ebook ) && isset( $stored_ebook['donate_or_download'] ) ) ? (string) $stored_ebook['donate_or_download'] : '';
3181 $submitted_mode = isset( $_POST['ebook']['donate_or_download'] ) ? (string) $_POST['ebook']['donate_or_download'] : '';
3182
3183 if ( $submitted_mode !== 'paid' && $submitted_mode !== $stored_mode ) {
3184 // A book that is already free or donate keeps that setting; only
3185 // a paid (or new) book is saved as paid.
3186 if ( $stored_mode === 'free' || $stored_mode === 'donate' ) {
3187 $_POST['ebook']['donate_or_download'] = $stored_mode;
3188 ebook_store_add_admin_notice(
3189 __( 'Switching between "Free" and "Donate" delivery is a Pro feature, so this ebook keeps its current delivery setting. Add your license key in the Ebook Store settings to unlock it.', 'ebook-store' ),
3190 'warning'
3191 );
3192 } else {
3193 $_POST['ebook']['donate_or_download'] = 'paid';
3194 ebook_store_add_admin_notice(
3195 __( '"Free" and "Donate" delivery are Pro features, so this ebook was saved as a paid download. Add your license key in the Ebook Store settings to unlock them.', 'ebook-store' ),
3196 'warning'
3197 );
3198 }
3199 }
3200
3201 // The editor posts the bonus selection as ebook[ebook_bonus][]; older
3202 // forms posted a top-level ebook_bonus. Both are guarded the same
3203 // way: an existing selection survives, a new one is refused.
3204 $bonus_refused = false;
3205
3206 $stored_inline = ( is_array( $stored_ebook ) && isset( $stored_ebook['ebook_bonus'] ) ) ? array_map( 'strval', (array) $stored_ebook['ebook_bonus'] ) : array();
3207 $submitted_inline = isset( $_POST['ebook']['ebook_bonus'] ) ? array_map( 'strval', (array) $_POST['ebook']['ebook_bonus'] ) : array();
3208 if ( $submitted_inline !== $stored_inline ) {
3209 $bonus_refused = $bonus_refused || ! empty( $submitted_inline );
3210 if ( empty( $stored_inline ) ) {
3211 unset( $_POST['ebook']['ebook_bonus'] );
3212 } else {
3213 $_POST['ebook']['ebook_bonus'] = $stored_inline;
3214 }
3215 }
3216
3217 $stored_bonus = get_post_meta( $id, 'ebook_bonus', true );
3218 $stored_bonus = is_array( $stored_bonus ) ? $stored_bonus : array();
3219 $submitted_bonus = isset( $_POST['ebook_bonus'] ) ? (array) $_POST['ebook_bonus'] : array();
3220 if ( array_map( 'strval', $submitted_bonus ) !== array_map( 'strval', $stored_bonus ) ) {
3221 $bonus_refused = $bonus_refused || ! empty( $submitted_bonus );
3222 $_POST['ebook_bonus'] = $stored_bonus;
3223 }
3224
3225 if ( $bonus_refused ) {
3226 ebook_store_add_admin_notice(
3227 __( 'Bonus books are a Pro feature, so the bonus book selection was not saved.', 'ebook-store' ),
3228 'warning'
3229 );
3230 }
3231 }
3232
3233 // A book that is not free must carry a price. This runs after the Pro
3234 // gate above, so it sees the delivery mode that is actually saved.
3235 if ($_POST['ebook']['donate_or_download'] != 'free' && $_POST['ebook']['ebook_price'] == 0) {
3236 $_POST['ebook']['ebook_price'] = 0.01;
3237 }
3238
3239 update_post_meta($id, 'ebook', $_POST['ebook']);
3240 update_post_meta($id, 'ebook_bonus', isset( $_POST['ebook_bonus'] ) ? (array) $_POST['ebook_bonus'] : array());
3241 update_post_meta($id, 'ebook_store_enable_custom_confirmation_page', ( isset( $_POST['ebook_store_enable_custom_confirmation_page'] ) && (string) wp_unslash( $_POST['ebook_store_enable_custom_confirmation_page'] ) === '1' ) ? '1' : '0');
3242 update_post_meta($id, 'ebook_store_enable_custom_delivery_email', ( isset( $_POST['ebook_store_enable_custom_delivery_email'] ) && (string) wp_unslash( $_POST['ebook_store_enable_custom_delivery_email'] ) === '1' ) ? '1' : '0');
3243 update_post_meta($id, 'ebook_store_custom_confirmation_page_content', isset( $_POST['ebook_store_custom_confirmation_page_content'] ) ? wp_kses_post( wp_unslash( $_POST['ebook_store_custom_confirmation_page_content'] ) ) : '');
3244 update_post_meta($id, 'ebook_store_custom_delivery_email_subject', isset( $_POST['ebook_store_custom_delivery_email_subject'] ) ? sanitize_text_field( wp_unslash( $_POST['ebook_store_custom_delivery_email_subject'] ) ) : '');
3245 update_post_meta($id, 'ebook_store_custom_delivery_email_text', isset( $_POST['ebook_store_custom_delivery_email_text'] ) ? wp_kses_post( wp_unslash( $_POST['ebook_store_custom_delivery_email_text'] ) ) : '');
3246
3247 // Make sure the file array isn't empty
3248
3249 if(!empty($_FILES['ebook_wp_custom_attachment_pdf']['name'])) {
3250 $_FILES['ebook_wp_custom_attachment'] = $_FILES['ebook_wp_custom_attachment_pdf'];
3251 // Setup the array of supported file types. In this case, it's just PDF.
3252 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3253
3254 // Get the file type of the upload
3255 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment']['name']));
3256 $uploaded_type = $arr_file_type['type'];
3257
3258 // Check if the type is supported. If not, throw an error.
3259 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment'], $supported_types ) ) {
3260
3261 // Use the WordPress API to upload the file
3262 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment']['tmp_name']));
3263
3264 if(isset($upload['error']) && $upload['error'] != 0) {
3265 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3266 } else {
3267 add_post_meta($id, 'ebook_wp_custom_attachment', $upload,true);
3268 update_post_meta($id, 'ebook_wp_custom_attachment', wp_slash($upload));
3269 update_post_meta($id, 'ebook_wp_custom_attachment_pdf', wp_slash($upload));
3270 } // end if/else
3271
3272 } else {
3273 wp_die( esc_html__( 'That file type is not allowed here. Check the list of accepted formats next to the upload field and try again.', 'ebook-store' ) );
3274 } // end if/else
3275
3276 } // end if
3277 $loop = array( 'mobi', 'txt', 'epub', 'zip', 'mp3', 'mp4' );
3278 $allowed_formats = ebook_store_get_allowed_upload_formats();
3279 foreach ($loop as $l) {
3280 // Enforced here, on the server, rather than only in the browser.
3281 if ( ! in_array( $l, $allowed_formats, true ) ) {
3282 continue;
3283 }
3284 if(!empty($_FILES['ebook_wp_custom_attachment_' . $l]['name'])) {
3285
3286 //wp_die(print_r($_FILES['ebook_wp_custom_attachment_' . $l],true));
3287 // Setup the array of supported file types. In this case, it's just PDF.
3288 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3289
3290 // Get the file type of the upload
3291 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_' . $l]['name']));
3292 $uploaded_type = $arr_file_type['type'];
3293
3294 // Check if the type is supported. If not, throw an error.
3295 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_' . $l], $supported_types ) ) {
3296
3297 // Use the WordPress API to upload the file
3298 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_' . $l]['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_' . $l]['tmp_name']));
3299 //wp_die(print_r($upload,true));
3300 if(isset($upload['error']) && $upload['error'] != 0) {
3301 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3302 } else {
3303 add_post_meta($id, 'ebook_wp_custom_attachment_' . $l, wp_slash($upload), true);
3304 update_post_meta($id, 'ebook_wp_custom_attachment_' . $l, wp_slash($upload));
3305 } // end if/else
3306
3307 } else {
3308 wp_die( esc_html__( 'That file type is not allowed here. Check the list of accepted formats next to the upload field and try again.', 'ebook-store' ) );
3309 } // end if/else
3310
3311 } // end if
3312 }
3313 //separate loop for deleting ebooks
3314 $loop = array('pdf','mobi','txt','epub','zip', 'mp3', 'mp4');
3315 foreach ($loop as $l) {
3316 $li = 'ebook_store_delete_' . $l;
3317 if (isset($_REQUEST[$li])) {
3318 $file_meta = get_post_meta($id, 'ebook_wp_custom_attachment_' . $l, true);
3319 unlink($file_meta['file']);
3320 delete_post_meta($id, 'ebook_wp_custom_attachment_' . $l);
3321 if ($l == 'pdf') {
3322 delete_post_meta($id, 'ebook_wp_custom_attachment');
3323 }
3324 }
3325 }
3326 //wp_die(print_r($_REQUEST));
3327 if ($_REQUEST['ebook_store_delete_preview'] == 'on') {
3328 //wp_die('delete preview');
3329 delete_post_meta($id, 'ebook_wp_custom_attachment_preview');
3330 }
3331 //separate loop for deleting ebooks - end
3332 add_filter( 'upload_dir', 'ebook_set_upload_dir_preview' );
3333
3334 if(!empty($_FILES['ebook_wp_custom_attachment_preview']['name'])) {
3335
3336 // Setup the array of supported file types. In this case, it's just PDF.
3337 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3338
3339 // Get the file type of the upload
3340 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_preview']['name']));
3341 $uploaded_type = $arr_file_type['type'];
3342
3343 // Check if the type is supported. If not, throw an error.
3344 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_preview'], $supported_types ) ) {
3345
3346 // Use the WordPress API to upload the file
3347 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_preview']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_preview']['tmp_name']));
3348
3349 if(isset($upload['error']) && $upload['error'] != 0) {
3350 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3351 } else {
3352 add_post_meta($id, 'ebook_wp_custom_attachment_preview', $upload);
3353 update_post_meta($id, 'ebook_wp_custom_attachment_preview', $upload);
3354 } // end if/else
3355
3356 } else {
3357 wp_die( esc_html__( 'That file type is not allowed here. Check the list of accepted formats next to the upload field and try again.', 'ebook-store' ) );
3358 } // end if/else
3359
3360 } // end if
3361
3362
3363 //check the side photo
3364 if(!empty($_FILES['ebook_wp_custom_attachment_side_photo']['name'])) {
3365
3366 // Setup the array of supported file types. In this case, it's just PDF.
3367 $supported_types = array('image/jpeg','image/gif','image/png','image/svg+xml');
3368
3369 // Get the file type of the upload
3370 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_side_photo']['name']));
3371 $uploaded_type = $arr_file_type['type'];
3372
3373 // Check if the type is supported. If not, throw an error.
3374 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_side_photo'], $supported_types ) ) {
3375
3376 // Use the WordPress API to upload the file
3377 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_side_photo']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_side_photo']['tmp_name']));
3378
3379 if(isset($upload['error']) && $upload['error'] != 0) {
3380 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3381 } else {
3382 add_post_meta($id, 'ebook_wp_custom_attachment_side_photo', $upload);
3383 update_post_meta($id, 'ebook_wp_custom_attachment_side_photo', $upload);
3384 } // end if/else
3385
3386 } else {
3387 wp_die( esc_html__( 'That file type is not allowed here. Check the list of accepted formats next to the upload field and try again.', 'ebook-store' ) );
3388 } // end if/else
3389
3390 } // end if
3391 //cover photo
3392 if(!empty($_FILES['ebook_wp_custom_attachment_cover']['name'])) {
3393 $filename = $_FILES['ebook_wp_custom_attachment_cover']['tmp_name'];
3394 // Setup the array of supported file types. In this case, it's just PDF.
3395 $supported_types = array('image/jpeg','image/gif','image/png','image/svg+xml');
3396
3397 // Get the file type of the upload
3398 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_cover']['name']));
3399 $uploaded_type = $arr_file_type['type'];
3400 // Check if the type is supported. If not, throw an error.
3401 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_cover'], $supported_types ) ) {
3402
3403 // Use the WordPress API to upload the file
3404 $img_tmp_name = $_FILES['ebook_wp_custom_attachment_cover']['tmp_name'];
3405 ebook_store_prepare_cover_for_upload($img_tmp_name);
3406 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_cover']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_cover']['tmp_name']));
3407
3408 if(isset($upload['error']) && $upload['error'] != 0) {
3409 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3410 } else {
3411 add_post_meta($id, 'ebook_wp_custom_attachment_cover', $upload);
3412 update_post_meta($id, 'ebook_wp_custom_attachment_cover', $upload);
3413 } // end if/else
3414
3415 } else {
3416 wp_die( esc_html__( 'That file type is not allowed here. Check the list of accepted formats next to the upload field and try again.', 'ebook-store' ) );
3417 } // end if/else
3418
3419 } // end if
3420
3421 if(!empty($_FILES['ebook_wp_custom_attachment_pdf']['name'])) {
3422 ebook_store_generate_cover_from_pdf($id);
3423 }
3424
3425
3426
3427 } // end save_custom_meta_data
3428
3429 function ebook_update_edit_form() {
3430 echo ' enctype="multipart/form-data"';
3431 } // end ebook_update_edit_form
3432 /**
3433 * Reads one field from an order array. Callers hand this either raw
3434 * get_post_meta() output (value at [0]) or plain scalars, as the gateway
3435 * callbacks build it; a string offset ("abc"[0]) must never be mistaken for
3436 * the former.
3437 */
3438 function ebook_store_order_field( $ebook_order, $key, $default = '' ) {
3439 if ( ! is_array( $ebook_order ) || ! isset( $ebook_order[ $key ] ) ) {
3440 return $default;
3441 }
3442 $value = $ebook_order[ $key ];
3443 if ( is_array( $value ) ) {
3444 $value = isset( $value[0] ) ? $value[0] : $default;
3445 }
3446 return is_scalar( $value ) ? $value : $default;
3447 }
3448
3449 /**
3450 * IDs of the bonus ebooks attached to an ebook. The edit form stores them
3451 * inside the 'ebook' meta array (ebook[ebook_bonus][]); the save handler also
3452 * writes a separate 'ebook_bonus' meta, so both locations are honoured.
3453 */
3454 function ebook_store_get_bonus_ebook_ids( $ebook_id ) {
3455 $ebook_id = absint( $ebook_id );
3456 if ( ! $ebook_id ) {
3457 return array();
3458 }
3459 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
3460 $bonus = ( is_array( $ebook_meta ) && isset( $ebook_meta['ebook_bonus'] ) ) ? (array) $ebook_meta['ebook_bonus'] : array();
3461 $separate = get_post_meta( $ebook_id, 'ebook_bonus', true );
3462 if ( is_array( $separate ) ) {
3463 $bonus = array_merge( $bonus, $separate );
3464 }
3465 $ids = array();
3466 foreach ( $bonus as $bonus_id ) {
3467 if ( is_scalar( $bonus_id ) && absint( $bonus_id ) > 0 ) {
3468 $ids[] = absint( $bonus_id );
3469 }
3470 }
3471 return array_values( array_unique( $ids ) );
3472 }
3473 function ebook_download_link($ebook_order, $free = false, $bonus = false, $bonus_ebook_id = null) {
3474 $action_name = ($free == 0) ? 'download' : 'download_free';
3475
3476 $ebook_key = esc_attr( ebook_store_order_field( $ebook_order, 'ebook_key' ) );
3477 $md5_nonce = esc_attr( ebook_store_order_field( $ebook_order, 'md5_nonce' ) );
3478 $ebook_id = absint( ebook_store_order_field( $ebook_order, 'ebook', 0 ) );
3479 $order_id = absint( ebook_store_order_field( $ebook_order, 'order_id', 0 ) );
3480
3481 $args = array(
3482 'ebook_key' => $ebook_key,
3483 'action' => $action_name,
3484 'md5_nonce' => $md5_nonce,
3485 );
3486
3487 if ( $order_id > 0 ) {
3488 $args['order_id'] = $order_id;
3489 }
3490
3491 $link = add_query_arg($args, get_permalink($ebook_id));
3492 $link = remove_query_arg('p', $link);
3493
3494 if ($action_name === 'download_free') {
3495 $link = add_query_arg(array('p' => $ebook_id), $link);
3496 }
3497
3498 $post = get_post($ebook_id);
3499 if ($post) {
3500 $slug = sanitize_title($post->post_name);
3501 $link = add_query_arg(array('ebook' => $slug), $link);
3502 }
3503
3504 if ($bonus > 0) {
3505 $link = add_query_arg(array('type' => 'bonus'), $link);
3506 $link = add_query_arg(array('ebook_id' => absint( $bonus_ebook_id )), $link);
3507 }
3508
3509 return esc_url($link);
3510 }
3511 function humanFileSize($size,$unit="") {
3512 if( (!$unit && $size >= 1<<30) || $unit == "GB")
3513 return number_format($size/(1<<30),2)."GB";
3514 if( (!$unit && $size >= 1<<20) || $unit == "MB")
3515 return number_format($size/(1<<20),2)."MB";
3516 if( (!$unit && $size >= 1<<10) || $unit == "KB")
3517 return number_format($size/(1<<10),2)."KB";
3518 return number_format($size)." bytes";
3519 }
3520
3521 function ebook_store_get_file_format_labels() {
3522 return array(
3523 'pdf' => 'PDF',
3524 'epub' => 'EPUB',
3525 'mobi' => 'MOBI',
3526 'txt' => 'TXT',
3527 'zip' => 'ZIP',
3528 'mp3' => 'MP3',
3529 'mp4' => 'MP4',
3530 );
3531 }
3532
3533 function ebook_store_get_ebook_file_assets( $ebook_id ) {
3534 $formats = array( 'pdf', 'epub', 'mobi', 'txt', 'zip', 'mp3', 'mp4' );
3535 $assets = array();
3536
3537 foreach ( $formats as $format ) {
3538 $meta_key = $format === 'pdf' ? 'ebook_wp_custom_attachment_pdf' : 'ebook_wp_custom_attachment_' . $format;
3539 $file_meta = get_post_meta( $ebook_id, $meta_key, true );
3540 if ( $format === 'pdf' && ( ! is_array( $file_meta ) || empty( $file_meta['file'] ) ) ) {
3541 $file_meta = get_post_meta( $ebook_id, 'ebook_wp_custom_attachment', true );
3542 }
3543
3544 if ( ! is_array( $file_meta ) || empty( $file_meta['file'] ) ) {
3545 continue;
3546 }
3547
3548 $assets[ $format ] = $file_meta;
3549 }
3550
3551 return $assets;
3552 }
3553
3554 function ebook_store_get_ebook_file_facts( $ebook_id ) {
3555 $assets = ebook_store_get_ebook_file_assets( $ebook_id );
3556 $labels = ebook_store_get_file_format_labels();
3557 $formats = array();
3558 $total_size = 0;
3559
3560 foreach ( $assets as $format => $asset ) {
3561 $formats[] = isset( $labels[ $format ] ) ? $labels[ $format ] : strtoupper( $format );
3562 if ( ! empty( $asset['file'] ) && file_exists( $asset['file'] ) ) {
3563 $total_size += (int) filesize( $asset['file'] );
3564 }
3565 }
3566
3567 if ( empty( $formats ) ) {
3568 return array();
3569 }
3570
3571 $facts = array();
3572 $facts[] = sprintf( __( 'Formats: %s', 'ebook-store' ), implode( ', ', $formats ) );
3573
3574 if ( $total_size > 0 ) {
3575 $facts[] = sprintf( __( 'Total download size: %s', 'ebook-store' ), humanFileSize( $total_size ) );
3576 }
3577
3578 $facts[] = sprintf(
3579 /* translators: %d is the number of included files */
3580 _n( '%d included file', '%d included files', count( $formats ), 'ebook-store' ),
3581 count( $formats )
3582 );
3583
3584 return $facts;
3585 }
3586
3587 function ebook_store_buy( $atts ){
3588 return ebook_store($atts, true);
3589 }
3590 function ebook_store_wp_super_cache_check() {
3591 // error_reporting(E_ALL);
3592 // ini_set('display_errors',true);
3593
3594 if (file_exists(WP_PLUGIN_DIR . '/wp-super-cache/wp-cache.php')) {
3595 if (strpos($_SERVER['QUERY_STRING'], 'action=thank_you') || strpos($_SERVER['QUERY_STRING'], 'ebook') || strpos($_SERVER['QUERY_STRING'], 'ipn')) {
3596 define('DONOTCACHEPAGE',true);
3597 }
3598 }
3599
3600 }
3601
3602 /**
3603 * Should the plugin attempt to recover an order from the gateway for this key?
3604 *
3605 * The thank-you page recovery path runs for any anonymous GET of
3606 * ?ebook_key=<anything>&action=thank_you. Each attempt made a live gateway API
3607 * call and wrote a log line, so it doubled as an unauthenticated way to burn
3608 * the store's API rate limit and fill its disk. One attempt per key per five
3609 * minutes is more than enough for a real buyer refreshing the page.
3610 *
3611 * @param string $ebook_key Order token from the request.
3612 * @return bool
3613 */
3614 function ebook_store_can_attempt_order_recovery( $ebook_key ) {
3615 $ebook_key = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $ebook_key );
3616
3617 if ( $ebook_key === '' ) {
3618 return false;
3619 }
3620
3621 $key = 'ebook_store_recover_' . md5( $ebook_key );
3622
3623 if ( get_transient( $key ) ) {
3624 return false;
3625 }
3626
3627 set_transient( $key, 1, 5 * MINUTE_IN_SECONDS );
3628
3629 return true;
3630 }
3631
3632 function ebook_store_get_form_templates() {
3633 $templates = ebook_store_get_core_form_templates();
3634
3635 foreach ( ebook_store_get_custom_form_templates() as $template_key => $template_config ) {
3636 $templates[ (string) $template_key ] = $template_config;
3637 }
3638
3639 return apply_filters( 'ebook_store_form_templates', $templates );
3640 }
3641
3642 function ebook_store_resolve_form_template( $requested = '' ) {
3643 $templates = ebook_store_get_form_templates();
3644 $requested = sanitize_key( (string) $requested );
3645
3646 if ( $requested === '' ) {
3647 $requested = sanitize_key( (string) get_option( 'ebook_store_form_template', '2026' ) );
3648 }
3649
3650 if ( $requested === 'current' ) {
3651 $requested = 'legacy';
3652 }
3653
3654 if ( ! isset( $templates[ $requested ] ) ) {
3655 $requested = '2026';
3656 }
3657
3658 return apply_filters( 'ebook_store_resolved_form_template', $requested, $templates );
3659 }
3660
3661 function ebook_store_get_gateway_button_default_text( $gateway ) {
3662 $defaults = array(
3663 'paypal' => __( 'Buy With PayPal (%%price%%)', 'ebook-store' ),
3664 'stripe' => __( 'Buy With Stripe (%%price%%)', 'ebook-store' ),
3665 'adyen' => __( 'Buy With Adyen (%%price%%)', 'ebook-store' ),
3666 );
3667
3668 $gateway = sanitize_key( (string) $gateway );
3669
3670 return isset( $defaults[ $gateway ] ) ? $defaults[ $gateway ] : __( 'Buy (%%price%%)', 'ebook-store' );
3671 }
3672
3673 function ebook_store_get_gateway_button_text( $gateway, $language = '' ) {
3674 $gateway = sanitize_key( (string) $gateway );
3675 $default_text = ebook_store_get_gateway_button_default_text( $gateway );
3676 $option_value = get_option( $gateway . '_button_text', array() );
3677 $option_value = is_array( $option_value ) ? $option_value : array();
3678
3679 if ( $language === '' ) {
3680 if ( function_exists( 'ebook_store_get_selected_language_code' ) ) {
3681 $language = ebook_store_get_selected_language_code();
3682 }
3683
3684 if ( $language === '' || $language === '0' ) {
3685 $language = substr( get_locale(), 0, 2 );
3686 }
3687 }
3688
3689 $language = sanitize_key( (string) $language );
3690
3691 if ( $language !== '' && $language !== '0' && ! empty( $option_value[ $language ] ) ) {
3692 return (string) $option_value[ $language ];
3693 }
3694
3695 if ( ! empty( $option_value[0] ) ) {
3696 return (string) $option_value[0];
3697 }
3698
3699 if ( ! empty( $option_value['0'] ) ) {
3700 return (string) $option_value['0'];
3701 }
3702
3703 return $default_text;
3704 }
3705
3706 function ebook_store_ga4_is_enabled() {
3707 return function_exists( 'ebook_store_has_pro_license' )
3708 && ebook_store_has_pro_license()
3709 && (string) get_option( 'ebook_store_google_analytics_enabled', '0' ) === '1'
3710 && trim( (string) get_option( 'ebook_store_google_analytics_measurement_id', '' ) ) !== '';
3711 }
3712
3713 function ebook_store_ga4_get_measurement_id() {
3714 return trim( (string) get_option( 'ebook_store_google_analytics_measurement_id', '' ) );
3715 }
3716
3717 function ebook_store_ga4_get_api_secret() {
3718 return trim( (string) get_option( 'ebook_store_google_analytics_api_secret', '' ) );
3719 }
3720
3721 function ebook_store_ga4_enqueue_client() {
3722 static $did_enqueue = false;
3723
3724 if ( $did_enqueue || ! ebook_store_ga4_is_enabled() || is_admin() ) {
3725 return;
3726 }
3727
3728 $measurement_id = ebook_store_ga4_get_measurement_id();
3729 if ( $measurement_id === '' ) {
3730 return;
3731 }
3732
3733 $did_enqueue = true;
3734
3735 wp_enqueue_script(
3736 'ebook-store-ga4',
3737 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $measurement_id ),
3738 array(),
3739 null,
3740 false
3741 );
3742
3743 $bootstrap = 'window.dataLayer=window.dataLayer||[];window.gtag=window.gtag||function(){dataLayer.push(arguments);};gtag("js",new Date());gtag("config",' . wp_json_encode( $measurement_id ) . ',{"send_page_view":false});window.ebookStoreGa4=window.ebookStoreGa4||{};window.ebookStoreGa4.measurementId=' . wp_json_encode( $measurement_id ) . ';window.ebookStoreGa4.track=function(name,params){if(typeof window.gtag!=="function"){return;}window.gtag("event",name,params||{});};document.addEventListener("click",function(event){var trigger=event.target.closest("[data-ebook-store-payment-method]");if(!trigger||!window.ebookStoreGa4){return;}var paymentMethod=trigger.getAttribute("data-ebook-store-payment-method")||"";var currency=trigger.getAttribute("data-ebook-store-currency")||"";var value=parseFloat(trigger.getAttribute("data-ebook-store-value")||"0");var itemId=trigger.getAttribute("data-ebook-store-item-id")||"";var itemName=trigger.getAttribute("data-ebook-store-item-name")||"";var payload={currency:currency,value:isNaN(value)?0:value,payment_type:paymentMethod,items:[{item_id:itemId,item_name:itemName,price:isNaN(value)?0:value,quantity:1,item_category:"ebook"}]};if(trigger.hasAttribute("data-ebook-store-add-payment-info")){window.ebookStoreGa4.track("add_payment_info",payload);}if(trigger.hasAttribute("data-ebook-store-begin-checkout")){window.ebookStoreGa4.track("begin_checkout",payload);}});';
3744
3745 wp_add_inline_script( 'ebook-store-ga4', $bootstrap, 'after' );
3746 }
3747
3748 function ebook_store_ga4_get_item_payload( $ebook_id, $price = null ) {
3749 $ebook_id = absint( $ebook_id );
3750 if ( ! $ebook_id ) {
3751 return array();
3752 }
3753
3754 if ( null === $price ) {
3755 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
3756 $price = is_array( $ebook_meta ) && isset( $ebook_meta['ebook_price'] ) ? (float) ebook_store_price_plus_vat( $ebook_meta['ebook_price'] ) : 0.0;
3757 }
3758
3759 return array(
3760 'item_id' => (string) $ebook_id,
3761 'item_name' => get_the_title( $ebook_id ),
3762 'price' => (float) $price,
3763 'quantity' => 1,
3764 'item_category' => 'ebook',
3765 );
3766 }
3767
3768 function ebook_store_ga4_render_view_item_tracking( $items ) {
3769 if ( ! ebook_store_ga4_is_enabled() || empty( $items ) || ! is_array( $items ) ) {
3770 return '';
3771 }
3772
3773 $payloads = array();
3774 foreach ( $items as $item ) {
3775 $ebook_id = isset( $item['ebook_id'] ) ? absint( $item['ebook_id'] ) : 0;
3776 if ( ! $ebook_id ) {
3777 continue;
3778 }
3779
3780 $price = isset( $item['ebook']['ebook_price'] ) ? (float) ebook_store_price_plus_vat( $item['ebook']['ebook_price'] ) : 0.0;
3781 $payloads[] = array(
3782 'event_key' => 'ebook-store-view-item-' . $ebook_id,
3783 'params' => array(
3784 'currency' => isset( $item['paypal_currency'] ) && $item['paypal_currency'] !== '' ? (string) $item['paypal_currency'] : ebook_store_get_store_currency(),
3785 'value' => $price,
3786 'items' => array( ebook_store_ga4_get_item_payload( $ebook_id, $price ) ),
3787 ),
3788 );
3789 }
3790
3791 if ( empty( $payloads ) ) {
3792 return '';
3793 }
3794
3795 return '<script>(function(){window.ebookStoreGa4Viewed=window.ebookStoreGa4Viewed||{};var payloads=' . wp_json_encode( $payloads ) . ';function fire(){if(!window.ebookStoreGa4||typeof window.ebookStoreGa4.track!=="function"){return;}payloads.forEach(function(entry){if(window.ebookStoreGa4Viewed[entry.event_key]){return;}window.ebookStoreGa4Viewed[entry.event_key]=true;window.ebookStoreGa4.track("view_item",entry.params);});}if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",fire,{once:true});}else{fire();}})();</script>';
3796 }
3797
3798 function ebook_store_ga4_get_order_client_id( $order_id, $order = array() ) {
3799 $order_id = absint( $order_id );
3800 $stored = trim( (string) get_post_meta( $order_id, 'ebook_store_ga4_client_id', true ) );
3801 if ( $stored !== '' ) {
3802 return $stored;
3803 }
3804
3805 $email = '';
3806 if ( is_array( $order ) ) {
3807 if ( isset( $order['payer_email'][0] ) ) {
3808 $email = (string) $order['payer_email'][0];
3809 } elseif ( isset( $order['payer_email'] ) ) {
3810 $email = (string) $order['payer_email'];
3811 }
3812 }
3813
3814 $hash = md5( $order_id . '|' . strtolower( trim( $email ) ) . '|' . home_url( '/' ) );
3815 $client_id = substr( $hash, 0, 10 ) . '.' . substr( $hash, 10, 10 );
3816 update_post_meta( $order_id, 'ebook_store_ga4_client_id', $client_id );
3817
3818 return $client_id;
3819 }
3820
3821 function ebook_store_ga4_send_measurement_protocol_event( $event_name, $params, $client_id ) {
3822 if ( ! ebook_store_ga4_is_enabled() ) {
3823 return false;
3824 }
3825
3826 $measurement_id = ebook_store_ga4_get_measurement_id();
3827 $api_secret = ebook_store_ga4_get_api_secret();
3828 $client_id = trim( (string) $client_id );
3829
3830 if ( $measurement_id === '' || $api_secret === '' || $client_id === '' ) {
3831 return false;
3832 }
3833
3834 $response = wp_remote_post(
3835 'https://www.google-analytics.com/mp/collect?measurement_id=' . rawurlencode( $measurement_id ) . '&api_secret=' . rawurlencode( $api_secret ),
3836 array(
3837 'timeout' => 4,
3838 'blocking' => false,
3839 'headers' => array(
3840 'Content-Type' => 'application/json',
3841 ),
3842 'body' => wp_json_encode(
3843 array(
3844 'client_id' => $client_id,
3845 'events' => array(
3846 array(
3847 'name' => $event_name,
3848 'params' => array_merge(
3849 array(
3850 'engagement_time_msec' => 1,
3851 ),
3852 (array) $params
3853 ),
3854 ),
3855 ),
3856 )
3857 ),
3858 )
3859 );
3860
3861 return ! is_wp_error( $response );
3862 }
3863
3864 function ebook_store_ga4_get_order_payload( $order_id ) {
3865 $order_id = absint( $order_id );
3866 if ( ! $order_id || get_post_type( $order_id ) !== 'ebook_order' ) {
3867 return array();
3868 }
3869
3870 $ebook_id = absint( get_post_meta( $order_id, 'ebook', true ) );
3871 $gross = (float) get_post_meta( $order_id, 'mc_gross', true );
3872 $currency = strtoupper( (string) get_post_meta( $order_id, 'mc_currency', true ) );
3873 $txn_id = trim( (string) get_post_meta( $order_id, 'txn_id', true ) );
3874
3875 if ( ! $ebook_id || $gross <= 0 ) {
3876 return array();
3877 }
3878
3879 return array(
3880 'transaction_id' => $txn_id !== '' ? $txn_id : (string) $order_id,
3881 'currency' => $currency !== '' ? $currency : ebook_store_get_store_currency(),
3882 'value' => $gross,
3883 'items' => array( ebook_store_ga4_get_item_payload( $ebook_id, $gross ) ),
3884 );
3885 }
3886
3887 function ebook_store_ga4_track_purchase_for_order( $order_id ) {
3888 $order_id = absint( $order_id );
3889 if ( ! $order_id || ! ebook_store_ga4_is_enabled() ) {
3890 return false;
3891 }
3892
3893 if ( get_post_meta( $order_id, 'ebook_store_ga4_purchase_tracked', true ) ) {
3894 return true;
3895 }
3896
3897 $payment_type = strtolower( trim( (string) get_post_meta( $order_id, 'payment_type', true ) ) );
3898 $txn_id = strtolower( trim( (string) get_post_meta( $order_id, 'txn_id', true ) ) );
3899 $gross = (float) get_post_meta( $order_id, 'mc_gross', true );
3900
3901 if ( $gross <= 0 || in_array( $payment_type, array( 'new_issue' ), true ) || strpos( $txn_id, 'bonus download' ) === 0 ) {
3902 return false;
3903 }
3904
3905 $params = ebook_store_ga4_get_order_payload( $order_id );
3906 if ( empty( $params ) ) {
3907 return false;
3908 }
3909
3910 $sent = ebook_store_ga4_send_measurement_protocol_event(
3911 'purchase',
3912 $params,
3913 ebook_store_ga4_get_order_client_id( $order_id, ebook_get_order( 'order_id', $order_id ) )
3914 );
3915
3916 if ( $sent ) {
3917 update_post_meta( $order_id, 'ebook_store_ga4_purchase_tracked', current_time( 'mysql', true ) );
3918 }
3919
3920 return $sent;
3921 }
3922
3923 function ebook_store_ga4_track_refund_for_order( $order_id ) {
3924 $order_id = absint( $order_id );
3925 if ( ! $order_id || ! ebook_store_ga4_is_enabled() ) {
3926 return false;
3927 }
3928
3929 if ( get_post_meta( $order_id, 'ebook_store_ga4_refund_tracked', true ) ) {
3930 return true;
3931 }
3932
3933 $params = ebook_store_ga4_get_order_payload( $order_id );
3934 if ( empty( $params ) ) {
3935 return false;
3936 }
3937
3938 $sent = ebook_store_ga4_send_measurement_protocol_event(
3939 'refund',
3940 $params,
3941 ebook_store_ga4_get_order_client_id( $order_id, ebook_get_order( 'order_id', $order_id ) )
3942 );
3943
3944 if ( $sent ) {
3945 update_post_meta( $order_id, 'ebook_store_ga4_refund_tracked', current_time( 'mysql', true ) );
3946 }
3947
3948 return $sent;
3949 }
3950
3951 function ebook_store_ga4_watch_order_status_changes( $meta_id, $object_id, $meta_key, $meta_value ) {
3952 if ( 'payment_status' !== $meta_key || get_post_type( $object_id ) !== 'ebook_order' ) {
3953 return;
3954 }
3955
3956 $current_status = trim( (string) $meta_value );
3957 $previous_status = trim( (string) get_post_meta( $object_id, 'ebook_store_ga4_last_known_status', true ) );
3958 update_post_meta( $object_id, 'ebook_store_ga4_last_known_status', $current_status );
3959
3960 if ( $current_status === '' || $current_status === $previous_status ) {
3961 return;
3962 }
3963
3964 if ( ebook_store_is_negative_order_status( $current_status ) && ! ebook_store_is_negative_order_status( $previous_status ) ) {
3965 ebook_store_ga4_track_refund_for_order( $object_id );
3966 }
3967 }
3968 add_action( 'added_post_meta', 'ebook_store_ga4_watch_order_status_changes', 10, 4 );
3969 add_action( 'updated_post_meta', 'ebook_store_ga4_watch_order_status_changes', 10, 4 );
3970
3971 function ebook_store_render_form_template( $template, $context ) {
3972 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
3973 $locale = ebook_store_get_locale_strings();
3974 $formats_locale = ebook_store_get_format_labels();
3975 $templates = ebook_store_get_form_templates();
3976 if ( ! isset( $templates[ $template ] ) ) {
3977 $template = 'modern';
3978 }
3979
3980 $template_file = isset( $templates[ $template ]['file'] ) ? $templates[ $template ]['file'] : '';
3981 if ( ! $template_file || ! file_exists( $template_file ) ) {
3982 return '';
3983 }
3984
3985 $context = is_array( $context ) ? $context : array();
3986
3987 ob_start();
3988 include $template_file;
3989 $template_output = ob_get_clean();
3990 return function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $template_output ) : $template_output;
3991 }
3992
3993 function ebook_store( $atts, $buyNowOnly = false ){
3994 if (!wp_script_is('jquery', 'enqueued')) {
3995 wp_enqueue_script('jquery');
3996 }
3997 define('DONOTCACHEPAGE',true);
3998 //@error_reporting(E_ALL);
3999 //ini_set('display_errors',1);
4000 if (is_array($atts) == false) {
4001 $atts = array('ebook_id' => $atts);
4002 }
4003 $atts = shortcode_atts(
4004 array(
4005 'ebook_id' => 0,
4006 'template' => '',
4007 ),
4008 $atts,
4009 'ebook_store'
4010 );
4011 try {
4012 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
4013 $locale = ebook_store_get_locale_strings();
4014 $formats_locale = ebook_store_get_format_labels();
4015
4016 global $ebook_store_messages;
4017 global $woocommerce;
4018
4019 if (is_array($ebook_store_messages) and count($ebook_store_messages) > 0) {
4020 foreach ($ebook_store_messages as $ebook_store_messages => $message) {
4021 echo $message;
4022 }
4023 }
4024 wp_enqueue_style( 'ebookstorestylesheet' );
4025 ebook_store_ga4_enqueue_client();
4026 //wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
4027 $post_id = $atts['ebook_id'];
4028 $template = '';
4029 if ( ! empty( $atts['template'] ) ) {
4030 $template = ebook_store_resolve_form_template( $atts['template'] );
4031 }
4032
4033 if (@$_REQUEST['ebook_key'] != false && @$_REQUEST['action'] == 'thank_you') {
4034
4035 add_action( 'wp_footer', 'ebook_store_deregister_embeds' );
4036 if ($_REQUEST['ebook_key'] == -1) {
4037 // Webhook-created orders are stored with user_id 0, so an anonymous
4038 // visitor (also id 0) must never be matched against them.
4039 if ( ! is_user_logged_in() || get_current_user_id() < 1 ) {
4040 return '';
4041 }
4042 $wp_query_statement = array (
4043 'post_type' => 'ebook_order',
4044 'meta_key' => 'user_id',
4045 'meta_value' => get_current_user_id(),
4046 'meta_compare' => '=',
4047 'orderby' => 'date',
4048 'posts_per_page' => -1,
4049 'order' => 'DESC' );
4050 // print_r($wp_query_statement);
4051 // die();
4052 $loop = new WP_Query($wp_query_statement);
4053 while ( $loop->have_posts() ) {
4054 $loop->the_post();
4055 $ebook_key = get_post_meta(get_the_ID(), 'ebook_key', true);
4056 $_REQUEST['ebook_key'] = $ebook_key;
4057 $uri = esc_url_raw($_SERVER['REQUEST_URI']);
4058 $uri = remove_query_arg('ebook_key',$uri);
4059 die("<script> if (window.location != window.parent.location) { window.parent.location = '".add_query_arg('ebook_key',$ebook_key)."'; } else { window.location = '".add_query_arg('ebook_key',$ebook_key)."'; }</script>");
4060 echo get_the_ID() . '<br />';
4061 @$toReset = 1;
4062 //break;
4063 }
4064 die();
4065 if (@$toReset) {
4066 wp_reset_postdata();
4067 }
4068
4069 }
4070 $content = ebook_store_get_effective_confirmation_page_content( 0 );
4071
4072 $ebook_order = ebook_get_order('ebook_key', $_REQUEST['ebook_key']);
4073 // Back from PayPal's hosted checkout: PayPal appends ?token=<order id>
4074 // to the return URL. Capture the approved order and deliver; the
4075 // webhook does the same for buyers who never come back.
4076 if (
4077 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
4078 && ! empty( $_REQUEST['token'] )
4079 && function_exists( 'ebook_store_paypal_rest_is_configured' )
4080 && ebook_store_paypal_rest_is_configured()
4081 ) {
4082 $paypal_token = sanitize_text_field( wp_unslash( $_REQUEST['token'] ) );
4083 if ( ebook_store_paypal_rest_can_attempt_recovery( $paypal_token ) ) {
4084 $recovered_order_id = ebook_store_recover_paypal_rest_order_by_token(
4085 $paypal_token,
4086 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
4087 );
4088 if ( is_wp_error( $recovered_order_id ) ) {
4089 ebook_store_log_paypal_event(
4090 'PayPal thank-you recovery skipped',
4091 array(
4092 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
4093 'token' => $paypal_token,
4094 'error' => $recovered_order_id->get_error_message(),
4095 )
4096 );
4097 } elseif ( $recovered_order_id ) {
4098 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
4099 }
4100 }
4101 }
4102 if (
4103 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
4104 && get_option( 'stripe_integration_enabled', 0 )
4105 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
4106 ) {
4107 $recovered_order_id = 0;
4108 $stripe_session_id = isset( $_REQUEST['stripe_session_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['stripe_session_id'] ) ) : '';
4109 if ( $stripe_session_id !== '' ) {
4110 $recovered_order_id = ebook_store_recover_stripe_order_by_session_id( $stripe_session_id );
4111 } else {
4112 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key(
4113 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
4114 );
4115 }
4116 if ( is_wp_error( $recovered_order_id ) ) {
4117 ebook_store_log_stripe_event(
4118 'Stripe thank-you recovery skipped',
4119 array(
4120 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
4121 'stripe_session_id' => $stripe_session_id,
4122 'error' => $recovered_order_id->get_error_message(),
4123 )
4124 );
4125 } elseif ( $recovered_order_id ) {
4126 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
4127 }
4128 }
4129 if (
4130 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
4131 && get_option( 'adyen_integration_enabled', 0 )
4132 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
4133 ) {
4134 $recovered_order_id = ebook_store_recover_adyen_order_by_ebook_key(
4135 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
4136 );
4137 if ( is_wp_error( $recovered_order_id ) ) {
4138 ebook_store_log_adyen_event(
4139 'Adyen thank-you recovery skipped',
4140 array(
4141 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
4142 'error' => $recovered_order_id->get_error_message(),
4143 )
4144 );
4145 } elseif ( $recovered_order_id ) {
4146 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
4147 }
4148 }
4149 //wp_die(var_dump($ebook_order));
4150 $time = strtotime(@$ebook_order['payment_date'][0]);
4151 //wp_die("$time-".time().'='.$time - time() );
4152 //print_r($ebook_order);
4153 if (@$ebook_order['item_name'][0] == '') {
4154 $ebook_order['item_name'][0] = get_the_title(@$ebook_order['ebook'][0]);
4155 }
4156
4157 //wp_die('ebook order var ' . var_export($ebook_order,true));
4158 if (!$ebook_order || @strtotime($ebook_order['payment_date'][0]) == 0 || isset($ebook_order['ebook']) == false) { // || time() < @strtotime($ebook_order['payment_date'][0]) + 25 ||
4159 return '<h4>' . $locale['confirmation'] . '</h4>' . '<script> window.setTimeout(\'location.reload()\', 5000);
4160 </script>';
4161 }
4162 $ebook_order_ebook_id = isset($ebook_order['ebook'][0]) ? absint($ebook_order['ebook'][0]) : 0;
4163 $content = ebook_store_get_effective_confirmation_page_content($ebook_order_ebook_id);
4164 $file = get_post_meta($ebook_order['ebook'][0],'ebook_wp_custom_attachment',true);
4165 //wp_die(print_r($file,true));
4166 if (@!$file) {
4167 $file = array();
4168 }
4169 $ebook_order['downloadlink'][0] = ebook_download_link($ebook_order);
4170 $ebook_order['downloadlink'][0] = str_replace('#038;','&',$ebook_order['downloadlink'][0]);
4171 $ebook_order['downloadlink'][0] = str_replace('&&','&',$ebook_order['downloadlink'][0]);
4172 //<a href="%%downloadlink%%" target="_blank" rel="noopener">%%item_name%%</a> (%%filesize%%)
4173 @$ebook_order['pdf_reader'][0] = ebook_pdf_reader($ebook_order);
4174 @$ebook_order['download_links'][0] = implode("<br />",ebook_download_links($ebook_order));
4175 @$ebook_order['ebook_bonus'][0] = (implode("<br />",ebook_download_links_bonus($ebook_order)) != '' ? implode("<br />",ebook_download_links_bonus($ebook_order)) : __('None','ebook-store'));
4176 @$ebook_order['filesize'][0] = humanFileSize(filesize($file['file']));
4177 // $ebook_order['downloadlink_html'][0] = '<a href="'.ebook_download_link($ebook_order).'" target="_blank" rel="noopener">'.$ebook_order['item_name'][0].'</a> ('.$ebook_order['filesize'][0].')';
4178 //print_r($ebook_order);
4179 //file_put_contents(__DIR__ . '/debug.log', 'downloadlink: ' . $ebook_order['downloadlink'][0] . "\n", FILE_APPEND);
4180 $ebookObj = new EbookStoreEbook(@$ebook_order['ebook'][0]);
4181 // This is a plugin order, not a WooCommerce one: hand link() the ebook_key
4182 // credentials so the non-PDF format links resolve to the download path
4183 // that this order is actually authorised for.
4184 $ebookObj->ebook_key = (strlen(@$ebook_order['ebook_key'][0]) == 32 ? @$ebook_order['ebook_key'][0] : @$ebook_order['ebook_key_array'][0]);
4185 $ebookObj->md5_nonce = @$ebook_order['md5_nonce'][0];
4186 $ebookObj->order_id = isset($ebook_order['order_id'][0]) ? absint($ebook_order['order_id'][0]) : 0;
4187 $ebookObj->setLink['pdf'] = $ebook_order['downloadlink'][0];
4188 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
4189 //$ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
4190
4191 if (@$ebook_order['password'] == '') {
4192 $ebook_order['password'] = $ebook_order['payer_email'];
4193 }
4194 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_1', true) != '') {
4195 $ebook_order['ebook_store_alternative_location_1'] = array(0 => '<p style="color:gray;">Download to Your Device: (This will take a very long time and requires some technical knowledge but we have provided an instructions page)</p><a href="' . get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_1', true) . '" target="_blank"><h1 style="color:red;"">Add to Your Device</h1>
4196 </a>');
4197
4198 }
4199 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_2', true) != '') {
4200 $ebook_order['ebook_store_alternative_location_2'] = array(0 => '<a href="' . get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_2', true) . '" target="_blank">
4201 <h1 style="color:red;">View Book Online Now</h1>
4202 </a>');
4203 }
4204
4205 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_3', true) != '') {
4206 $ebook_order['ebook_store_alternative_location_3'] = array(0 => '<a href="' . get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_3', true) . '" target="_blank">
4207 <h1 style="color:red;">Download PDF</h1>
4208 </a>');
4209 }
4210 foreach ($ebook_order as $k => $arr) {
4211 $content = str_replace('%%' . $k . '%%', $arr[0], $content);
4212 }
4213 return apply_filters('the_content',$content);
4214 //return 'test 123' . $content;
4215 }
4216
4217 $template_items = array();
4218 $args = array( 'post_type' => 'ebook', 'posts_per_page' => -1, 'p' => @$atts['ebook_id'] );
4219 $loop = new WP_Query( $args );
4220
4221 while ( $loop->have_posts() ) : $loop->the_post();
4222 /*the_title();
4223 echo '<div class="entry-content">';
4224 the_content();
4225 echo '</div>';*/
4226 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
4227 if ( $template === '' && is_array( $ebook ) && ! empty( $ebook['form_template'] ) ) {
4228 $template = ebook_store_resolve_form_template( $ebook['form_template'] );
4229 }
4230 $ebook['ebook_price'] = floatval($ebook['ebook_price']);
4231 //error_log(var_export($ebook,true));
4232 $ebook_key = md5(NONCE_KEY . get_the_ID() . $ebook['ebook_price'] . mt_rand(1,100000));
4233 //$ebook['ebook_price'] = is_float($ebook['ebook_price']) ? $ebook['ebook_price'] : 0.0;
4234 $md5_nonce = md5(mt_rand(1,9999) . NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','));
4235 //$custom = get_the_ID() . '|' . $md5_nonce;
4236 // The currency the PayPal form charges in: the ebook's own when it has a
4237 // valid one, otherwise the store's.
4238 $paypal_currency = ( ! empty( $ebook['paypal_currency'] ) ? strtoupper( trim( (string) $ebook['paypal_currency'] ) ) : ebook_store_get_store_currency() );
4239 if ( ! preg_match( '/^[A-Z]{3}$/', $paypal_currency ) ) {
4240 $paypal_currency = ebook_store_get_store_currency();
4241 }
4242 // Segment 3 binds the net price and the currency the form charges in, so
4243 // the posted form cannot be replayed with a different currency_code; the
4244 // IPN checks the tax separately, so dropping the tax line fails too. The
4245 // net price rather than the gross is signed because the IPN recovers it
4246 // exactly (mc_gross - tax), while the gross depends on how PayPal rounds a
4247 // half cent of tax. Segment 2 stays exactly as it was for orders already
4248 // in flight when the site upgrades.
4249 $custom = get_the_ID() . '|' . md5(NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','))
4250 . '|' . ebook_store_paypal_amount_signature( get_the_ID(), $ebook['ebook_price'], $paypal_currency );
4251
4252 if (get_option('ebook_store_wp_affiliate_integration') == 1) { /* && file_exists('../affiliates-manager/config.php')*/
4253 $parameters = apply_filters('ebook_store_payment_gateway_parameters', array('custom' => $custom));
4254 }
4255
4256 $c = new Currencies();
4257 $img = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment', true);
4258 $preview = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_preview', true);
4259 if (is_array($preview) == false) { $preview = array(); }
4260 if (@$preview['url'] != '') {
4261 if (get_option( 'ebook_store_no_viewerjs_previews', false ) == false) {
4262 @$preview['url'] = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html#' . $preview['url'];
4263 }
4264 }
4265
4266 $cover = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_cover',true);
4267 $side = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_side_photo',true);
4268 if (!$side) {
4269 $side = $cover;
4270 }
4271 //style="background-url:(' . $side['url'] . ');"
4272 if (is_array(@$ebook['ebook_publisher'])) {
4273 $publishers = array();
4274 foreach ($ebook['ebook_publisher'] as $p => $pp) {
4275 $publishers[] = get_the_title($pp);
4276 }
4277 }
4278 if (is_array(@$ebook['ebook_author'])) {
4279 $authors = array();
4280 foreach ($ebook['ebook_author'] as $p => $pp) {
4281 $authors[] = get_the_title($pp);
4282 }
4283 }
4284 @$publishers = is_array($publishers) ? $publishers : array();
4285 @$authors = is_array($authors) ? $authors : array();
4286 $publishers = @implode(", ", $publishers);
4287 $authors = @implode(", ", $authors);
4288 /* div class front, where is it?*/
4289
4290 if ( $img == '' && ! ebook_store_has_pro_license() && current_user_can( 'edit_post', get_the_ID() ) ) {
4291 printf(
4292 '<div class="ebook-store-notice"><h3>%1$s</h3><p><a href="%2$s">%3$s</a></p></div>',
4293 esc_html__( 'This ebook has no file attached yet, so there is nothing for buyers to download.', 'ebook-store' ),
4294 esc_url( get_edit_post_link( get_the_ID() ) ),
4295 esc_html__( 'Open the ebook and upload a file', 'ebook-store' )
4296 );
4297 }
4298 $md5rand = md5(rand(1,10000) . microtime());
4299 $payment_methods = array();
4300 // PayPal REST (Orders API) replaces the deprecated Standard form + IPN as
4301 // soon as a Client ID and Secret are saved; the account email alone still
4302 // drives the legacy form for stores that have not switched yet.
4303 $paypal_rest_active = function_exists( 'ebook_store_paypal_rest_is_configured' ) && ebook_store_paypal_rest_is_configured();
4304 $paypal_ready = ebook_store_is_paypal_enabled() && ( $paypal_rest_active || trim( (string) get_option( 'paypal_account', '' ) ) !== '' );
4305
4306 $formatted_paypal_price = $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2);
4307 $buyNowLinkText = str_replace( '%%price%%', $formatted_paypal_price, ebook_store_get_gateway_button_text( 'paypal' ) );
4308 $buyNowLinkOnClick = 'document.getElementById(\'' . $md5rand . '\').submit(); return false;';
4309 $buyNowLinkHref = '';
4310 $form = '';
4311 // With REST credentials the PayPal order is created when the buyer clicks
4312 // (includes/ebook-store-paypal-rest.php); no Standard form is posted.
4313 // Donation-priced books still need the legacy form, where the buyer types
4314 // the amount at PayPal.
4315 $use_paypal_rest = $paypal_rest_active && $ebook['ebook_price'] > 0
4316 && ! ( isset( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] == 'donate' );
4317 if ( $use_paypal_rest ) {
4318 $buyNowLinkOnClick = 'ebook_store_paypal_checkout(this, ' . (int) get_the_ID() . '); return false;';
4319 $buyNowLinkHref = '#';
4320 ebook_store_enqueue_paypal_rest_checkout_script();
4321 }
4322 $show_primary_buy_link = false;
4323 $primary_payment_method = '';
4324 // The price comes out of post meta as a string ("0", "0.00"), so a strict
4325 // comparison against the integer 0 was never true and a zero-priced book
4326 // only behaved as free when its delivery mode said so explicitly. A book
4327 // explicitly saved as paid is never treated as free, whatever its price
4328 // field holds (see ebook_store_is_free_ebook()).
4329 if ( ebook_store_is_free_ebook( $ebook ) ) {
4330 $buyNowLinkText = $locale['download'];
4331 $buyNowLinkOnClickOriginal = ebook_download_link(array('ebook' => array(0 => get_the_ID()), 'ebook_key' => array(0 => $ebook_key), 'md5_nonce' => array(0 => $md5_nonce)),1);
4332 $buyNowLinkOnClick = "window.location = '" . $buyNowLinkOnClickOriginal . "'; return false;";
4333 $buyNowLinkHref = $buyNowLinkOnClickOriginal;
4334 $show_primary_buy_link = true;
4335 } else if ( isset( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] == 'donate' ) {
4336 $buyNowLinkText = $locale['download'];
4337 $ebook['ebook_price'] = 0;
4338 $show_primary_buy_link = true;
4339 } else if ( $paypal_ready ) {
4340 $payment_methods['paypal'] = array(
4341 'key' => 'paypal',
4342 'label' => 'PayPal',
4343 'href' => '#',
4344 'onclick' => $buyNowLinkOnClick,
4345 'price' => $formatted_paypal_price,
4346 );
4347 $show_primary_buy_link = true;
4348 $primary_payment_method = 'paypal';
4349 }
4350
4351 if ($img == '' && get_option( 'ebook_store_license_key') == '') {
4352 $buyNowLinkOnClick = 'alert(\'There is no ebook file uploaded. Please upload a file first in the Ebook Store plugin.\');';
4353 }
4354 if (get_option('formEnabled') == 1) {
4355 $buyNowLinkOnClickOriginal = $buyNowLinkOnClick;
4356 $buyNowLinkOnClick = "ebook_store_popup(function () {" . $buyNowLinkOnClick . "}, this);";
4357 wp_register_style( 'ebook_store_tinglecss', plugins_url('/includes/tingle-master/dist/tingle.min.css', __FILE__) );
4358 wp_enqueue_style( 'ebook_store_tinglecss' );
4359 wp_enqueue_script( 'ebook_store_site_modal', plugins_url( '/includes/tingle-master/dist/tingle.min.js' , __FILE__ ), array(), '1.0.0', true );
4360 wp_enqueue_script( 'ebook_store_site', plugins_url( '/js/ebook_store_site.js' , __FILE__ ), array(), '1.0.0', true );
4361
4362 }
4363
4364 wp_enqueue_style( 'ebookstorestylesheet' );
4365 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4366 if (!$checkoutPage) {
4367 $checkoutPage = $post_id;
4368 }
4369
4370 $bookpost = get_post(get_the_ID());
4371 $add_to_cart_label = ! empty( $locale['addtocart'] ) ? $locale['addtocart'] : __( 'Add to cart', 'ebook-store' );
4372 $woocommerce_product_id = ebook_store_get_wc_product_reference_for_ebook( get_the_ID() );
4373 if ($woocommerce_product_id > 0 && class_exists('woocommerce') && get_option('ebook_store_woocommerce_integration')) {
4374 $woocommerce_checkout_url = add_query_arg(array('woocommerce_product_id' => $woocommerce_product_id), get_permalink(get_the_ID()));
4375 $addToCartButton = '<a href="' . esc_url( $woocommerce_checkout_url ) . '" class="add_to_cart_link ebook-store-add-to-cart-link" data-ebook-store-payment-method="woocommerce" data-ebook-store-begin-checkout="1" data-ebook-store-item-id="' . esc_attr( get_the_ID() ) . '" data-ebook-store-item-name="' . esc_attr( $bookpost->post_title ) . '" data-ebook-store-currency="' . esc_attr( $paypal_currency ) . '" data-ebook-store-value="' . esc_attr( (string) ebook_store_price_plus_vat($ebook['ebook_price']) ) . '">' . esc_html( $add_to_cart_label ) . '</a>';
4376 }
4377 if (!is_array($cover)) {
4378 $cover = array();
4379 }
4380 //$extraButtons[] = array();
4381
4382 // if (!is_array(@$extraButtons)) {
4383 // @$extraButtons = array();
4384 // }
4385 if (!is_array($side)) {
4386 $side = array();
4387 }
4388 @$extraButtons = apply_filters('ebook_store_extra_buttons',$extraButtons, $ebook['ebook_price'], $bookpost->post_title, $cover['url'], $md5_nonce, $post_id);
4389
4390 // Add Stripe button if enabled
4391 if (get_option('stripe_integration_enabled', 0)) {
4392 $button_text = ebook_store_get_gateway_button_text( 'stripe' );
4393
4394 // Only show Stripe button if price is greater than 0
4395
4396 if ($ebook['ebook_price'] > 0) {
4397 try {
4398 // The Checkout Session is created when the buyer clicks, not on
4399 // every page render: a session was one Stripe API call per view,
4400 // and a page cache could serve the same (24h-expiring) session URL
4401 // to everyone who opened the page.
4402 $c = new Currencies;
4403 // Stripe charges in the store currency. Kept apart from
4404 // $paypal_currency, which the PayPal form and its signature use.
4405 $stripe_currency = ebook_store_get_store_currency();
4406 $formatted_price = $c->getSymbol($stripe_currency) . number_format( ebook_store_price_plus_vat($ebook['ebook_price']), 2 );
4407 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4408
4409 ebook_store_enqueue_stripe_checkout_script();
4410
4411 // The click is intercepted on data-ebook-store-payment-method,
4412 // which both this markup and the bundled templates already emit,
4413 // and the ebook id comes from data-ebook-store-item-id.
4414 $extraButtons[] = sprintf(
4415 '<a href="%s" class="stripe-button ebook-store-stripe-checkout" data-ebook-store-payment-method="stripe" data-ebook-store-add-payment-info="1" data-ebook-store-begin-checkout="1" data-ebook-store-item-id="%s" data-ebook-store-item-name="%s" data-ebook-store-currency="%s" data-ebook-store-value="%s">%s</a>',
4416 esc_url( get_permalink( get_the_ID() ) ),
4417 esc_attr( get_the_ID() ),
4418 esc_attr( $bookpost->post_title ),
4419 esc_attr( $stripe_currency ),
4420 esc_attr( (string) ebook_store_price_plus_vat($ebook['ebook_price']) ),
4421 esc_html($display_text)
4422 );
4423 $payment_methods['stripe'] = array(
4424 'key' => 'stripe',
4425 'label' => 'Stripe',
4426 'href' => get_permalink( get_the_ID() ),
4427 'onclick' => '',
4428 'price' => $formatted_price,
4429 );
4430 } catch (Exception $e) {
4431 error_log('Stripe button creation failed: ' . $e->getMessage());
4432 }
4433 }
4434 }
4435
4436 // Add Adyen button if enabled
4437 if (get_option('adyen_integration_enabled', 0)) {
4438 $button_text = ebook_store_get_gateway_button_text( 'adyen' );
4439
4440 if ($ebook['ebook_price'] > 0) {
4441 try {
4442 $adyen_checkout_url = ebook_store_create_adyen_payment_link(
4443 get_the_ID(),
4444 ebook_store_price_plus_vat($ebook['ebook_price']),
4445 $bookpost->post_title,
4446 $ebook_key
4447 );
4448
4449 if ( $adyen_checkout_url ) {
4450 $c = new Currencies;
4451 $adyen_currency = ebook_store_get_store_currency();
4452 $adyen_amount = (float) ebook_store_price_plus_vat($ebook['ebook_price']);
4453 $formatted_price = $c->getSymbol($adyen_currency) . number_format( $adyen_amount, 2 );
4454 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4455
4456 $extraButtons[] = sprintf(
4457 '<a href="%s" class="stripe-button adyen-button" data-ebook-store-payment-method="adyen" data-ebook-store-add-payment-info="1" data-ebook-store-begin-checkout="1" data-ebook-store-item-id="%s" data-ebook-store-item-name="%s" data-ebook-store-currency="%s" data-ebook-store-value="%s">%s</a>',
4458 esc_url($adyen_checkout_url),
4459 esc_attr( get_the_ID() ),
4460 esc_attr( $bookpost->post_title ),
4461 esc_attr( $adyen_currency ),
4462 esc_attr( (string) $adyen_amount ),
4463 esc_html($display_text)
4464 );
4465 $payment_methods['adyen'] = array(
4466 'key' => 'adyen',
4467 'label' => 'Adyen',
4468 'href' => $adyen_checkout_url,
4469 'onclick' => '',
4470 'price' => $formatted_price,
4471 );
4472 }
4473 } catch (Exception $e) {
4474 error_log('Adyen button creation failed: ' . $e->getMessage());
4475 }
4476 }
4477 }
4478
4479 $extraButtons = is_array($extraButtons) ? $extraButtons : array();
4480 if ( $paypal_ready && ! $use_paypal_rest ) {
4481 $form = '<form method="post" id="' . $md5rand . '" name="dmp_order_form" action="https://www' . (get_option('paypal_sandbox') != '' ? '.sandbox' : '') . '.paypal.com/cgi-bin/webscr">
4482 <input type="hidden" name="rm" value="0">
4483 <input type="hidden" name="discount_rate" value="0">
4484 <input type="hidden" name="cmd" value="_xclick">
4485 <input type="hidden" name="charset" value="utf-8">
4486 <input type="hidden" name="md5_nonce" value="' . $md5_nonce . '">
4487 <input type="hidden" name="lc" value="' . get_option('paypal_language') . '">
4488 <input type="hidden" name="no_shipping" value="' . get_option('ebook_store_require_shipping') . '">
4489 <input type="hidden" name="button_subtype" value="products">
4490 <input type="hidden" name="return" value="' . add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage)) . '">
4491 <input type="hidden" name="cancel_return" value="' . (get_option('paypal_sandbox') == 1 ? add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage)) : add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'cancel'),get_permalink(get_option('ebook_store_cancel_page')))) . '">
4492 <input type="hidden" name="notify_url" value="' . add_query_arg(array('task' => 'ipn','ebook_key' => $ebook_key, 'md5_nonce' => $md5_nonce), home_url('/')) . '">
4493 <input type="hidden" name="item_name" value="' . $bookpost->post_title . '">
4494 <input type="hidden" name="item_number" value="1">
4495 <input type="hidden" name="tax_rate" value="' . ebook_store_get_vat_percent() . '">
4496 <input type="hidden" name="amount" value="' . $ebook['ebook_price'] . '">
4497 <input type="hidden" name="upload" value="1">
4498 <input type="hidden" name="custom" value="' . $custom .'">
4499 <input type="hidden" name="business" id="af69ae50c1be74757508c8f7fae10abd" value="' . get_option('paypal_account') . '">
4500 <input type="hidden" name="receiver_email" id="af69ae50c1be74757508c8f7fae10abd0xff" value="' . get_option('paypal_account') . '">
4501 <input type="hidden" name="currency_code" value="' . $paypal_currency . '">
4502 <input type="hidden" name="cbt" value="' . get_option('paypal_return_button_text') . '">
4503 <input type="hidden" name="no_note" value="1">
4504 </form>';
4505 $buyNowLinkHref = '#';
4506 }
4507
4508 //$form .= var_export($bookpost, true);
4509 if (@$_GET['task'] == 'direct_order') {
4510 $is_free_ebook = ebook_store_is_free_ebook( $ebook );
4511 if ( $ebook['ebook_price'] > 0 && $use_paypal_rest ) {
4512 // Exits on success; on failure the normal page renders below.
4513 ebook_store_paypal_rest_redirect_to_checkout( get_the_ID(), $ebook_key );
4514 }
4515 if ($ebook['ebook_price'] > 0 && $paypal_ready && $form !== '') {
4516 die($form .
4517 '<script>
4518 document.getElementById("' . $md5rand . '").submit();
4519 </script>'
4520 );
4521 } else if ( $is_free_ebook && get_option( 'formEnabled' ) == 1 ) {
4522 // The download endpoint insists on the address the "Your Details"
4523 // form collects, so the direct link shows the ebook page — where
4524 // that form opens — instead of jumping to a download it would
4525 // refuse. Nothing to do here: rendering continues below.
4526 } else if ( $is_free_ebook ) {
4527 $direct_url = html_entity_decode( ebook_download_link( array( 'ebook' => array( 0 => get_the_ID() ), 'ebook_key' => array( 0 => $ebook_key ), 'md5_nonce' => array( 0 => $md5_nonce ) ), 1 ) );
4528 if ( ! headers_sent() ) {
4529 wp_safe_redirect( $direct_url );
4530 exit;
4531 }
4532 // The shortcode runs after the theme has started output, so a header
4533 // redirect is not always possible; fall back to a client-side one.
4534 die( '<meta http-equiv="refresh" content="0;url=' . esc_attr( $direct_url ) . '"><script>window.location = ' . wp_json_encode( $direct_url ) . ';</script>' );
4535 } else if ( $paypal_ready && $form !== '' ) {
4536 die($form .
4537 '<script>
4538 document.getElementById("' . $md5rand . '").submit();
4539 </script>'
4540 );
4541 }
4542 // Nothing to submit automatically (Stripe/Adyen-only store): render the normal page.
4543 }
4544 $date_text = (!empty($ebook['ebook_date']) && strtotime($ebook['ebook_date'])) ? date(get_option('date_format'), strtotime($ebook['ebook_date'])) : '';
4545 $pages_text = (!empty($ebook['ebook_pages']) && (int) $ebook['ebook_pages'] > 0) ? $ebook['ebook_pages'] . ' ' . $locale['pages'] : '';
4546 $preview_label = ! empty( $locale['preview'] ) ? $locale['preview'] : __( 'Preview', 'ebook-store' );
4547 $file_facts = ebook_store_get_ebook_file_facts( get_the_ID() );
4548 $template_items[] = array(
4549 'ebook_id' => get_the_ID(),
4550 'title' => $bookpost->post_title,
4551 'content_html' => wpautop( do_shortcode( shortcode_unautop( get_the_content() ) ) ),
4552 'content_raw' => get_the_content(),
4553 'authors' => $authors,
4554 'publishers' => $publishers,
4555 'date_text' => $date_text,
4556 'pages_text' => $pages_text,
4557 'file_facts' => $file_facts,
4558 'cover_url' => isset( $cover['url'] ) ? $cover['url'] : '',
4559 'side_url' => isset( $side['url'] ) ? $side['url'] : '',
4560 'preview_url' => isset( $preview['url'] ) ? $preview['url'] : '',
4561 'add_to_cart_html' => isset( $addToCartButton ) ? $addToCartButton : '',
4562 'extra_buttons_html' => implode( '', $extraButtons ),
4563 'extra_buttons' => $extraButtons,
4564 'payment_methods' => array_values( $payment_methods ),
4565 'buy_link_text' => $buyNowLinkText,
4566 'buy_link_onclick' => $buyNowLinkOnClick,
4567 'buy_link_href' => $buyNowLinkHref,
4568 'primary_payment_method' => $primary_payment_method,
4569 'show_primary_buy_link' => $show_primary_buy_link ? 1 : 0,
4570 'form_html' => $form,
4571 'md5_nonce' => $md5_nonce,
4572 'locale' => $locale,
4573 'preview_label' => $preview_label,
4574 'ebook' => $ebook,
4575 'paypal_currency' => $paypal_currency,
4576 'currency_symbol' => $c->getSymbol($paypal_currency),
4577 'price_display' => $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2),
4578 'disable_cover_buy_now' => (int) get_option('ebook_store_disable_cover_buy_now',0),
4579 'hide_buy_now' => (int) get_option('ebook_store_hide_buy_now',0),
4580 'buy_link_font_size' => get_option('ebook_store_buy_link_font_size', 0.65),
4581 'title_font_size' => get_option('ebook_store_title_font_size', 1.8),
4582 'form_scale' => get_option('form_scale', 1),
4583 );
4584 endwhile;
4585 wp_reset_postdata();
4586 if ( $template === '' ) {
4587 $template = ebook_store_resolve_form_template( '' );
4588 }
4589
4590 if ($buyNowOnly == true) {
4591 if ( ! $show_primary_buy_link ) {
4592 return '';
4593 }
4594 $ga_attributes = '';
4595 if ( ebook_store_ga4_is_enabled() && $primary_payment_method !== '' ) {
4596 $ga_attributes = ' data-ebook-store-payment-method="' . esc_attr( $primary_payment_method ) . '" data-ebook-store-add-payment-info="1" data-ebook-store-begin-checkout="1" data-ebook-store-item-id="' . esc_attr( $post_id ) . '" data-ebook-store-item-name="' . esc_attr( get_the_title( $post_id ) ) . '" data-ebook-store-currency="' . esc_attr( $paypal_currency ) . '" data-ebook-store-value="' . esc_attr( (string) ebook_store_price_plus_vat($ebook['ebook_price']) ) . '"';
4597 }
4598 return $form . '<button title="' . esc_attr( $buyNowLinkText ) . '" aria-label="' . esc_attr( $buyNowLinkText ) . '" data-md5_nonce="' . esc_attr( $md5_nonce ) . '" class="ebook_buy_link"' . $ga_attributes . ' onClick="' . $buyNowLinkOnClick . '">' . esc_html( $buyNowLinkText ) . '</button>';
4599 }
4600 if ( $template === 'legacy' ) {
4601 wp_enqueue_style( 'ebookstore-bookblock', plugins_url('css/bookblock.css',__FILE__) );
4602 wp_enqueue_style( 'ebookstore-component', plugins_url('css/component.css',__FILE__) );
4603 wp_enqueue_script( 'ebookstore-modernizr', plugins_url('js/modernizr.custom.js',__FILE__), array(), null, false );
4604 wp_enqueue_script( 'ebookstore-bookblock', plugins_url('js/bookblock.min.js',__FILE__), array(), null, true );
4605 wp_enqueue_script( 'ebookstore-classie', plugins_url('js/classie.js',__FILE__), array(), null, true );
4606 wp_enqueue_script( 'ebookstore-bookshelf', plugins_url('js/bookshelf.js',__FILE__), array('ebookstore-bookblock','ebookstore-classie'), null, true );
4607 }
4608
4609 $context = array(
4610 'items' => $template_items,
4611 'template' => $template,
4612 'plugin_url' => plugins_url('', __FILE__),
4613 'locale' => $locale,
4614 );
4615
4616 return ebook_store_render_form_template( $template, $context ) . ebook_store_ga4_render_view_item_tracking( $template_items );
4617 } catch ( Throwable $e ) {
4618 error_log(
4619 sprintf(
4620 'Ebook Store render error for ebook %d: %s in %s:%d',
4621 (int) $atts['ebook_id'],
4622 $e->getMessage(),
4623 $e->getFile(),
4624 (int) $e->getLine()
4625 )
4626 );
4627
4628 return '<div class="ebook-store-alert ebook-store-alert-error">' . esc_html__( 'The ebook order form could not be loaded.', 'ebook-store' ) . '</div>';
4629 }
4630 }
4631 function custom_enter_title_author( $input ) {
4632 global $post_type;
4633
4634 if ( is_admin() && 'ebook_author' == $post_type )
4635 return __( 'Enter Author Name' );
4636
4637 return $input;
4638 }
4639
4640 function custom_enter_title_publisher( $input ) {
4641 global $post_type;
4642
4643 if ( is_admin() && 'ebook_publisher' == $post_type )
4644 return __( 'Enter Publisher Name' );
4645
4646 return $input;
4647 }
4648
4649 class Currencies {
4650
4651 public $currencies = array(
4652
4653 'AUD' => array('name' => "Australian Dollar", 'symbol' => "A$", 'ASCII' => "A&#36;"),
4654
4655 'CAD' => array('name' => "Canadian Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4656
4657 'CZK' => array('name' => "Czech Koruna", 'symbol' => "kr", 'ASCII' => "kr"),
4658
4659 'DKK' => array('name' => "Danish Krone", 'symbol' => "Kr", 'ASCII' => ""),
4660
4661 'EUR' => array('name' => "Euro", 'symbol' => "€", 'ASCII' => "&#128;"),
4662
4663 'HKD' => array('name' => "Hong Kong Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4664
4665 'HUF' => array('name' => "Hungarian Forint", 'symbol' => "Ft", 'ASCII' => "Ft"),
4666
4667 'ILS' => array('name' => "Israeli New Sheqel", 'symbol' => "₪", 'ASCII' => "&#x20AA;"),
4668
4669 'INR' => array('name' => "Indian Rupee", 'symbol' => "₹", 'ASCII' => "&#8377;"),
4670
4671 'JPY' => array('name' => "Japanese Yen", 'symbol' => "¥", 'ASCII' => "&#165;"),
4672
4673 'MXN' => array('name' => "Mexican Peso", 'symbol' => "$", 'ASCII' => "&#36;"),
4674
4675 'NOK' => array('name' => "Norwegian Krone", 'symbol' => "Kr", 'ASCII' => ""),
4676
4677 'NZD' => array('name' => "New Zealand Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4678
4679 'PHP' => array('name' => "Philippine Peso", 'symbol' => "₱", 'ASCII' => ""),
4680
4681 'PLN' => array('name' => "Polish Zloty", 'symbol' => "zł‚", 'ASCII' => "zł"),
4682
4683 'GBP' => array('name' => "Pound Sterling", 'symbol' => "£", 'ASCII' => "&#163;"),
4684
4685 'SGD' => array('name' => "Singapore Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4686
4687 'SEK' => array('name' => "Swedish Krona", 'symbol' => "kr", 'ASCII' => ""),
4688
4689 'CHF' => array('name' => "Swiss Franc", 'symbol' => "CHF", 'ASCII' => "Fr"),
4690
4691 'TWD' => array('name' => "Taiwan New Dollar", 'symbol' => "NT$", 'ASCII' => "NT&#36;"),
4692
4693 'THB' => array('name' => "Thai Baht", 'symbol' => "฿", 'ASCII' => "&#3647;"),
4694
4695 'USD' => array('name' => "U.S. Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4696
4697 'BRL' => array('name' => "Brazilian Real", 'symbol' => "R$", 'ASCII' => "&#x0024;"),
4698
4699 'TRY' => array('name' => "Turkish Lira", 'symbol' => "TL", 'ASCII' => "TL"),
4700
4701 'MYR' => array('name' => "Malaysian Ringgit", 'symbol' => "RM", 'ASCII' => "RM"),
4702
4703 'NGN' => array('name' => "Nigerian naira", 'symbol' => "₦", 'ASCII' => "₦"),
4704
4705 'RON' => array('name' => "Romanian Leu", 'symbol' => "Lei", 'ASCII' => "Lei"),
4706
4707 'ZAR' => array('name' => "South African Rands", 'symbol' => "R", 'ASCII' => "R"),
4708 );
4709
4710 public function getSymbol($code = 'USD') {
4711
4712 if (!empty($this->currencies[$code]['ASCII'])) {
4713
4714 return (string) $this->currencies[$code]['ASCII'];
4715
4716 }
4717
4718 return (string) @$this->currencies[$code]['symbol'];
4719
4720 }
4721
4722 }
4723
4724 function order_columns($columns)
4725 {
4726 $columns = array(
4727 'cb' => '<input type="checkbox" />',
4728 'order_id' => __('Order', 'ebook-store'),
4729 'title' => __('Buyer', 'ebook-store'),
4730 'product' => __('Product', 'ebook-store'),
4731 'amount' => __('Amount', 'ebook-store'),
4732 'paypal' => __('Payment', 'ebook-store'),
4733 'downloadlink' => __('Delivery', 'ebook-store'),
4734 'formData' => __('Form Data', 'ebook-store'),
4735 'date' => __('Date', 'ebook-store'),
4736 );
4737 return $columns;
4738 }
4739 function order_custom_columns($column)
4740 {
4741 global $post;
4742 $c = new Currencies();
4743 switch ($column) {
4744 case 'order_id':
4745 $payment_status = get_post_meta($post->ID,'payment_status',true);
4746 $downloads = intval(get_post_meta($post->ID,'downloads',true));
4747 echo '<div class="ebook-order-list">';
4748 echo '<div class="ebook-order-list__title">#' . esc_html($post->ID) . '</div>';
4749 echo '<div class="ebook-order-list__badges">';
4750 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status ? $payment_status : __('Unknown', 'ebook-store'), ebook_store_order_status_class($payment_status) );
4751 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), number_format_i18n($downloads) );
4752 echo '</div>';
4753 $txn_id = get_post_meta($post->ID,'txn_id',true);
4754 if ($txn_id) {
4755 echo '<span class="ebook-order-list__inline-code">' . esc_html($txn_id) . '</span>';
4756 }
4757 echo '</div>';
4758 break;
4759 case "title":
4760 $first_name = trim((string)get_post_meta($post->ID,'first_name',true));
4761 $last_name = trim((string)get_post_meta($post->ID,'last_name',true));
4762 $buyer_name = trim($first_name . ' ' . $last_name);
4763 $mail = trim((string)get_post_meta($post->ID,'payer_email',true));
4764 $country = trim((string)get_post_meta($post->ID,'residence_country',true));
4765 echo '<div class="ebook-order-list">';
4766 echo '<div class="ebook-order-list__title">' . esc_html($buyer_name !== '' ? $buyer_name : __('Unnamed buyer', 'ebook-store')) . '</div>';
4767 if ($mail !== '') {
4768 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4769 }
4770 echo '<div class="ebook-order-list__badges">';
4771 if ($country !== '') {
4772 echo ebook_store_render_order_badge( __('Country', 'ebook-store'), $country );
4773 }
4774 $payer_status = get_post_meta($post->ID,'payer_status',true);
4775 if ($payer_status !== '') {
4776 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status, ebook_store_order_status_class($payer_status) );
4777 }
4778 echo '</div>';
4779 echo '</div>';
4780 break;
4781 case "product":
4782 $ebook_id = intval(get_post_meta($post->ID,'ebook',true));
4783 $product_title = $ebook_id ? get_the_title($ebook_id) : '';
4784 $item_number = get_post_meta($post->ID,'item_number',true);
4785 $quantity = get_post_meta($post->ID,'quantity',true);
4786 echo '<div class="ebook-order-list">';
4787 if ($ebook_id && $product_title) {
4788 echo '<div class="ebook-order-list__title"><a href="post.php?post=' . esc_attr($ebook_id) . '&action=edit">' . esc_html($product_title) . '</a></div>';
4789 } else {
4790 echo '<div class="ebook-order-list__title">' . esc_html(get_post_meta($post->ID,'item_name',true)) . '</div>';
4791 }
4792 echo '<div class="ebook-order-list__badges">';
4793 if ($quantity !== '') {
4794 echo ebook_store_render_order_badge( __('Qty', 'ebook-store'), $quantity );
4795 }
4796 if ($item_number !== '') {
4797 echo ebook_store_render_order_badge( __('SKU', 'ebook-store'), $item_number );
4798 }
4799 echo '</div>';
4800 echo '</div>';
4801 break;
4802 case "amount":
4803 $mc_currency = get_post_meta($post->ID,'mc_currency',true);
4804 $mc_gross = get_post_meta($post->ID,'mc_gross',true);
4805 $mc_fee = get_post_meta($post->ID,'mc_fee',true);
4806 $total = floatval($mc_gross) - floatval($mc_fee);
4807 echo '<div class="ebook-order-list">';
4808 echo '<div class="ebook-order-list__title">' . esc_html( ebook_store_order_money_display( $mc_gross, $mc_currency ) ) . '</div>';
4809 echo '<div class="ebook-order-list__stack">';
4810 echo '<span>' . esc_html__( 'Fee:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $mc_fee, $mc_currency ) ) . '</span>';
4811 echo '<span>' . esc_html__( 'Net:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $total, $mc_currency ) ) . '</span>';
4812 echo '</div>';
4813 echo '</div>';
4814 break;
4815 case "paypal":
4816 $mail = esc_attr(get_post_meta($post->ID,'payer_email',true));
4817 $payment_type = get_post_meta($post->ID,'payment_type',true);
4818 $payment_status = get_post_meta($post->ID,'payment_status',true);
4819 $refund_gateway = ebook_store_get_order_refund_gateway( $post->ID );
4820 echo '<div class="ebook-order-list">';
4821 echo '<div class="ebook-order-list__title">' . esc_html( $payment_type !== '' ? $payment_type : __('PayPal', 'ebook-store') ) . '</div>';
4822 echo '<div class="ebook-order-list__badges">';
4823 if ( $payment_status !== '' ) {
4824 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status, ebook_store_order_status_class($payment_status) );
4825 }
4826 $payer_status = get_post_meta($post->ID,'payer_status',true);
4827 if ( $payer_status !== '' ) {
4828 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status );
4829 }
4830 echo '</div>';
4831 if ($mail !== '') {
4832 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4833 }
4834 if ( $refund_gateway !== '' ) {
4835 echo '<div class="ebook-order-list__actions">';
4836 echo '<a href="' . esc_url( ebook_store_get_order_refund_url( $post->ID ) ) . '" class="button button-small" onclick="return window.confirm(\'' . esc_js( __( 'Are you sure you want to refund this order?', 'ebook-store' ) ) . '\');">' . esc_html__( 'Refund', 'ebook-store' ) . '</a>';
4837 echo '</div>';
4838 }
4839 echo '</div>';
4840 break;
4841 case 'downloadlink':
4842 $downloadlink = ebook_download_link(
4843 array(
4844 'ebook_key' => array( get_post_meta( $post->ID, 'ebook_key', true ) ),
4845 'md5_nonce' => array( get_post_meta( $post->ID, 'md5_nonce', true ) ),
4846 'ebook' => array( get_post_meta( $post->ID, 'ebook', true ) ),
4847 'order_id' => array( $post->ID ),
4848 )
4849 );
4850 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4851 $link = add_query_arg(array('ebook_key' => get_post_meta($post->ID,'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
4852 $password = get_post_meta($post->ID,'password',true);
4853 echo '<div class="ebook-order-list">';
4854 echo '<div class="ebook-order-list__actions">';
4855 if ($downloadlink != '') {
4856 echo '<a href="' . esc_url($downloadlink) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Download', 'ebook-store') . '</a>';
4857 } else {
4858 echo '<span class="ebook-order-list__empty">' . esc_html__('No direct link saved', 'ebook-store') . '</span>';
4859 }
4860 echo '<a href="' . esc_url( ebook_store_get_order_reset_downloads_url( $post->ID ) ) . '" class="button button-small">' . esc_html__( 'Reset downloads', 'ebook-store' ) . '</a>';
4861 echo '<a href="' . esc_url( ebook_store_get_order_resend_delivery_url( $post->ID ) ) . '" class="button button-small">' . esc_html__( 'Re-send email', 'ebook-store' ) . '</a>';
4862 echo '<a href="' . esc_url($link) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Thank you page', 'ebook-store') . '</a>';
4863 echo '</div>';
4864 echo '<div class="ebook-order-list__badges">';
4865 $downloads = get_post_meta($post->ID,'downloads',true);
4866 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), $downloads !== '' ? $downloads : '0' );
4867 if ( $password !== '' ) {
4868 echo ebook_store_render_order_badge( __('Password', 'ebook-store'), $password );
4869 }
4870 echo '</div>';
4871 echo '</div>';
4872 break;
4873 case 'formData':
4874 $md5_nonce = get_post_meta($post->ID,'md5_nonce',true);
4875 $data = get_post_meta($post->ID,'formData',true);
4876 $data = json_decode($data, true);
4877 if (is_array($data) && !empty($data)) {
4878 unset($data['md5_nonce']);
4879 echo '<div class="ebook-order-list ebook-order-list__pairs">';
4880 $count = 0;
4881 foreach ($data as $key => $value) {
4882 if ($count >= 5) {
4883 break;
4884 }
4885 echo '<div class="ebook-order-list__pair"><strong>' . esc_html(ebook_store_prepare_order_display_value($key)) . '</strong><span>' . esc_html(ebook_store_prepare_order_display_value($value, __('Saved value', 'ebook-store'))) . '</span></div>';
4886 $count++;
4887 }
4888 if ($count === 0) {
4889 echo '<span class="ebook-order-list__empty">' . esc_html__('No form fields saved', 'ebook-store') . '</span>';
4890 }
4891 echo '</div>';
4892 } else {
4893 echo '<span class="ebook-order-list__empty">' . esc_html__('No extra form data', 'ebook-store') . '</span>';
4894 }
4895
4896 break;
4897 }
4898 }
4899 function ebook_readfile_chunked( $file, $retbytes = TRUE ) {
4900 $chunksize = 1 * (1024 * 1024);
4901 $buffer = '';
4902 $cnt = 0;
4903 $handle = fopen( $file, 'r' );
4904 header('Cache-Control: no-store');
4905
4906 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4907 return readfile($file);
4908 //if ob_clean / flush are executed on some hosts, the file is not properly downloaded.
4909
4910 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4911 //wp_die($file);
4912 ob_clean();
4913 flush();
4914 return readfile($file);
4915
4916 if ( $handle === FALSE ) return FALSE;
4917
4918 while ( ! feof( $handle ) ) :
4919 $buffer = fread( $handle, $chunksize );
4920 echo $buffer;
4921 //ob_flush();
4922 //flush();
4923
4924 if ( $retbytes ) $cnt += strlen( $buffer );
4925 endwhile;
4926
4927 $status = fclose( $handle );
4928
4929 if ( $retbytes AND $status ) return $cnt;
4930
4931 return $status;
4932 }
4933 function ebook_get_file_ctype( $extension ) {
4934 switch( $extension ):
4935 case 'ac' : $ctype = "application/pkix-attr-cert"; break;
4936 case 'adp' : $ctype = "audio/adpcm"; break;
4937 case 'ai' : $ctype = "application/postscript"; break;
4938 case 'aif' : $ctype = "audio/x-aiff"; break;
4939 case 'aifc' : $ctype = "audio/x-aiff"; break;
4940 case 'aiff' : $ctype = "audio/x-aiff"; break;
4941 case 'air' : $ctype = "application/vnd.adobe.air-application-installer-package+zip"; break;
4942 case 'apk' : $ctype = "application/vnd.android.package-archive"; break;
4943 case 'asc' : $ctype = "application/pgp-signature"; break;
4944 case 'atom' : $ctype = "application/atom+xml"; break;
4945 case 'atomcat' : $ctype = "application/atomcat+xml"; break;
4946 case 'atomsvc' : $ctype = "application/atomsvc+xml"; break;
4947 case 'au' : $ctype = "audio/basic"; break;
4948 case 'aw' : $ctype = "application/applixware"; break;
4949 case 'avi' : $ctype = "video/x-msvideo"; break;
4950 case 'bcpio' : $ctype = "application/x-bcpio"; break;
4951 case 'bin' : $ctype = "application/octet-stream"; break;
4952 case 'bmp' : $ctype = "image/bmp"; break;
4953 case 'boz' : $ctype = "application/x-bzip2"; break;
4954 case 'bpk' : $ctype = "application/octet-stream"; break;
4955 case 'bz' : $ctype = "application/x-bzip"; break;
4956 case 'bz2' : $ctype = "application/x-bzip2"; break;
4957 case 'ccxml' : $ctype = "application/ccxml+xml"; break;
4958 case 'cdmia' : $ctype = "application/cdmi-capability"; break;
4959 case 'cdmic' : $ctype = "application/cdmi-container"; break;
4960 case 'cdmid' : $ctype = "application/cdmi-domain"; break;
4961 case 'cdmio' : $ctype = "application/cdmi-object"; break;
4962 case 'cdmiq' : $ctype = "application/cdmi-queue"; break;
4963 case 'cdf' : $ctype = "application/x-netcdf"; break;
4964 case 'cer' : $ctype = "application/pkix-cert"; break;
4965 case 'cgm' : $ctype = "image/cgm"; break;
4966 case 'class' : $ctype = "application/octet-stream"; break;
4967 case 'cpio' : $ctype = "application/x-cpio"; break;
4968 case 'cpt' : $ctype = "application/mac-compactpro"; break;
4969 case 'crl' : $ctype = "application/pkix-crl"; break;
4970 case 'csh' : $ctype = "application/x-csh"; break;
4971 case 'css' : $ctype = "text/css"; break;
4972 case 'cu' : $ctype = "application/cu-seeme"; break;
4973 case 'davmount' : $ctype = "application/davmount+xml"; break;
4974 case 'dbk' : $ctype = "application/docbook+xml"; break;
4975 case 'dcr' : $ctype = "application/x-director"; break;
4976 case 'deploy' : $ctype = "application/octet-stream"; break;
4977 case 'dif' : $ctype = "video/x-dv"; break;
4978 case 'dir' : $ctype = "application/x-director"; break;
4979 case 'dist' : $ctype = "application/octet-stream"; break;
4980 case 'distz' : $ctype = "application/octet-stream"; break;
4981 case 'djv' : $ctype = "image/vnd.djvu"; break;
4982 case 'djvu' : $ctype = "image/vnd.djvu"; break;
4983 case 'dll' : $ctype = "application/octet-stream"; break;
4984 case 'dmg' : $ctype = "application/octet-stream"; break;
4985 case 'dms' : $ctype = "application/octet-stream"; break;
4986 case 'doc' : $ctype = "application/msword"; break;
4987 case 'docx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; break;
4988 case 'dotx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; break;
4989 case 'dssc' : $ctype = "application/dssc+der"; break;
4990 case 'dtd' : $ctype = "application/xml-dtd"; break;
4991 case 'dump' : $ctype = "application/octet-stream"; break;
4992 case 'dv' : $ctype = "video/x-dv"; break;
4993 case 'dvi' : $ctype = "application/x-dvi"; break;
4994 case 'dxr' : $ctype = "application/x-director"; break;
4995 case 'ecma' : $ctype = "application/ecmascript"; break;
4996 case 'elc' : $ctype = "application/octet-stream"; break;
4997 case 'emma' : $ctype = "application/emma+xml"; break;
4998 case 'eps' : $ctype = "application/postscript"; break;
4999 case 'epub' : $ctype = "application/epub+zip"; break;
5000 case 'etx' : $ctype = "text/x-setext"; break;
5001 case 'exe' : $ctype = "application/octet-stream"; break;
5002 case 'exi' : $ctype = "application/exi"; break;
5003 case 'ez' : $ctype = "application/andrew-inset"; break;
5004 case 'f4v' : $ctype = "video/x-f4v"; break;
5005 case 'fli' : $ctype = "video/x-fli"; break;
5006 case 'flv' : $ctype = "video/x-flv"; break;
5007 case 'gif' : $ctype = "image/gif"; break;
5008 case 'gml' : $ctype = "application/srgs"; break;
5009 case 'gpx' : $ctype = "application/gml+xml"; break;
5010 case 'gram' : $ctype = "application/gpx+xml"; break;
5011 case 'grxml' : $ctype = "application/srgs+xml"; break;
5012 case 'gtar' : $ctype = "application/x-gtar"; break;
5013 case 'gxf' : $ctype = "application/gxf"; break;
5014 case 'hdf' : $ctype = "application/x-hdf"; break;
5015 case 'hqx' : $ctype = "application/mac-binhex40"; break;
5016 case 'htm' : $ctype = "text/html"; break;
5017 case 'html' : $ctype = "text/html"; break;
5018 case 'ice' : $ctype = "x-conference/x-cooltalk"; break;
5019 case 'ico' : $ctype = "image/x-icon"; break;
5020 case 'ics' : $ctype = "text/calendar"; break;
5021 case 'ief' : $ctype = "image/ief"; break;
5022 case 'ifb' : $ctype = "text/calendar"; break;
5023 case 'iges' : $ctype = "model/iges"; break;
5024 case 'igs' : $ctype = "model/iges"; break;
5025 case 'ink' : $ctype = "application/inkml+xml"; break;
5026 case 'inkml' : $ctype = "application/inkml+xml"; break;
5027 case 'ipfix' : $ctype = "application/ipfix"; break;
5028 case 'jar' : $ctype = "application/java-archive"; break;
5029 case 'jnlp' : $ctype = "application/x-java-jnlp-file"; break;
5030 case 'jp2' : $ctype = "image/jp2"; break;
5031 case 'jpe' : $ctype = "image/jpeg"; break;
5032 case 'jpeg' : $ctype = "image/jpeg"; break;
5033 case 'jpg' : $ctype = "image/jpeg"; break;
5034 case 'js' : $ctype = "application/javascript"; break;
5035 case 'json' : $ctype = "application/json"; break;
5036 case 'jsonml' : $ctype = "application/jsonml+json"; break;
5037 case 'kar' : $ctype = "audio/midi"; break;
5038 case 'latex' : $ctype = "application/x-latex"; break;
5039 case 'lha' : $ctype = "application/octet-stream"; break;
5040 case 'lrf' : $ctype = "application/octet-stream"; break;
5041 case 'lzh' : $ctype = "application/octet-stream"; break;
5042 case 'lostxml' : $ctype = "application/lost+xml"; break;
5043 case 'm3u' : $ctype = "audio/x-mpegurl"; break;
5044 case 'm4a' : $ctype = "audio/mp4a-latm"; break;
5045 case 'm4b' : $ctype = "audio/mp4a-latm"; break;
5046 case 'm4p' : $ctype = "audio/mp4a-latm"; break;
5047 case 'm4u' : $ctype = "video/vnd.mpegurl"; break;
5048 case 'm4v' : $ctype = "video/x-m4v"; break;
5049 case 'm21' : $ctype = "application/mp21"; break;
5050 case 'ma' : $ctype = "application/mathematica"; break;
5051 case 'mac' : $ctype = "image/x-macpaint"; break;
5052 case 'mads' : $ctype = "application/mads+xml"; break;
5053 case 'man' : $ctype = "application/x-troff-man"; break;
5054 case 'mar' : $ctype = "application/octet-stream"; break;
5055 case 'mathml' : $ctype = "application/mathml+xml"; break;
5056 case 'mbox' : $ctype = "application/mbox"; break;
5057 case 'me' : $ctype = "application/x-troff-me"; break;
5058 case 'mesh' : $ctype = "model/mesh"; break;
5059 case 'metalink' : $ctype = "application/metalink+xml"; break;
5060 case 'meta4' : $ctype = "application/metalink4+xml"; break;
5061 case 'mets' : $ctype = "application/mets+xml"; break;
5062 case 'mid' : $ctype = "audio/midi"; break;
5063 case 'midi' : $ctype = "audio/midi"; break;
5064 case 'mif' : $ctype = "application/vnd.mif"; break;
5065 case 'mods' : $ctype = "application/mods+xml"; break;
5066 case 'mov' : $ctype = "video/quicktime"; break;
5067 case 'movie' : $ctype = "video/x-sgi-movie"; break;
5068 case 'm1v' : $ctype = "video/mpeg"; break;
5069 case 'm2v' : $ctype = "video/mpeg"; break;
5070 case 'mp2' : $ctype = "audio/mpeg"; break;
5071 case 'mp2a' : $ctype = "audio/mpeg"; break;
5072 case 'mp21' : $ctype = "application/mp21"; break;
5073 case 'mp3' : $ctype = "audio/mpeg"; break;
5074 case 'mp3a' : $ctype = "audio/mpeg"; break;
5075 case 'mp4' : $ctype = "video/mp4"; break;
5076 case 'mp4s' : $ctype = "application/mp4"; break;
5077 case 'mpe' : $ctype = "video/mpeg"; break;
5078 case 'mpeg' : $ctype = "video/mpeg"; break;
5079 case 'mpg' : $ctype = "video/mpeg"; break;
5080 case 'mpg4' : $ctype = "video/mpeg"; break;
5081 case 'mpga' : $ctype = "audio/mpeg"; break;
5082 case 'mrc' : $ctype = "application/marc"; break;
5083 case 'mrcx' : $ctype = "application/marcxml+xml"; break;
5084 case 'ms' : $ctype = "application/x-troff-ms"; break;
5085 case 'mscml' : $ctype = "application/mediaservercontrol+xml"; break;
5086 case 'msh' : $ctype = "model/mesh"; break;
5087 case 'mxf' : $ctype = "application/mxf"; break;
5088 case 'mxu' : $ctype = "video/vnd.mpegurl"; break;
5089 case 'nc' : $ctype = "application/x-netcdf"; break;
5090 case 'oda' : $ctype = "application/oda"; break;
5091 case 'oga' : $ctype = "application/ogg"; break;
5092 case 'ogg' : $ctype = "application/ogg"; break;
5093 case 'ogx' : $ctype = "application/ogg"; break;
5094 case 'omdoc' : $ctype = "application/omdoc+xml"; break;
5095 case 'onetoc' : $ctype = "application/onenote"; break;
5096 case 'onetoc2' : $ctype = "application/onenote"; break;
5097 case 'onetmp' : $ctype = "application/onenote"; break;
5098 case 'onepkg' : $ctype = "application/onenote"; break;
5099 case 'opf' : $ctype = "application/oebps-package+xml"; break;
5100 case 'oxps' : $ctype = "application/oxps"; break;
5101 case 'p7c' : $ctype = "application/pkcs7-mime"; break;
5102 case 'p7m' : $ctype = "application/pkcs7-mime"; break;
5103 case 'p7s' : $ctype = "application/pkcs7-signature"; break;
5104 case 'p8' : $ctype = "application/pkcs8"; break;
5105 case 'p10' : $ctype = "application/pkcs10"; break;
5106 case 'pbm' : $ctype = "image/x-portable-bitmap"; break;
5107 case 'pct' : $ctype = "image/pict"; break;
5108 case 'pdb' : $ctype = "chemical/x-pdb"; break;
5109 case 'pdf' : $ctype = "application/pdf"; break;
5110 case 'pki' : $ctype = "application/pkixcmp"; break;
5111 case 'pkipath' : $ctype = "application/pkix-pkipath"; break;
5112 case 'pfr' : $ctype = "application/font-tdpfr"; break;
5113 case 'pgm' : $ctype = "image/x-portable-graymap"; break;
5114 case 'pgn' : $ctype = "application/x-chess-pgn"; break;
5115 case 'pgp' : $ctype = "application/pgp-encrypted"; break;
5116 case 'pic' : $ctype = "image/pict"; break;
5117 case 'pict' : $ctype = "image/pict"; break;
5118 case 'pkg' : $ctype = "application/octet-stream"; break;
5119 case 'png' : $ctype = "image/png"; break;
5120 case 'pnm' : $ctype = "image/x-portable-anymap"; break;
5121 case 'pnt' : $ctype = "image/x-macpaint"; break;
5122 case 'pntg' : $ctype = "image/x-macpaint"; break;
5123 case 'pot' : $ctype = "application/vnd.ms-powerpoint"; break;
5124 case 'potx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.template"; break;
5125 case 'ppm' : $ctype = "image/x-portable-pixmap"; break;
5126 case 'pps' : $ctype = "application/vnd.ms-powerpoint"; break;
5127 case 'ppsx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; break;
5128 case 'ppt' : $ctype = "application/vnd.ms-powerpoint"; break;
5129 case 'pptx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; break;
5130 case 'prf' : $ctype = "application/pics-rules"; break;
5131 case 'ps' : $ctype = "application/postscript"; break;
5132 case 'psd' : $ctype = "image/photoshop"; break;
5133 case 'qt' : $ctype = "video/quicktime"; break;
5134 case 'qti' : $ctype = "image/x-quicktime"; break;
5135 case 'qtif' : $ctype = "image/x-quicktime"; break;
5136 case 'ra' : $ctype = "audio/x-pn-realaudio"; break;
5137 case 'ram' : $ctype = "audio/x-pn-realaudio"; break;
5138 case 'ras' : $ctype = "image/x-cmu-raster"; break;
5139 case 'rdf' : $ctype = "application/rdf+xml"; break;
5140 case 'rgb' : $ctype = "image/x-rgb"; break;
5141 case 'rm' : $ctype = "application/vnd.rn-realmedia"; break;
5142 case 'rmi' : $ctype = "audio/midi"; break;
5143 case 'roff' : $ctype = "application/x-troff"; break;
5144 case 'rss' : $ctype = "application/rss+xml"; break;
5145 case 'rtf' : $ctype = "text/rtf"; break;
5146 case 'rtx' : $ctype = "text/richtext"; break;
5147 case 'sgm' : $ctype = "text/sgml"; break;
5148 case 'sgml' : $ctype = "text/sgml"; break;
5149 case 'sh' : $ctype = "application/x-sh"; break;
5150 case 'shar' : $ctype = "application/x-shar"; break;
5151 case 'sig' : $ctype = "application/pgp-signature"; break;
5152 case 'silo' : $ctype = "model/mesh"; break;
5153 case 'sit' : $ctype = "application/x-stuffit"; break;
5154 case 'skd' : $ctype = "application/x-koan"; break;
5155 case 'skm' : $ctype = "application/x-koan"; break;
5156 case 'skp' : $ctype = "application/x-koan"; break;
5157 case 'skt' : $ctype = "application/x-koan"; break;
5158 case 'sldx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slide"; break;
5159 case 'smi' : $ctype = "application/smil"; break;
5160 case 'smil' : $ctype = "application/smil"; break;
5161 case 'snd' : $ctype = "audio/basic"; break;
5162 case 'so' : $ctype = "application/octet-stream"; break;
5163 case 'spl' : $ctype = "application/x-futuresplash"; break;
5164 case 'spx' : $ctype = "audio/ogg"; break;
5165 case 'src' : $ctype = "application/x-wais-source"; break;
5166 case 'stk' : $ctype = "application/hyperstudio"; break;
5167 case 'sv4cpio' : $ctype = "application/x-sv4cpio"; break;
5168 case 'sv4crc' : $ctype = "application/x-sv4crc"; break;
5169 case 'svg' : $ctype = "image/svg+xml"; break;
5170 case 'swf' : $ctype = "application/x-shockwave-flash"; break;
5171 case 't' : $ctype = "application/x-troff"; break;
5172 case 'tar' : $ctype = "application/x-tar"; break;
5173 case 'tcl' : $ctype = "application/x-tcl"; break;
5174 case 'tex' : $ctype = "application/x-tex"; break;
5175 case 'texi' : $ctype = "application/x-texinfo"; break;
5176 case 'texinfo' : $ctype = "application/x-texinfo"; break;
5177 case 'tif' : $ctype = "image/tiff"; break;
5178 case 'tiff' : $ctype = "image/tiff"; break;
5179 case 'torrent' : $ctype = "application/x-bittorrent"; break;
5180 case 'tr' : $ctype = "application/x-troff"; break;
5181 case 'tsv' : $ctype = "text/tab-separated-values"; break;
5182 case 'txt' : $ctype = "text/plain"; break;
5183 case 'ustar' : $ctype = "application/x-ustar"; break;
5184 case 'vcd' : $ctype = "application/x-cdlink"; break;
5185 case 'vrml' : $ctype = "model/vrml"; break;
5186 case 'vsd' : $ctype = "application/vnd.visio"; break;
5187 case 'vss' : $ctype = "application/vnd.visio"; break;
5188 case 'vst' : $ctype = "application/vnd.visio"; break;
5189 case 'vsw' : $ctype = "application/vnd.visio"; break;
5190 case 'vxml' : $ctype = "application/voicexml+xml"; break;
5191 case 'wav' : $ctype = "audio/x-wav"; break;
5192 case 'wbmp' : $ctype = "image/vnd.wap.wbmp"; break;
5193 case 'wbmxl' : $ctype = "application/vnd.wap.wbxml"; break;
5194 case 'wm' : $ctype = "video/x-ms-wm"; break;
5195 case 'wml' : $ctype = "text/vnd.wap.wml"; break;
5196 case 'wmlc' : $ctype = "application/vnd.wap.wmlc"; break;
5197 case 'wmls' : $ctype = "text/vnd.wap.wmlscript"; break;
5198 case 'wmlsc' : $ctype = "application/vnd.wap.wmlscriptc"; break;
5199 case 'wmv' : $ctype = "video/x-ms-wmv"; break;
5200 case 'wmx' : $ctype = "video/x-ms-wmx"; break;
5201 case 'wrl' : $ctype = "model/vrml"; break;
5202 case 'xbm' : $ctype = "image/x-xbitmap"; break;
5203 case 'xdssc' : $ctype = "application/dssc+xml"; break;
5204 case 'xer' : $ctype = "application/patch-ops-error+xml"; break;
5205 case 'xht' : $ctype = "application/xhtml+xml"; break;
5206 case 'xhtml' : $ctype = "application/xhtml+xml"; break;
5207 case 'xla' : $ctype = "application/vnd.ms-excel"; break;
5208 case 'xlam' : $ctype = "application/vnd.ms-excel.addin.macroEnabled.12"; break;
5209 case 'xlc' : $ctype = "application/vnd.ms-excel"; break;
5210 case 'xlm' : $ctype = "application/vnd.ms-excel"; break;
5211 case 'xls' : $ctype = "application/vnd.ms-excel"; break;
5212 case 'xlsx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; break;
5213 case 'xlsb' : $ctype = "application/vnd.ms-excel.sheet.binary.macroEnabled.12"; break;
5214 case 'xlt' : $ctype = "application/vnd.ms-excel"; break;
5215 case 'xltx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; break;
5216 case 'xlw' : $ctype = "application/vnd.ms-excel"; break;
5217 case 'xml' : $ctype = "application/xml"; break;
5218 case 'xpm' : $ctype = "image/x-xpixmap"; break;
5219 case 'xsl' : $ctype = "application/xml"; break;
5220 case 'xslt' : $ctype = "application/xslt+xml"; break;
5221 case 'xul' : $ctype = "application/vnd.mozilla.xul+xml"; break;
5222 case 'xwd' : $ctype = "image/x-xwindowdump"; break;
5223 case 'xyz' : $ctype = "chemical/x-xyz"; break;
5224 case 'zip' : $ctype = "application/zip"; break;
5225 default : $ctype = "application/force-download";
5226 endswitch;
5227
5228 return apply_filters( 'ebook_file_ctype', $ctype );
5229 }
5230 function ebook_process_download($data = false) {
5231 $QSWPOptions = new QSWPOptions();
5232 if ($data) {
5233 extract($data);
5234 }
5235 // Errors must not be printed into a binary file download, but the site's
5236 // error *reporting* level is the site owner's business, not this plugin's.
5237 @ini_set( 'display_errors', '0' );
5238
5239 $formats = array('mobi','txt','epub','zip', 'mp3','mp4');
5240 if (@$_GET['wc_order'] != '') {
5241 $formats[] = 'pdf';
5242 }
5243 if ((@$_REQUEST['ebook_key'] != false && $_REQUEST['action'] == 'download') || (@$_REQUEST['action'] == 'wc_order_download')) {
5244 //error_reporting(E_ALL);
5245 if( function_exists( 'apache_setenv' ) ) @apache_setenv('no-gzip', 1);
5246 @ini_set( 'zlib.output_compression', 'Off' );
5247 nocache_headers();
5248 $order_id = 0;
5249 $ebook = 0;
5250 $request_ebook_key = isset( $_REQUEST['ebook_key'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ) : '';
5251 $request_order_id = isset( $_REQUEST['order_id'] ) ? absint( $_REQUEST['order_id'] ) : 0;
5252
5253 if ( $request_order_id > 0 && $request_ebook_key !== '' ) {
5254 $stored_ebook_key = (string) get_post_meta( $request_order_id, 'ebook_key', true );
5255 if ( $stored_ebook_key !== '' && hash_equals( $stored_ebook_key, $request_ebook_key ) ) {
5256 $order_id = $request_order_id;
5257 $ebook = (int) get_post_meta( $order_id, 'ebook', true );
5258 }
5259 }
5260
5261 if ( ! $order_id && $request_ebook_key !== '' ) {
5262 $loop = new WP_Query(
5263 array(
5264 'post_type' => 'ebook_order',
5265 'meta_key' => 'ebook_key',
5266 'meta_value' => $request_ebook_key,
5267 'posts_per_page' => 1,
5268 'orderby' => 'ID',
5269 'order' => 'DESC',
5270 )
5271 );
5272 while ( $loop->have_posts() ) : $loop->the_post();
5273 $order_id = get_the_ID();
5274 $ebook = get_post_meta(get_the_ID(),'ebook',true);
5275 endwhile;
5276 wp_reset_postdata();
5277 }
5278
5279 if (@$_REQUEST['action'] == 'wc_order_download') {
5280 // A WooCommerce download is only authorised by the order key. Verify it
5281 // here, before any file is resolved: this branch used to trust order_id
5282 // and ebook_id straight from the query string, which handed every paid
5283 // ebook to anonymous visitors.
5284 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
5285 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5286
5287 if ( ! ebook_store_wc_order_key_matches( $order_id, isset( $_GET['wc_order'] ) ? wp_unslash( $_GET['wc_order'] ) : '' ) ) {
5288 wp_die( esc_html__( 'This download link is not valid for that order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5289 }
5290
5291 if ( ! ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) ) {
5292 wp_die( esc_html__( 'That ebook is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5293 }
5294
5295 if ( ! ebook_store_wc_order_is_deliverable( $order_id ) ) {
5296 wp_die( esc_html__( 'This order is not eligible for downloads yet. If you have already paid, please contact the store.', 'ebook-store' ), '', array( 'response' => 403 ) );
5297 }
5298
5299 $ebook = $ebook_id;
5300 }
5301 if (@$_REQUEST['action'] == 'download' && @$_REQUEST['type'] == 'bonus') {
5302 $bonus_ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5303 // The key must have resolved to an order, and the requested book must be a
5304 // bonus of the ebook that order was placed for; otherwise any key holder
5305 // could pull every PDF in the catalogue through this branch.
5306 if ( ! $order_id || ! $bonus_ebook_id || ! in_array( $bonus_ebook_id, ebook_store_get_bonus_ebook_ids( $ebook ), true ) ) {
5307 wp_die( esc_html__( 'That bonus book is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5308 }
5309 $ebook = $bonus_ebook_id;
5310 $ebookObj = new EbookStoreEbook($ebook);
5311 $requested_file = $ebookObj->encrypted($order_id, 'pdf');
5312 }
5313 $attachment = get_post_meta($ebook,'ebook_wp_custom_attachment');
5314 //wp_die(print_r($attachment,true));
5315 if (@!$requested_file) {
5316 $requested_file = $attachment[0]['file'];
5317 }
5318
5319 // The in_progress marker only concerns the order's own protected PDF copy;
5320 // other formats and bonus books never wait on it.
5321 $requested_format = ( isset( $_GET['format'] ) && is_string( $_GET['format'] ) ) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : '';
5322 $wants_order_pdf = ( $requested_format === '' || $requested_format === 'pdf' || ! in_array( $requested_format, $formats, true ) ) && @$_REQUEST['type'] != 'bonus';
5323 if ($wants_order_pdf && get_post_meta($order_id,'encrypted_pdf',true) == 'in_progress') {
5324 if (!ebook_store_encryptable($requested_file)) {
5325 wp_die('The uploaded file is not in the proper format and can not be encrypted, please re-upload it in Acrobat 5.0 PDF 1.5 Compatibility mode or disable encryption. See video tutorial here: <a href="https://youtu.be/-yDr-76W4gk" target="_blank">https://youtu.be/-yDr-76W4gk</a>');
5326 }
5327 $encryption_started = (int) get_post_meta( $order_id, 'encrypted_pdf_started', true );
5328 if ( $encryption_started === 0 || ( time() - $encryption_started ) > 10 * MINUTE_IN_SECONDS ) {
5329 // The request that was building the copy died part-way (or the marker
5330 // predates the timestamp): clear it and build the copy for this buyer now.
5331 delete_post_meta( $order_id, 'encrypted_pdf' );
5332 delete_post_meta( $order_id, 'encrypted_pdf_started' );
5333 if ( get_post_type( $order_id ) === 'ebook_order' && get_option( 'encrypt_pdf' ) ) {
5334 $regenerate = array();
5335 foreach ( (array) get_post_meta( $order_id ) as $meta_key => $meta_values ) {
5336 if ( isset( $meta_values[0] ) && is_scalar( $meta_values[0] ) ) {
5337 $regenerate[ $meta_key ] = $meta_values[0];
5338 }
5339 }
5340 $regenerate['order_id'] = $order_id;
5341 $GLOBALS['attachment'] = array( array( 'file' => $requested_file ) );
5342 $requested_file = ebook_encrypt_pdf( $regenerate, $order_id );
5343 }
5344 } elseif (get_option('ebook_store_no_autorefresh', false) == false) {
5345 wp_die('<h3>Your copy is being generated right now, the page will refresh automatically and the download will start shortly...</h3><script type="text/javascript">
5346 setInterval(\'window.location.reload()\', 15000);
5347 </script>');
5348 } else {
5349 wp_die("Your copy is being generated right now, please try again by refreshing the page in a few minutes or by clicking <a href='#' onClick='window.location.reload();' id='retryLink'>retry download</a>
5350 ");
5351 }
5352
5353 }
5354 if (get_post_meta($order_id,'encrypted_pdf',true) != '' && $_REQUEST['type'] != 'bonus') {
5355 $requested_file = get_post_meta($order_id,'encrypted_pdf',true);
5356 }
5357 if (@in_array($_GET['format'], $formats)) {
5358 $format_meta_tag = 'ebook_wp_custom_attachment' . (@$_GET['format'] != 'pdf' ? '_' . $_GET['format'] : '');
5359 $attachment = get_post_meta($ebook,$format_meta_tag);
5360 //wp_die(print_r($attachment,true));
5361 $requested_file = $attachment[0]['file'];
5362 if ( @$_GET['format'] === 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
5363 // Standalone (non-WooCommerce) epub downloads were served raw; run
5364 // them through the watermarker too, using the resolved order id.
5365 $ebookObj = new EbookStoreEbook( $ebook );
5366 $ebookObj->order_id = $order_id;
5367 $requested_file = $ebookObj->encrypted( $order_id, 'epub' );
5368 }
5369 if (@$_GET['wc_order'] != '') {
5370 $wc_order = sanitize_text_field($_GET['wc_order']);
5371 $ebookObj = new EbookStoreEbook($ebook);
5372 $requested_file = $ebookObj->encrypted((int)$_GET['order_id'], $_GET['format']);
5373 }
5374 }
5375 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5376 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
5377 if ( $payment_status !== '' && ebook_store_is_negative_order_status( $payment_status ) ) {
5378 wp_die( esc_html__( 'Oops, this order is no longer eligible for downloads.', 'ebook-store' ) );
5379 }
5380 // Reading a PDF in the browser's viewer is not a download: it neither
5381 // counts against the allowance nor is served as an attachment. Only the
5382 // WooCommerce "Read online" reader asks for it, and only a PDF can be
5383 // shown that way; any other request is a download, limited and counted.
5384 $is_inline_view = ! empty( $_GET['inline'] ) && isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'wc_order_download'
5385 && strtolower( pathinfo( (string) $requested_file, PATHINFO_EXTENSION ) ) === 'pdf';
5386 $downloads = ebook_store_get_download_count( $order_id, $ebook );
5387 $downloads_limit = ebook_store_get_download_limit();
5388 //wp_die('Downloads ' . $downloads);
5389 if ( ! $is_inline_view && $downloads >= $downloads_limit ) {
5390 wp_die('Oops, you have reached the maximum amount of downloads for this order, you need to order again.');
5391 }
5392 // $gmt_timestamp = get_post_time();
5393 // if (!$gmt_timestamp) {
5394 // $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5395 // }
5396 //wp_die("GMT timestamp $gmt_timestamp, time " . time() . ' post id ' . get_the_ID());
5397
5398
5399 $gmt_timestamp = get_post_time('U', false, ($order_id > 0 ? $order_id : get_the_ID()));
5400 if (!$gmt_timestamp) {
5401 $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5402 }
5403
5404
5405 $link_expiration = trim( (string) get_option( 'link_expiration', $QSWPOptions->link_expiration ) );
5406 if ( $link_expiration === '' ) {
5407 $link_expiration = '1 year';
5408 }
5409
5410 $strtotime = strtotime("+" . $link_expiration,$gmt_timestamp);
5411
5412 if (WP_DEBUG == true) {
5413 //error_log("strtotime $strtotime vs time " . time() . ' vs GMT TIMESTAMP ' . $gmt_timestamp);
5414 }
5415 // A value strtotime() cannot parse, or one that does not land after the
5416 // order time, is "no expiry" here, never "already expired".
5417 if ($strtotime !== false && $strtotime > $gmt_timestamp && $strtotime < time()) {
5418 wp_die('Oops, the link you are using has expired, you need to order again.');
5419 }
5420
5421 if ( ! $requested_file || ! file_exists( $requested_file ) ) {
5422 wp_die( esc_html__( 'Sorry, the ebook file was not found for that order. It may have expired.', 'ebook-store' ) );
5423 }
5424
5425 // A PDF opened in the reader is shown in the browser; everything else
5426 // is a download.
5427 $disposition = $is_inline_view ? 'inline' : 'attachment';
5428
5429 header("Robots: none");
5430 header("Content-Type: " . $ctype . "");
5431 header("Content-Description: File Transfer");
5432 header("Content-Disposition: " . $disposition . "; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5433 header("Content-Transfer-Encoding: binary");
5434 ebook_readfile_chunked($requested_file);
5435 if ( $disposition !== 'inline' ) {
5436 ebook_count_download($order_id, $ebook);
5437 }
5438 die();
5439 //endwhile;
5440 } else if (@$_REQUEST['action'] == 'download_free') {
5441
5442 @$formData = ebook_store_get_form($_REQUEST['md5_nonce']);
5443 if (is_object($formData)) {
5444 $marketing_emails = array_unique(
5445 array_filter(
5446 array(
5447 isset( $formData->email ) ? $formData->email : '',
5448 isset( $formData->payer_email ) ? $formData->payer_email : '',
5449 )
5450 )
5451 );
5452 foreach ( $marketing_emails as $marketing_email ) {
5453 ebook_store_subscribe_buyer_to_marketing_lists( $marketing_email );
5454 }
5455 }
5456
5457 // "Ask for an email before the free download" was enforced only by the
5458 // pop-up in the browser, while the download URL itself sat in the
5459 // button's href — so the form could simply be skipped and the order
5460 // was recorded with no address at all. Require it here too.
5461 if ( get_option( 'formEnabled' ) == 1 ) {
5462 $free_email = '';
5463 if ( is_object( $formData ) ) {
5464 foreach ( array( 'email', 'payer_email' ) as $free_email_field ) {
5465 if ( ! empty( $formData->$free_email_field ) && is_email( $formData->$free_email_field ) ) {
5466 $free_email = (string) $formData->$free_email_field;
5467 break;
5468 }
5469 }
5470 }
5471
5472 if ( $free_email === '' ) {
5473 $free_ebook_id = isset( $_REQUEST['p'] ) ? absint( $_REQUEST['p'] ) : 0;
5474 $free_ebook_link = $free_ebook_id ? get_permalink( $free_ebook_id ) : home_url( '/' );
5475 wp_die(
5476 sprintf(
5477 /* translators: %s: link back to the ebook page. */
5478 esc_html__( 'Please request this download using the form on the ebook page so we know where to send it. %s', 'ebook-store' ),
5479 '<a href="' . esc_url( $free_ebook_link ) . '">' . esc_html__( 'Back to the ebook', 'ebook-store' ) . '</a>'
5480 ),
5481 '',
5482 array( 'response' => 403 )
5483 );
5484 }
5485 }
5486 // Only suppress *display* of errors — this runs while streaming a file.
5487 @ini_set( 'display_errors', '0' );
5488
5489 $id = $_REQUEST['p'];
5490 $loop = new WP_Query( array ( 'post_type' => 'ebook', 'p' => $id ) );
5491
5492
5493
5494
5495 while ( $loop->have_posts() ) : $loop->the_post();
5496 $attachment = get_post_meta($id,'ebook_wp_custom_attachment');
5497 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
5498
5499 if (get_option('encrypt_pdf') == 1) {
5500 if (!ebook_store_encryptable($attachment[0]['file'])) {
5501 echo('The uploaded file is not in the proper format, please re-upload it in Acrobat 5.0 PDF 1.5 Compatibility mode. See video tutorial here: <a href="https://youtu.be/-yDr-76W4gk" target="_blank">https://youtu.be/-yDr-76W4gk</a>');
5502 }
5503 }
5504 //die();
5505 // A zero-priced book whose mode was never set is free, and a donation
5506 // book with no price can be downloaded too. A book saved as paid is
5507 // never free here, even when its price field is blank.
5508 $ebook['donate_or_download'] = isset( $ebook['donate_or_download'] ) ? (string) $ebook['donate_or_download'] : '';
5509 $free_price = ( isset( $ebook['ebook_price'] ) && is_numeric( $ebook['ebook_price'] ) ) ? (float) $ebook['ebook_price'] : 0.0;
5510 if ( ebook_store_is_free_ebook( $ebook ) || ( $ebook['donate_or_download'] === 'donate' && $free_price == 0 ) ) {
5511 $ebook['donate_or_download'] = 'free';
5512 }
5513 if ( $ebook['donate_or_download'] != 'free' ) {
5514 wp_die('You are trying to download a file that is not free - ' . get_the_title());
5515 }
5516 $status = get_post_status();
5517 erl($status);
5518
5519 if ($status != 'publish') {
5520 wp_die('You are trying to download a file that is not available.');
5521 }
5522
5523 $requested_file = $attachment[0]['file'];
5524
5525 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5526 $gmt_timestamp = get_post_time('U');
5527 //insert order
5528 $my_post = array(
5529 'post_title' => 'Free Download',
5530 'post_type' => 'ebook_order',
5531 'post_status' => 'publish',
5532 'post_author' => 1,
5533 'post_category' => array(8,39));
5534 $post_id = wp_insert_post( $my_post, @$wp_error );
5535 //txn_id payment_date payer_email
5536 //encrypt here.
5537 global $current_user;
5538 $current_user = wp_get_current_user();
5539 $data = array();
5540
5541 $data['txn_id'] = 'free';
5542 //$data['payment_date'] = date("F j, Y, g:i a");
5543 $data['payment_date'] = current_time('mysql');
5544
5545
5546
5547 if (get_option('encrypt_files_for_logged_in_users') == 1) {
5548 $data['payer_email'] = $current_user->user_email;
5549 } else {
5550 $data['payer_email'] = '';
5551 if (@$formData->email != '') {
5552 $data['payer_email'] = $formData->email;
5553 }
5554 }
5555
5556 if ($formData->payer_email != '') {
5557 $data['payer_email'] = $formData->payer_email;
5558 }
5559
5560 //error_log(print_r($formData,true));
5561
5562
5563 $data['first_name'] = $current_user->user_firstname;
5564 $data['last_name'] = $current_user->user_lastname;
5565 $data['mc_currency'] = ebook_store_get_store_currency();
5566 $data['tax'] = 0;
5567 $data['ip'] = $_SERVER['REMOTE_ADDR'];
5568 $data['residence_country'] = 'n/a';
5569 //error_log('data ' . print_r($data,true));
5570 if ($data['first_name'] == '') {
5571 if ($formData->name != '') {
5572 $data['first_name'] = $formData->name;
5573 $data['last_name'] = '';
5574 } else if ($formData->first_name != '') {
5575 $data['first_name'] = $formData->first_name;
5576 $data['last_name'] = $formData->last_name;
5577 } else {
5578 $data['first_name'] = 'Anonymous';
5579 $data['last_name'] = 'User';
5580
5581 }
5582 }
5583
5584
5585 //wp_die($data['payer_email']);
5586
5587 $md5_nonce = strip_tags($_REQUEST['md5_nonce']);
5588 $ebook_key = $md5_nonce;
5589
5590 $data['password'] = ebook_store_get_password_for_order($post_id, array(
5591 'password' => isset($data['password']) ? $data['password'] : '',
5592 'payer_email' => $data['payer_email'],
5593 'md5_nonce' => $md5_nonce,
5594 'ebook_key' => $ebook_key,
5595 ));
5596
5597 global $attachment;
5598 @$attachment[0]['file'] = $requested_file;
5599
5600 $fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
5601 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
5602 //ebook_encrypt_pdf($data, $post_id);
5603 $requested_file = ebook_encrypt_pdf($data, $post_id);
5604 }
5605
5606 $formData = ebook_store_get_form($md5_nonce);
5607 $formData = json_encode($formData);
5608 $item_name = get_the_title((int)$_REQUEST['p']);
5609 //die($item_name);
5610 update_post_meta($post_id,'ebook_key',$md5_nonce);
5611 update_post_meta($post_id,'downloads',0);
5612 update_post_meta($post_id,'formData',wp_slash($formData));
5613 update_post_meta($post_id, 'downloadlink', 'http://' . sanitize_text_field($_SERVER['HTTP_HOST']) . esc_url_raw($_SERVER['REQUEST_URI']));
5614 update_post_meta($post_id,'ebook',(int)$_REQUEST['p']);
5615 update_post_meta($post_id,'item_name',$item_name);
5616 update_post_meta($post_id,'payer_email',(@$data['payer_email'] != '' ? $data['payer_email'] : 'N/A'));
5617 update_post_meta($post_id,'mc_gross',0);
5618 update_post_meta($post_id,'mc_fee',0);
5619 update_post_meta($post_id,'payment_date',date("F j, Y, g:i a"));
5620 foreach ($data as $dk => $dv) {
5621 update_post_meta($post_id,$dk,$dv);
5622 }
5623 //get_post_meta($post->ID,'item_name',true)
5624 //insert order end
5625 if (headers_sent()) {
5626 die('Sorry, headers sent.');
5627 }
5628 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
5629 if (!$checkoutPage) {
5630 $checkoutPage = $post_id;
5631 }
5632 update_post_meta($post_id,'encrypted_pdf',wp_slash($requested_file));
5633 header("Location: ". add_query_arg(array('ebook_key' => $md5_nonce, 'action' => 'thank_you'),get_permalink($checkoutPage)));
5634
5635 // header("Robots: none");
5636 // header("Content-Type: " . $ctype . "");
5637 // header("Content-Description: File Transfer");
5638 // header("Content-Disposition: attachment; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5639 // header("Content-Transfer-Encoding: binary");
5640 // ebook_readfile_chunked($requested_file);
5641 endwhile;
5642 die();
5643 } else if ($_REQUEST['action'] == 'ebook_bonus_download') {
5644 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5645 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', (string) wp_unslash( $_GET['ebook_key'] ) ) : '';
5646
5647 // The key must resolve to a real order before we do anything. Without this
5648 // an anonymous GET with a bogus key created a junk ebook_order (null email,
5649 // garbage details) and could trigger a marketing subscribe + PDF encrypt.
5650 $ebook_order_meta = $ebook_key !== '' ? ebook_get_order( 'ebook_key', $ebook_key ) : null;
5651 if ( empty( $ebook_order_meta['order_id'][0] ) ) {
5652 wp_die( esc_html__( 'This download link is not valid.', 'ebook-store' ), '', array( 'response' => 403 ) );
5653 }
5654
5655 // The requested bonus book must actually be a bonus of that order's ebook.
5656 $parent_ebook = isset( $ebook_order_meta['ebook'][0] ) ? absint( $ebook_order_meta['ebook'][0] ) : 0;
5657 $allowed_bonus = $parent_ebook ? (array) get_post_meta( $parent_ebook, 'ebook_bonus', true ) : array();
5658 $allowed_bonus = array_map( 'absint', $allowed_bonus );
5659 if ( ! in_array( $ebook_id, $allowed_bonus, true ) ) {
5660 wp_die( esc_html__( 'That bonus book is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5661 }
5662
5663 $data['ebook'] = get_post_meta( $ebook_id, 'ebook_bonus', true );
5664 $data['txn_id'] = 'Bonus Download (' . ( isset( $ebook_order_meta['item_name'][0] ) ? $ebook_order_meta['item_name'][0] : '' ) . ')';
5665 $data['payer_email'] = isset( $ebook_order_meta['payer_email'][0] ) ? $ebook_order_meta['payer_email'][0] : '';
5666 $data['first_name'] = 'Bonus';
5667 $data['last_name'] = 'Download (Order #' . $ebook_order_meta['order_id'][0] . ')';
5668 ebook_store_add_order($data, true);
5669 @$ebook_order['ebook_key'][0] = $ebook_key;
5670 @$ebook_order['ebook'][0] = $data['ebook'];
5671 $downloadlink = ebook_download_link($ebook_order, 0, 1);
5672 header("Location: " . $downloadlink); //add proper ebook id here and allow it.
5673 //wp_die($downloadlink);
5674 }
5675 }
5676 /**
5677 * The WooCommerce order behind an id, or false when the id is not one.
5678 *
5679 * WooCommerce orders live in wp_posts or in the High-Performance Order Storage
5680 * tables depending on the shop, so the CRUD layer is the only reliable way to
5681 * read and write their meta; get_post_meta() silently returns nothing on HPOS.
5682 *
5683 * @param int $order_id Order ID.
5684 * @return WC_Order|false
5685 */
5686 function ebook_store_get_wc_order_object( $order_id ) {
5687 $order_id = absint( $order_id );
5688
5689 if ( $order_id < 1 || ! function_exists( 'wc_get_order' ) ) {
5690 return false;
5691 }
5692
5693 // The plugin's own orders are ordinary posts and keep using post meta.
5694 if ( get_post_type( $order_id ) === 'ebook_order' ) {
5695 return false;
5696 }
5697
5698 $order = wc_get_order( $order_id );
5699
5700 return ( $order && is_a( $order, 'WC_Order' ) ) ? $order : false;
5701 }
5702
5703 /**
5704 * Meta key holding an order's download count for one ebook.
5705 *
5706 * @param int $ebook_id Ebook post ID, 0 for the order-wide legacy counter.
5707 * @return string
5708 */
5709 function ebook_store_download_count_meta_key( $ebook_id = 0 ) {
5710 $ebook_id = absint( $ebook_id );
5711
5712 return $ebook_id > 0 ? 'downloads_' . $ebook_id : 'downloads';
5713 }
5714
5715 /**
5716 * How many times this order has downloaded this ebook.
5717 *
5718 * A WooCommerce order can carry several ebooks in several formats. Counting all
5719 * of them against one number meant a three-book bundle reached the limit on its
5720 * fifth file, and on HPOS shops the post-meta counter was never stored at all,
5721 * so the limit did not apply. WooCommerce orders now count per ebook, through
5722 * the CRUD layer; the plugin's own orders keep their single counter.
5723 *
5724 * @param int $order_id Order ID.
5725 * @param int $ebook_id Ebook post ID.
5726 * @return int
5727 */
5728 function ebook_store_get_download_count( $order_id, $ebook_id = 0 ) {
5729 $wc_order = ebook_store_get_wc_order_object( $order_id );
5730 $meta_key = ebook_store_download_count_meta_key( $wc_order ? $ebook_id : 0 );
5731
5732 if ( $wc_order ) {
5733 return (int) $wc_order->get_meta( $meta_key, true );
5734 }
5735
5736 return (int) get_post_meta( absint( $order_id ), $meta_key, true );
5737 }
5738
5739 /**
5740 * Record one more download of this ebook against this order.
5741 *
5742 * @param int $order_id Order ID.
5743 * @param int $ebook_id Ebook post ID.
5744 * @return bool
5745 */
5746 function ebook_store_increment_download_count( $order_id, $ebook_id = 0 ) {
5747 $wc_order = ebook_store_get_wc_order_object( $order_id );
5748 $meta_key = ebook_store_download_count_meta_key( $wc_order ? $ebook_id : 0 );
5749 $count = ebook_store_get_download_count( $order_id, $ebook_id ) + 1;
5750
5751 if ( $wc_order ) {
5752 $wc_order->update_meta_data( $meta_key, $count );
5753 $wc_order->save();
5754
5755 return true;
5756 }
5757
5758 return (bool) update_post_meta( absint( $order_id ), $meta_key, $count );
5759 }
5760
5761 function ebook_count_download($order_id, $ebook_id = 0) {
5762 return ebook_store_increment_download_count( $order_id, $ebook_id );
5763 }
5764 function ebook_get_order($key = 'ebook_key',$val = '') {
5765 // Not served from the request's query cache: this lookup also decides
5766 // whether another request has just created the order.
5767 $loop = new WP_Query( array ( 'post_type' => 'ebook_order', 'meta_key' => $key, 'meta_value' => $val, 'cache_results' => false ) );
5768 while ( $loop->have_posts() ) : $loop->the_post();
5769 $meta = get_post_meta(get_the_ID(),null,true);
5770 $meta['order_id'][0] = get_the_ID();
5771 $gross = isset( $meta['mc_gross'][0] ) ? (float) $meta['mc_gross'][0] : 0;
5772 $tax = isset( $meta['tax'][0] ) ? (float) $meta['tax'][0] : 0;
5773 $meta['total'][0] = number_format( round( $gross + $tax, 2 ), 2 );
5774 return $meta;
5775 endwhile;
5776 }
5777
5778 function toMoney($val,$symbol='',$r=2)
5779 {
5780 if ( is_array( $val ) ) {
5781 $val = reset( $val );
5782 }
5783
5784 return number_format( (float) $val, 2 );
5785
5786 }
5787
5788 function ebook_store_normalize_email_log_headers( $headers ) {
5789 if ( empty( $headers ) ) {
5790 return array();
5791 }
5792
5793 if ( is_string( $headers ) ) {
5794 $headers = preg_split( '/\r\n|\r|\n/', $headers );
5795 }
5796
5797 if ( ! is_array( $headers ) ) {
5798 $headers = array( (string) $headers );
5799 }
5800
5801 $normalized = array();
5802
5803 foreach ( $headers as $header ) {
5804 $header = trim( wp_strip_all_tags( (string) $header ) );
5805 if ( $header === '' ) {
5806 continue;
5807 }
5808 $normalized[] = $header;
5809 }
5810
5811 return array_values( array_unique( $normalized ) );
5812 }
5813
5814 function ebook_store_normalize_email_log_attachments( $attachments ) {
5815 if ( empty( $attachments ) ) {
5816 return array();
5817 }
5818
5819 if ( is_string( $attachments ) ) {
5820 $attachments = array( $attachments );
5821 }
5822
5823 if ( ! is_array( $attachments ) ) {
5824 return array();
5825 }
5826
5827 $normalized = array();
5828
5829 foreach ( $attachments as $attachment ) {
5830 if ( is_array( $attachment ) && isset( $attachment['file'] ) ) {
5831 $attachment = $attachment['file'];
5832 }
5833
5834 $attachment = trim( (string) $attachment );
5835
5836 if ( $attachment === '' ) {
5837 continue;
5838 }
5839
5840 $normalized[] = $attachment;
5841 }
5842
5843 return array_values( array_unique( $normalized ) );
5844 }
5845
5846 function ebook_store_json_encode_for_log( $value ) {
5847 $encoded = wp_json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
5848
5849 return $encoded ? $encoded : '';
5850 }
5851
5852 function ebook_store_get_first_meta_value( $value ) {
5853 if ( is_array( $value ) ) {
5854 $value = reset( $value );
5855 }
5856
5857 return $value;
5858 }
5859
5860 function ebook_store_create_email_log_entry( $args = array() ) {
5861 $defaults = array(
5862 'to' => '',
5863 'subject' => '',
5864 'body' => '',
5865 'headers' => array(),
5866 'attachments' => array(),
5867 'result' => false,
5868 'source' => 'standalone',
5869 'related_order_type' => '',
5870 'related_order_id' => 0,
5871 'ebook_id' => 0,
5872 'ebook_key' => '',
5873 'gateway' => '',
5874 'order' => array(),
5875 'context' => array(),
5876 'parent_log_id' => 0,
5877 );
5878
5879 $args = wp_parse_args( $args, $defaults );
5880
5881 $recipient = trim( (string) $args['to'] );
5882 $subject = wp_strip_all_tags( (string) $args['subject'] );
5883 $sent_at = current_time( 'mysql' );
5884 $sent_at_gmt = current_time( 'mysql', true );
5885 $status = $args['result'] ? 'sent' : 'failed';
5886 $order = is_array( $args['order'] ) ? $args['order'] : array();
5887 $context = is_array( $args['context'] ) ? $args['context'] : array();
5888 $headers = ebook_store_normalize_email_log_headers( $args['headers'] );
5889 $attachments = ebook_store_normalize_email_log_attachments( $args['attachments'] );
5890 $ebook_id = absint( ebook_store_get_first_meta_value( $args['ebook_id'] ) );
5891 $related_order_id = absint( ebook_store_get_first_meta_value( $args['related_order_id'] ) );
5892 $parent_log_id = absint( $args['parent_log_id'] );
5893 $gateway = sanitize_key( (string) $args['gateway'] );
5894 $source = sanitize_key( (string) $args['source'] );
5895 $related_order_type = sanitize_key( (string) $args['related_order_type'] );
5896
5897 if ( $ebook_id < 1 && ! empty( $order['ebook'] ) ) {
5898 $ebook_id = absint( ebook_store_get_first_meta_value( $order['ebook'] ) );
5899 }
5900
5901 if ( $related_order_id < 1 && ! empty( $order['order_id'] ) ) {
5902 $related_order_id = absint( ebook_store_get_first_meta_value( $order['order_id'] ) );
5903 }
5904
5905 if ( $related_order_type === '' ) {
5906 $related_order_type = $source === 'woocommerce' ? 'shop_order' : 'ebook_order';
5907 }
5908
5909 $title_bits = array();
5910
5911 if ( $recipient !== '' ) {
5912 $title_bits[] = $recipient;
5913 }
5914
5915 if ( $subject !== '' ) {
5916 $title_bits[] = $subject;
5917 }
5918
5919 if ( empty( $title_bits ) ) {
5920 $title_bits[] = __( 'Order email', 'ebook-store' );
5921 }
5922
5923 $post_id = wp_insert_post(
5924 array(
5925 'post_type' => 'ebook_email_log',
5926 'post_status' => 'publish',
5927 'post_title' => implode( ' - ', array_slice( $title_bits, 0, 2 ) ),
5928 ),
5929 true
5930 );
5931
5932 if ( is_wp_error( $post_id ) || ! $post_id ) {
5933 return 0;
5934 }
5935
5936 update_post_meta( $post_id, 'status', $status );
5937 update_post_meta( $post_id, 'sent_at', $sent_at );
5938 update_post_meta( $post_id, 'sent_at_gmt', $sent_at_gmt );
5939 update_post_meta( $post_id, 'recipient', $recipient );
5940 update_post_meta( $post_id, 'subject', (string) $args['subject'] );
5941 update_post_meta( $post_id, 'body_html', (string) $args['body'] );
5942 update_post_meta( $post_id, 'headers', $headers );
5943 update_post_meta( $post_id, 'attachments', $attachments );
5944 update_post_meta( $post_id, 'source', $source );
5945 update_post_meta( $post_id, 'related_order_type', $related_order_type );
5946 update_post_meta( $post_id, 'related_order_id', $related_order_id );
5947 update_post_meta( $post_id, 'ebook_id', $ebook_id );
5948 update_post_meta( $post_id, 'ebook_key', isset( $args['ebook_key'] ) ? sanitize_text_field( (string) ebook_store_get_first_meta_value( $args['ebook_key'] ) ) : '' );
5949 update_post_meta( $post_id, 'gateway', $gateway );
5950 update_post_meta( $post_id, 'order_snapshot', ebook_store_json_encode_for_log( $order ) );
5951 update_post_meta( $post_id, 'context_snapshot', ebook_store_json_encode_for_log( $context ) );
5952
5953 if ( $parent_log_id > 0 ) {
5954 update_post_meta( $post_id, 'parent_log_id', $parent_log_id );
5955 }
5956
5957 return $post_id;
5958 }
5959
5960 function ebook_store_get_email_log_order_edit_link( $log_id ) {
5961 $order_id = (int) get_post_meta( $log_id, 'related_order_id', true );
5962 $order_type = (string) get_post_meta( $log_id, 'related_order_type', true );
5963
5964 if ( $order_id < 1 ) {
5965 return '';
5966 }
5967
5968 if ( $order_type === 'ebook_order' && get_post_type( $order_id ) === 'ebook_order' ) {
5969 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5970 }
5971
5972 if ( $order_type === 'shop_order' || get_post_type( $order_id ) === 'shop_order' ) {
5973 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5974 }
5975
5976 return '';
5977 }
5978
5979 function ebook_store_get_email_log_ebook_edit_link( $log_id ) {
5980 $ebook_id = (int) get_post_meta( $log_id, 'ebook_id', true );
5981
5982 if ( $ebook_id < 1 || get_post_type( $ebook_id ) !== 'ebook' ) {
5983 return '';
5984 }
5985
5986 return admin_url( 'post.php?post=' . $ebook_id . '&action=edit' );
5987 }
5988
5989 function ebook_store_get_email_log_resend_url( $log_id ) {
5990 return wp_nonce_url(
5991 admin_url( 'admin-post.php?action=ebook_store_resend_email_log&log_id=' . absint( $log_id ) ),
5992 'ebook_store_resend_email_log_' . absint( $log_id )
5993 );
5994 }
5995
5996 function ebook_store_get_order_reset_downloads_url( $order_id ) {
5997 return wp_nonce_url(
5998 admin_url( 'admin-post.php?action=ebook_store_reset_order_downloads&order_id=' . absint( $order_id ) ),
5999 'ebook_store_reset_order_downloads_' . absint( $order_id )
6000 );
6001 }
6002
6003 function ebook_store_get_order_resend_delivery_url( $order_id ) {
6004 return wp_nonce_url(
6005 admin_url( 'admin-post.php?action=ebook_store_resend_order_delivery&order_id=' . absint( $order_id ) ),
6006 'ebook_store_resend_order_delivery_' . absint( $order_id )
6007 );
6008 }
6009
6010 function ebook_store_get_order_refund_url( $order_id ) {
6011 return wp_nonce_url(
6012 admin_url( 'admin-post.php?action=ebook_store_refund_order&order_id=' . absint( $order_id ) ),
6013 'ebook_store_refund_order_' . absint( $order_id )
6014 );
6015 }
6016
6017 function ebook_store_get_order_refund_gateway( $order_id ) {
6018 $order_id = absint( $order_id );
6019 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6020 return '';
6021 }
6022
6023 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
6024 if ( ebook_store_is_negative_order_status( $payment_status ) ) {
6025 return '';
6026 }
6027
6028 if ( get_post_meta( $order_id, 'refund_gateway_id', true ) !== '' ) {
6029 return '';
6030 }
6031
6032 $payment_type = strtolower( trim( (string) get_post_meta( $order_id, 'payment_type', true ) ) );
6033
6034 if (
6035 $payment_type === 'stripe'
6036 && trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) ) !== ''
6037 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== ''
6038 ) {
6039 return 'stripe';
6040 }
6041
6042 if (
6043 $payment_type === 'paypal'
6044 && trim( (string) get_post_meta( $order_id, 'paypal_capture_id', true ) ) !== ''
6045 && function_exists( 'ebook_store_paypal_rest_is_configured' )
6046 && ebook_store_paypal_rest_is_configured()
6047 ) {
6048 return 'paypal';
6049 }
6050
6051 if (
6052 $payment_type === 'adyen'
6053 && trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) ) !== ''
6054 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
6055 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
6056 ) {
6057 return 'adyen';
6058 }
6059
6060 return '';
6061 }
6062
6063 function ebook_store_get_email_log_status_badge( $status ) {
6064 $status = sanitize_key( (string) $status );
6065 $label = $status === 'sent' ? __( 'Sent', 'ebook-store' ) : __( 'Failed', 'ebook-store' );
6066
6067 return '<span class="ebook-email-log-badge ebook-email-log-badge--' . esc_attr( $status === 'sent' ? 'sent' : 'failed' ) . '">' . esc_html( $label ) . '</span>';
6068 }
6069
6070 function ebook_store_render_email_log_box( $post ) {
6071 $recipient = (string) get_post_meta( $post->ID, 'recipient', true );
6072 $subject = (string) get_post_meta( $post->ID, 'subject', true );
6073 $body_html = (string) get_post_meta( $post->ID, 'body_html', true );
6074 $headers = get_post_meta( $post->ID, 'headers', true );
6075 $attachments = get_post_meta( $post->ID, 'attachments', true );
6076 $status = (string) get_post_meta( $post->ID, 'status', true );
6077 $source = (string) get_post_meta( $post->ID, 'source', true );
6078 $sent_at = (string) get_post_meta( $post->ID, 'sent_at', true );
6079 $order_snapshot = (string) get_post_meta( $post->ID, 'order_snapshot', true );
6080 $context_snapshot = (string) get_post_meta( $post->ID, 'context_snapshot', true );
6081 $ebook_key = (string) get_post_meta( $post->ID, 'ebook_key', true );
6082 $resend_count = (int) get_post_meta( $post->ID, 'resend_count', true );
6083 $last_resend_at = (string) get_post_meta( $post->ID, 'last_resend_at', true );
6084 $last_resend_status = (string) get_post_meta( $post->ID, 'last_resend_status', true );
6085 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
6086 $ebook_link = ebook_store_get_email_log_ebook_edit_link( $post->ID );
6087
6088 if ( ! is_array( $headers ) ) {
6089 $headers = array();
6090 }
6091
6092 if ( ! is_array( $attachments ) ) {
6093 $attachments = array();
6094 }
6095
6096 echo '<div class="ebook-email-log">';
6097 echo '<div class="ebook-email-log__actions">';
6098 echo '<a class="button button-primary" href="' . esc_url( ebook_store_get_email_log_resend_url( $post->ID ) ) . '">' . esc_html__( 'Resend email', 'ebook-store' ) . '</a>';
6099 if ( $order_link !== '' ) {
6100 echo '<a class="button button-secondary" href="' . esc_url( $order_link ) . '">' . esc_html__( 'View related order', 'ebook-store' ) . '</a>';
6101 }
6102 if ( $ebook_link !== '' ) {
6103 echo '<a class="button button-secondary" href="' . esc_url( $ebook_link ) . '">' . esc_html__( 'Edit ebook', 'ebook-store' ) . '</a>';
6104 }
6105 echo '</div>';
6106
6107 echo '<div class="ebook-email-log__summary">';
6108 echo '<div><span>' . esc_html__( 'Status', 'ebook-store' ) . '</span><strong>' . wp_kses_post( ebook_store_get_email_log_status_badge( $status ) ) . '</strong></div>';
6109 echo '<div><span>' . esc_html__( 'Recipient', 'ebook-store' ) . '</span><strong>' . ( $recipient !== '' ? '<a href="mailto:' . esc_attr( $recipient ) . '">' . esc_html( $recipient ) . '</a>' : esc_html__( 'Not set', 'ebook-store' ) ) . '</strong></div>';
6110 echo '<div><span>' . esc_html__( 'Source', 'ebook-store' ) . '</span><strong>' . esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
6111 echo '<div><span>' . esc_html__( 'Sent at', 'ebook-store' ) . '</span><strong>' . esc_html( $sent_at !== '' ? $sent_at : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
6112 echo '<div><span>' . esc_html__( 'Ebook key', 'ebook-store' ) . '</span><strong>' . esc_html( $ebook_key !== '' ? $ebook_key : __( 'Not set', 'ebook-store' ) ) . '</strong></div>';
6113 echo '<div><span>' . esc_html__( 'Resends', 'ebook-store' ) . '</span><strong>' . esc_html( number_format_i18n( $resend_count ) ) . ( $last_resend_at !== '' ? '<small>' . esc_html( sprintf( __( 'Last attempt: %1$s (%2$s)', 'ebook-store' ), $last_resend_at, $last_resend_status !== '' ? $last_resend_status : __( 'unknown', 'ebook-store' ) ) ) . '</small>' : '' ) . '</strong></div>';
6114 echo '</div>';
6115
6116 echo '<div class="ebook-email-log__section">';
6117 echo '<h2>' . esc_html__( 'Message', 'ebook-store' ) . '</h2>';
6118 echo '<p><strong>' . esc_html__( 'Subject', 'ebook-store' ) . ':</strong> ' . esc_html( $subject ) . '</p>';
6119 echo '<div class="ebook-email-log__preview">' . wp_kses_post( $body_html ) . '</div>';
6120 echo '<textarea class="widefat ebook-email-log__code" rows="10" readonly>' . esc_textarea( $body_html ) . '</textarea>';
6121 echo '</div>';
6122
6123 echo '<div class="ebook-email-log__section">';
6124 echo '<h2>' . esc_html__( 'Headers and attachments', 'ebook-store' ) . '</h2>';
6125 echo '<p><strong>' . esc_html__( 'Headers', 'ebook-store' ) . ':</strong></p>';
6126 echo '<textarea class="widefat ebook-email-log__code" rows="4" readonly>' . esc_textarea( implode( "\n", $headers ) ) . '</textarea>';
6127 echo '<p><strong>' . esc_html__( 'Attachments', 'ebook-store' ) . ':</strong></p>';
6128 if ( empty( $attachments ) ) {
6129 echo '<p>' . esc_html__( 'No attachments were sent with this email.', 'ebook-store' ) . '</p>';
6130 } else {
6131 echo '<ul class="ebook-email-log__attachments">';
6132 foreach ( $attachments as $attachment ) {
6133 $label = basename( (string) $attachment );
6134 echo '<li><code>' . esc_html( $label ) . '</code><span>' . esc_html( (string) $attachment ) . '</span></li>';
6135 }
6136 echo '</ul>';
6137 }
6138 echo '</div>';
6139
6140 echo '<div class="ebook-email-log__section">';
6141 echo '<h2>' . esc_html__( 'Raw order snapshot', 'ebook-store' ) . '</h2>';
6142 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $order_snapshot ) . '</textarea>';
6143 echo '</div>';
6144
6145 echo '<div class="ebook-email-log__section">';
6146 echo '<h2>' . esc_html__( 'Raw email context', 'ebook-store' ) . '</h2>';
6147 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $context_snapshot ) . '</textarea>';
6148 echo '</div>';
6149 echo '</div>';
6150 }
6151
6152 /**
6153 * Build the list of files to attach to a delivery email.
6154 *
6155 * Always keeps the core file the caller already resolved (e.g. the encrypted
6156 * PDF), and additionally attaches a per-buyer watermarked ePub when the ebook
6157 * offers one and ePub watermarking is enabled. So a buyer who opts into email
6158 * attachments gets the ePub attached AND watermarked, for every gateway — not
6159 * only via the download link. encrypted() caches per order, so this is idempotent.
6160 *
6161 * @param string|null $core_file Path already chosen for attachment (may be null).
6162 * @param int $order_id Order post ID.
6163 * @param int $ebook_id Ebook post ID.
6164 * @return string|array|null A single path, an array of paths, or null.
6165 */
6166 function ebook_store_collect_delivery_attachments( $core_file, $order_id, $ebook_id ) {
6167 $order_id = (int) $order_id;
6168 $ebook_id = (int) $ebook_id;
6169 $files = array();
6170
6171 if ( is_string( $core_file ) && $core_file !== '' && file_exists( $core_file ) ) {
6172 $files[] = $core_file;
6173 }
6174
6175 if ( $ebook_id > 0 && get_option( 'ebook_store_epub_watermark' ) && function_exists( 'ebook_store_watermark_epub' ) ) {
6176 $obj = new EbookStoreEbook( $ebook_id );
6177 if ( ! empty( $obj->files['epub'] ) && file_exists( $obj->files['epub'] ) ) {
6178 $obj->order_id = $order_id;
6179 $epub = $obj->encrypted( $order_id, 'epub' );
6180 if ( is_string( $epub ) && $epub !== '' && file_exists( $epub ) && ! in_array( $epub, $files, true ) ) {
6181 $files[] = $epub;
6182 }
6183 }
6184 }
6185
6186 if ( empty( $files ) ) {
6187 return null;
6188 }
6189
6190 return count( $files ) === 1 ? $files[0] : $files;
6191 }
6192
6193 function ebook_email_delivery($post_id = null) {
6194 $QSWPOptions = new QSWPOptions();
6195 global $ebook_email_delivery, $formData;
6196 if (@$ebook_email_delivery['done'] == 1) {
6197 return;
6198 }
6199 $ebook_email_delivery['done'] = 1;
6200
6201 if ( isset( $formData ) ) {
6202 if ( is_string( $formData ) && $formData !== '' ) {
6203 $decoded_form_data = json_decode( $formData, true );
6204 $formData = is_array( $decoded_form_data ) ? $decoded_form_data : array();
6205 } elseif ( is_array( $formData ) ) {
6206 $formData = $formData;
6207 } else {
6208 $formData = array();
6209 }
6210 } else {
6211 $formData = array();
6212 }
6213
6214 // error_log(var_export(debug_backtrace(),true));
6215 $ebook_email_delivery['order']['download_links'] = implode("<br />",ebook_download_links($ebook_email_delivery['order']));
6216 // WooCommerce (and any caller) may already have set downloadlinks to the
6217 // full per-order format list; only fill it in when nothing was supplied.
6218 if ( empty( $ebook_email_delivery['order']['downloadlinks'] ) ) {
6219 $ebook_email_delivery['order']['downloadlinks'] = $ebook_email_delivery['order']['download_links'];
6220 }
6221 $ebook_email_delivery['order']['ebook_bonus'] = (implode("<br />",ebook_download_links_bonus($ebook_email_delivery['order'])) != '' ? implode("<br />",ebook_download_links_bonus($ebook_email_delivery['order'])) : __('None','ebook-store'));
6222
6223 $order_total = isset( $ebook_email_delivery['order']['total'] ) ? $ebook_email_delivery['order']['total'] : false;
6224 $order_tax = isset( $ebook_email_delivery['order']['tax'] ) ? $ebook_email_delivery['order']['tax'] : 0;
6225 $order_gross = isset( $ebook_email_delivery['order']['mc_gross'] ) ? $ebook_email_delivery['order']['mc_gross'] : 0;
6226
6227 if ( is_array( $order_total ) ) {
6228 $order_total = reset( $order_total );
6229 }
6230 if ( is_array( $order_tax ) ) {
6231 $order_tax = reset( $order_tax );
6232 }
6233 if ( is_array( $order_gross ) ) {
6234 $order_gross = reset( $order_gross );
6235 }
6236
6237 if ( $order_total === false || $order_total === '' ) {
6238 $order_total = (float) $order_gross + (float) $order_tax;
6239 }
6240
6241 $ebook_email_delivery['order']['total'] = toMoney( $order_total );
6242 $ebook_email_delivery['order']['tax'] = toMoney( $order_tax );
6243
6244 if (!isset($ebook_email_delivery['order']['password'])) {
6245 $ebook_email_delivery['order']['password'] = ebook_store_get_password_for_order(
6246 isset($ebook_email_delivery['order']['order_id']) ? (int)$ebook_email_delivery['order']['order_id'] : 0,
6247 array(
6248 'payer_email' => isset($ebook_email_delivery['order']['payer_email']) ? $ebook_email_delivery['order']['payer_email'] : '',
6249 'password' => '',
6250 'md5_nonce' => isset($ebook_email_delivery['order']['md5_nonce']) ? $ebook_email_delivery['order']['md5_nonce'] : '',
6251 'ebook_key' => isset($ebook_email_delivery['order']['ebook_key']) ? $ebook_email_delivery['order']['ebook_key'] : '',
6252 )
6253 );
6254 }
6255
6256 @$attachmentFile = $ebook_email_delivery['attachment'][0]['file'];
6257 $ebook_email_delivery['order']['filesize'] = ( $attachmentFile && file_exists( $attachmentFile ) ) ? ebook_store_human_filesize(filesize($attachmentFile)) : '';
6258 if (get_option('attach_files') != 1) {
6259 $attachmentFile = null;
6260 } else {
6261 // Attachments are on: attach the core file AND a watermarked ePub if the
6262 // ebook offers one, for every gateway (not only the download link).
6263 $ebook_attach_order = isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : $post_id;
6264 if ( is_array( $ebook_attach_order ) ) { $ebook_attach_order = reset( $ebook_attach_order ); }
6265 if ( ! $ebook_attach_order ) { $ebook_attach_order = $post_id; }
6266 $ebook_attach_id = isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0;
6267 if ( is_array( $ebook_attach_id ) ) { $ebook_attach_id = reset( $ebook_attach_id ); }
6268 $attachmentFile = ebook_store_collect_delivery_attachments( $attachmentFile, (int) $ebook_attach_order, (int) $ebook_attach_id );
6269 }
6270
6271
6272 //error_reporting(E_ALL);
6273 foreach ($ebook_email_delivery['order'] as $k => $v) {
6274 if (is_array($v)) {
6275 $v = $v[0];
6276 }
6277 $ebook_email_delivery['text'] = str_replace('%%' . $k . '%%', $v, $ebook_email_delivery['text']);
6278 }
6279
6280
6281 $ebook_email_delivery['text'] = nl2br($ebook_email_delivery['text']);
6282 //$ebook_email_delivery['text'] = apply_filters('the_content',$ebook_email_delivery['text']);
6283 //error_log('ebook_email_delivery ' . print_r($ebook_email_delivery,true));
6284 add_filter( 'wp_mail_content_type', 'ebook_set_content_type' );
6285 $ebook_email_delivery['mail_result'] = false;
6286 if (floatval(get_option('email_delivery',$QSWPOptions->email_delivery)) == 1) {
6287 //echo "Sending mail, data: " . implode("; ", $ebook_email_delivery);
6288 // Send-to-Kindle is an extra copy, never a replacement for the buyer's own email.
6289 $kindle_email = '';
6290 if (floatval(get_option('kindleDelivery',$QSWPOptions->kindleDelivery)) == 1) {
6291 if ( ! empty( $formData['kindle_email'] ) && is_string( $formData['kindle_email'] ) && strpos( $formData['kindle_email'], '@kindle.com' ) !== false ) {
6292 $kindle_email = (string) filter_var( trim( $formData['kindle_email'] ), FILTER_VALIDATE_EMAIL );
6293 }
6294 }
6295 if ( ! empty( $formData['gift_email'] ) ) {
6296 $formData['gift_email'] = filter_var($formData['gift_email'], FILTER_VALIDATE_EMAIL);
6297 $ebook_email_delivery['to'] = $formData['gift_email'];
6298 }
6299 $headers = array('Content-Type: text/html; charset=UTF-8');
6300
6301 $mail_result = wp_mail($ebook_email_delivery['to'],$ebook_email_delivery['subject'], $ebook_email_delivery['text'],$headers,$attachmentFile);
6302 $ebook_email_delivery['mail_result'] = (bool) $mail_result;
6303
6304 ebook_store_create_email_log_entry(
6305 array(
6306 'to' => isset( $ebook_email_delivery['to'] ) ? $ebook_email_delivery['to'] : '',
6307 'subject' => isset( $ebook_email_delivery['subject'] ) ? $ebook_email_delivery['subject'] : '',
6308 'body' => isset( $ebook_email_delivery['text'] ) ? $ebook_email_delivery['text'] : '',
6309 'headers' => $headers,
6310 'attachments' => $attachmentFile,
6311 'result' => $mail_result,
6312 'source' => isset( $ebook_email_delivery['source'] ) ? $ebook_email_delivery['source'] : 'standalone',
6313 'related_order_type' => isset( $ebook_email_delivery['related_order_type'] ) ? $ebook_email_delivery['related_order_type'] : '',
6314 'related_order_id' => isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : 0,
6315 'ebook_id' => isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0,
6316 'ebook_key' => isset( $ebook_email_delivery['order']['ebook_key'] ) ? $ebook_email_delivery['order']['ebook_key'] : '',
6317 'gateway' => isset( $ebook_email_delivery['order']['gateway'] ) ? $ebook_email_delivery['order']['gateway'] : '',
6318 'order' => isset( $ebook_email_delivery['order'] ) ? $ebook_email_delivery['order'] : array(),
6319 'context' => $ebook_email_delivery,
6320 )
6321 );
6322
6323 // Amazon only processes the attachment, so the Kindle copy is sent when there is one.
6324 if ( $kindle_email !== '' && $kindle_email !== $ebook_email_delivery['to'] && ! empty( $attachmentFile ) ) {
6325 $kindle_mail_result = wp_mail( $kindle_email, $ebook_email_delivery['subject'], $ebook_email_delivery['text'], $headers, $attachmentFile );
6326
6327 ebook_store_create_email_log_entry(
6328 array(
6329 'to' => $kindle_email,
6330 'subject' => isset( $ebook_email_delivery['subject'] ) ? $ebook_email_delivery['subject'] : '',
6331 'body' => isset( $ebook_email_delivery['text'] ) ? $ebook_email_delivery['text'] : '',
6332 'headers' => $headers,
6333 'attachments' => $attachmentFile,
6334 'result' => $kindle_mail_result,
6335 'source' => isset( $ebook_email_delivery['source'] ) ? $ebook_email_delivery['source'] : 'standalone',
6336 'related_order_type' => isset( $ebook_email_delivery['related_order_type'] ) ? $ebook_email_delivery['related_order_type'] : '',
6337 'related_order_id' => isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : 0,
6338 'ebook_id' => isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0,
6339 'ebook_key' => isset( $ebook_email_delivery['order']['ebook_key'] ) ? $ebook_email_delivery['order']['ebook_key'] : '',
6340 'gateway' => isset( $ebook_email_delivery['order']['gateway'] ) ? $ebook_email_delivery['order']['gateway'] : '',
6341 'order' => isset( $ebook_email_delivery['order'] ) ? $ebook_email_delivery['order'] : array(),
6342 'context' => $ebook_email_delivery,
6343 )
6344 );
6345 }
6346 }
6347 }
6348
6349 function ebook_set_content_type( $content_type ){
6350 return 'text/html';
6351 }
6352 function ebook_attachment($post_id, $ignoreSetting = false) {
6353 if (get_option('attach_files') == 1 || $ignoreSetting == true) {
6354 $attachment = get_post_meta($post_id,'ebook_wp_custom_attachment');
6355 if (WP_DEBUG == true) error_log('attachment - ' . print_r($attachment,true));
6356 return $attachment;
6357 }
6358 }
6359
6360 function ebook_store_set_email_log_columns( $columns ) {
6361 return array(
6362 'cb' => '<input type="checkbox" />',
6363 'title' => __( 'Email', 'ebook-store' ),
6364 'recipient' => __( 'Recipient', 'ebook-store' ),
6365 'source' => __( 'Source', 'ebook-store' ),
6366 'related_order' => __( 'Order', 'ebook-store' ),
6367 'ebook_reference' => __( 'Ebook', 'ebook-store' ),
6368 'status' => __( 'Status', 'ebook-store' ),
6369 'resends' => __( 'Resends', 'ebook-store' ),
6370 'date' => __( 'Logged', 'ebook-store' ),
6371 );
6372 }
6373
6374 function ebook_store_email_log_columns_output( $column, $post_id ) {
6375 switch ( $column ) {
6376 case 'recipient':
6377 $recipient = (string) get_post_meta( $post_id, 'recipient', true );
6378 echo $recipient !== '' ? '<a href="mailto:' . esc_attr( $recipient ) . '">' . esc_html( $recipient ) . '</a>' : '&mdash;';
6379 break;
6380 case 'source':
6381 $source = (string) get_post_meta( $post_id, 'source', true );
6382 echo esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) );
6383 break;
6384 case 'related_order':
6385 $order_id = (int) get_post_meta( $post_id, 'related_order_id', true );
6386 $link = ebook_store_get_email_log_order_edit_link( $post_id );
6387 if ( $order_id < 1 ) {
6388 echo '&mdash;';
6389 break;
6390 }
6391 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">#' . esc_html( $order_id ) . '</a>' : '#' . esc_html( $order_id );
6392 break;
6393 case 'ebook_reference':
6394 $ebook_id = (int) get_post_meta( $post_id, 'ebook_id', true );
6395 $link = ebook_store_get_email_log_ebook_edit_link( $post_id );
6396 if ( $ebook_id < 1 ) {
6397 echo '&mdash;';
6398 break;
6399 }
6400 $title = get_the_title( $ebook_id );
6401 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title !== '' ? $title : '#' . $ebook_id ) . '</a>' : esc_html( $title !== '' ? $title : '#' . $ebook_id );
6402 break;
6403 case 'status':
6404 echo wp_kses_post( ebook_store_get_email_log_status_badge( get_post_meta( $post_id, 'status', true ) ) );
6405 break;
6406 case 'resends':
6407 $resend_count = (int) get_post_meta( $post_id, 'resend_count', true );
6408 $last_resend_at = (string) get_post_meta( $post_id, 'last_resend_at', true );
6409 echo '<strong>' . esc_html( number_format_i18n( $resend_count ) ) . '</strong>';
6410 if ( $last_resend_at !== '' ) {
6411 echo '<div class="ebook-email-log-list__meta">' . esc_html( $last_resend_at ) . '</div>';
6412 }
6413 break;
6414 }
6415 }
6416
6417 function ebook_store_email_log_row_actions( $actions, $post ) {
6418 if ( ! $post || $post->post_type !== 'ebook_email_log' ) {
6419 return $actions;
6420 }
6421
6422 $actions['resend_email'] = '<a href="' . esc_url( ebook_store_get_email_log_resend_url( $post->ID ) ) . '">' . esc_html__( 'Resend', 'ebook-store' ) . '</a>';
6423
6424 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
6425 if ( $order_link !== '' ) {
6426 $actions['view_related_order'] = '<a href="' . esc_url( $order_link ) . '">' . esc_html__( 'View order', 'ebook-store' ) . '</a>';
6427 }
6428
6429 return $actions;
6430 }
6431
6432 function ebook_store_remove_email_log_submit_box() {
6433 remove_meta_box( 'submitdiv', 'ebook_email_log', 'side' );
6434 }
6435
6436 function ebook_store_cleanup_email_log_submenu() {
6437 remove_submenu_page( 'edit.php?post_type=ebook', 'post-new.php?post_type=ebook_email_log' );
6438 }
6439
6440 function ebook_store_redirect_new_email_log_entry() {
6441 global $pagenow;
6442
6443 if ( ! is_admin() || $pagenow !== 'post-new.php' ) {
6444 return;
6445 }
6446
6447 if ( empty( $_GET['post_type'] ) || wp_unslash( $_GET['post_type'] ) !== 'ebook_email_log' ) {
6448 return;
6449 }
6450
6451 wp_safe_redirect( admin_url( 'edit.php?post_type=ebook_email_log' ) );
6452 exit;
6453 }
6454
6455 function ebook_store_render_email_log_admin_styles() {
6456 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6457 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6458 return;
6459 }
6460
6461 echo '<style>
6462 .post-type-ebook_email_log #titlediv,
6463 .post-type-ebook_email_log #minor-publishing,
6464 .post-type-ebook_email_log #misc-publishing-actions,
6465 .post-type-ebook_email_log #major-publishing-actions .open-if-no-js {
6466 display: none;
6467 }
6468 .post-type-ebook_email_log .page-title-action {
6469 display: none;
6470 }
6471 .post-type-ebook_email_log #major-publishing-actions {
6472 padding: 0;
6473 border-top: 0;
6474 }
6475 .ebook-email-log {
6476 display: grid;
6477 gap: 16px;
6478 }
6479 .ebook-email-log__actions {
6480 display: flex;
6481 flex-wrap: wrap;
6482 gap: 10px;
6483 }
6484 .ebook-email-log__summary {
6485 display: grid;
6486 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
6487 gap: 12px;
6488 }
6489 .ebook-email-log__summary > div,
6490 .ebook-email-log__section {
6491 background: #fff;
6492 border: 1px solid #dcdcde;
6493 border-radius: 14px;
6494 padding: 16px;
6495 }
6496 .ebook-email-log__summary span {
6497 display: block;
6498 margin-bottom: 6px;
6499 color: #646970;
6500 font-size: 12px;
6501 text-transform: uppercase;
6502 letter-spacing: .04em;
6503 }
6504 .ebook-email-log__summary strong {
6505 display: block;
6506 font-size: 14px;
6507 line-height: 1.5;
6508 }
6509 .ebook-email-log__summary small {
6510 display: block;
6511 margin-top: 6px;
6512 color: #646970;
6513 font-weight: 400;
6514 }
6515 .ebook-email-log__section h2 {
6516 margin: 0 0 12px;
6517 font-size: 15px;
6518 }
6519 .ebook-email-log__preview {
6520 max-height: 360px;
6521 overflow: auto;
6522 padding: 16px;
6523 background: #f6f7f7;
6524 border: 1px solid #dcdcde;
6525 border-radius: 12px;
6526 margin-bottom: 12px;
6527 }
6528 .ebook-email-log__code {
6529 font-family: Consolas, Monaco, monospace;
6530 font-size: 12px;
6531 }
6532 .ebook-email-log__attachments {
6533 margin: 0;
6534 padding: 0;
6535 list-style: none;
6536 display: grid;
6537 gap: 10px;
6538 }
6539 .ebook-email-log__attachments li {
6540 display: grid;
6541 gap: 4px;
6542 padding: 10px 12px;
6543 background: #f6f7f7;
6544 border-radius: 10px;
6545 }
6546 .ebook-email-log-badge {
6547 display: inline-flex;
6548 align-items: center;
6549 padding: 4px 10px;
6550 border-radius: 999px;
6551 font-size: 12px;
6552 font-weight: 600;
6553 }
6554 .ebook-email-log-badge--sent {
6555 background: #e7f7ed;
6556 color: #116b38;
6557 }
6558 .ebook-email-log-badge--failed {
6559 background: #fdeaea;
6560 color: #b42318;
6561 }
6562 .ebook-email-log-list__meta {
6563 color: #646970;
6564 font-size: 12px;
6565 margin-top: 4px;
6566 }
6567 </style>';
6568 }
6569
6570 function ebook_store_email_log_admin_notice() {
6571 if ( ! is_admin() || empty( $_GET['ebook_store_email_log_notice'] ) ) {
6572 return;
6573 }
6574
6575 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6576 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6577 return;
6578 }
6579
6580 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_email_log_notice'] ) );
6581 if ( ! in_array( $notice, array( 'resent', 'resend_failed' ), true ) ) {
6582 return;
6583 }
6584
6585 $class = $notice === 'resent' ? 'notice-success' : 'notice-error';
6586 $message = $notice === 'resent'
6587 ? __( 'The email was resent and logged as a new entry.', 'ebook-store' )
6588 : __( 'The resend attempt failed. Check your mail configuration and the attachment paths.', 'ebook-store' );
6589
6590 echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6591 }
6592
6593 function ebook_store_order_admin_notice() {
6594 if ( ! is_admin() || empty( $_GET['ebook_store_order_notice'] ) ) {
6595 return;
6596 }
6597
6598 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6599 if ( ! $screen || $screen->post_type !== 'ebook_order' ) {
6600 return;
6601 }
6602
6603 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_order_notice'] ) );
6604 $messages = array(
6605 'downloads_reset' => array(
6606 'class' => 'notice-success',
6607 'text' => __( 'The order download counter was reset to 0.', 'ebook-store' ),
6608 ),
6609 'delivery_resent' => array(
6610 'class' => 'notice-success',
6611 'text' => __( 'The delivery email was sent again and logged as a new email entry.', 'ebook-store' ),
6612 ),
6613 'delivery_failed' => array(
6614 'class' => 'notice-error',
6615 'text' => __( 'The delivery email resend failed. Check the order email address, attachment file, and mail configuration.', 'ebook-store' ),
6616 ),
6617 'refund_succeeded' => array(
6618 'class' => 'notice-success',
6619 'text' => __( 'The payment was refunded successfully.', 'ebook-store' ),
6620 ),
6621 'refund_failed' => array(
6622 'class' => 'notice-error',
6623 'text' => __( 'The payment refund failed.', 'ebook-store' ),
6624 ),
6625 );
6626
6627 if ( ! isset( $messages[ $notice ] ) ) {
6628 return;
6629 }
6630
6631 $message = $messages[ $notice ]['text'];
6632 if ( ! empty( $_GET['ebook_store_order_message'] ) ) {
6633 $message = sanitize_text_field( wp_unslash( $_GET['ebook_store_order_message'] ) );
6634 }
6635
6636 echo '<div class="notice ' . esc_attr( $messages[ $notice ]['class'] ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6637 }
6638
6639 function ebook_store_get_order_admin_list_url( $notice = '', $args = array() ) {
6640 $args = array(
6641 'post_type' => 'ebook_order',
6642 ) + (array) $args;
6643
6644 if ( $notice !== '' ) {
6645 $args['ebook_store_order_notice'] = $notice;
6646 }
6647
6648 return add_query_arg( $args, admin_url( 'edit.php' ) );
6649 }
6650
6651 function ebook_store_mark_order_refunded( $order_id, $gateway, $refund_id, $refund_status, $amount = '', $currency = '' ) {
6652 $order_id = absint( $order_id );
6653 if ( $order_id < 1 ) {
6654 return;
6655 }
6656
6657 update_post_meta( $order_id, 'payment_status', 'Refunded' );
6658 update_post_meta( $order_id, 'refund_gateway', sanitize_key( (string) $gateway ) );
6659 update_post_meta( $order_id, 'refund_gateway_id', sanitize_text_field( (string) $refund_id ) );
6660 update_post_meta( $order_id, 'refund_gateway_status', sanitize_text_field( (string) $refund_status ) );
6661 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
6662
6663 if ( $amount !== '' && is_numeric( $amount ) ) {
6664 update_post_meta( $order_id, 'refund_amount', (string) $amount );
6665 }
6666
6667 if ( $currency !== '' ) {
6668 update_post_meta( $order_id, 'refund_currency', strtoupper( sanitize_text_field( (string) $currency ) ) );
6669 }
6670 }
6671
6672 function ebook_store_refund_stripe_order( $order_id ) {
6673 $payment_intent_id = trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) );
6674 if ( $payment_intent_id === '' ) {
6675 return new WP_Error( 'ebook_store_missing_stripe_payment_intent', __( 'This Stripe order is missing its payment intent ID.', 'ebook-store' ) );
6676 }
6677
6678 $bootstrap = ebook_store_bootstrap_stripe_sdk();
6679 if ( is_wp_error( $bootstrap ) ) {
6680 return $bootstrap;
6681 }
6682
6683 try {
6684 $refund = \Stripe\Refund::create(
6685 array(
6686 'payment_intent' => $payment_intent_id,
6687 'reason' => 'requested_by_customer',
6688 'metadata' => array(
6689 'ebook_order_id' => (string) $order_id,
6690 ),
6691 )
6692 );
6693 } catch ( Exception $e ) {
6694 return new WP_Error( 'ebook_store_stripe_refund_failed', $e->getMessage() );
6695 }
6696
6697 $refund_amount = isset( $refund->amount ) ? ((float) $refund->amount / 100) : (float) get_post_meta( $order_id, 'mc_gross', true );
6698 $refund_currency = isset( $refund->currency ) ? strtoupper( (string) $refund->currency ) : (string) get_post_meta( $order_id, 'mc_currency', true );
6699 $refund_status = isset( $refund->status ) ? (string) $refund->status : 'succeeded';
6700 $refund_id = isset( $refund->id ) ? (string) $refund->id : '';
6701
6702 ebook_store_mark_order_refunded( $order_id, 'stripe', $refund_id, $refund_status, $refund_amount, $refund_currency );
6703
6704 return array(
6705 'gateway' => 'stripe',
6706 'refund_id' => $refund_id,
6707 'status' => $refund_status,
6708 'amount' => $refund_amount,
6709 'currency' => $refund_currency,
6710 );
6711 }
6712
6713 function ebook_store_refund_adyen_order( $order_id ) {
6714 $psp_reference = trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) );
6715 if ( $psp_reference === '' ) {
6716 return new WP_Error( 'ebook_store_missing_adyen_psp_reference', __( 'This Adyen order is missing its PSP reference.', 'ebook-store' ) );
6717 }
6718
6719 $amount = (float) get_post_meta( $order_id, 'mc_gross', true );
6720 $currency = strtoupper( trim( (string) get_post_meta( $order_id, 'mc_currency', true ) ) );
6721 if ( $amount <= 0 || $currency === '' ) {
6722 return new WP_Error( 'ebook_store_invalid_adyen_refund_amount', __( 'This Adyen order is missing a valid refund amount or currency.', 'ebook-store' ) );
6723 }
6724
6725 $response = ebook_store_adyen_request(
6726 'POST',
6727 'payments/' . rawurlencode( $psp_reference ) . '/refunds',
6728 array(
6729 'merchantAccount' => trim( (string) get_option( 'adyen_merchant_account', '' ) ),
6730 'amount' => array(
6731 'currency' => $currency,
6732 'value' => (int) round( $amount * ebook_store_currency_minor_unit_factor( $currency ) ),
6733 ),
6734 'reference' => 'refund-' . $order_id . '-' . wp_generate_password( 8, false, false ),
6735 ),
6736 array(
6737 'Idempotency-Key' => wp_generate_uuid4(),
6738 )
6739 );
6740
6741 if ( is_wp_error( $response ) ) {
6742 return $response;
6743 }
6744
6745 $refund_id = ! empty( $response['pspReference'] ) ? (string) $response['pspReference'] : '';
6746 ebook_store_mark_order_refunded( $order_id, 'adyen', $refund_id, 'received', $amount, $currency );
6747
6748 return array(
6749 'gateway' => 'adyen',
6750 'refund_id' => $refund_id,
6751 'status' => 'received',
6752 'amount' => $amount,
6753 'currency' => $currency,
6754 );
6755 }
6756
6757 function ebook_store_process_order_refund( $order_id ) {
6758 $order_id = absint( $order_id );
6759 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6760 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6761 }
6762
6763 $gateway = ebook_store_get_order_refund_gateway( $order_id );
6764 if ( $gateway === '' ) {
6765 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this order or payment method.', 'ebook-store' ) );
6766 }
6767
6768 if ( $gateway === 'stripe' ) {
6769 return ebook_store_refund_stripe_order( $order_id );
6770 }
6771
6772 if ( $gateway === 'adyen' ) {
6773 return ebook_store_refund_adyen_order( $order_id );
6774 }
6775
6776 if ( $gateway === 'paypal' ) {
6777 return ebook_store_refund_paypal_rest_order( $order_id );
6778 }
6779
6780 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this payment method.', 'ebook-store' ) );
6781 }
6782
6783 function ebook_store_prepare_order_delivery_email( $order_id ) {
6784 $order_id = absint( $order_id );
6785 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6786 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6787 }
6788
6789 $order_meta = get_post_meta( $order_id, null, true );
6790 if ( ! is_array( $order_meta ) || empty( $order_meta['ebook'][0] ) ) {
6791 return new WP_Error( 'ebook_store_invalid_order_meta', __( 'This order is missing ebook delivery data.', 'ebook-store' ) );
6792 }
6793
6794 $ebook_id = absint( $order_meta['ebook'][0] );
6795 if ( $ebook_id < 1 ) {
6796 return new WP_Error( 'ebook_store_invalid_order_ebook', __( 'This order is not linked to a valid ebook.', 'ebook-store' ) );
6797 }
6798
6799 $QSWPOptions = new QSWPOptions();
6800 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_pdf', true );
6801 if ( $attachment_file === '' ) {
6802 // Fall back to a watermarked EPUB copy if one was generated for this order.
6803 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_epub', true );
6804 }
6805 if ( $attachment_file === '' || ! file_exists( $attachment_file ) ) {
6806 $master_attachment = ebook_attachment( $ebook_id, true );
6807 $attachment_file = isset( $master_attachment[0]['file'] ) ? (string) $master_attachment[0]['file'] : '';
6808 if ( $attachment_file !== '' && get_option( 'encrypt_pdf' ) && strtolower( pathinfo( $attachment_file, PATHINFO_EXTENSION ) ) === 'pdf' ) {
6809 // The per-order protected copy is missing (or stuck at 'in_progress'):
6810 // rebuild it instead of mailing the unprotected master file.
6811 global $attachment;
6812 $attachment = $master_attachment;
6813 $encrypt_data = array();
6814 foreach ( $order_meta as $meta_key => $meta_values ) {
6815 if ( isset( $meta_values[0] ) && is_scalar( $meta_values[0] ) ) {
6816 $encrypt_data[ $meta_key ] = $meta_values[0];
6817 }
6818 }
6819 $encrypt_data['order_id'] = $order_id;
6820 $encrypted_file = ebook_encrypt_pdf( $encrypt_data, $order_id );
6821 $attachment_file = ( is_string( $encrypted_file ) && $encrypted_file !== '' && file_exists( $encrypted_file ) ) ? $encrypted_file : '';
6822 }
6823 }
6824
6825 $order_meta['order_id'][0] = $order_id;
6826 $order_meta['downloadlink'][0] = ebook_download_link( $order_meta );
6827
6828 // The delivery-email template substitutes %%downloadlink_html%%. The checkout
6829 // path builds it, but the admin resend path (which rebuilds the order array
6830 // from post meta) did not — so a resent email showed the raw placeholder and
6831 // the buyer had no way to download. Build it here too.
6832 $delivery_ebook = new EbookStoreEbook( $ebook_id );
6833 // Plugin order: give link() the ebook_key credentials, otherwise the non-PDF
6834 // format links come out as wc_order_download URLs with no order key, which
6835 // the download endpoint (rightly) refuses.
6836 $delivery_ebook->ebook_key = isset( $order_meta['ebook_key'][0] ) ? (string) $order_meta['ebook_key'][0] : '';
6837 $delivery_ebook->md5_nonce = isset( $order_meta['md5_nonce'][0] ) ? (string) $order_meta['md5_nonce'][0] : '';
6838 $delivery_ebook->order_id = $order_id;
6839 $delivery_ebook->setLink['pdf'] = $order_meta['downloadlink'][0];
6840 $order_meta['downloadlink_html'][0] = $delivery_ebook->format_links();
6841
6842 $recipient = isset( $order_meta['payer_email'][0] ) ? sanitize_email( (string) $order_meta['payer_email'][0] ) : '';
6843 if ( $recipient === '' ) {
6844 return new WP_Error( 'ebook_store_missing_order_email', __( 'This order does not have a valid delivery email address.', 'ebook-store' ) );
6845 }
6846 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_id );
6847
6848 return array(
6849 'to' => $recipient,
6850 'subject' => $email_template['subject'],
6851 'text' => $email_template['text'],
6852 'attachment' => array(
6853 array(
6854 'file' => $attachment_file,
6855 ),
6856 ),
6857 'order' => $order_meta,
6858 'source' => 'admin_resend',
6859 'related_order_type' => 'ebook_order',
6860 );
6861 }
6862
6863 /**
6864 * Clear every download counter a WooCommerce order carries.
6865 *
6866 * Counts are per ebook ('downloads_<id>'), plus the order-wide 'downloads' key
6867 * older versions wrote, so all of them are removed.
6868 *
6869 * @param WC_Order $order Order object.
6870 * @return int Number of counters cleared.
6871 */
6872 function ebook_store_reset_wc_order_downloads( $order ) {
6873 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
6874 return 0;
6875 }
6876
6877 $cleared = 0;
6878 foreach ( $order->get_meta_data() as $meta ) {
6879 $meta_key = isset( $meta->key ) ? (string) $meta->key : '';
6880 if ( $meta_key === 'downloads' || strpos( $meta_key, 'downloads_' ) === 0 ) {
6881 $order->delete_meta_data( $meta_key );
6882 $cleared++;
6883 }
6884 }
6885
6886 if ( $cleared > 0 ) {
6887 $order->save();
6888 }
6889
6890 return $cleared;
6891 }
6892
6893 function ebook_store_reset_order_downloads() {
6894 if ( ! current_user_can( 'manage_options' ) ) {
6895 wp_die( esc_html__( 'You are not allowed to reset order downloads.', 'ebook-store' ) );
6896 }
6897
6898 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6899 if ( $order_id < 1 ) {
6900 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6901 }
6902
6903 // A WooCommerce order is not an 'ebook_order' post (and on HPOS is not a post
6904 // at all), so this action used to refuse every WooCommerce buyer.
6905 $wc_order = ( get_post_type( $order_id ) !== 'ebook_order' ) ? ebook_store_get_wc_order_object( $order_id ) : false;
6906
6907 if ( ! $wc_order && get_post_type( $order_id ) !== 'ebook_order' ) {
6908 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6909 }
6910
6911 check_admin_referer( 'ebook_store_reset_order_downloads_' . $order_id );
6912
6913 if ( $wc_order ) {
6914 ebook_store_reset_wc_order_downloads( $wc_order );
6915 } else {
6916 update_post_meta( $order_id, 'downloads', 0 );
6917 }
6918
6919 wp_safe_redirect( ebook_store_get_order_admin_list_url( 'downloads_reset' ) );
6920 exit;
6921 }
6922
6923 /**
6924 * Offer the reset on the WooCommerce order screen, where the buyer's ebook
6925 * downloads actually live.
6926 *
6927 * @param array $actions Existing order actions.
6928 * @return array
6929 */
6930 function ebook_store_wc_order_actions( $actions ) {
6931 $actions['ebook_store_reset_downloads'] = __( 'Reset Ebook Store downloads', 'ebook-store' );
6932
6933 return $actions;
6934 }
6935 add_filter( 'woocommerce_order_actions', 'ebook_store_wc_order_actions' );
6936
6937 /**
6938 * Run the reset chosen from the WooCommerce order screen. WooCommerce has
6939 * already checked the nonce and the capability by the time this fires.
6940 *
6941 * @param WC_Order $order Order object.
6942 */
6943 function ebook_store_wc_order_action_reset_downloads( $order ) {
6944 $cleared = ebook_store_reset_wc_order_downloads( $order );
6945
6946 if ( is_a( $order, 'WC_Order' ) ) {
6947 $order->add_order_note( __( 'Ebook Store download counters were reset.', 'ebook-store' ) );
6948 }
6949
6950 return $cleared;
6951 }
6952 add_action( 'woocommerce_order_action_ebook_store_reset_downloads', 'ebook_store_wc_order_action_reset_downloads' );
6953
6954 function ebook_store_resend_order_delivery() {
6955 if ( ! current_user_can( 'manage_options' ) ) {
6956 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
6957 }
6958
6959 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6960 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6961 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6962 }
6963
6964 check_admin_referer( 'ebook_store_resend_order_delivery_' . $order_id );
6965
6966 $email_context = ebook_store_prepare_order_delivery_email( $order_id );
6967 if ( is_wp_error( $email_context ) ) {
6968 wp_die( esc_html( $email_context->get_error_message() ) );
6969 }
6970
6971 global $ebook_email_delivery, $formData;
6972 $previous_email_delivery = isset( $ebook_email_delivery ) ? $ebook_email_delivery : null;
6973 $previous_form_data = isset( $formData ) ? $formData : null;
6974 $previous_email_delivery_setting = get_option( 'email_delivery', null );
6975
6976 $ebook_email_delivery = $email_context;
6977 unset( $ebook_email_delivery['done'] );
6978 // Restore the buyer's checkout form data (gift / Kindle addresses) for this order.
6979 $formData = (string) get_post_meta( $order_id, 'formData', true );
6980 update_option( 'email_delivery', 1 );
6981
6982 ob_start();
6983 ebook_email_delivery( $order_id );
6984 ob_end_clean();
6985
6986 $resend_succeeded = ! empty( $ebook_email_delivery['mail_result'] );
6987
6988 if ( null === $previous_email_delivery_setting ) {
6989 delete_option( 'email_delivery' );
6990 } else {
6991 update_option( 'email_delivery', $previous_email_delivery_setting );
6992 }
6993 $ebook_email_delivery = $previous_email_delivery;
6994 $formData = $previous_form_data;
6995
6996 wp_safe_redirect( ebook_store_get_order_admin_list_url( $resend_succeeded ? 'delivery_resent' : 'delivery_failed' ) );
6997 exit;
6998 }
6999
7000 function ebook_store_refund_order() {
7001 if ( ! current_user_can( 'manage_options' ) ) {
7002 wp_die( esc_html__( 'You are not allowed to refund ebook orders.', 'ebook-store' ) );
7003 }
7004
7005 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
7006 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
7007 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
7008 }
7009
7010 check_admin_referer( 'ebook_store_refund_order_' . $order_id );
7011
7012 $result = ebook_store_process_order_refund( $order_id );
7013 if ( is_wp_error( $result ) ) {
7014 wp_safe_redirect(
7015 ebook_store_get_order_admin_list_url(
7016 'refund_failed',
7017 array(
7018 'ebook_store_order_message' => $result->get_error_message(),
7019 )
7020 )
7021 );
7022 exit;
7023 }
7024
7025 $gateway_label = ucfirst( (string) $result['gateway'] );
7026 $amount_label = ebook_store_order_money_display(
7027 isset( $result['amount'] ) ? $result['amount'] : '',
7028 isset( $result['currency'] ) ? $result['currency'] : ''
7029 );
7030 $message = sprintf(
7031 /* translators: 1: payment gateway, 2: refunded amount */
7032 __( '%1$s refund created successfully for %2$s.', 'ebook-store' ),
7033 $gateway_label,
7034 $amount_label
7035 );
7036
7037 wp_safe_redirect(
7038 ebook_store_get_order_admin_list_url(
7039 'refund_succeeded',
7040 array(
7041 'ebook_store_order_message' => $message,
7042 )
7043 )
7044 );
7045 exit;
7046 }
7047
7048 function ebook_store_resend_email_log() {
7049 if ( ! current_user_can( 'manage_options' ) ) {
7050 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
7051 }
7052
7053 $log_id = isset( $_GET['log_id'] ) ? absint( wp_unslash( $_GET['log_id'] ) ) : 0;
7054 if ( $log_id < 1 || get_post_type( $log_id ) !== 'ebook_email_log' ) {
7055 wp_die( esc_html__( 'Invalid email log entry.', 'ebook-store' ) );
7056 }
7057
7058 check_admin_referer( 'ebook_store_resend_email_log_' . $log_id );
7059
7060 $recipient = (string) get_post_meta( $log_id, 'recipient', true );
7061 $subject = (string) get_post_meta( $log_id, 'subject', true );
7062 $body_html = (string) get_post_meta( $log_id, 'body_html', true );
7063 $headers = ebook_store_normalize_email_log_headers( get_post_meta( $log_id, 'headers', true ) );
7064 $attachments = ebook_store_normalize_email_log_attachments( get_post_meta( $log_id, 'attachments', true ) );
7065 $order_json = (string) get_post_meta( $log_id, 'order_snapshot', true );
7066 $context_json = (string) get_post_meta( $log_id, 'context_snapshot', true );
7067 $source = (string) get_post_meta( $log_id, 'source', true );
7068 $order = json_decode( $order_json, true );
7069 $context = json_decode( $context_json, true );
7070
7071 if ( ! is_array( $order ) ) {
7072 $order = array();
7073 }
7074
7075 if ( ! is_array( $context ) ) {
7076 $context = array();
7077 }
7078
7079 $attachments = array_values(
7080 array_filter(
7081 $attachments,
7082 static function ( $path ) {
7083 return is_string( $path ) && $path !== '' && file_exists( $path );
7084 }
7085 )
7086 );
7087
7088 $mail_result = wp_mail( $recipient, $subject, $body_html, $headers, $attachments );
7089
7090 $resend_count = (int) get_post_meta( $log_id, 'resend_count', true );
7091 update_post_meta( $log_id, 'resend_count', $resend_count + 1 );
7092 update_post_meta( $log_id, 'last_resend_at', current_time( 'mysql' ) );
7093 update_post_meta( $log_id, 'last_resend_status', $mail_result ? 'sent' : 'failed' );
7094
7095 $new_log_id = ebook_store_create_email_log_entry(
7096 array(
7097 'to' => $recipient,
7098 'subject' => $subject,
7099 'body' => $body_html,
7100 'headers' => $headers,
7101 'attachments' => $attachments,
7102 'result' => $mail_result,
7103 'source' => 'resend',
7104 'related_order_type' => get_post_meta( $log_id, 'related_order_type', true ),
7105 'related_order_id' => get_post_meta( $log_id, 'related_order_id', true ),
7106 'ebook_id' => get_post_meta( $log_id, 'ebook_id', true ),
7107 'ebook_key' => get_post_meta( $log_id, 'ebook_key', true ),
7108 'gateway' => get_post_meta( $log_id, 'gateway', true ),
7109 'order' => $order,
7110 'context' => array_merge(
7111 $context,
7112 array(
7113 'resend_of' => $log_id,
7114 'original_source' => $source,
7115 )
7116 ),
7117 'parent_log_id' => $log_id,
7118 )
7119 );
7120
7121 if ( $new_log_id > 0 ) {
7122 update_post_meta( $log_id, 'last_resend_log_id', $new_log_id );
7123 }
7124
7125 wp_safe_redirect(
7126 add_query_arg(
7127 array(
7128 'post' => $log_id,
7129 'action' => 'edit',
7130 'ebook_store_email_log_notice' => $mail_result ? 'resent' : 'resend_failed',
7131 ),
7132 admin_url( 'post.php' )
7133 )
7134 );
7135 exit;
7136 }
7137
7138 add_filter( 'manage_edit-ebook_email_log_columns', 'ebook_store_set_email_log_columns' );
7139 add_action( 'manage_ebook_email_log_posts_custom_column', 'ebook_store_email_log_columns_output', 10, 2 );
7140 add_filter( 'post_row_actions', 'ebook_store_email_log_row_actions', 10, 2 );
7141 add_action( 'add_meta_boxes', 'ebook_store_remove_email_log_submit_box', 20 );
7142 add_action( 'admin_menu', 'ebook_store_cleanup_email_log_submenu', 999 );
7143 add_action( 'admin_init', 'ebook_store_redirect_new_email_log_entry' );
7144 add_action( 'admin_head', 'ebook_store_render_email_log_admin_styles' );
7145 add_action( 'admin_notices', 'ebook_store_email_log_admin_notice' );
7146 add_action( 'admin_notices', 'ebook_store_order_admin_notice' );
7147 add_action( 'admin_post_ebook_store_resend_email_log', 'ebook_store_resend_email_log' );
7148 add_action( 'admin_post_ebook_store_reset_order_downloads', 'ebook_store_reset_order_downloads' );
7149 add_action( 'admin_post_ebook_store_resend_order_delivery', 'ebook_store_resend_order_delivery' );
7150 add_action( 'admin_post_ebook_store_refund_order', 'ebook_store_refund_order' );
7151
7152 function ebook_store_get_order_verification_url( $ebook_key ) {
7153 $ebook_key = preg_replace( '/[^a-zA-Z0-9]+/', '', (string) $ebook_key );
7154
7155 return add_query_arg(
7156 array(
7157 'ebook_action' => 'verify_order',
7158 'ebook_key' => $ebook_key,
7159 ),
7160 home_url( '/' )
7161 );
7162 }
7163
7164 /**
7165 * The QR-verification key used for a Test-button copy, so the verification page
7166 * can recognise it as a test (there is no real order behind it) instead of
7167 * showing "record not found". Kept uppercase so it can never collide with a real
7168 * order's md5 (lowercase-hex) key.
7169 *
7170 * @param int $ebook_id Ebook post ID.
7171 * @return string
7172 */
7173 function ebook_store_test_verification_key( $ebook_id ) {
7174 return 'EBOOKSTORETEST' . (int) $ebook_id;
7175 }
7176
7177 /**
7178 * If a verification key is a Test-button key, return the ebook ID it was made
7179 * from; otherwise false.
7180 *
7181 * @param string $key Sanitised (alphanumeric) verification key.
7182 * @return int|false
7183 */
7184 function ebook_store_parse_test_verification_key( $key ) {
7185 $prefix = 'EBOOKSTORETEST';
7186 if ( strpos( (string) $key, $prefix ) !== 0 ) {
7187 return false;
7188 }
7189 return (int) substr( (string) $key, strlen( $prefix ) );
7190 }
7191
7192 function ebook_store_render_public_order_field( $label, $value ) {
7193 if ( $value === '' || $value === null ) {
7194 return '';
7195 }
7196
7197 return '<div class="ebook-store-verify__field"><span>' . esc_html( $label ) . '</span><strong>' . esc_html( $value ) . '</strong></div>';
7198 }
7199
7200 function ebook_store_render_public_order_verification_page() {
7201 if ( ! isset( $_GET['ebook_action'] ) || sanitize_text_field( wp_unslash( $_GET['ebook_action'] ) ) !== 'verify_order' ) {
7202 return;
7203 }
7204
7205 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]+/', '', wp_unslash( $_GET['ebook_key'] ) ) : '';
7206
7207 // A QR scanned from a Test-button copy carries no real order, so recognise it
7208 // and show a "test ebook" confirmation rather than a scary "not found".
7209 $test_ebook_id = ebook_store_parse_test_verification_key( $ebook_key );
7210 $is_test = ( $test_ebook_id !== false );
7211
7212 $wc_order = false;
7213 if ( $is_test ) {
7214 $order = false;
7215 } elseif ( $ebook_key === '' ) {
7216 $order = false;
7217 } else {
7218 $order = ebook_get_order( 'ebook_key', $ebook_key );
7219 if ( ! $order && get_option( 'stripe_integration_enabled', 0 ) ) {
7220 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key( $ebook_key );
7221 if ( ! is_wp_error( $recovered_order_id ) && $recovered_order_id ) {
7222 $order = ebook_get_order( 'ebook_key', $ebook_key );
7223 }
7224 }
7225 if ( ! $order && function_exists( 'wc_get_orders' ) ) {
7226 // WooCommerce copies carry the key on the order itself; wc_get_orders()
7227 // finds it whether orders live in wp_posts or the HPOS tables.
7228 $wc_orders = wc_get_orders( array( 'meta_key' => 'ebook_key', 'meta_value' => $ebook_key, 'limit' => 1 ) );
7229 if ( ! empty( $wc_orders ) && is_a( $wc_orders[0], 'WC_Order' ) ) {
7230 $wc_order = $wc_orders[0];
7231 }
7232 }
7233 }
7234
7235 $found_order = is_array( $order ) && ! empty( $order['ebook'][0] );
7236 $found_wc_order = ( $wc_order && is_a( $wc_order, 'WC_Order' ) );
7237 status_header( ( $is_test || $found_order || $found_wc_order ) ? 200 : 404 );
7238 nocache_headers();
7239
7240 $site_name = wp_strip_all_tags( get_bloginfo( 'name' ) );
7241 if ( $is_test ) {
7242 $page_title = __( 'Test Copy', 'ebook-store' );
7243 $status_label = __( 'Test copy', 'ebook-store' );
7244 $status_class = 'is-test';
7245 } else {
7246 $page_title = ( $found_order || $found_wc_order ) ? __( 'Ebook Copy Verification', 'ebook-store' ) : __( 'Verification Record Not Found', 'ebook-store' );
7247 if ( $found_wc_order ) {
7248 $status_label = wc_get_order_status_name( $wc_order->get_status() );
7249 } else {
7250 $status_label = $found_order
7251 ? ( ! empty( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : __( 'Completed', 'ebook-store' ) )
7252 : __( 'Missing', 'ebook-store' );
7253 }
7254 $status_class = ( $found_order || $found_wc_order ) ? 'is-valid' : 'is-missing';
7255 }
7256
7257 $content = '<!DOCTYPE html><html ' . get_language_attributes() . '><head><meta charset="' . esc_attr( get_bloginfo( 'charset' ) ) . '" /><meta name="viewport" content="width=device-width, initial-scale=1" /><meta name="robots" content="noindex,nofollow" /><title>' . esc_html( $page_title . ' - ' . $site_name ) . '</title><style>
7258 :root{--ebook-verify-bg:#f5f7f2;--ebook-verify-card:#ffffff;--ebook-verify-ink:#163326;--ebook-verify-muted:#617565;--ebook-verify-border:#d8e3d6;--ebook-verify-green:#1b8f4d;--ebook-verify-red:#b33a2f;--ebook-verify-red-soft:#fff1ef;--ebook-verify-green-soft:#eefaf2;--ebook-verify-shadow:0 24px 60px rgba(14,36,24,.08)}
7259 *{box-sizing:border-box}body{margin:0;font-family:Georgia,\"Times New Roman\",serif;background:radial-gradient(circle at top,#ffffff 0,#f5f7f2 45%,#edf2ea 100%);color:var(--ebook-verify-ink)}
7260 a{color:inherit} .ebook-store-verify{max-width:980px;margin:0 auto;padding:32px 20px 56px}
7261 .ebook-store-verify__hero{background:var(--ebook-verify-card);border:1px solid var(--ebook-verify-border);border-radius:28px;padding:28px;box-shadow:var(--ebook-verify-shadow);position:relative;overflow:hidden}
7262 .ebook-store-verify__hero:before{content:\"\";position:absolute;inset:auto -80px -120px auto;width:240px;height:240px;border-radius:999px;background:linear-gradient(135deg,rgba(27,143,77,.12),rgba(27,143,77,0))}
7263 .ebook-store-verify__eyebrow{display:inline-block;font:600 12px/1.2 Arial,sans-serif;letter-spacing:.14em;text-transform:uppercase;color:var(--ebook-verify-muted);margin-bottom:14px}
7264 .ebook-store-verify__hero h1{margin:0 0 10px;font-size:clamp(2rem,4vw,3.5rem);line-height:.98}
7265 .ebook-store-verify__hero p{margin:0;max-width:62ch;font:400 16px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
7266 .ebook-store-verify__status{display:inline-flex;align-items:center;gap:10px;margin-top:18px;padding:10px 14px;border-radius:999px;font:700 13px/1 Arial,sans-serif;letter-spacing:.04em;text-transform:uppercase}
7267 .ebook-store-verify__status.is-valid{background:var(--ebook-verify-green-soft);color:var(--ebook-verify-green)}
7268 .ebook-store-verify__status.is-missing{background:var(--ebook-verify-red-soft);color:var(--ebook-verify-red)}
7269 .ebook-store-verify__status.is-test{background:#fbf1de;color:#7a4d00}
7270 .ebook-store-verify__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-top:20px}
7271 .ebook-store-verify__card{background:var(--ebook-verify-card);border:1px solid var(--ebook-verify-border);border-radius:24px;padding:22px;box-shadow:var(--ebook-verify-shadow)}
7272 .ebook-store-verify__card h2{margin:0 0 14px;font-size:1.1rem}
7273 .ebook-store-verify__field{display:flex;justify-content:space-between;gap:18px;padding:12px 0;border-top:1px solid #edf1eb}
7274 .ebook-store-verify__field:first-child{border-top:0;padding-top:0}
7275 .ebook-store-verify__field span{font:600 12px/1.3 Arial,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--ebook-verify-muted)}
7276 .ebook-store-verify__field strong{font:600 15px/1.4 Arial,sans-serif;text-align:right;max-width:65%}
7277 .ebook-store-verify__notice{margin-top:20px;background:var(--ebook-verify-card);border:1px dashed var(--ebook-verify-border);border-radius:22px;padding:18px 20px;font:400 15px/1.7 Arial,sans-serif;color:var(--ebook-verify-muted)}
7278 .ebook-store-verify__footer{margin-top:16px;font:400 13px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
7279 @media (max-width:780px){.ebook-store-verify__grid{grid-template-columns:1fr}.ebook-store-verify__field{flex-direction:column;align-items:flex-start}.ebook-store-verify__field strong{max-width:none;text-align:left}}
7280 </style></head><body><main class="ebook-store-verify"><section class="ebook-store-verify__hero"><span class="ebook-store-verify__eyebrow">' . esc_html__( 'Ebook Watermark Verification', 'ebook-store' ) . '</span><h1>' . esc_html( $page_title ) . '</h1><p>' . esc_html__( 'This page is generated from the QR code embedded inside a protected ebook. It helps identify the original transaction and buyer if a copy has been shared or redistributed without permission.', 'ebook-store' ) . '</p><div class="ebook-store-verify__status ' . esc_attr( $status_class ) . '">' . esc_html( $status_label ) . '</div></section>';
7281
7282 if ( $is_test ) {
7283 $test_title = $test_ebook_id ? get_the_title( $test_ebook_id ) : '';
7284 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'This is a test ebook.', 'ebook-store' ) . '</strong> ';
7285 $content .= esc_html__( 'You are verifying a copy generated with the Test button in the store admin, not a real purchase.', 'ebook-store' );
7286 if ( $test_title !== '' ) {
7287 $content .= ' ' . esc_html(
7288 sprintf(
7289 /* translators: 1: ebook title, 2: ebook ID. */
7290 __( 'It was made from "%1$s" (ebook #%2$d).', 'ebook-store' ),
7291 $test_title,
7292 $test_ebook_id
7293 )
7294 );
7295 }
7296 $content .= ' ' . esc_html__( 'No buyer or order is recorded for a test copy. On a real customer\'s copy, this same code opens the full order and buyer details that the watermark traces back to.', 'ebook-store' ) . '</section>';
7297 } elseif ( $found_order ) {
7298 $ebook_id = isset( $order['ebook'][0] ) ? (int) $order['ebook'][0] : 0;
7299 $ebook_title = $ebook_id ? get_the_title( $ebook_id ) : '';
7300 $order_id = isset( $order['order_id'][0] ) ? (int) $order['order_id'][0] : 0;
7301 $buyer_name = trim( ( isset( $order['first_name'][0] ) ? $order['first_name'][0] : '' ) . ' ' . ( isset( $order['last_name'][0] ) ? $order['last_name'][0] : '' ) );
7302 $payer_email = isset( $order['payer_email'][0] ) ? (string) $order['payer_email'][0] : '';
7303 $payment_date = isset( $order['payment_date'][0] ) ? (string) $order['payment_date'][0] : '';
7304 $payment_type = isset( $order['payment_type'][0] ) ? (string) $order['payment_type'][0] : '';
7305 $payment_status = isset( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : '';
7306 $txn_id = isset( $order['txn_id'][0] ) ? (string) $order['txn_id'][0] : '';
7307 $gross = isset( $order['mc_gross'][0] ) ? (string) $order['mc_gross'][0] : '';
7308 $currency = isset( $order['mc_currency'][0] ) && $order['mc_currency'][0] !== '' ? (string) $order['mc_currency'][0] : ebook_store_get_store_currency();
7309 $country = isset( $order['address_country'][0] ) && $order['address_country'][0] !== '' ? (string) $order['address_country'][0] : ( isset( $order['residence_country'][0] ) ? (string) $order['residence_country'][0] : '' );
7310 $city = isset( $order['address_city'][0] ) ? (string) $order['address_city'][0] : '';
7311 $state = isset( $order['address_state'][0] ) ? (string) $order['address_state'][0] : '';
7312 $zip = isset( $order['address_zip'][0] ) ? (string) $order['address_zip'][0] : '';
7313 $address_street = isset( $order['address_street'][0] ) ? (string) $order['address_street'][0] : '';
7314
7315 $content .= '<section class="ebook-store-verify__grid">';
7316 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Order Record', 'ebook-store' ) . '</h2>';
7317 $content .= ebook_store_render_public_order_field( __( 'Order ID', 'ebook-store' ), $order_id ? '#' . $order_id : '' );
7318 $content .= ebook_store_render_public_order_field( __( 'Ebook', 'ebook-store' ), $ebook_title );
7319 $content .= ebook_store_render_public_order_field( __( 'Ebook ID', 'ebook-store' ), $ebook_id ? (string) $ebook_id : '' );
7320 $content .= ebook_store_render_public_order_field( __( 'Payment date', 'ebook-store' ), $payment_date );
7321 $content .= ebook_store_render_public_order_field( __( 'Gateway', 'ebook-store' ), $payment_type !== '' ? $payment_type : __( 'PayPal', 'ebook-store' ) );
7322 $content .= ebook_store_render_public_order_field( __( 'Status', 'ebook-store' ), $payment_status );
7323 $content .= ebook_store_render_public_order_field( __( 'Transaction', 'ebook-store' ), $txn_id );
7324 $content .= ebook_store_render_public_order_field( __( 'Amount', 'ebook-store' ), trim( $gross . ' ' . $currency ) );
7325 $content .= '</article>';
7326 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Buyer Details', 'ebook-store' ) . '</h2>';
7327 $content .= ebook_store_render_public_order_field( __( 'Name', 'ebook-store' ), $buyer_name );
7328 $content .= ebook_store_render_public_order_field( __( 'Email', 'ebook-store' ), $payer_email );
7329 $content .= ebook_store_render_public_order_field( __( 'Street', 'ebook-store' ), $address_street );
7330 $content .= ebook_store_render_public_order_field( __( 'City', 'ebook-store' ), $city );
7331 $content .= ebook_store_render_public_order_field( __( 'State', 'ebook-store' ), $state );
7332 $content .= ebook_store_render_public_order_field( __( 'Postal code', 'ebook-store' ), $zip );
7333 $content .= ebook_store_render_public_order_field( __( 'Country', 'ebook-store' ), $country );
7334 $content .= ebook_store_render_public_order_field( __( 'Order key', 'ebook-store' ), $ebook_key );
7335 $content .= '</article>';
7336 $content .= '</section>';
7337 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'Traceability note:', 'ebook-store' ) . '</strong> ' . esc_html__( 'If this PDF was found on a third-party site or shared without permission, the order record above identifies the original licensed copy that the watermark was generated for.', 'ebook-store' ) . '</section>';
7338 } elseif ( $found_wc_order ) {
7339 $wc_titles = array();
7340 foreach ( $wc_order->get_items() as $wc_item ) {
7341 $wc_mapping = ebook_store_get_wc_item_ebook_mapping( $wc_item );
7342 $wc_ebook_id = isset( $wc_mapping['ebook_id'] ) ? (int) $wc_mapping['ebook_id'] : 0;
7343 if ( $wc_ebook_id > 0 ) {
7344 $wc_titles[] = get_the_title( $wc_ebook_id ) . ' (#' . $wc_ebook_id . ')';
7345 }
7346 }
7347 $wc_paid_date = $wc_order->get_date_paid();
7348 if ( ! $wc_paid_date ) {
7349 $wc_paid_date = $wc_order->get_date_created();
7350 }
7351 $wc_gateway = (string) $wc_order->get_payment_method_title();
7352
7353 $content .= '<section class="ebook-store-verify__grid">';
7354 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Order Record', 'ebook-store' ) . '</h2>';
7355 $content .= ebook_store_render_public_order_field( __( 'Order ID', 'ebook-store' ), '#' . $wc_order->get_id() );
7356 $content .= ebook_store_render_public_order_field( __( 'Ebook', 'ebook-store' ), implode( ', ', $wc_titles ) );
7357 $content .= ebook_store_render_public_order_field( __( 'Payment date', 'ebook-store' ), $wc_paid_date ? $wc_paid_date->date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ) ) : '' );
7358 $content .= ebook_store_render_public_order_field( __( 'Gateway', 'ebook-store' ), $wc_gateway !== '' ? $wc_gateway : __( 'WooCommerce', 'ebook-store' ) );
7359 $content .= ebook_store_render_public_order_field( __( 'Status', 'ebook-store' ), wc_get_order_status_name( $wc_order->get_status() ) );
7360 $content .= ebook_store_render_public_order_field( __( 'Transaction', 'ebook-store' ), (string) $wc_order->get_transaction_id() );
7361 $content .= ebook_store_render_public_order_field( __( 'Amount', 'ebook-store' ), trim( $wc_order->get_total() . ' ' . $wc_order->get_currency() ) );
7362 $content .= '</article>';
7363 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Buyer Details', 'ebook-store' ) . '</h2>';
7364 $content .= ebook_store_render_public_order_field( __( 'Name', 'ebook-store' ), trim( (string) $wc_order->get_formatted_billing_full_name() ) );
7365 $content .= ebook_store_render_public_order_field( __( 'Email', 'ebook-store' ), (string) $wc_order->get_billing_email() );
7366 $content .= ebook_store_render_public_order_field( __( 'Street', 'ebook-store' ), (string) $wc_order->get_billing_address_1() );
7367 $content .= ebook_store_render_public_order_field( __( 'City', 'ebook-store' ), (string) $wc_order->get_billing_city() );
7368 $content .= ebook_store_render_public_order_field( __( 'State', 'ebook-store' ), (string) $wc_order->get_billing_state() );
7369 $content .= ebook_store_render_public_order_field( __( 'Postal code', 'ebook-store' ), (string) $wc_order->get_billing_postcode() );
7370 $content .= ebook_store_render_public_order_field( __( 'Country', 'ebook-store' ), (string) $wc_order->get_billing_country() );
7371 $content .= ebook_store_render_public_order_field( __( 'Order key', 'ebook-store' ), $ebook_key );
7372 $content .= '</article>';
7373 $content .= '</section>';
7374 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'Traceability note:', 'ebook-store' ) . '</strong> ' . esc_html__( 'If this PDF was found on a third-party site or shared without permission, the order record above identifies the original licensed copy that the watermark was generated for.', 'ebook-store' ) . '</section>';
7375 } else {
7376 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'No active order was found for this QR code.', 'ebook-store' ) . '</strong> ' . esc_html__( 'The linked order may have been deleted, the key may be invalid, or the record is not available on this site.', 'ebook-store' ) . '</section>';
7377 }
7378
7379 $content .= '<div class="ebook-store-verify__footer">' . esc_html( $site_name ) . '</div></main></body></html>';
7380
7381 echo $content;
7382 exit;
7383 }
7384
7385 /**
7386 * Clean a string that is about to be drawn into a PDF watermark.
7387 *
7388 * PDF text is NOT HTML, so esc_attr()/esc_html() are wrong here — they turn
7389 * "Tom & Jerry" into "Tom &amp; Jerry" inside the document. Just strip control
7390 * characters and collapse whitespace.
7391 *
7392 * @param string $text Raw text (may contain %%merge%% codes before substitution).
7393 * @return string
7394 */
7395 function ebook_store_sanitize_pdf_text( $text ) {
7396 $text = (string) $text;
7397 // Drop ASCII control characters except tab/newline, then trim.
7398 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text );
7399
7400 return trim( (string) $text );
7401 }
7402
7403 /**
7404 * Resolve the buyer watermark text for an order, substituting the %%merge%%
7405 * codes. Shared by the PDF and EPUB watermark paths so the tokens resolve the
7406 * same way: WooCommerce-style meta first, then the built-in order meta.
7407 *
7408 * @param int $order_id Order post ID.
7409 * @param string $template Raw template, e.g. "Licensed to %%first_name%% %%last_name%%".
7410 * @return string Resolved, control-char-stripped text (NOT HTML-escaped).
7411 */
7412 function ebook_store_resolve_buyer_watermark( $order_id, $template ) {
7413 $order_id = (int) $order_id;
7414 $meta = $order_id > 0 ? get_post_meta( $order_id ) : array();
7415 $meta = is_array( $meta ) ? $meta : array();
7416 $mv = function ( $key ) use ( $meta ) {
7417 return isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
7418 };
7419
7420 $tokens = array(
7421 'payer_email' => $mv( '_billing_email' ),
7422 'payment_date' => $mv( '_paid_date' ),
7423 'txn_id' => $mv( '_transaction_id' ),
7424 'first_name' => $mv( '_billing_first_name' ),
7425 'last_name' => $mv( '_billing_last_name' ),
7426 'residence_country' => $mv( '_billing_country' ),
7427 );
7428
7429 // Built-in (non-Woo) orders store the buyer under their own keys. Read each
7430 // field from its own key (an earlier copy of this logic took first_name from
7431 // the last_name meta — fixed here).
7432 if ( $tokens['payer_email'] === '' ) {
7433 $tokens['payer_email'] = $mv( 'payer_email' );
7434 $tokens['payment_date'] = $mv( 'payment_date' );
7435 $tokens['txn_id'] = $mv( 'txn_id' );
7436 $tokens['first_name'] = $mv( 'first_name' );
7437 $tokens['last_name'] = $mv( 'last_name' );
7438 $tokens['residence_country'] = $mv( 'residence_country' );
7439 }
7440
7441 // Under HPOS a WooCommerce order has no post meta at all, so read the buyer
7442 // through the order object when both meta lookups came back empty.
7443 if ( $tokens['payer_email'] === '' && $order_id > 0 && function_exists( 'wc_get_order' ) ) {
7444 $wc_order = wc_get_order( $order_id );
7445 if ( $wc_order && is_a( $wc_order, 'WC_Order' ) ) {
7446 $wc_paid_date = $wc_order->get_date_paid();
7447 if ( ! $wc_paid_date ) {
7448 $wc_paid_date = $wc_order->get_date_created();
7449 }
7450 $tokens['payer_email'] = (string) $wc_order->get_billing_email();
7451 $tokens['payment_date'] = $wc_paid_date ? $wc_paid_date->date_i18n( 'Y-m-d H:i:s' ) : '';
7452 $tokens['txn_id'] = (string) $wc_order->get_transaction_id();
7453 $tokens['first_name'] = (string) $wc_order->get_billing_first_name();
7454 $tokens['last_name'] = (string) $wc_order->get_billing_last_name();
7455 $tokens['residence_country'] = (string) $wc_order->get_billing_country();
7456 }
7457 }
7458
7459 $text = ebook_store_sanitize_pdf_text( (string) $template );
7460 foreach ( $tokens as $key => $value ) {
7461 if ( is_scalar( $value ) ) {
7462 $text = str_replace( '%%' . $key . '%%', ebook_store_sanitize_pdf_text( (string) $value ), $text );
7463 }
7464 }
7465
7466 return $text;
7467 }
7468
7469 /**
7470 * Build the visible watermark HTML fragment injected into EPUB content.
7471 *
7472 * Inline CSS only, so no stylesheet or manifest edit is needed. dir="auto" lets
7473 * the reader lay the buyer line out right-to-left for Hebrew/Arabic names.
7474 *
7475 * @param string $text Buyer text (raw, not yet escaped).
7476 * @param string $style 'subtle' | 'box'.
7477 * @return string XML-well-formed XHTML fragment.
7478 */
7479 function ebook_store_epub_watermark_fragment( $text, $style ) {
7480 $wm = htmlspecialchars( (string) $text, ENT_QUOTES, 'UTF-8' );
7481 if ( $style === 'box' ) {
7482 $css = 'margin:1.5em 0;padding:.6em .8em;border:1px solid rgba(0,0,0,.35);border-radius:3px;font-size:.8em;line-height:1.4;text-align:center;font-family:serif';
7483 } else {
7484 $css = 'margin:1.5em 0;padding:.4em 0;border-top:1px solid rgba(0,0,0,.15);font-size:.75em;line-height:1.4;opacity:.65;text-align:center;font-family:serif';
7485 }
7486
7487 return '<div class="ebook-store-wm" dir="auto" style="' . $css . '">' . $wm . '</div>';
7488 }
7489
7490 /**
7491 * Insert an XHTML fragment into a content document via string ops, NOT DOM.
7492 *
7493 * Real content XHTML routinely contains named entities like &nbsp; that a strict
7494 * DOM parser rejects, so we never DOM-parse content documents — we splice the
7495 * fragment next to the <body> tag and leave everything else byte-for-byte intact.
7496 *
7497 * @param string $html The XHTML document.
7498 * @param string $fragment The fragment to insert.
7499 * @param string $position 'top' | 'bottom'.
7500 * @return string|null Modified document, or null if no <body> boundary was found.
7501 */
7502 function ebook_store_epub_inject_fragment( $html, $fragment, $position ) {
7503 $html = (string) $html;
7504
7505 if ( $position === 'top' ) {
7506 if ( preg_match( '/<body\b[^>]*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
7507 $at = $m[0][1] + strlen( $m[0][0] );
7508 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
7509 }
7510 return null;
7511 }
7512
7513 if ( preg_match( '/<\/body\s*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
7514 $at = $m[0][1];
7515 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
7516 }
7517
7518 return null;
7519 }
7520
7521 /**
7522 * Relative path from a directory inside the EPUB to a file inside it, so an
7523 * <img> in a content document (or a manifest href) can point at the QR image
7524 * wherever the two happen to sit in the archive.
7525 *
7526 * @param string $from_dir Directory of the referencing file (archive path).
7527 * @param string $to_path Target file (archive path).
7528 * @return string Relative reference.
7529 */
7530 function ebook_store_epub_relative_path( $from_dir, $to_path ) {
7531 $from_dir = ( $from_dir === '.' || $from_dir === '/' ) ? '' : trim( (string) $from_dir, '/' );
7532 $to_path = ltrim( (string) $to_path, '/' );
7533 $from = $from_dir === '' ? array() : explode( '/', $from_dir );
7534 $to = explode( '/', $to_path );
7535 while ( ! empty( $from ) && ! empty( $to ) && $from[0] === $to[0] ) {
7536 array_shift( $from );
7537 array_shift( $to );
7538 }
7539 $rel = str_repeat( '../', count( $from ) ) . implode( '/', $to );
7540 return $rel === '' ? basename( $to_path ) : $rel;
7541 }
7542
7543 /**
7544 * Render a QR code to PNG bytes on a solid white background with a quiet zone,
7545 * so it stays scannable on any reader background (including night mode).
7546 *
7547 * Uses the bundled TCPDF 2D-barcode generator, loaded narrowly so the EPUB path
7548 * never pulls in the full PDF engine. Returns '' on any problem (fail-open: the
7549 * book is still delivered, just without a QR).
7550 *
7551 * @param string $text The URL/text to encode.
7552 * @param int $module_px Pixels per QR module.
7553 * @param int $quiet Quiet-zone width in modules.
7554 * @return string PNG bytes, or '' if a QR could not be produced.
7555 */
7556 function ebook_store_generate_qr_png( $text, $module_px = 8, $quiet = 4 ) {
7557 $text = (string) $text;
7558 if ( $text === '' || ! function_exists( 'imagecreatetruecolor' ) || ! function_exists( 'imagepng' ) ) {
7559 return '';
7560 }
7561
7562 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
7563 $base = __DIR__ . '/fpdi/vendor/tecnickcom/tcpdf';
7564 $qr = $base . '/include/barcodes/qrcode.php';
7565 $bc = $base . '/tcpdf_barcodes_2d.php';
7566 if ( is_readable( $qr ) && is_readable( $bc ) ) {
7567 require_once $qr;
7568 require_once $bc;
7569 }
7570 }
7571 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
7572 return '';
7573 }
7574
7575 try {
7576 $obj = new TCPDF2DBarcode( $text, 'QRCODE,M' );
7577 $arr = $obj->getBarcodeArray();
7578 if ( empty( $arr['num_rows'] ) || empty( $arr['num_cols'] ) || empty( $arr['bcode'] ) ) {
7579 return '';
7580 }
7581 $rows = (int) $arr['num_rows'];
7582 $cols = (int) $arr['num_cols'];
7583 $w = ( $cols + 2 * $quiet ) * $module_px;
7584 $h = ( $rows + 2 * $quiet ) * $module_px;
7585
7586 $img = imagecreatetruecolor( $w, $h );
7587 $white = imagecolorallocate( $img, 255, 255, 255 );
7588 $black = imagecolorallocate( $img, 0, 0, 0 );
7589 imagefilledrectangle( $img, 0, 0, $w - 1, $h - 1, $white );
7590 for ( $r = 0; $r < $rows; $r++ ) {
7591 for ( $c = 0; $c < $cols; $c++ ) {
7592 if ( ! empty( $arr['bcode'][ $r ][ $c ] ) ) {
7593 $x0 = ( $c + $quiet ) * $module_px;
7594 $y0 = ( $r + $quiet ) * $module_px;
7595 imagefilledrectangle( $img, $x0, $y0, $x0 + $module_px - 1, $y0 + $module_px - 1, $black );
7596 }
7597 }
7598 }
7599 ob_start();
7600 imagepng( $img );
7601 $data = ob_get_clean();
7602 imagedestroy( $img );
7603
7604 return is_string( $data ) ? $data : '';
7605 } catch ( \Throwable $e ) {
7606 return '';
7607 }
7608 }
7609
7610 /**
7611 * The XHTML fragment that shows the QR image, pointing at a relative src.
7612 *
7613 * @param string $src Relative image reference from the referencing document.
7614 * @return string Well-formed XHTML fragment.
7615 */
7616 function ebook_store_epub_qr_fragment( $src ) {
7617 $src = htmlspecialchars( (string) $src, ENT_QUOTES, 'UTF-8' );
7618 $alt = htmlspecialchars( __( 'Scan to verify this copy', 'ebook-store' ), ENT_QUOTES, 'UTF-8' );
7619 return '<div class="ebook-store-qr" style="text-align:center;margin:1.2em 0">'
7620 . '<img src="' . $src . '" alt="' . $alt . '" style="width:120px;height:120px;max-width:40%"/></div>';
7621 }
7622
7623 /**
7624 * Watermark an EPUB with the buyer's identity (social DRM) while keeping it a
7625 * valid, reader-compatible book.
7626 *
7627 * EPUB cannot be password-encrypted and still open in e-readers, so instead the
7628 * buyer line is injected into the content and (optionally) the OPF metadata, and
7629 * the archive is rewritten with "mimetype" kept FIRST and STORED. This is a plain
7630 * function that never loads the PDF stack, and it FAILS OPEN: on any problem it
7631 * returns the untouched $src_path so a paid download is never blocked. It never
7632 * throws.
7633 *
7634 * @param string $src_path Absolute path to the source .epub.
7635 * @param string $dest_path Absolute path to write the watermarked copy to.
7636 * @param string $watermark_text Buyer text, already merge-resolved (raw, not escaped).
7637 * @param array $opts scope|position|style|stamp_metadata|order_id.
7638 * @return string $dest_path on success, else $src_path.
7639 */
7640 function ebook_store_watermark_epub( $src_path, $dest_path, $watermark_text, $opts = array() ) {
7641 if ( ! class_exists( 'ZipArchive' ) || ! class_exists( 'DOMDocument' ) || ! is_readable( $src_path ) ) {
7642 return $src_path;
7643 }
7644
7645 $log = function ( $message, $context = array() ) {
7646 if ( function_exists( 'ebook_store_log' ) ) {
7647 ebook_store_log( 'epub', $message, $context );
7648 }
7649 };
7650
7651 // The archive is held in memory ~twice while repacking, and an OOM is
7652 // uncatchable, so refuse very large books rather than 500 the download.
7653 $max = (int) apply_filters( 'ebook_store_epub_watermark_max_bytes', 64 * 1024 * 1024 );
7654 $size = @filesize( $src_path );
7655 if ( $size !== false && $size > $max ) {
7656 $log( 'too large, serving source', array( 'bytes' => $size ) );
7657 return $src_path;
7658 }
7659 if ( function_exists( 'ebook_store_raise_pdf_memory' ) ) {
7660 ebook_store_raise_pdf_memory();
7661 }
7662
7663 try {
7664 $scope = in_array( isset( $opts['scope'] ) ? $opts['scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['scope'] : 'first';
7665 $position = in_array( isset( $opts['position'] ) ? $opts['position'] : '', array( 'top', 'bottom' ), true ) ? $opts['position'] : 'bottom';
7666 $style = in_array( isset( $opts['style'] ) ? $opts['style'] : '', array( 'subtle', 'box' ), true ) ? $opts['style'] : 'subtle';
7667 $stamp = ! empty( $opts['stamp_metadata'] );
7668 $order_id = isset( $opts['order_id'] ) ? (int) $opts['order_id'] : 0;
7669
7670 // QR code carries its own placement so the buyer can put it where they like.
7671 $qr_scope = in_array( isset( $opts['qr_scope'] ) ? $opts['qr_scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['qr_scope'] : 'first';
7672 $qr_position = in_array( isset( $opts['qr_position'] ) ? $opts['qr_position'] : '', array( 'top', 'bottom' ), true ) ? $opts['qr_position'] : 'bottom';
7673 $qr_text = isset( $opts['qr_text'] ) ? trim( (string) $opts['qr_text'] ) : '';
7674 $qr_enabled = ! empty( $opts['qr'] ) && $qr_text !== '';
7675
7676 $text = ebook_store_sanitize_pdf_text( (string) $watermark_text );
7677 if ( $text === '' && ! $qr_enabled && ! $stamp ) {
7678 return $src_path; // nothing to add
7679 }
7680
7681 // --- STEP 1: read the whole archive into memory, then close it. ---
7682 $in = new ZipArchive();
7683 if ( $in->open( $src_path ) !== true ) {
7684 return $src_path;
7685 }
7686 $entries = array();
7687 $order = array();
7688 for ( $i = 0; $i < $in->numFiles; $i++ ) {
7689 $stat = $in->statIndex( $i );
7690 if ( $stat === false ) {
7691 $in->close();
7692 return $src_path;
7693 }
7694 $name = $stat['name'];
7695 if ( $name === '' || substr( $name, -1 ) === '/' ) {
7696 continue; // directory marker
7697 }
7698 $content = $in->getFromIndex( $i );
7699 if ( $content === false ) {
7700 $in->close();
7701 return $src_path; // corrupt entry: never ship a hollow book
7702 }
7703 if ( ! isset( $entries[ $name ] ) ) {
7704 $order[] = $name;
7705 }
7706 $entries[ $name ] = $content;
7707 }
7708 $in->close();
7709
7710 if ( ! isset( $entries['mimetype'] ) || trim( $entries['mimetype'] ) !== 'application/epub+zip' ) {
7711 return $src_path; // not a real EPUB, don't touch it
7712 }
7713
7714 // --- STEP 2: never rewrite signed or DRM-encrypted containers. ---
7715 if ( isset( $entries['META-INF/signatures.xml'] ) ) {
7716 $log( 'signed container, serving source' );
7717 return $src_path;
7718 }
7719 if ( isset( $entries['META-INF/encryption.xml'] ) ) {
7720 $enc = $entries['META-INF/encryption.xml'];
7721 $font_obfuscation_only = ( strpos( $enc, 'http://www.idpf.org/2008/embedding' ) !== false || strpos( $enc, 'http://ns.adobe.com/pdf/enc#RC' ) !== false )
7722 && stripos( $enc, 'xmlenc#aes' ) === false
7723 && stripos( $enc, 'adept' ) === false;
7724 if ( ! $font_obfuscation_only ) {
7725 $log( 'encrypted/DRM container, serving source' );
7726 return $src_path;
7727 }
7728 // Font obfuscation: every entry name/bytes are preserved verbatim and
7729 // dc:identifier is never touched, so the deobfuscation key still derives.
7730 }
7731
7732 // --- STEP 3: container.xml -> OPF path (clean XML, DOM-safe). ---
7733 $opf_path = '';
7734 if ( isset( $entries['META-INF/container.xml'] ) ) {
7735 $cx = new DOMDocument();
7736 $prev = libxml_use_internal_errors( true );
7737 $loaded = $cx->loadXML( $entries['META-INF/container.xml'], LIBXML_NONET );
7738 libxml_clear_errors();
7739 libxml_use_internal_errors( $prev );
7740 if ( $loaded ) {
7741 foreach ( $cx->getElementsByTagName( 'rootfile' ) as $rf ) {
7742 $fp = $rf->getAttribute( 'full-path' );
7743 if ( $fp !== '' ) {
7744 $opf_path = $fp;
7745 break;
7746 }
7747 }
7748 }
7749 }
7750 if ( $opf_path === '' || ! isset( $entries[ $opf_path ] ) ) {
7751 return $src_path;
7752 }
7753 $opf_dir = trim( dirname( $opf_path ), '/.' );
7754 $resolve = function ( $href ) use ( $opf_dir ) {
7755 $href = preg_replace( '/[?#].*$/', '', (string) $href );
7756 $path = ( $opf_dir === '' ? '' : $opf_dir . '/' ) . $href;
7757 $out = array();
7758 foreach ( explode( '/', $path ) as $seg ) {
7759 if ( $seg === '' || $seg === '.' ) {
7760 continue;
7761 }
7762 if ( $seg === '..' ) {
7763 array_pop( $out );
7764 continue;
7765 }
7766 $out[] = $seg;
7767 }
7768 return implode( '/', $out );
7769 };
7770
7771 // --- STEP 4: parse the OPF (clean XML), read version, manifest, spine. ---
7772 $opf = new DOMDocument();
7773 $prev = libxml_use_internal_errors( true );
7774 $opf_loaded = $opf->loadXML( $entries[ $opf_path ], LIBXML_NONET );
7775 libxml_clear_errors();
7776 libxml_use_internal_errors( $prev );
7777 if ( ! $opf_loaded ) {
7778 return $src_path;
7779 }
7780
7781 $manifest_href = array();
7782 $manifest_media = array();
7783 foreach ( $opf->getElementsByTagName( 'item' ) as $item ) {
7784 $id = $item->getAttribute( 'id' );
7785 if ( $id === '' ) {
7786 continue;
7787 }
7788 $manifest_href[ $id ] = $resolve( $item->getAttribute( 'href' ) );
7789 $manifest_media[ $id ] = $item->getAttribute( 'media-type' );
7790 }
7791 $spine_docs = array();
7792 foreach ( $opf->getElementsByTagName( 'itemref' ) as $iref ) {
7793 $idref = $iref->getAttribute( 'idref' );
7794 if ( $idref === '' || ! isset( $manifest_href[ $idref ] ) ) {
7795 continue;
7796 }
7797 $href = $manifest_href[ $idref ];
7798 $media = isset( $manifest_media[ $idref ] ) ? $manifest_media[ $idref ] : '';
7799 if ( $media === 'application/xhtml+xml' && isset( $entries[ $href ] ) ) {
7800 $spine_docs[] = $href;
7801 }
7802 }
7803
7804 $applied = false;
7805 $opf_dirty = false;
7806
7807 $manifest_node = $opf->getElementsByTagName( 'manifest' )->item( 0 );
7808 $spine_node = $opf->getElementsByTagName( 'spine' )->item( 0 );
7809
7810 // Which documents a scope targets: a list of content docs, or the string
7811 // 'colophon' (a dedicated page we append).
7812 $targets_for = function ( $target_scope ) use ( $spine_docs ) {
7813 if ( $target_scope === 'colophon' ) {
7814 return 'colophon';
7815 }
7816 if ( empty( $spine_docs ) ) {
7817 return array();
7818 }
7819 return $target_scope === 'every' ? $spine_docs : array( $spine_docs[0] );
7820 };
7821
7822 // Prepare the QR image up front (fail-open: no QR if it can't be made).
7823 $qr_png = '';
7824 $qr_abs = '';
7825 if ( $qr_enabled ) {
7826 $qr_png = ebook_store_generate_qr_png( $qr_text );
7827 if ( $qr_png === '' ) {
7828 $qr_enabled = false;
7829 $log( 'QR unavailable (no GD/barcode support), delivering without QR' );
7830 } else {
7831 $n = 0;
7832 do {
7833 $qr_rel = 'ebook-store-qr' . ( $n === 0 ? '' : '-' . $n ) . '.png';
7834 $qr_abs = $resolve( $qr_rel );
7835 $n++;
7836 } while ( isset( $entries[ $qr_abs ] ) && $n < 50 );
7837 }
7838 }
7839
7840 $text_targets = $text !== '' ? $targets_for( $scope ) : array();
7841 $qr_targets = $qr_enabled ? $targets_for( $qr_scope ) : array();
7842 $qr_referenced = false;
7843
7844 // --- STEP 5: inject text and/or QR into existing content documents. ---
7845 $content_targets = array();
7846 if ( is_array( $text_targets ) ) {
7847 $content_targets = array_merge( $content_targets, $text_targets );
7848 }
7849 if ( is_array( $qr_targets ) ) {
7850 $content_targets = array_merge( $content_targets, $qr_targets );
7851 }
7852 $content_targets = array_values( array_unique( $content_targets ) );
7853
7854 foreach ( $content_targets as $doc ) {
7855 $html = $entries[ $doc ];
7856 $changed = false;
7857
7858 if ( is_array( $text_targets ) && in_array( $doc, $text_targets, true ) ) {
7859 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_watermark_fragment( $text, $style ), $position );
7860 if ( $new !== null ) {
7861 $html = $new;
7862 $changed = true;
7863 } else {
7864 $log( 'no <body> boundary, doc left unmarked', array( 'doc' => $doc ) );
7865 }
7866 }
7867
7868 if ( is_array( $qr_targets ) && in_array( $doc, $qr_targets, true ) ) {
7869 $qr_src = ebook_store_epub_relative_path( dirname( $doc ), $qr_abs );
7870 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_qr_fragment( $qr_src ), $qr_position );
7871 if ( $new !== null ) {
7872 $html = $new;
7873 $changed = true;
7874 $qr_referenced = true;
7875 } else {
7876 $log( 'no <body> boundary, QR not placed', array( 'doc' => $doc ) );
7877 }
7878 }
7879
7880 if ( $changed ) {
7881 $entries[ $doc ] = $html;
7882 $applied = true;
7883 }
7884 }
7885
7886 // --- STEP 6: colophon page carrying whatever targets 'colophon'. ---
7887 if ( ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) && $manifest_node && $spine_node ) {
7888 $n = 0;
7889 do {
7890 $rel = 'ebook-store-licence' . ( $n === 0 ? '' : '-' . $n ) . '.xhtml';
7891 $abs = $resolve( $rel );
7892 $n++;
7893 } while ( isset( $entries[ $abs ] ) && $n < 50 );
7894
7895 $body = '';
7896 if ( $text_targets === 'colophon' && $text !== '' ) {
7897 $body .= ebook_store_epub_watermark_fragment( $text, 'box' );
7898 }
7899 if ( $qr_targets === 'colophon' ) {
7900 $body .= ebook_store_epub_qr_fragment( ebook_store_epub_relative_path( dirname( $abs ), $qr_abs ) );
7901 $qr_referenced = true;
7902 }
7903
7904 $page = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
7905 . '<!DOCTYPE html>' . "\n"
7906 . '<html xmlns="http://www.w3.org/1999/xhtml"><head><meta charset="utf-8"/><title>'
7907 . htmlspecialchars( __( 'Licence', 'ebook-store' ), ENT_QUOTES, 'UTF-8' )
7908 . '</title></head><body>' . $body . '</body></html>';
7909
7910 $mid = 'ebook-store-licence';
7911 $existing_ids = array_keys( $manifest_href );
7912 $k = 0;
7913 while ( in_array( $mid, $existing_ids, true ) ) {
7914 $k++;
7915 $mid = 'ebook-store-licence-' . $k;
7916 }
7917
7918 $entries[ $abs ] = $page;
7919 $order[] = $abs;
7920
7921 $item_el = $opf->createElement( 'item' );
7922 $item_el->setAttribute( 'id', $mid );
7923 $item_el->setAttribute( 'href', $rel );
7924 $item_el->setAttribute( 'media-type', 'application/xhtml+xml' );
7925 $manifest_node->appendChild( $item_el );
7926
7927 $iref_el = $opf->createElement( 'itemref' );
7928 $iref_el->setAttribute( 'idref', $mid );
7929 $spine_node->appendChild( $iref_el );
7930
7931 $applied = true;
7932 $opf_dirty = true;
7933 } elseif ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) {
7934 $log( 'no manifest/spine, cannot add colophon' );
7935 }
7936
7937 // --- Register the QR image in the archive + manifest (once, if used). ---
7938 if ( $qr_referenced && $manifest_node ) {
7939 $entries[ $qr_abs ] = $qr_png;
7940 $order[] = $qr_abs;
7941
7942 $qr_id = 'ebook-store-qr';
7943 $existing_ids = array();
7944 foreach ( $opf->getElementsByTagName( 'item' ) as $it ) {
7945 $existing_ids[] = $it->getAttribute( 'id' );
7946 }
7947 $k = 0;
7948 while ( in_array( $qr_id, $existing_ids, true ) ) {
7949 $k++;
7950 $qr_id = 'ebook-store-qr-' . $k;
7951 }
7952 $qr_item = $opf->createElement( 'item' );
7953 $qr_item->setAttribute( 'id', $qr_id );
7954 $qr_item->setAttribute( 'href', ebook_store_epub_relative_path( $opf_dir, $qr_abs ) );
7955 $qr_item->setAttribute( 'media-type', 'image/png' );
7956 $manifest_node->appendChild( $qr_item );
7957
7958 $opf_dirty = true;
7959 }
7960
7961 // --- STEP 7: metadata stamp (DOM auto-escapes; pass raw text). ---
7962 if ( $stamp && $text !== '' ) {
7963 $metadata = $opf->getElementsByTagName( 'metadata' )->item( 0 );
7964 if ( $metadata ) {
7965 $dc_ns = 'http://purl.org/dc/elements/1.1/';
7966
7967 // Stamp the resolved buyer line verbatim; it already reads as a
7968 // licence statement, so we don't prepend our own prefix (which
7969 // would double up on the default "Licensed to ..." template).
7970 $contributor = $opf->createElementNS( $dc_ns, 'dc:contributor' );
7971 $contributor->appendChild( $opf->createTextNode( $text ) );
7972 $metadata->appendChild( $contributor );
7973
7974 $has_rights = false;
7975 foreach ( $metadata->getElementsByTagName( 'rights' ) as $unused ) {
7976 $has_rights = true;
7977 break;
7978 }
7979 if ( ! $has_rights ) {
7980 $rights = $opf->createElementNS( $dc_ns, 'dc:rights' );
7981 $rights->appendChild( $opf->createTextNode( $text ) );
7982 $metadata->appendChild( $rights );
7983 }
7984
7985 $meta_wm = $opf->createElement( 'meta' );
7986 $meta_wm->setAttribute( 'name', 'ebook-store:watermark' );
7987 $meta_wm->setAttribute( 'content', $text );
7988 $metadata->appendChild( $meta_wm );
7989
7990 if ( $order_id > 0 ) {
7991 $meta_order = $opf->createElement( 'meta' );
7992 $meta_order->setAttribute( 'name', 'ebook-store:order' );
7993 $meta_order->setAttribute( 'content', (string) $order_id );
7994 $metadata->appendChild( $meta_order );
7995 }
7996
7997 $applied = true;
7998 $opf_dirty = true;
7999 }
8000 }
8001
8002 // Re-serialize the OPF only if we changed it.
8003 if ( $opf_dirty ) {
8004 $xml = $opf->saveXML();
8005 if ( $xml !== false && $xml !== '' ) {
8006 $entries[ $opf_path ] = $xml; // saveXML()===false must never blank the OPF
8007 }
8008 }
8009
8010 if ( ! $applied ) {
8011 return $src_path; // nothing actually changed; don't rewrite
8012 }
8013
8014 // --- STEP 8: repack, mimetype FIRST + STORED (the make-or-break rule). ---
8015 $out = new ZipArchive();
8016 if ( $out->open( $dest_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== true ) {
8017 return $src_path;
8018 }
8019 $out->addFromString( 'mimetype', 'application/epub+zip' );
8020 // Class constant only — the global ZIP_CM_STORE is undefined on this build.
8021 $out->setCompressionName( 'mimetype', ZipArchive::CM_STORE );
8022 foreach ( $order as $name ) {
8023 if ( $name === 'mimetype' || ! isset( $entries[ $name ] ) || ! is_string( $entries[ $name ] ) ) {
8024 continue;
8025 }
8026 if ( ! $out->addFromString( $name, $entries[ $name ] ) ) {
8027 $out->close();
8028 @unlink( $dest_path );
8029 return $src_path;
8030 }
8031 }
8032 if ( $out->close() !== true || ! file_exists( $dest_path ) ) {
8033 @unlink( $dest_path );
8034 return $src_path;
8035 }
8036
8037 // --- STEP 9: verify the invariant held, else discard and fail open. ---
8038 $verify = new ZipArchive();
8039 if ( $verify->open( $dest_path ) !== true ) {
8040 @unlink( $dest_path );
8041 return $src_path;
8042 }
8043 $first = $verify->statIndex( 0 );
8044 $verify->close();
8045 if ( ! $first || $first['name'] !== 'mimetype' || (int) $first['comp_method'] !== ZipArchive::CM_STORE ) {
8046 @unlink( $dest_path );
8047 return $src_path;
8048 }
8049
8050 return $dest_path;
8051 } catch ( \Throwable $e ) {
8052 if ( file_exists( $dest_path ) ) {
8053 @unlink( $dest_path );
8054 }
8055 $log( 'watermark failed', array( 'err' => $e->getMessage() ) );
8056 return $src_path;
8057 }
8058 }
8059
8060 function ebook_encrypt_pdf($r = null, $post_id = null) {
8061 //@ini_set('display_errors',1);
8062 //@error_reporting(E_ALL);
8063 if ($r == false) {
8064 $r = $_REQUEST;
8065 }
8066 global $ebook_email_delivery, $ebook_qr_text, $ebook_png_path, $ebook_pngname, $attachment, $pdfHeaderText;
8067 require_once('fpdi/qrcode.class.php');
8068
8069 // Resolve the order this encryption belongs to first, so the output can go in
8070 // that order's protected cache directory (in uploads, HTTP-denied) rather than
8071 // the web-accessible plugin folder — and so a missing folder can't fatal.
8072 $target_order_id = 0;
8073 if ( ! empty( $r['order_id'] ) ) {
8074 $target_order_id = (int) $r['order_id'];
8075 } elseif ( ! empty( $ebook_email_delivery['order']['order_id'] ) ) {
8076 $target_order_id = (int) $ebook_email_delivery['order']['order_id'];
8077 } elseif ( ! empty( $post_id ) ) {
8078 $target_order_id = (int) $post_id;
8079 }
8080
8081 $cache_dir = ebook_store_get_cache_dir( $target_order_id );
8082 if ( $cache_dir === '' ) {
8083 // Uploads unusable — fall back to the system temp dir, never the web root.
8084 $cache_dir = untrailingslashit( get_temp_dir() );
8085 }
8086
8087 $qr_ebook_key = ( isset( $r['ebook_key'] ) && is_scalar( $r['ebook_key'] ) ) ? (string) $r['ebook_key'] : '';
8088 if ( $qr_ebook_key === '' && $target_order_id ) {
8089 $qr_ebook_key = (string) get_post_meta( $target_order_id, 'ebook_key', true );
8090 }
8091 $ebook_qr_text = ebook_store_get_order_verification_url( $qr_ebook_key );
8092 // Watermark text is drawn into a PDF, not HTML — esc_attr() would leak entities
8093 // ("Tom & Jerry" -> "Tom &amp; Jerry"). Strip control chars instead.
8094 $pdfHeaderText = ebook_store_sanitize_pdf_text( (string) get_option('buyer_info_text') );
8095
8096 foreach ($r as $k => $v) {
8097 if ( is_scalar( $v ) ) {
8098 $pdfHeaderText = str_replace("%%$k%%", ebook_store_sanitize_pdf_text( (string) $v ), $pdfHeaderText);
8099 }
8100 }
8101 // The QR code is now drawn natively by TCPDF as vectors; no PNG is fetched
8102 // from an external service, and no GD / allow_url_fopen is needed.
8103 if (@is_array($attachment)) {
8104 @$file = $attachment[0]['file'];
8105 }
8106 if (@!$file) {
8107 $file = $ebook_email_delivery['attachment'][0]['file'];
8108 }
8109 // ($target_order_id was resolved at the top so the cache dir could be built.)
8110 $password = ebook_store_get_password_for_order($target_order_id, array(
8111 'password' => isset($r['password']) ? $r['password'] : '',
8112 'payer_email' => isset($r['payer_email']) ? $r['payer_email'] : '',
8113 'md5_nonce' => isset($r['md5_nonce']) ? $r['md5_nonce'] : '',
8114 'ebook_key' => isset($r['ebook_key']) ? $r['ebook_key'] : '',
8115 ));
8116
8117 if (isset($ebook_email_delivery['order']) && is_array($ebook_email_delivery['order'])) {
8118 $ebook_email_delivery['order']['password'] = $password;
8119 }
8120
8121 //free files encrypt with user email?
8122
8123
8124 //$password = $r['payer_email'];
8125 // if (get_option('ebook_store_random_password') == 1) {
8126 // if ($ebook_store_random_password != '') {
8127 // $r['password'] = $ebook_store_random_password;
8128 // }
8129 // if (@$r['password'] != '') {
8130 // $password = $r['password'];
8131 // }
8132 // }
8133
8134 // if (get_option('ebook_store_blank_password') == 1) {
8135 // $r['password'] = '';
8136 // $password = '';
8137 // }
8138
8139 //random password for non logged in users
8140 // if (!is_user_logged_in() && get_option('formEnabled') < 1) {
8141 // if (get_option('ebook_store_random_password') == 1) {
8142 // $password = substr(md5(rand()),0,8);
8143 // //$password = 'proba';
8144 // $ebook_store_random_password = $password;
8145 // if ($post_id != null) {
8146 // @update_post_meta($post_id,'password',$password);
8147 // }
8148
8149 // // Additional code to handle the generated password
8150 // }
8151 //}
8152 // if (is_user_logged_in()) {
8153 // if (get_option('ebook_store_random_password') == 1) {
8154 // $password = substr(md5(rand()),0,8);
8155 // //$password = 'proba';
8156 // $ebook_store_random_password = $password;
8157 // if ($post_id != null) {
8158 // @update_post_meta($post_id,'password',$password);
8159 // }
8160
8161 // // Additional code to handle the generated password
8162 // } else {
8163 // if (get_option('ebook_store_blank_password') < 1) {
8164 // $current_user = wp_get_current_user();
8165 // $password = $current_user->user_email;
8166 // if ($post_id != null) {
8167 // @update_post_meta($post_id,'password',$password);
8168 // }
8169 // }
8170 // }
8171 // }
8172
8173 //wp_die($password . ' post id ' . $post_id);
8174 //error_log('password ' . $password);
8175
8176 $owner_password = (string) get_option('ebook_store_owner_password');
8177
8178 // Write the finished, buyer-watermarked PDF into the protected cache dir (in
8179 // uploads, HTTP-denied), NOT the plugin folder — those files carry the buyer's
8180 // email/IP in the watermark and were previously fetchable, since the plugin
8181 // .htaccess denies .txt/.log but not .pdf.
8182 $destfile = $cache_dir . '/' . basename( $file );
8183
8184
8185 if ( function_exists( 'ebook_store_raise_pdf_memory' ) ) {
8186 ebook_store_raise_pdf_memory();
8187 }
8188 $pdf = new QRPDF();
8189 $pdf->watermarkText = $pdfHeaderText;
8190 $pdf->qrText = $ebook_qr_text;
8191 $pdf->stampCount = 0;
8192 try {
8193 $pagecount = $pdf->setSourceFile($file);
8194 } catch (Exception $e) {
8195 wp_die( esc_html( sprintf(
8196 /* translators: %s: PDF import error. */
8197 __( 'This PDF could not be processed for protection. Please re-save it as a standard PDF or turn encryption off. Details: %s', 'ebook-store' ),
8198 $e->getMessage()
8199 ) ) );
8200 }
8201
8202 // Mark the order only now that generation is actually about to run, and keep
8203 // the previous value so a failure (exception, fatal, timeout) restores it
8204 // instead of leaving 'in_progress' behind for good.
8205 if ($target_order_id) {
8206 if ( ! isset( $GLOBALS['ebook_store_pdf_in_progress'] ) || ! is_array( $GLOBALS['ebook_store_pdf_in_progress'] ) ) {
8207 $GLOBALS['ebook_store_pdf_in_progress'] = array();
8208 }
8209 $GLOBALS['ebook_store_pdf_in_progress'][$target_order_id] = (string) get_post_meta($target_order_id,'encrypted_pdf',true);
8210 update_post_meta($target_order_id,'encrypted_pdf_started',time());
8211 update_post_meta($target_order_id,'encrypted_pdf','in_progress');
8212 register_shutdown_function('ebook_store_restore_pdf_marker', $target_order_id);
8213 }
8214
8215 $generated = false;
8216 try {
8217 for ($loop = 1; $loop <= $pagecount; $loop++) {
8218 $tplidx = $pdf->importPage($loop);
8219 $size = $pdf->getTemplateSize($tplidx);
8220 $pdf->AddPage($size['orientation'], array($size['width'], $size['height']));
8221 $pdf->useTemplate($tplidx);
8222 $pdf->stampCurrentPage();
8223 }
8224
8225 $pdf->applyProtection($password, $owner_password);
8226 $pdf->Output($destfile, 'F');
8227 $generated = file_exists($destfile);
8228 } finally {
8229 if ($target_order_id) {
8230 if ($generated) {
8231 unset($GLOBALS['ebook_store_pdf_in_progress'][$target_order_id]);
8232 delete_post_meta($target_order_id,'encrypted_pdf_started');
8233 update_post_meta($target_order_id,'encrypted_pdf',wp_slash($destfile));
8234 } else {
8235 ebook_store_restore_pdf_marker($target_order_id);
8236 }
8237 }
8238 }
8239 //make sure enc file is attached
8240 //file_put_contents('ebook-store-password-debug.txt', $password . "\n" . print_r($r, true), FILE_APPEND);
8241
8242 $ebook_email_delivery['attachment'][0]['file'] = $destfile;
8243 $isPdf = true;
8244 return $destfile;
8245 }
8246 /**
8247 * Puts an order's encrypted_pdf meta back to what it was before a generation
8248 * attempt that did not finish. Also registered as a shutdown function by
8249 * ebook_encrypt_pdf(), so a fatal or timeout mid-encryption cannot leave the
8250 * 'in_progress' marker behind permanently.
8251 */
8252 function ebook_store_restore_pdf_marker( $order_id ) {
8253 $order_id = (int) $order_id;
8254 if ( ! $order_id || ! isset( $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ] ) ) {
8255 return;
8256 }
8257 $previous = $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ];
8258 unset( $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ] );
8259 delete_post_meta( $order_id, 'encrypted_pdf_started' );
8260 if ( $previous !== '' && $previous !== 'in_progress' ) {
8261 update_post_meta( $order_id, 'encrypted_pdf', wp_slash( $previous ) );
8262 } else {
8263 delete_post_meta( $order_id, 'encrypted_pdf' );
8264 }
8265 }
8266 function ebook_store_missing_gd() {
8267 ?>
8268 <div class="updated">
8269 <p><?php _e( 'Your hosting provide is missing GD PNG support, please contact your provider to enable GD in order to be able to process PNG files.', 'ebook-store' ); ?></p>
8270 </div>
8271 <?php
8272 }
8273 function ebook_store_admin_notice() {
8274 $fix_url = wp_nonce_url( admin_url( 'admin-post.php?action=ebook_store_fix_thankyou' ), 'ebook_store_fix_thankyou' );
8275 ?>
8276 <div class="updated">
8277 <p><?php _e( 'You need to create a "Thank you" landing page with the text/shortcode <input type="text" size=20 value="[ebook_thank_you]" />, where you want the "Thank You" page content to appear, then select that page in Ebook Store options under Settings menu.', 'ebook-store' ); ?></p>
8278 <p class="submit"><a class="button button-primary" href="<?php echo esc_url( $fix_url ); ?>"><?php esc_html_e( 'Fix automatically', 'ebook-store' ); ?></a></p>
8279 </div>
8280 <?php
8281 }
8282 function ebook_store_admin_notice_paypal() {
8283 if ( ! ebook_store_is_paypal_enabled() ) {
8284 return;
8285 }
8286 ?>
8287 <div class="updated">
8288 <p><?php _e( 'You have not yet filled in your <b>PayPal account</b>. Please do that in <a href="options-general.php?page=ebook_options.php&tab=PayPal">Settings > Ebook Store > PayPal</a> in order to be able to receive payments.', 'ebooks-store' ); ?></p>
8289 </div>
8290 <?php
8291 }
8292 function ebook_store_admin_notice_autocompleteorders() {
8293 ?>
8294 <div class="updated">
8295 <p><?php _e( 'If you plan to use Ebook Store + WooCommerce integration, we recommend installing the free plugin <a target="_blank" href="https://wordpress.org/plugins/autocomplete-woocommerce-orders/">Autocomplete WooCommerce Orders</a> so you can set it up to automatically complete orders for virtual goods to avoid processing status.', 'ebooks-store' ); ?></p>
8296 </div>
8297 <?php
8298 }
8299
8300 function ebook_store_set_messages($messages) {
8301 global $post, $post_ID;
8302 $post_type = get_post_type( $post_ID );
8303
8304 $obj = get_post_type_object($post_type);
8305 $singular = $obj->labels->singular_name;
8306 if ($post_type == 'ebook') {
8307 $messages[$post_type][1] = 'Ebook has been saved! Now copy and paste this code in the article / page where you want the ebook order form to appear: <input onClick="" type="text" size=30 value=\'[ebook_store ebook_id="' . $post_ID . '"]\'>';
8308 }
8309 return $messages;
8310 }
8311 function ebook_store_remove_parmelink( $return ) {
8312 if (@$post_id == false) {
8313 $post_id = get_the_ID();
8314 }
8315 if ('ebook' === get_post_type( $post_id )) {
8316 $return = '';
8317 }
8318 return $return;
8319 }
8320
8321 function ebook_admin_css() {
8322 global $post_type;
8323 $post_types = array(
8324 /* set post types */
8325 'ebook',
8326 );
8327 if(in_array($post_type, $post_types))
8328 echo '<style type="text/css">#post-preview, #view-post-btn{display: none;}</style>';
8329 }
8330 function ebook_store_set_columns( $columns ) {
8331
8332 $columns = array(
8333 'cb' => '<input type="checkbox" />',
8334 'cover' => __( 'Cover', 'ebook-store' ),
8335 'title' => __( 'Ebook', 'ebook-store' ),
8336 'date' => __( 'Date', 'ebook-store' ),
8337 'price' => __( 'Price', 'ebook-store' ),
8338 'formats' => __( 'Formats', 'ebook-store' ),
8339 'sales' => __( 'Sales', 'ebook-store' ),
8340 'embed_code' => __('Shortcode & Links', 'ebook-store')
8341 );
8342
8343 return $columns;
8344 }
8345
8346 function ebook_store_columns_output( $column, $post_id ) {
8347 global $post;
8348 $ebook = new EbookStoreEbook($post_id);
8349 switch( $column ) {
8350 case 'embed_code':
8351 $shortcode_id = 'ebook-shortcode-' . $post_id;
8352 $link_id = 'ebook-direct-link-' . $post_id;
8353 $shortcode = '[ebook_store ebook_id="' . $post_id . '"]';
8354 $order_url = add_query_arg( 'task', 'direct_order', get_permalink( $post_id ) );
8355
8356 echo '<div class="ebook-list-shortcuts">';
8357 echo '<div class="ebook-list-shortcuts__field">';
8358 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Shortcode', 'ebook-store' ) . '</span>';
8359 echo '<div class="ebook-list-shortcuts__control">';
8360 echo '<input type="text" id="' . esc_attr( $shortcode_id ) . '" readonly value="' . esc_attr( $shortcode ) . '" />';
8361 echo '<button type="button" class="button button-secondary ebook-list-copy" data-copy-target="#' . esc_attr( $shortcode_id ) . '" data-copied-text="' . esc_attr__( 'Copied!', 'ebook-store' ) . '">' . esc_html__( 'Copy', 'ebook-store' ) . '</button>';
8362 echo '</div>';
8363 echo '</div>';
8364 echo '<div class="ebook-list-shortcuts__field">';
8365 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Direct order link', 'ebook-store' ) . '</span>';
8366 echo '<div class="ebook-list-shortcuts__control">';
8367 echo '<input type="text" id="' . esc_attr( $link_id ) . '" readonly value="' . esc_attr( $order_url ) . '" />';
8368 echo '<button type="button" class="button button-secondary ebook-list-copy" data-copy-target="#' . esc_attr( $link_id ) . '" data-copied-text="' . esc_attr__( 'Copied!', 'ebook-store' ) . '">' . esc_html__( 'Copy', 'ebook-store' ) . '</button>';
8369 echo '</div>';
8370 echo '</div>';
8371 echo '</div>';
8372 break;
8373 case 'sales':
8374 $sales_index = ebook_store_get_ebook_sales_index();
8375 $sales = isset( $sales_index[ $post_id ] ) ? $sales_index[ $post_id ] : array(
8376 'orders' => 0,
8377 'total' => 0.0,
8378 'last_7d' => 0.0,
8379 'last_30d' => 0.0,
8380 'latest_order_gmt' => '',
8381 );
8382 $currency = ebook_store_get_store_currency();
8383 $latest_order = $sales['latest_order_gmt'] !== '' ? human_time_diff( strtotime( $sales['latest_order_gmt'] . ' UTC' ), current_time( 'timestamp', true ) ) : __( 'No orders yet', 'ebook-store' );
8384
8385 echo '<div class="ebook-list-sales">';
8386 echo '<strong class="ebook-list-sales__amount">' . esc_html( ebook_store_order_money_display( $sales['total'], $currency ) ) . '</strong>';
8387 echo '<div class="ebook-list-sales__pills">';
8388 echo '<span class="ebook-list-pill is-green">' . esc_html( sprintf( __( '%s orders', 'ebook-store' ), number_format_i18n( $sales['orders'] ) ) ) . '</span>';
8389 echo '<span class="ebook-list-pill">' . esc_html( sprintf( __( '7d %s', 'ebook-store' ), ebook_store_order_money_display( $sales['last_7d'], $currency ) ) ) . '</span>';
8390 echo '<span class="ebook-list-pill is-warm">' . esc_html( sprintf( __( '30d %s', 'ebook-store' ), ebook_store_order_money_display( $sales['last_30d'], $currency ) ) ) . '</span>';
8391 echo '</div>';
8392 echo '<div class="ebook-list-sales__meta">' . esc_html( $sales['latest_order_gmt'] !== '' ? sprintf( __( 'Latest order %s ago', 'ebook-store' ), $latest_order ) : $latest_order ) . '</div>';
8393 echo '</div>';
8394 break;
8395 case 'price':
8396 $ebook = get_post_meta( get_the_ID(), 'ebook', true );
8397 $price = 0.0;
8398 if ( ! is_array( $ebook ) ) {
8399 echo '<div class="ebook-list-price"><strong class="ebook-list-price__amount">-</strong></div>';
8400 break;
8401 }
8402 if ( isset( $ebook['ebook_price'] ) ) {
8403 $price = (float) $ebook['ebook_price'];
8404 }
8405 echo '<div class="ebook-list-price">';
8406 echo '<strong class="ebook-list-price__amount">' . esc_html( ebook_store_order_money_display( $price, ebook_store_get_store_currency() ) ) . '</strong>';
8407 echo '<div class="ebook-list-price__pills">';
8408 echo '<span class="ebook-list-pill ' . esc_attr( $price > 0 ? 'is-green' : 'is-warm' ) . '">' . esc_html( $price > 0 ? __( 'Paid title', 'ebook-store' ) : __( 'Free title', 'ebook-store' ) ) . '</span>';
8409 if ( ! empty( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] === 'donate' ) {
8410 echo '<span class="ebook-list-pill">' . esc_html__( 'Donation mode', 'ebook-store' ) . '</span>';
8411 }
8412 echo '</div>';
8413 echo '</div>';
8414 break;
8415 case 'cover':
8416 $cover = get_post_meta( get_the_ID(), 'ebook_wp_custom_attachment_cover', true );
8417 $cover = is_array( $cover ) ? $cover : array();
8418
8419 echo '<div class="ebook-list-cover">';
8420 echo '<div class="ebook-list-cover__frame">';
8421 if ( ! empty( $cover['url'] ) ) {
8422 echo '<img src="' . esc_url( $cover['url'] ) . '" alt="" />';
8423 } else {
8424 echo '<span class="ebook-list-cover__placeholder">' . esc_html__( 'No cover uploaded', 'ebook-store' ) . '</span>';
8425 }
8426 echo '</div>';
8427 echo '<span class="ebook-list-status">' . esc_html( get_post_status( $post_id ) ) . '</span>';
8428 echo '</div>';
8429
8430 break;
8431 case 'formats':
8432 $formats_found = array();
8433 if ( is_array( $ebook->files ) ) {
8434 foreach ( array_keys( $ebook->files ) as $key => $value ) {
8435 if ( strpos( $value, '_size' ) === false ) {
8436 $format = $value;
8437 $icon_url = plugins_url( 'img/' . $value . '.png', __FILE__ );
8438 $title = basename( $ebook->files[ $format ] ) . ' ' . $ebook->human_filesize( $ebook->files[ $format . '_size' ] );
8439 $formats_found[] = '<span class="ebook-list-formats__item"><img title="' . esc_attr( $title ) . '" width="30" src="' . esc_url( $icon_url ) . '" alt="' . esc_attr( strtoupper( $format ) ) . '" /></span>';
8440 }
8441 }
8442 echo '<div class="ebook-list-formats">';
8443 echo implode( '', $formats_found );
8444 if ( empty( $formats_found ) ) {
8445 echo '<span class="ebook-list-pill">' . esc_html__( 'No files attached', 'ebook-store' ) . '</span>';
8446 }
8447 echo '</div>';
8448 $testable = array();
8449 if ( ! empty( $ebook->files['pdf'] ) && file_exists( $ebook->files['pdf'] ) ) {
8450 $testable['pdf'] = __( 'PDF', 'ebook-store' );
8451 }
8452 if ( ! empty( $ebook->files['epub'] ) && file_exists( $ebook->files['epub'] ) ) {
8453 $testable['epub'] = __( 'ePub', 'ebook-store' );
8454 }
8455 if ( ! empty( $testable ) ) {
8456 $ebook_test_link = function ( $format ) use ( $post_id ) {
8457 return wp_nonce_url(
8458 admin_url( 'admin-post.php?action=ebook_store_test_encryption&ebook_id=' . $post_id . '&format=' . $format ),
8459 'ebook_store_test_encryption_' . $post_id
8460 );
8461 };
8462 if ( count( $testable ) === 1 ) {
8463 $only = array_key_first( $testable );
8464 $label = $only === 'epub'
8465 ? __( 'Test watermark', 'ebook-store' )
8466 : __( 'Test encryption (password: test)', 'ebook-store' );
8467 echo '<div class="ebook-format-test-link"><a class="button button-small" href="' . esc_url( $ebook_test_link( $only ) ) . '">' . esc_html( $label ) . '</a></div>';
8468 } else {
8469 // Both formats present: one Test control that asks which file to use.
8470 echo '<details class="ebook-format-test">';
8471 echo '<summary class="button button-small">' . esc_html__( 'Test protection…', 'ebook-store' ) . '</summary>';
8472 echo '<div class="ebook-format-test__menu">';
8473 echo '<span class="ebook-format-test__prompt">' . esc_html__( 'Test with which file?', 'ebook-store' ) . '</span>';
8474 foreach ( $testable as $format => $flabel ) {
8475 /* translators: %s: file format, e.g. PDF or ePub. */
8476 echo '<a class="button button-small" href="' . esc_url( $ebook_test_link( $format ) ) . '">' . esc_html( sprintf( __( 'Test %s', 'ebook-store' ), $flabel ) ) . '</a>';
8477 }
8478 echo '</div></details>';
8479 }
8480 }
8481 }
8482 break;
8483
8484 /* Just break out of the switch statement for everything else. */
8485 default :
8486 break;
8487 }
8488 }
8489 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
8490 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
8491 function ebook_store_pre_get_shortlink( $false, $post_id ) {
8492 return 'ebook' === get_post_type( $post_id ) ? '' : $false;
8493 }
8494 function ebook_mime_types($mime_types){
8495 // These have to be the real types. fileinfo reports an ePub as
8496 // application/epub+zip, and when the map here said octet-stream instead,
8497 // wp_check_filetype_and_ext() saw a mismatch and returned ext=false and
8498 // type=false for a perfectly valid book — which is what made the uploader
8499 // answer "that file type is not allowed here" for every .epub.
8500 $mime_types['epub'] = 'application/epub+zip';
8501 $mime_types['mobi'] = 'application/x-mobipocket-ebook';
8502 $mime_types['txt'] = 'text/plain';
8503 $mime_types['mp3'] = 'audio/mpeg';
8504
8505 return $mime_types;
8506 }
8507 /**
8508 * The directory that holds captured checkout-form data.
8509 *
8510 * @return string Absolute path with a trailing slash, or '' on failure.
8511 */
8512 function ebook_store_get_form_data_dir() {
8513 static $dir = null;
8514
8515 if ( $dir !== null ) {
8516 return $dir;
8517 }
8518
8519 $uploads = wp_upload_dir();
8520 if ( ! empty( $uploads['error'] ) ) {
8521 $dir = '';
8522 return $dir;
8523 }
8524
8525 $dir = trailingslashit( $uploads['basedir'] ) . 'ebook_store_data/';
8526
8527 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
8528 $dir = '';
8529 return $dir;
8530 }
8531
8532 // Buyer names and email addresses live here; they must not be fetchable.
8533 if ( ! file_exists( $dir . '.htaccess' ) ) {
8534 @file_put_contents( $dir . '.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" );
8535 }
8536 if ( ! file_exists( $dir . 'index.php' ) ) {
8537 @file_put_contents( $dir . 'index.php', "<?php\n// Silence is golden.\n" );
8538 }
8539
8540 return $dir;
8541 }
8542
8543 /**
8544 * Turn a request-supplied nonce into a safe, fixed-shape filename.
8545 *
8546 * The read side used to interpolate this value straight into a path, giving an
8547 * arbitrary-file-read primitive via ?md5_nonce=../../../etc/passwd.
8548 *
8549 * @param string $md5_nonce Value from the request.
8550 * @return string Filename, or '' when the value is not a plausible token.
8551 */
8552 function ebook_store_get_form_filename( $md5_nonce ) {
8553 $md5_nonce = (string) $md5_nonce;
8554
8555 // Every token the plugin issues is an md5() hash. Anything else (including
8556 // the literal "undefined" a button without data-md5_nonce used to send)
8557 // must not get a file of its own.
8558 if ( ! preg_match( '/^[a-fA-F0-9]{32}$/', $md5_nonce ) ) {
8559 return '';
8560 }
8561
8562 return 'ebook_store_form_' . $md5_nonce . '.json';
8563 }
8564
8565 function ebook_store_formContent() {
8566 $task = isset( $_REQUEST['task'] ) ? sanitize_key( wp_unslash( $_REQUEST['task'] ) ) : '';
8567
8568 if ( $task === 'formContent' ) {
8569 // The stored markup is authored by an administrator, but it is served to
8570 // anonymous visitors, so it still goes through wp_kses(). The 'post'
8571 // allowlist has no form controls, so those are added here (no script).
8572 $allowed_html = wp_kses_allowed_html( 'post' );
8573 $form_attributes = array_fill_keys( array( 'name', 'type', 'value', 'id', 'class', 'placeholder', 'required', 'action', 'method', 'for', 'checked', 'selected', 'rows', 'cols' ), true );
8574 foreach ( array( 'form', 'input', 'select', 'option', 'textarea', 'button', 'label' ) as $form_tag ) {
8575 $allowed_html[ $form_tag ] = ( isset( $allowed_html[ $form_tag ] ) && is_array( $allowed_html[ $form_tag ] ) ) ? array_merge( $allowed_html[ $form_tag ], $form_attributes ) : $form_attributes;
8576 }
8577 $QSWPOptions = new QSWPOptions();
8578 echo wp_kses( get_option( 'formContent', $QSWPOptions->formContent ), $allowed_html );
8579 die();
8580 }
8581
8582 if ( $task === 'ebook_store_stripe_session' ) {
8583 // A Checkout Session is created here, when the buyer clicks, instead of
8584 // on every page render. No nonce: the button may be served from a page
8585 // cache. Nothing is written to the database and every value below is
8586 // resolved server-side, so the only thing worth guarding is the rate of
8587 // calls into the Stripe API.
8588 nocache_headers();
8589 header( 'Content-Type: application/json; charset=utf-8' );
8590
8591 $stripe_reply = function( $status, $payload ) {
8592 status_header( $status );
8593 echo wp_json_encode( $payload );
8594 die();
8595 };
8596
8597 if ( ! get_option( 'stripe_integration_enabled', 0 ) ) {
8598 $stripe_reply( 403, array( 'error' => __( 'Card checkout is not available on this store.', 'ebook-store' ) ) );
8599 }
8600
8601 $stripe_ebook_id = isset( $_REQUEST['ebook_id'] ) ? absint( wp_unslash( $_REQUEST['ebook_id'] ) ) : 0;
8602 $stripe_post = $stripe_ebook_id ? get_post( $stripe_ebook_id ) : null;
8603
8604 if ( ! $stripe_post || $stripe_post->post_type !== 'ebook' || $stripe_post->post_status !== 'publish' ) {
8605 $stripe_reply( 404, array( 'error' => __( 'This ebook is not available.', 'ebook-store' ) ) );
8606 }
8607
8608 $stripe_meta = get_post_meta( $stripe_ebook_id, 'ebook', true );
8609 $stripe_price = ( is_array( $stripe_meta ) && isset( $stripe_meta['ebook_price'] ) ) ? (float) $stripe_meta['ebook_price'] : 0;
8610
8611 if ( $stripe_price <= 0 ) {
8612 $stripe_reply( 400, array( 'error' => __( 'This ebook is not sold through card checkout.', 'ebook-store' ) ) );
8613 }
8614
8615 $stripe_rate_key = 'ebook_store_stripe_rl_' . md5( $stripe_ebook_id . '|' . ( isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '' ) );
8616 if ( get_transient( $stripe_rate_key ) ) {
8617 $stripe_reply( 429, array( 'error' => __( 'Please wait a moment and try again.', 'ebook-store' ) ) );
8618 }
8619 set_transient( $stripe_rate_key, 1, 5 );
8620
8621 // Same shape as the key the buy form generates, so the thank-you page and
8622 // the webhook recognise it exactly as before.
8623 $stripe_order_key = md5( NONCE_KEY . $stripe_ebook_id . $stripe_price . mt_rand( 1, 100000 ) );
8624
8625 $stripe_session_url = ebook_store_create_stripe_checkout(
8626 $stripe_ebook_id,
8627 ebook_store_price_plus_vat( $stripe_price ),
8628 get_the_title( $stripe_ebook_id ),
8629 $stripe_order_key
8630 );
8631
8632 if ( empty( $stripe_session_url ) ) {
8633 // The buyer is invited to try again, so the next click must not
8634 // have to wait out the limiter.
8635 delete_transient( $stripe_rate_key );
8636 $stripe_reply( 502, array( 'error' => __( 'Sorry, the checkout could not be opened. Please try again.', 'ebook-store' ) ) );
8637 }
8638
8639 $stripe_reply( 200, array( 'url' => $stripe_session_url ) );
8640 }
8641
8642 if ( $task === 'ebook_store_paypal_order' ) {
8643 // The PayPal order is created on click, like the Stripe session above.
8644 ebook_store_paypal_rest_handle_checkout_task();
8645 }
8646
8647 if ( $task === 'ebook_store_form_input' ) {
8648 ebook_store_save_form( isset( $_POST['md5_nonce'] ) ? wp_unslash( $_POST['md5_nonce'] ) : '', $_POST );
8649 }
8650 }
8651 /**
8652 * Persist the buyer details captured by the checkout form.
8653 *
8654 * Called from an unauthenticated request (the buyer has not logged in), so it
8655 * is deliberately narrow: a fixed filename shape, a size cap, a field cap, and
8656 * a rate limit so it cannot be used to fill the disk.
8657 *
8658 * @param string $md5_nonce Order token.
8659 * @param array $data Posted fields.
8660 */
8661 function ebook_store_save_form( $md5_nonce, $data ) {
8662 $filename = ebook_store_get_form_filename( $md5_nonce );
8663 $dir = ebook_store_get_form_data_dir();
8664
8665 if ( $filename === '' || $dir === '' ) {
8666 status_header( 400 );
8667 die( esc_html__( 'Invalid request.', 'ebook-store' ) );
8668 }
8669
8670 // One write per token per minute is plenty for a real checkout, and it stops
8671 // this endpoint being used as an unlimited disk-fill primitive.
8672 $rate_key = 'ebook_store_form_rl_' . md5( $filename );
8673 if ( get_transient( $rate_key ) ) {
8674 status_header( 429 );
8675 die( esc_html__( 'Please wait a moment and try again.', 'ebook-store' ) );
8676 }
8677 set_transient( $rate_key, 1, MINUTE_IN_SECONDS );
8678
8679 $json = array();
8680 $fields = 0;
8681 $skip = array( 'task', 'md5_nonce' );
8682
8683 foreach ( (array) $data as $k => $v ) {
8684 if ( ++$fields > 50 ) {
8685 break;
8686 }
8687 $key = sanitize_key( $k );
8688 if ( $key === '' || in_array( $key, $skip, true ) || ! is_scalar( $v ) ) {
8689 continue;
8690 }
8691 $json[ $key ] = sanitize_text_field( substr( (string) $v, 0, 500 ) );
8692 }
8693
8694 if ( file_put_contents( $dir . $filename, wp_json_encode( $json ), LOCK_EX ) === false ) {
8695 status_header( 500 );
8696 die( esc_html__( 'The form data could not be saved.', 'ebook-store' ) );
8697 }
8698
8699 status_header( 200 );
8700 die( esc_html__( 'Form data saved successfully.', 'ebook-store' ) );
8701 }
8702
8703 /**
8704 * Read back the buyer details captured at checkout.
8705 *
8706 * This used to read from get_temp_dir() with no '.json' suffix, i.e. a
8707 * different directory and a different filename than ebook_store_save_form()
8708 * writes, so it always returned false and every order fell back to
8709 * "Anonymous User".
8710 *
8711 * @param string $md5_nonce Order token.
8712 * @return object|false
8713 */
8714 function ebook_store_get_form( $md5_nonce ) {
8715 $filename = ebook_store_get_form_filename( $md5_nonce );
8716 $dir = ebook_store_get_form_data_dir();
8717
8718 if ( $filename === '' || $dir === '' ) {
8719 return false;
8720 }
8721
8722 $path = $dir . $filename;
8723 if ( ! is_readable( $path ) ) {
8724 return false;
8725 }
8726
8727 $form_data = file_get_contents( $path );
8728 if ( ! $form_data ) {
8729 return false;
8730 }
8731
8732 $decoded = json_decode( $form_data );
8733
8734 return $decoded === null ? false : $decoded;
8735 }
8736 function ebook_store_get_mailchimp_lists() {
8737 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8738 if ( $api_key === '' ) {
8739 return false;
8740 }
8741
8742 $response = ebook_store_mailchimp_request(
8743 'GET',
8744 '/lists',
8745 array(
8746 'count' => 1000,
8747 'fields' => 'lists.id,lists.name,total_items',
8748 )
8749 );
8750
8751 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8752 return false;
8753 }
8754
8755 return array_map(
8756 static function( $list ) {
8757 return (object) array(
8758 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8759 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8760 );
8761 },
8762 $response['lists']
8763 );
8764 }
8765 function ebook_store_get_brevo_lists() {
8766 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8767 if ( $api_key === '' ) {
8768 return false;
8769 }
8770
8771 $lists = array();
8772 $offset = 0;
8773 $limit = 50;
8774
8775 do {
8776 $response = ebook_store_brevo_request(
8777 'GET',
8778 '/contacts/lists',
8779 array(
8780 'limit' => $limit,
8781 'offset' => $offset,
8782 )
8783 );
8784
8785 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8786 return false;
8787 }
8788
8789 $batch = $response['lists'];
8790 $lists = array_merge( $lists, $batch );
8791 $offset += count( $batch );
8792 $total = isset( $response['count'] ) ? (int) $response['count'] : count( $lists );
8793 } while ( ! empty( $batch ) && $offset < $total );
8794
8795 return array_map(
8796 static function( $list ) {
8797 return (object) array(
8798 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8799 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8800 );
8801 },
8802 $lists
8803 );
8804 }
8805 function ebook_store_get_mailchimp_subscribe($email) {
8806 //error_log('MAILCHIMP Subscribing ' . $email);
8807 $list_id = trim( (string) get_option( 'mailchimp_lists' ) );
8808 $integration_enabled = (string) get_option( 'mailchimp_integration_enabled', $list_id !== '' ? '1' : '' );
8809 if ( $integration_enabled !== '1' ) {
8810 return false;
8811 }
8812
8813 if ( $list_id === '' ) {
8814 return false;
8815 }
8816
8817 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8818 if ( $api_key === '' ) {
8819 return false;
8820 }
8821
8822 $email = sanitize_email( (string) $email );
8823 if ( $email === '' ) {
8824 return false;
8825 }
8826
8827 $subscriber_hash = md5( strtolower( $email ) );
8828
8829 return ebook_store_mailchimp_request(
8830 'PUT',
8831 '/lists/' . rawurlencode( $list_id ) . '/members/' . $subscriber_hash,
8832 array(),
8833 array(
8834 'email_address' => $email,
8835 'status_if_new' => 'subscribed',
8836 'status' => 'subscribed',
8837 )
8838 );
8839 }
8840 function ebook_store_get_brevo_subscribe( $email ) {
8841 $list_id = trim( (string) get_option( 'brevo_lists' ) );
8842 $integration_enabled = (string) get_option( 'brevo_integration_enabled', $list_id !== '' ? '1' : '' );
8843 if ( $integration_enabled !== '1' ) {
8844 return false;
8845 }
8846
8847 if ( $list_id === '' || ! ctype_digit( $list_id ) ) {
8848 return false;
8849 }
8850
8851 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8852 if ( $api_key === '' ) {
8853 return false;
8854 }
8855
8856 $email = sanitize_email( (string) $email );
8857 if ( $email === '' ) {
8858 return false;
8859 }
8860
8861 return ebook_store_brevo_request(
8862 'POST',
8863 '/contacts',
8864 array(),
8865 array(
8866 'email' => $email,
8867 'listIds' => array( (int) $list_id ),
8868 'emailBlacklisted' => false,
8869 'smsBlacklisted' => false,
8870 'updateEnabled' => true,
8871 )
8872 );
8873 }
8874
8875 function ebook_store_subscribe_buyer_to_marketing_lists( $email ) {
8876 $email = sanitize_email( (string) $email );
8877 if ( $email === '' ) {
8878 return false;
8879 }
8880
8881 $results = array();
8882 $results['mailchimp'] = ebook_store_get_mailchimp_subscribe( $email );
8883 $results['brevo'] = ebook_store_get_brevo_subscribe( $email );
8884
8885 return $results;
8886 }
8887
8888 function ebook_store_get_mailchimp_server_prefix( $api_key ) {
8889 $api_key = trim( (string) $api_key );
8890 if ( $api_key === '' || strpos( $api_key, '-' ) === false ) {
8891 return '';
8892 }
8893
8894 $parts = explode( '-', $api_key );
8895 $server_prefix = strtolower( trim( (string) end( $parts ) ) );
8896
8897 return preg_match( '/^[a-z0-9]+$/', $server_prefix ) ? $server_prefix : '';
8898 }
8899
8900 function ebook_store_mailchimp_request( $method, $path, $query = array(), $body = null ) {
8901 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8902 if ( $api_key === '' ) {
8903 return new WP_Error( 'mailchimp_missing_api_key', 'Mailchimp API key not configured.' );
8904 }
8905
8906 $server_prefix = ebook_store_get_mailchimp_server_prefix( $api_key );
8907 if ( $server_prefix === '' ) {
8908 return new WP_Error( 'mailchimp_invalid_api_key', 'Mailchimp API key is missing a valid server prefix.' );
8909 }
8910
8911 $url = 'https://' . $server_prefix . '.api.mailchimp.com/3.0' . $path;
8912 if ( ! empty( $query ) ) {
8913 $url = add_query_arg( $query, $url );
8914 }
8915
8916 $args = array(
8917 'method' => strtoupper( (string) $method ),
8918 'timeout' => 20,
8919 'headers' => array(
8920 'Authorization' => 'Basic ' . base64_encode( 'anystring:' . $api_key ),
8921 'Accept' => 'application/json',
8922 ),
8923 );
8924
8925 if ( null !== $body ) {
8926 $args['headers']['Content-Type'] = 'application/json';
8927 $args['body'] = wp_json_encode( $body );
8928 }
8929
8930 $response = wp_remote_request( $url, $args );
8931 if ( is_wp_error( $response ) ) {
8932 return $response;
8933 }
8934
8935 $status_code = (int) wp_remote_retrieve_response_code( $response );
8936 $response_body = wp_remote_retrieve_body( $response );
8937 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8938
8939 if ( $status_code < 200 || $status_code >= 300 ) {
8940 $message = is_array( $data ) && ! empty( $data['detail'] ) ? (string) $data['detail'] : 'Mailchimp request failed.';
8941 return new WP_Error( 'mailchimp_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8942 }
8943
8944 return is_array( $data ) ? $data : array();
8945 }
8946 function ebook_store_brevo_request( $method, $path, $query = array(), $body = null ) {
8947 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8948 if ( $api_key === '' ) {
8949 return new WP_Error( 'brevo_missing_api_key', 'Brevo API key not configured.' );
8950 }
8951
8952 $url = 'https://api.brevo.com/v3' . $path;
8953 if ( ! empty( $query ) ) {
8954 $url = add_query_arg( $query, $url );
8955 }
8956
8957 $args = array(
8958 'method' => strtoupper( (string) $method ),
8959 'timeout' => 20,
8960 'headers' => array(
8961 'api-key' => $api_key,
8962 'Accept' => 'application/json',
8963 ),
8964 );
8965
8966 if ( null !== $body ) {
8967 $args['headers']['Content-Type'] = 'application/json';
8968 $args['body'] = wp_json_encode( $body );
8969 }
8970
8971 $response = wp_remote_request( $url, $args );
8972 if ( is_wp_error( $response ) ) {
8973 return $response;
8974 }
8975
8976 $status_code = (int) wp_remote_retrieve_response_code( $response );
8977 $response_body = wp_remote_retrieve_body( $response );
8978 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8979
8980 if ( $status_code < 200 || $status_code >= 300 ) {
8981 $message = 'Brevo request failed.';
8982 if ( is_array( $data ) ) {
8983 if ( ! empty( $data['message'] ) ) {
8984 $message = (string) $data['message'];
8985 } elseif ( ! empty( $data['code'] ) ) {
8986 $message = (string) $data['code'];
8987 }
8988 }
8989
8990 return new WP_Error( 'brevo_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8991 }
8992
8993 return is_array( $data ) ? $data : array();
8994 }
8995 function ebook_wp_embed_ebook() {
8996 wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
8997 ?>
8998 <div class="ebook_store_embed_all_items" style=" display:inline-block; clear:both; max-height:200px; overflow-x:auto; width:100%;">
8999 <?php
9000 $new = new WP_Query('post_type=ebook');
9001 while ($new->have_posts()) : $new->the_post();
9002 @$ebooks_found++;
9003 $img_cover = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_cover', true);
9004 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
9005 $c = new Currencies();
9006 $price = $c->getSymbol(get_option('paypal_currency','USD')) . number_format((float)$ebook['ebook_price'],2)
9007 ?>
9008 <div class="ebook_store_embed_ebook_item" style="width:100%; clear:both;">
9009 <?php
9010 if (!is_array($img_cover)) {
9011 $img_cover = array();
9012 }
9013 if (@$img_cover['url'] != '') {
9014 ?><img src="<?php echo $img_cover['url']; ?>" onClick="ebook_store_embed_code(<?php echo get_the_ID(); ?>);" style="background:gray; width:45px; height:65px; float: left; margin:10px; cursor: pointer; cursor: hand;" />
9015 <?php
9016 } else {
9017 ?>
9018 <div style="background:gray; width:45px; height:65px; float: left; margin:10px;" onClick="ebook_store_embed_code(<?php echo get_the_ID(); ?>);"></div>
9019 <?php
9020 }
9021
9022 ?><span class="ebook_store_embed_ebook_item_title" style="margin-top: 11px;display: inline-block;">
9023 <a href="javascript:void(0);" onClick="ebook_store_embed_code(<?php echo get_the_ID(); ?>);" title="Click to embed this ebook in the post"><b><?php echo get_the_title() ?></b></a>
9024 <a href="post.php?post=<?php echo get_the_ID(); ?>&action=edit" title="Click to edit this ebook in a new window." target="_blank"><small>[Edit]</small></a>
9025 <br />Form: <input onClick="this.select()" readonly type="text" size="29" value='[ebook_store ebook_id="<?php echo get_the_ID(); ?>"]' />
9026 <label style="white-space: nowrap;">Buy Button Only: <input onClick="this.select()" readonly type="text" size="29" value='[ebook_store_buy ebook_id="<?php echo get_the_ID(); ?>"]' /></label>
9027
9028 </span>
9029 <p>
9030 Price: <?php echo $price; ?> Download type: <?php echo $ebook['donate_or_download']; ?>
9031
9032 </p>
9033 </div>
9034 <?php
9035 endwhile;
9036 if (@$ebooks_found == 0) {
9037 echo "<p>You have not created any ebooks yet, please click <a href='post-new.php?post_type=ebook'>here</a> to start.</p>";
9038 }
9039 ?>
9040 </div>
9041 <?php
9042 }
9043 /**
9044 * Kept as a no-op shim: third-party code and older templates call this directly.
9045 * Registration now happens once in includes/ebook-store-assets.php and the
9046 * stylesheet is enqueued only where store markup is actually rendered.
9047 */
9048 function ebookstorestylesheet() {
9049 ebook_store_enqueue_frontend_styles();
9050 }
9051 function ebook_store_post_type_view( $content ) {
9052 // is_singular() asks about the main query, not about the post whose content
9053 // is being filtered. Without the in_the_loop()/is_main_query() guard, a
9054 // related-posts widget or an excerpt on a single-ebook page also had its
9055 // content replaced by the buy form.
9056 if ( ! in_the_loop() || ! is_main_query() ) {
9057 return $content;
9058 }
9059
9060 if ( get_post_type() !== 'ebook' ) {
9061 return $content;
9062 }
9063
9064 return ebook_store( get_the_ID() );
9065 }
9066 function ebook_download_links($ebook_order, $free = 0) {
9067 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
9068 $locale = ebook_store_get_locale_strings();
9069 $formats_locale = ebook_store_get_format_labels();
9070 if ($free == 0) {
9071 $action_name = 'download';
9072 } else {
9073 $action_name = 'download_free';
9074 }
9075 $ebook_id = absint( ebook_store_order_field( $ebook_order, 'ebook', 0 ) );
9076 $ebook_key = (string) ebook_store_order_field( $ebook_order, 'ebook_key' );
9077 if ( strlen( $ebook_key ) != 32 ) {
9078 $ebook_key = (string) ebook_store_order_field( $ebook_order, 'ebook_key_array', $ebook_key );
9079 }
9080 $md5_nonce = (string) ebook_store_order_field( $ebook_order, 'md5_nonce' );
9081 $order_id = absint( ebook_store_order_field( $ebook_order, 'order_id', 0 ) );
9082 $loop = array('mobi','txt','epub','zip', 'mp3', 'mp4');
9083 $meta = get_post_meta($ebook_id);
9084 $links = array();
9085 $post = get_post($ebook_id);
9086 @$slug = $post->post_name;
9087 foreach ($loop as $l) {
9088 if (@is_array($meta['ebook_wp_custom_attachment_' . $l])) {
9089 $book = unserialize($meta['ebook_wp_custom_attachment_' . $l][0]);
9090 $file = $book['file'];
9091 $link_args = array(
9092 'format' => $l,
9093 'ebook_key' => $ebook_key,
9094 'action' => $action_name,
9095 'md5_nonce' => $md5_nonce,
9096 );
9097 if ( $order_id > 0 ) {
9098 $link_args['order_id'] = $order_id;
9099 }
9100
9101 $link = add_query_arg($link_args,get_permalink($ebook_id));
9102 $link = remove_query_arg('p',$link);
9103 $link = add_query_arg(array('ebook' => $slug),$link);
9104
9105 @$links[] = "<a href=\"$link\">" . $formats_locale[$l] . " Format</a>";
9106 }
9107 }
9108 if (count($links) == 0) {
9109 $links[] = 'No other file formats are available for this ebook.';
9110 }
9111 //error_log($link);
9112 return $links;
9113 }
9114 function ebook_download_links_bonus($ebook_order, $free = 0) {
9115 // Callers pass either raw meta arrays (value at [0]) or, for gateway-built
9116 // orders, a mix of arrays and scalars (a scalar order_id / md5_nonce next
9117 // to an ebook_key array). Normalise every field to the [0] shape.
9118 $new_order = array();
9119 foreach ( (array) $ebook_order as $k => $v ) {
9120 $new_order[ $k ][0] = ebook_store_order_field( $ebook_order, $k );
9121 }
9122 $ebook_order = $new_order;
9123 //error_log(__LINE__ . ' ' . print_r($ebook_order,true));
9124
9125 $ebook_id = absint( $ebook_order['ebook'][0] );
9126 $links = array();
9127 foreach ( ebook_store_get_bonus_ebook_ids( $ebook_id ) as $bonus_id ) {
9128 $post = get_post( $bonus_id );
9129 if ( ! $post ) {
9130 continue;
9131 }
9132 $link = ebook_download_link($ebook_order, 0, 1, $bonus_id);
9133 $links[] = "<a href=\"$link\" target=\"_blank\">" . esc_html( $post->post_title ) ."</a>";
9134 }
9135 //$links[] = '<pre>'.print_r($ebook_order,true) . '</pre>';
9136 return $links;
9137 }
9138 function ebook_store_stats($ebook_id, $days) {
9139 $seconds = 86400 * $days;
9140 if ($seconds == 0) {
9141 $seconds = 157680000;
9142 }
9143 $args = array(
9144 'post_type' => 'ebook_order',
9145 'meta_query' => array(
9146 array(
9147 'key' => 'ebook',
9148 'value' => $ebook_id,
9149 )
9150 ),
9151 'date_query' => array(
9152 array(
9153 'column' => 'post_date_gmt',
9154 'after' => $seconds . ' seconds ago',
9155 ),
9156 ));
9157 // The Query
9158 $the_query = new WP_Query( $args );
9159 $out = 0;
9160 // The Loop
9161 if ( $the_query->have_posts() ) {
9162 // echo '<ul>';
9163 while ( $the_query->have_posts() ) {
9164 $the_query->the_post();
9165 $id = get_the_ID();
9166 //echo "$id<br/>";
9167 $meta = get_post_meta($id);
9168 //print_r($meta);
9169 $out += $meta['mc_gross'][0];
9170 // echo '<li>' . get_the_title() . '</li>';
9171 }
9172 // echo '</ul>';
9173 } else {
9174 // no posts found
9175 }
9176 /* Restore original Post Data */
9177 wp_reset_postdata();
9178 return $out;
9179 }
9180
9181 /**
9182 * Sales totals bucketed by date, computed in SQL.
9183 *
9184 * Both report functions used to run WP_Query with posts_per_page => -1 and then
9185 * call get_post_meta() three times per row inside the loop. Because the PayPal
9186 * IPN handler writes one meta row per field, a store with 10k orders hydrated
9187 * ~10k WP_Post objects plus several hundred thousand meta rows in a single
9188 * request. That does not get slow, it exhausts the memory limit and white-screens
9189 * the reports page — and the cache is flushed on every incoming order, so the
9190 * expensive path is the normal path on a busy store.
9191 *
9192 * @param int $ebook_id Limit to one ebook, or 0 for the whole store.
9193 * @param string $format 'Y-m-d' for daily buckets, 'Y-m' for monthly.
9194 * @param string $since Anything strtotime() understands, e.g. '30 days ago'.
9195 * @return array bucket => total.
9196 */
9197 function ebook_store_get_sales_buckets( $ebook_id, $format, $since ) {
9198 global $wpdb;
9199
9200 $ebook_id = absint( $ebook_id );
9201 $after = gmdate( 'Y-m-d H:i:s', strtotime( $since, current_time( 'timestamp', true ) ) );
9202
9203 // MySQL's DATE_FORMAT equivalents of the PHP formats used by the callers.
9204 $sql_format = $format === 'Y-m' ? '%Y-%m' : '%Y-%m-%d';
9205
9206 // Prefer the gateway's payment_date when present, fall back to the post date,
9207 // so an order recorded late still lands in the right bucket.
9208 $join = " INNER JOIN {$wpdb->postmeta} gross ON ( gross.post_id = p.ID AND gross.meta_key = 'mc_gross' )";
9209 $join .= " LEFT JOIN {$wpdb->postmeta} pdate ON ( pdate.post_id = p.ID AND pdate.meta_key = 'payment_date' )";
9210 $join .= " LEFT JOIN {$wpdb->postmeta} pstatus ON ( pstatus.post_id = p.ID AND pstatus.meta_key = 'payment_status' )";
9211
9212 $where = $wpdb->prepare( " WHERE p.post_type = 'ebook_order' AND p.post_status = 'publish' AND p.post_date_gmt >= %s", $after );
9213 $where .= " AND ( pstatus.meta_value IS NULL OR LOWER( pstatus.meta_value ) NOT IN ( 'refunded', 'reversed', 'canceled_reversal', 'denied', 'failed', 'voided', 'expired', 'partially_refunded' ) )";
9214
9215 if ( $ebook_id > 0 ) {
9216 $join .= " INNER JOIN {$wpdb->postmeta} ebook ON ( ebook.post_id = p.ID AND ebook.meta_key = 'ebook' )";
9217 $where .= $wpdb->prepare( ' AND ebook.meta_value = %d', $ebook_id );
9218 }
9219
9220 // payment_date is stored in whatever shape the gateway sent: PayPal uses
9221 // "11:51:05 Apr 02, 2026 PDT", the built-in checkout writes "04/02/2026 11:51:05".
9222 // Try both, and fall back to post_date_gmt, which is always well formed.
9223 $paid_at = "COALESCE(
9224 STR_TO_DATE( pdate.meta_value, '%H:%i:%s %b %d, %Y' ),
9225 STR_TO_DATE( pdate.meta_value, '%m/%d/%Y %H:%i:%s' ),
9226 p.post_date_gmt
9227 )";
9228
9229 $bucket = "DATE_FORMAT( {$paid_at}, '{$sql_format}' )";
9230
9231 $rows = $wpdb->get_results(
9232 "SELECT {$bucket} AS bucket, SUM( gross.meta_value + 0 ) AS total
9233 FROM {$wpdb->posts} p
9234 {$join}
9235 {$where}
9236 GROUP BY bucket",
9237 ARRAY_A
9238 );
9239
9240 $out = array();
9241 foreach ( (array) $rows as $row ) {
9242 if ( ! empty( $row['bucket'] ) ) {
9243 $out[ $row['bucket'] ] = (float) $row['total'];
9244 }
9245 }
9246
9247 return $out;
9248 }
9249
9250 function ebook_store_stats_report_30days( $ebook_id = 0 ) {
9251 $out = array();
9252
9253 for ( $i = 29; $i >= 0; $i-- ) {
9254 $timestamp = current_time( 'timestamp', true ) - ( DAY_IN_SECONDS * $i );
9255 $out[ gmdate( 'Y-m-d', $timestamp ) ] = 0;
9256 }
9257
9258 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m-d', '30 days ago' ) as $bucket => $total ) {
9259 $out[ $bucket ] = $total;
9260 }
9261
9262 return $out;
9263 }
9264
9265 function ebook_store_stats_report_2years( $ebook_id = 0 ) {
9266 $out = array();
9267
9268 for ( $i = 23; $i >= 0; $i-- ) {
9269 $timestamp = strtotime( gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp', true ) ) . " -{$i} months" );
9270 $out[ gmdate( 'Y-m', $timestamp ) ] = 0;
9271 }
9272
9273 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m', '24 months ago' ) as $bucket => $total ) {
9274 $out[ $bucket ] = $total;
9275 }
9276
9277 return $out;
9278 }
9279
9280 function ebook_store_get_reports_data( $ebook_id = 0 ) {
9281 $ebook_id = absint( $ebook_id );
9282 $cache_key = ebook_store_get_reports_cache_key( $ebook_id );
9283 $cached = get_transient( $cache_key );
9284 if ( false !== $cached && is_array( $cached ) ) {
9285 return $cached;
9286 }
9287
9288 global $wpdb;
9289
9290 $query = "
9291 SELECT
9292 p.ID,
9293 p.post_date_gmt,
9294 status.meta_value AS payment_status,
9295 gross.meta_value AS mc_gross,
9296 fee.meta_value AS mc_fee,
9297 currency.meta_value AS mc_currency,
9298 ebook.meta_value AS ebook_id,
9299 item.meta_value AS item_name,
9300 quantity.meta_value AS quantity,
9301 payment_type.meta_value AS payment_type,
9302 payer_email.meta_value AS payer_email
9303 FROM {$wpdb->posts} p
9304 LEFT JOIN {$wpdb->postmeta} status ON (p.ID = status.post_id AND status.meta_key = 'payment_status')
9305 LEFT JOIN {$wpdb->postmeta} gross ON (p.ID = gross.post_id AND gross.meta_key = 'mc_gross')
9306 LEFT JOIN {$wpdb->postmeta} fee ON (p.ID = fee.post_id AND fee.meta_key = 'mc_fee')
9307 LEFT JOIN {$wpdb->postmeta} currency ON (p.ID = currency.post_id AND currency.meta_key = 'mc_currency')
9308 LEFT JOIN {$wpdb->postmeta} ebook ON (p.ID = ebook.post_id AND ebook.meta_key = 'ebook')
9309 LEFT JOIN {$wpdb->postmeta} item ON (p.ID = item.post_id AND item.meta_key = 'item_name')
9310 LEFT JOIN {$wpdb->postmeta} quantity ON (p.ID = quantity.post_id AND quantity.meta_key = 'quantity')
9311 LEFT JOIN {$wpdb->postmeta} payment_type ON (p.ID = payment_type.post_id AND payment_type.meta_key = 'payment_type')
9312 LEFT JOIN {$wpdb->postmeta} payer_email ON (p.ID = payer_email.post_id AND payer_email.meta_key = 'payer_email')
9313 WHERE p.post_type = 'ebook_order'
9314 AND p.post_status = 'publish'
9315 ";
9316
9317 if ( $ebook_id > 0 ) {
9318 $query .= $wpdb->prepare( " AND ebook.meta_value = %d", $ebook_id );
9319 }
9320
9321 $query .= " ORDER BY p.post_date_gmt DESC";
9322
9323 $rows = $wpdb->get_results( $query, ARRAY_A );
9324
9325 $default_currency = ebook_store_get_store_currency();
9326 $currency = $default_currency;
9327 $gateway_totals = array();
9328 $status_totals = array();
9329 $currency_totals = array();
9330 $recent_orders = array();
9331 $top_ebooks = array();
9332 $item_totals = array();
9333 $currencies_seen = array();
9334 $now_gmt = current_time( 'timestamp', true );
9335 $last_24h_cutoff = $now_gmt - DAY_IN_SECONDS;
9336 $last_7d_cutoff = $now_gmt - ( 7 * DAY_IN_SECONDS );
9337 $summary = array(
9338 'total_sales' => 0.0,
9339 'total_profit' => 0.0,
9340 'last_24h_sales' => 0.0,
9341 'last_7d_sales' => 0.0,
9342 'total_orders' => count( $rows ),
9343 'reportable_orders' => 0,
9344 'paid_orders' => 0,
9345 'free_orders' => 0,
9346 'average_order' => 0.0,
9347 'latest_order_gmt' => '',
9348 'latest_order_id' => 0,
9349 'latest_order_amount' => 0.0,
9350 'currency' => $default_currency,
9351 'mixed_currencies' => false,
9352 'currency_count' => 0,
9353 'top_item' => array(
9354 'label' => __( 'No ebook sales yet', 'ebook-store' ),
9355 'units' => 0,
9356 'orders' => 0,
9357 'revenue' => 0.0,
9358 'ebook_id' => 0,
9359 ),
9360 );
9361
9362 foreach ( $rows as $row ) {
9363 $status = trim( (string) $row['payment_status'] );
9364 $status_key = $status !== '' ? sanitize_key( $status ) : 'unknown';
9365 if ( ! isset( $status_totals[ $status_key ] ) ) {
9366 $status_totals[ $status_key ] = array(
9367 'label' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9368 'count' => 0,
9369 );
9370 }
9371 $status_totals[ $status_key ]['count']++;
9372
9373 if ( ebook_store_is_negative_order_status( $status ) ) {
9374 continue;
9375 }
9376
9377 $summary['reportable_orders']++;
9378
9379 $gross = isset( $row['mc_gross'] ) ? (float) $row['mc_gross'] : 0.0;
9380 $fee = isset( $row['mc_fee'] ) ? (float) $row['mc_fee'] : 0.0;
9381 $gateway = sanitize_key( (string) $row['payment_type'] );
9382 $gateway_label = $gateway !== '' ? ucfirst( str_replace( '_', ' ', $gateway ) ) : __( 'Manual / unknown', 'ebook-store' );
9383 $order_currency = trim( (string) $row['mc_currency'] );
9384 $order_currency = $order_currency !== '' ? $order_currency : $currency;
9385 $timestamp = ! empty( $row['post_date_gmt'] ) ? strtotime( $row['post_date_gmt'] . ' UTC' ) : false;
9386 $quantity = isset( $row['quantity'] ) && is_numeric( $row['quantity'] ) && (float) $row['quantity'] > 0 ? (float) $row['quantity'] : 1.0;
9387 $row_ebook_id = isset( $row['ebook_id'] ) ? absint( $row['ebook_id'] ) : 0;
9388 $item_name = trim( isset( $row['item_name'] ) ? (string) $row['item_name'] : '' );
9389
9390 $summary['total_sales'] += $gross;
9391 $summary['total_profit'] += ( $gross - $fee );
9392
9393 if ( $gross > 0 ) {
9394 $summary['paid_orders']++;
9395 } else {
9396 $summary['free_orders']++;
9397 }
9398
9399 if ( $order_currency !== '' ) {
9400 $currencies_seen[ $order_currency ] = true;
9401 if ( $summary['currency'] === '' || $summary['currency'] === $default_currency ) {
9402 $summary['currency'] = $order_currency;
9403 }
9404 $currency = $summary['currency'];
9405 }
9406
9407 if ( $timestamp ) {
9408 if ( $timestamp >= $last_24h_cutoff ) {
9409 $summary['last_24h_sales'] += $gross;
9410 }
9411 if ( $timestamp >= $last_7d_cutoff ) {
9412 $summary['last_7d_sales'] += $gross;
9413 }
9414 if ( $summary['latest_order_gmt'] === '' || strtotime( $summary['latest_order_gmt'] . ' UTC' ) < $timestamp ) {
9415 $summary['latest_order_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
9416 $summary['latest_order_id'] = (int) $row['ID'];
9417 $summary['latest_order_amount'] = $gross;
9418 }
9419 }
9420
9421 $item_key = $row_ebook_id > 0 ? 'ebook:' . $row_ebook_id : 'item:' . md5( $item_name !== '' ? $item_name : __( 'Unknown ebook', 'ebook-store' ) );
9422 if ( ! isset( $item_totals[ $item_key ] ) ) {
9423 $item_totals[ $item_key ] = array(
9424 'ebook_id' => $row_ebook_id,
9425 'title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : '',
9426 'orders' => 0,
9427 'total' => 0.0,
9428 'last_7d' => 0.0,
9429 'last_30d' => 0.0,
9430 'units' => 0.0,
9431 'edit_url' => $row_ebook_id > 0 ? get_edit_post_link( $row_ebook_id, 'raw' ) : '',
9432 );
9433 if ( $item_totals[ $item_key ]['title'] === '' ) {
9434 $item_totals[ $item_key ]['title'] = $item_name !== '' ? $item_name : __( 'Untitled ebook', 'ebook-store' );
9435 }
9436 }
9437
9438 $item_totals[ $item_key ]['orders']++;
9439 $item_totals[ $item_key ]['total'] += $gross;
9440 $item_totals[ $item_key ]['units'] += $quantity;
9441 if ( $timestamp && $timestamp >= $last_7d_cutoff ) {
9442 $item_totals[ $item_key ]['last_7d'] += $gross;
9443 }
9444 if ( $timestamp && $timestamp >= ( $now_gmt - ( 30 * DAY_IN_SECONDS ) ) ) {
9445 $item_totals[ $item_key ]['last_30d'] += $gross;
9446 }
9447
9448 if ( ! isset( $gateway_totals[ $gateway ?: 'manual' ] ) ) {
9449 $gateway_totals[ $gateway ?: 'manual' ] = array(
9450 'label' => $gateway_label,
9451 'count' => 0,
9452 'revenue' => 0.0,
9453 );
9454 }
9455
9456 $gateway_totals[ $gateway ?: 'manual' ]['count']++;
9457 $gateway_totals[ $gateway ?: 'manual' ]['revenue'] += $gross;
9458
9459 if ( ! isset( $currency_totals[ $order_currency ] ) ) {
9460 $currency_totals[ $order_currency ] = array(
9461 'label' => $order_currency,
9462 'count' => 0,
9463 'revenue' => 0.0,
9464 );
9465 }
9466
9467 $currency_totals[ $order_currency ]['count']++;
9468 $currency_totals[ $order_currency ]['revenue'] += $gross;
9469
9470 if ( count( $recent_orders ) < 8 ) {
9471 $recent_orders[] = array(
9472 'order_id' => (int) $row['ID'],
9473 'date_gmt' => (string) $row['post_date_gmt'],
9474 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9475 'amount' => $gross,
9476 'fee' => $fee,
9477 'currency' => $order_currency,
9478 'email' => (string) $row['payer_email'],
9479 'gateway' => $gateway_label,
9480 'ebook_id' => $row_ebook_id,
9481 'ebook_title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : ( (string) $row['item_name'] !== '' ? (string) $row['item_name'] : __( 'Unknown ebook', 'ebook-store' ) ),
9482 'edit_url' => get_edit_post_link( (int) $row['ID'], 'raw' ),
9483 );
9484 }
9485 }
9486
9487 if ( $summary['paid_orders'] > 0 ) {
9488 $summary['average_order'] = $summary['total_sales'] / $summary['paid_orders'];
9489 }
9490
9491 $summary['currency_count'] = count( $currencies_seen );
9492 $summary['mixed_currencies'] = $summary['currency_count'] > 1;
9493
9494 if ( ! empty( $item_totals ) ) {
9495 uasort(
9496 $item_totals,
9497 static function ( $left, $right ) {
9498 if ( $left['total'] === $right['total'] ) {
9499 return $right['orders'] <=> $left['orders'];
9500 }
9501 return $right['total'] <=> $left['total'];
9502 }
9503 );
9504 $top_ebooks = array_values( $item_totals );
9505 $summary['top_item'] = array(
9506 'label' => $top_ebooks[0]['title'],
9507 'units' => $top_ebooks[0]['units'],
9508 'orders' => $top_ebooks[0]['orders'],
9509 'revenue' => $top_ebooks[0]['total'],
9510 'ebook_id' => $top_ebooks[0]['ebook_id'],
9511 );
9512 if ( $ebook_id === 0 ) {
9513 $top_ebooks = array_slice( $top_ebooks, 0, 8 );
9514 }
9515 }
9516
9517 uasort(
9518 $gateway_totals,
9519 static function ( $left, $right ) {
9520 if ( $left['revenue'] === $right['revenue'] ) {
9521 return $right['count'] <=> $left['count'];
9522 }
9523 return $right['revenue'] <=> $left['revenue'];
9524 }
9525 );
9526
9527 uasort(
9528 $status_totals,
9529 static function ( $left, $right ) {
9530 return $right['count'] <=> $left['count'];
9531 }
9532 );
9533
9534 uasort(
9535 $currency_totals,
9536 static function ( $left, $right ) {
9537 return $right['revenue'] <=> $left['revenue'];
9538 }
9539 );
9540
9541 $daily = ebook_store_stats_report_30days( $ebook_id );
9542 $monthly = ebook_store_stats_report_2years( $ebook_id );
9543 if ( is_array( $daily ) ) {
9544 ksort( $daily );
9545 }
9546 if ( is_array( $monthly ) ) {
9547 ksort( $monthly );
9548 }
9549
9550 $data = array(
9551 'summary' => $summary,
9552 'currency' => $currency,
9553 'ebook_id' => $ebook_id,
9554 'ebook_title' => $ebook_id > 0 ? get_the_title( $ebook_id ) : '',
9555 'daily' => is_array( $daily ) ? $daily : array(),
9556 'monthly' => is_array( $monthly ) ? $monthly : array(),
9557 'gateways' => array_values( $gateway_totals ),
9558 'statuses' => array_values( $status_totals ),
9559 'currencies' => array_values( $currency_totals ),
9560 'top_ebooks' => $top_ebooks,
9561 'recent_orders' => $recent_orders,
9562 );
9563
9564 set_transient( $cache_key, $data, 10 * MINUTE_IN_SECONDS );
9565
9566 return $data;
9567 }
9568
9569 function ebook_store_get_setup_wizard_url( $args = array() ) {
9570 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-setup-wizard' );
9571
9572 if ( ! empty( $args ) ) {
9573 $url = add_query_arg( $args, $url );
9574 }
9575
9576 return $url;
9577 }
9578
9579 function ebook_store_get_template_editor_url( $args = array() ) {
9580 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-template-editor' );
9581
9582 if ( ! empty( $args ) ) {
9583 $url = add_query_arg( $args, $url );
9584 }
9585
9586 return $url;
9587 }
9588
9589 function ebook_store_get_add_issue_url( $args = array() ) {
9590 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-add-issue-page' );
9591
9592 if ( ! empty( $args ) ) {
9593 $url = add_query_arg( $args, $url );
9594 }
9595
9596 return $url;
9597 }
9598
9599 function ebook_store_sanitize_issue_purchase_period( $raw_period, $default = '1 year' ) {
9600 $period = strtolower( trim( preg_replace( '/[^0-9a-z\s]/i', '', (string) $raw_period ) ) );
9601
9602 if ( $period === '' ) {
9603 $period = $default;
9604 }
9605
9606 $timestamp = strtotime( $period . ' ago', current_time( 'timestamp', true ) );
9607 if ( false === $timestamp ) {
9608 $period = $default;
9609 }
9610
9611 return $period;
9612 }
9613
9614 function ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period ) {
9615 $source_ebook_id = absint( $source_ebook_id );
9616 $target_ebook_id = absint( $target_ebook_id );
9617 $purchase_period = ebook_store_sanitize_issue_purchase_period( $purchase_period );
9618 $cutoff_timestamp = strtotime( $purchase_period . ' ago', current_time( 'timestamp', true ) );
9619
9620 $result = array(
9621 'purchase_period' => $purchase_period,
9622 'cutoff_timestamp' => $cutoff_timestamp,
9623 'orders' => array(),
9624 'eligible' => array(),
9625 'skipped_existing' => array(),
9626 'skipped_invalid' => array(),
9627 'skipped_negative' => array(),
9628 'counts' => array(
9629 'total' => 0,
9630 'eligible' => 0,
9631 'skipped_existing' => 0,
9632 'skipped_invalid' => 0,
9633 'skipped_negative' => 0,
9634 ),
9635 );
9636
9637 if ( $source_ebook_id < 1 ) {
9638 return $result;
9639 }
9640
9641 $query = new WP_Query(
9642 array(
9643 'post_type' => 'ebook_order',
9644 'post_status' => 'publish',
9645 'posts_per_page' => -1,
9646 'orderby' => 'date',
9647 'order' => 'DESC',
9648 'meta_query' => array(
9649 'relation' => 'AND',
9650 array(
9651 'key' => 'ebook',
9652 'value' => $source_ebook_id,
9653 ),
9654 array(
9655 'relation' => 'OR',
9656 array(
9657 'key' => 'issue_delivery_kind',
9658 'compare' => 'NOT EXISTS',
9659 ),
9660 array(
9661 'key' => 'issue_delivery_kind',
9662 'value' => 'new_issue',
9663 'compare' => '!=',
9664 ),
9665 ),
9666 ),
9667 'date_query' => array(
9668 array(
9669 'column' => 'post_date_gmt',
9670 'after' => gmdate( 'Y-m-d H:i:s', $cutoff_timestamp ),
9671 'inclusive' => true,
9672 ),
9673 ),
9674 )
9675 );
9676
9677 $orders = array();
9678 $source_order_ids = array();
9679
9680 if ( $query->have_posts() ) {
9681 while ( $query->have_posts() ) {
9682 $query->the_post();
9683 $order_id = get_the_ID();
9684 $status = (string) get_post_meta( $order_id, 'payment_status', true );
9685 $email = sanitize_email( (string) get_post_meta( $order_id, 'payer_email', true ) );
9686 $item_name = (string) get_post_meta( $order_id, 'item_name', true );
9687 $order_date = (string) get_post_meta( $order_id, 'payment_date', true );
9688 $order_timestamp = $order_date !== '' ? strtotime( $order_date ) : false;
9689 if ( ! $order_timestamp ) {
9690 $order_timestamp = strtotime( get_post_field( 'post_date_gmt', $order_id ) . ' UTC' );
9691 }
9692
9693 $order = array(
9694 'source_order_id' => $order_id,
9695 'first_name' => (string) get_post_meta( $order_id, 'first_name', true ),
9696 'last_name' => (string) get_post_meta( $order_id, 'last_name', true ),
9697 'payer_email' => $email,
9698 'user_id' => absint( get_post_meta( $order_id, 'user_id', true ) ),
9699 'residence_country' => (string) get_post_meta( $order_id, 'residence_country', true ),
9700 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9701 'item_name' => $item_name !== '' ? $item_name : get_the_title( $source_ebook_id ),
9702 'order_date' => $order_date,
9703 'order_timestamp' => $order_timestamp,
9704 'order_edit_url' => get_edit_post_link( $order_id, 'raw' ),
9705 'already_sent' => false,
9706 'existing_issue_order_id' => 0,
9707 );
9708
9709 $orders[ $order_id ] = $order;
9710 $source_order_ids[] = $order_id;
9711 }
9712 }
9713 wp_reset_postdata();
9714
9715 $existing_map = array();
9716 if ( $target_ebook_id > 0 && ! empty( $source_order_ids ) ) {
9717 $existing_query = new WP_Query(
9718 array(
9719 'post_type' => 'ebook_order',
9720 'post_status' => 'publish',
9721 'posts_per_page' => -1,
9722 'fields' => 'ids',
9723 'meta_query' => array(
9724 array(
9725 'key' => 'ebook',
9726 'value' => $target_ebook_id,
9727 ),
9728 array(
9729 'key' => 'issue_delivery_kind',
9730 'value' => 'new_issue',
9731 ),
9732 array(
9733 'key' => 'issue_source_order_id',
9734 'value' => $source_order_ids,
9735 'compare' => 'IN',
9736 ),
9737 ),
9738 )
9739 );
9740
9741 if ( $existing_query->have_posts() ) {
9742 foreach ( $existing_query->posts as $issue_order_id ) {
9743 $source_order_id = absint( get_post_meta( $issue_order_id, 'issue_source_order_id', true ) );
9744 if ( $source_order_id > 0 ) {
9745 $existing_map[ $source_order_id ] = (int) $issue_order_id;
9746 }
9747 }
9748 }
9749 wp_reset_postdata();
9750 }
9751
9752 foreach ( $orders as $order_id => $order ) {
9753 $result['counts']['total']++;
9754
9755 if ( ebook_store_is_negative_order_status( $order['status'] ) ) {
9756 $result['skipped_negative'][] = $order;
9757 $result['counts']['skipped_negative']++;
9758 continue;
9759 }
9760
9761 if ( $order['payer_email'] === '' ) {
9762 $result['skipped_invalid'][] = $order;
9763 $result['counts']['skipped_invalid']++;
9764 continue;
9765 }
9766
9767 if ( isset( $existing_map[ $order_id ] ) ) {
9768 $order['already_sent'] = true;
9769 $order['existing_issue_order_id'] = $existing_map[ $order_id ];
9770 $result['skipped_existing'][] = $order;
9771 $result['counts']['skipped_existing']++;
9772 continue;
9773 }
9774
9775 $result['eligible'][] = $order;
9776 $result['counts']['eligible']++;
9777 }
9778
9779 $result['orders'] = array_values( $orders );
9780
9781 return $result;
9782 }
9783
9784 function ebook_store_process_new_issue_batch( $source_ebook_id, $target_ebook_id, $purchase_period, $offset = 0, $limit = 20 ) {
9785 $source_ebook_id = absint( $source_ebook_id );
9786 $target_ebook_id = absint( $target_ebook_id );
9787 $offset = max( 0, absint( $offset ) );
9788 $limit = max( 1, absint( $limit ) );
9789 $preview = ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period );
9790 $eligible = isset( $preview['eligible'] ) ? $preview['eligible'] : array();
9791 $batch = array_slice( $eligible, $offset, $limit );
9792 $created = array();
9793 $failed = array();
9794
9795 foreach ( $batch as $order ) {
9796 $new_order_data = array(
9797 'first_name' => $order['first_name'],
9798 'last_name' => $order['last_name'],
9799 'mc_gross' => 0,
9800 'ebook' => $target_ebook_id,
9801 'payer_email' => $order['payer_email'],
9802 'mc_fee' => 0,
9803 'item_name' => get_the_title( $target_ebook_id ),
9804 'payment_date' => date( 'm/d/Y H:i:s' ),
9805 'txn_id' => 'Issue Delivery',
9806 'residence_country' => $order['residence_country'],
9807 'md5_nonce' => md5( microtime() . mt_rand( 1, 99999999 ) . NONCE_KEY ),
9808 'user_id' => isset( $order['user_id'] ) ? $order['user_id'] : 0,
9809 'payment_status' => 'Completed',
9810 'payment_type' => 'new_issue',
9811 );
9812
9813 $created_order_id = ebook_store_add_order( $new_order_data, true, false );
9814
9815 if ( $created_order_id ) {
9816 update_post_meta( $created_order_id, 'issue_delivery_kind', 'new_issue' );
9817 update_post_meta( $created_order_id, 'issue_source_order_id', $order['source_order_id'] );
9818 update_post_meta( $created_order_id, 'issue_source_ebook_id', $source_ebook_id );
9819 update_post_meta( $created_order_id, 'issue_target_ebook_id', $target_ebook_id );
9820 update_post_meta( $created_order_id, 'issue_purchase_period', $preview['purchase_period'] );
9821 $created[] = array(
9822 'new_order_id' => $created_order_id,
9823 'source_order_id' => $order['source_order_id'],
9824 'payer_email' => $order['payer_email'],
9825 );
9826 } else {
9827 $failed[] = $order;
9828 }
9829 }
9830
9831 return array(
9832 'preview' => $preview,
9833 'offset' => $offset,
9834 'limit' => $limit,
9835 'processed' => count( $batch ),
9836 'created' => $created,
9837 'failed' => $failed,
9838 'next_offset' => $offset + count( $batch ),
9839 'completed' => ( $offset + count( $batch ) ) >= count( $eligible ),
9840 );
9841 }
9842
9843 function ebook_store_is_wordfence_active() {
9844 if ( ! function_exists( 'is_plugin_active' ) ) {
9845 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9846 }
9847
9848 return function_exists( 'is_plugin_active' ) && is_plugin_active( 'wordfence/wordfence.php' );
9849 }
9850
9851 function ebook_store_get_template_directory_path() {
9852 return trailingslashit( plugin_dir_path( __FILE__ ) ) . 'templates/';
9853 }
9854
9855 function ebook_store_get_legacy_custom_template_directory_path() {
9856 return ebook_store_get_template_directory_path();
9857 }
9858
9859 function ebook_store_get_custom_template_directory_path() {
9860 $uploads = wp_upload_dir();
9861 if ( ! empty( $uploads['basedir'] ) ) {
9862 return trailingslashit( $uploads['basedir'] ) . 'ebook-store/templates/';
9863 }
9864
9865 return trailingslashit( WP_CONTENT_DIR ) . 'uploads/ebook-store/templates/';
9866 }
9867
9868 function ebook_store_ensure_custom_template_directory() {
9869 $directory = ebook_store_get_custom_template_directory_path();
9870 if ( is_dir( $directory ) ) {
9871 return $directory;
9872 }
9873
9874 if ( wp_mkdir_p( $directory ) ) {
9875 return $directory;
9876 }
9877
9878 return '';
9879 }
9880
9881 function ebook_store_sync_custom_template_name( $template_code, $template_name ) {
9882 $template_code = (string) $template_code;
9883 $template_name = trim( (string) $template_name );
9884 if ( $template_name === '' ) {
9885 return $template_code;
9886 }
9887
9888 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
9889 return (string) preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
9890 }
9891
9892 return "/*\nTemplate Name: " . $template_name . "\n*/\n" . $template_code;
9893 }
9894
9895 function ebook_store_get_unique_custom_template_identity( $preferred_name, $preferred_key = '' ) {
9896 $preferred_name = trim( (string) $preferred_name );
9897 $preferred_key = sanitize_key( (string) $preferred_key );
9898 if ( $preferred_name === '' ) {
9899 $preferred_name = __( 'Custom template', 'ebook-store' );
9900 }
9901 if ( $preferred_key === '' ) {
9902 $preferred_key = sanitize_title( $preferred_name );
9903 }
9904 if ( $preferred_key === '' ) {
9905 $preferred_key = 'custom-template';
9906 }
9907
9908 $editable = ebook_store_get_template_editor_files();
9909 $directory = ebook_store_get_custom_template_directory_path();
9910 $name = $preferred_name;
9911 $key = $preferred_key;
9912 $index = 2;
9913
9914 while ( isset( $editable[ $key ] ) || file_exists( $directory . $key . '.php' ) ) {
9915 $key = $preferred_key . '-' . $index;
9916 $name = $preferred_name . ' ' . $index;
9917 $index++;
9918 }
9919
9920 return array(
9921 'name' => $name,
9922 'key' => $key,
9923 );
9924 }
9925
9926 function ebook_store_create_custom_template_from_code( $template_name, $template_code, $preferred_key = '' ) {
9927 $directory = ebook_store_ensure_custom_template_directory();
9928 if ( $directory === '' ) {
9929 return new WP_Error( 'ebook_store_template_directory_unavailable', __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) );
9930 }
9931
9932 $identity = ebook_store_get_unique_custom_template_identity( $template_name, $preferred_key );
9933 $template_file = $directory . $identity['key'] . '.php';
9934 $template_code = ebook_store_sync_custom_template_name( $template_code, $identity['name'] );
9935 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
9936 if ( is_wp_error( $syntax_check ) ) {
9937 return $syntax_check;
9938 }
9939
9940 $result = @file_put_contents( $template_file, $template_code );
9941 if ( $result === false ) {
9942 return new WP_Error( 'ebook_store_template_create_failed', __( 'WordPress could not save the custom template file.', 'ebook-store' ) );
9943 }
9944
9945 return array(
9946 'key' => $identity['key'],
9947 'name' => $identity['name'],
9948 'file' => $template_file,
9949 'code' => $template_code,
9950 );
9951 }
9952
9953 function ebook_store_migrate_legacy_custom_templates() {
9954 static $migrated = false;
9955 if ( $migrated ) {
9956 return;
9957 }
9958 $migrated = true;
9959
9960 $legacy_dir = ebook_store_get_legacy_custom_template_directory_path();
9961 $custom_dir = ebook_store_ensure_custom_template_directory();
9962 if ( $custom_dir === '' || ! is_dir( $legacy_dir ) ) {
9963 return;
9964 }
9965
9966 $core_files = array_map( 'basename', wp_list_pluck( ebook_store_get_core_form_templates(), 'file' ) );
9967 foreach ( (array) glob( $legacy_dir . '*.php' ) as $file ) {
9968 $basename = basename( $file );
9969 if ( in_array( $basename, $core_files, true ) ) {
9970 continue;
9971 }
9972
9973 $destination = $custom_dir . $basename;
9974 if ( file_exists( $destination ) ) {
9975 continue;
9976 }
9977
9978 @copy( $file, $destination );
9979 }
9980 }
9981
9982 function ebook_store_get_core_form_templates() {
9983 return array(
9984 'modern' => array(
9985 'label' => __( 'Modern', 'ebook-store' ),
9986 'file' => ebook_store_get_template_directory_path() . 'modern.php',
9987 'core' => true,
9988 ),
9989 '2026' => array(
9990 'label' => __( '2026', 'ebook-store' ),
9991 'file' => ebook_store_get_template_directory_path() . '2026.php',
9992 'core' => true,
9993 ),
9994 'hello-elementor-2026' => array(
9995 'label' => __( 'Hello Elementor 2026', 'ebook-store' ),
9996 'file' => ebook_store_get_template_directory_path() . 'hello-elementor-2026.php',
9997 'core' => true,
9998 ),
9999 'legacy' => array(
10000 'label' => __( 'Legacy', 'ebook-store' ),
10001 'file' => ebook_store_get_template_directory_path() . 'current.php',
10002 'core' => true,
10003 ),
10004 );
10005 }
10006
10007 function ebook_store_get_core_template_keys() {
10008 return array_keys( ebook_store_get_core_form_templates() );
10009 }
10010
10011 function ebook_store_template_editor_humanize_key( $key ) {
10012 $key = trim( (string) $key );
10013 if ( $key === '' ) {
10014 return __( 'Custom template', 'ebook-store' );
10015 }
10016
10017 return ucwords( str_replace( array( '-', '_' ), ' ', $key ) );
10018 }
10019
10020 function ebook_store_can_access_template_editor() {
10021 if ( is_multisite() ) {
10022 return is_super_admin();
10023 }
10024
10025 return current_user_can( 'manage_options' );
10026 }
10027
10028 function ebook_store_get_template_header_label( $file ) {
10029 if ( ! file_exists( $file ) ) {
10030 return '';
10031 }
10032
10033 $contents = (string) @file_get_contents( $file, false, null, 0, 2048 );
10034 if ( $contents === '' ) {
10035 return '';
10036 }
10037
10038 if ( preg_match( '/Template Name:\s*(.+)$/mi', $contents, $matches ) ) {
10039 return trim( $matches[1] );
10040 }
10041
10042 return '';
10043 }
10044
10045 function ebook_store_get_custom_form_templates() {
10046 ebook_store_migrate_legacy_custom_templates();
10047 $template_dir = ebook_store_get_custom_template_directory_path();
10048 $core_templates = ebook_store_get_core_form_templates();
10049 $core_files = array_map( 'basename', wp_list_pluck( $core_templates, 'file' ) );
10050 $templates = array();
10051
10052 foreach ( (array) glob( $template_dir . '*.php' ) as $file ) {
10053 $basename = basename( $file );
10054 if ( in_array( $basename, $core_files, true ) ) {
10055 continue;
10056 }
10057
10058 $key = sanitize_key( basename( $basename, '.php' ) );
10059 if ( $key === '' ) {
10060 continue;
10061 }
10062
10063 $label = ebook_store_get_template_header_label( $file );
10064 if ( $label === '' ) {
10065 $label = ebook_store_template_editor_humanize_key( $key );
10066 }
10067
10068 $templates[ $key ] = array(
10069 'label' => $label,
10070 'file' => $file,
10071 'core' => false,
10072 );
10073 }
10074
10075 ksort( $templates );
10076
10077 return $templates;
10078 }
10079
10080 function ebook_store_get_template_usage_stats( $template_key ) {
10081 $template_key = sanitize_key( (string) $template_key );
10082 $stats = array(
10083 'ebooks' => 0,
10084 'default' => false,
10085 );
10086
10087 if ( $template_key === '' ) {
10088 return $stats;
10089 }
10090
10091 $ebook_ids = get_posts(
10092 array(
10093 'post_type' => 'ebook',
10094 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
10095 'posts_per_page' => -1,
10096 'fields' => 'ids',
10097 'no_found_rows' => true,
10098 )
10099 );
10100
10101 foreach ( (array) $ebook_ids as $ebook_id ) {
10102 $ebook_meta = get_post_meta( (int) $ebook_id, 'ebook', true );
10103 $current_template = '';
10104 if ( is_array( $ebook_meta ) && ! empty( $ebook_meta['form_template'] ) ) {
10105 $current_template = (string) $ebook_meta['form_template'];
10106 }
10107
10108 if ( function_exists( 'ebook_store_resolve_form_template' ) && ebook_store_resolve_form_template( $current_template ) === $template_key ) {
10109 $stats['ebooks']++;
10110 }
10111 }
10112
10113 $default_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( get_option( 'ebook_store_form_template', '2026' ) ) : sanitize_key( (string) get_option( 'ebook_store_form_template', '2026' ) );
10114 $stats['default'] = $default_template === $template_key;
10115
10116 return $stats;
10117 }
10118
10119 function ebook_store_reassign_template_usage( $from_template, $to_template ) {
10120 $from_template = sanitize_key( (string) $from_template );
10121 $to_template = sanitize_key( (string) $to_template );
10122 $result = array(
10123 'ebook_count' => 0,
10124 'default_updated' => false,
10125 );
10126
10127 if ( $from_template === '' || $to_template === '' || $from_template === $to_template ) {
10128 return $result;
10129 }
10130
10131 $ebook_ids = get_posts(
10132 array(
10133 'post_type' => 'ebook',
10134 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
10135 'posts_per_page' => -1,
10136 'fields' => 'ids',
10137 'no_found_rows' => true,
10138 )
10139 );
10140
10141 foreach ( (array) $ebook_ids as $ebook_id ) {
10142 $ebook_id = (int) $ebook_id;
10143 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
10144 if ( ! is_array( $ebook_meta ) ) {
10145 $ebook_meta = array();
10146 }
10147
10148 $current_template = isset( $ebook_meta['form_template'] ) ? (string) $ebook_meta['form_template'] : '';
10149 $resolved_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( $current_template ) : sanitize_key( $current_template );
10150 if ( $resolved_template !== $from_template ) {
10151 continue;
10152 }
10153
10154 $ebook_meta['form_template'] = $to_template;
10155 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
10156 $result['ebook_count']++;
10157 }
10158
10159 $default_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( get_option( 'ebook_store_form_template', '2026' ) ) : sanitize_key( (string) get_option( 'ebook_store_form_template', '2026' ) );
10160 if ( $default_template === $from_template ) {
10161 update_option( 'ebook_store_form_template', $to_template );
10162 $result['default_updated'] = true;
10163 }
10164
10165 return $result;
10166 }
10167
10168 function ebook_store_is_core_template( $template_key ) {
10169 return in_array( sanitize_key( (string) $template_key ), ebook_store_get_core_template_keys(), true );
10170 }
10171
10172 function ebook_store_get_template_editor_files() {
10173 $templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
10174 $editable = array();
10175
10176 foreach ( $templates as $template_key => $template ) {
10177 if ( empty( $template['file'] ) || ! file_exists( $template['file'] ) ) {
10178 continue;
10179 }
10180
10181 $editable[ $template_key ] = array(
10182 'key' => (string) $template_key,
10183 'label' => isset( $template['label'] ) ? $template['label'] : ucfirst( $template_key ),
10184 'file' => $template['file'],
10185 'filename' => basename( $template['file'] ),
10186 'core' => ! empty( $template['core'] ),
10187 );
10188 }
10189
10190 return $editable;
10191 }
10192
10193 function ebook_store_get_template_editor_current_key( $requested = '' ) {
10194 $requested = sanitize_key( (string) $requested );
10195 $editable = ebook_store_get_template_editor_files();
10196
10197 if ( $requested !== '' && isset( $editable[ $requested ] ) ) {
10198 return $requested;
10199 }
10200
10201 $keys = array_keys( $editable );
10202
10203 return ! empty( $keys ) ? $keys[0] : '';
10204 }
10205
10206 function ebook_store_get_empty_template_scaffold( $template_name ) {
10207 $template_name = trim( (string) $template_name );
10208
10209 return "<?php\n/*\nTemplate Name: " . $template_name . "\n*/\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\n\$items = isset( \$context['items'] ) && is_array( \$context['items'] ) ? \$context['items'] : array();\n?>\n<div class=\"ebook-store-template-custom\">\n\t<?php foreach ( \$items as \$item ) : ?>\n\t\t<article class=\"ebook-custom-card\">\n\t\t\t<h3><?php echo esc_html( \$item['title'] ); ?></h3>\n\t\t\t<div><?php echo \$item['content_html']; ?></div>\n\t\t\t<?php echo \$item['form_html']; ?>\n\t\t</article>\n\t<?php endforeach; ?>\n</div>\n";
10210 }
10211
10212 function ebook_store_validate_template_php_syntax( $template_code ) {
10213 $template_code = (string) $template_code;
10214
10215 if ( trim( $template_code ) === '' ) {
10216 return new WP_Error( 'ebook_store_template_empty', __( 'Template code cannot be empty.', 'ebook-store' ) );
10217 }
10218
10219 if ( strpos( $template_code, '<?php' ) === false ) {
10220 return new WP_Error( 'ebook_store_template_missing_php', __( 'Template code must contain PHP tags so it can be loaded safely by the plugin.', 'ebook-store' ) );
10221 }
10222
10223 if ( ! function_exists( 'shell_exec' ) ) {
10224 return true;
10225 }
10226
10227 $php_binary = defined( 'PHP_BINARY' ) && PHP_BINARY ? PHP_BINARY : '';
10228 if ( ! $php_binary || ! is_executable( $php_binary ) ) {
10229 $php_binary = trim( (string) @shell_exec( 'command -v php 2>/dev/null' ) );
10230 }
10231
10232 if ( $php_binary === '' ) {
10233 return true;
10234 }
10235
10236 $temp_file = wp_tempnam( 'ebook-store-template-lint' );
10237 if ( ! $temp_file ) {
10238 return true;
10239 }
10240
10241 @file_put_contents( $temp_file, $template_code );
10242 $command = escapeshellarg( $php_binary ) . ' -l ' . escapeshellarg( $temp_file ) . ' 2>&1';
10243 $output = @shell_exec( $command );
10244 @unlink( $temp_file );
10245
10246 if ( ! is_string( $output ) ) {
10247 return true;
10248 }
10249
10250 if ( stripos( $output, 'No syntax errors detected' ) !== false ) {
10251 return true;
10252 }
10253
10254 $message = trim( preg_replace( '/\s+in\s+.+?on line\s+/i', ' on line ', $output ) );
10255 if ( $message === '' ) {
10256 $message = __( 'PHP syntax validation failed for the template code.', 'ebook-store' );
10257 }
10258
10259 return new WP_Error( 'ebook_store_template_syntax_error', $message );
10260 }
10261
10262 function ebook_store_template_editor_create_template() {
10263 $template_name = isset( $_POST['new_template_name'] ) ? sanitize_text_field( wp_unslash( $_POST['new_template_name'] ) ) : '';
10264 $template_source = isset( $_POST['new_template_source'] ) ? sanitize_key( wp_unslash( $_POST['new_template_source'] ) ) : '';
10265 $template_key = sanitize_title( $template_name );
10266 $editable = ebook_store_get_template_editor_files();
10267
10268 if ( $template_name === '' ) {
10269 return array(
10270 'errors' => array( __( 'Please enter a template name.', 'ebook-store' ) ),
10271 );
10272 }
10273
10274 if ( $template_key === '' ) {
10275 return array(
10276 'errors' => array( __( 'The template name must contain letters or numbers.', 'ebook-store' ) ),
10277 );
10278 }
10279
10280 if ( isset( $editable[ $template_key ] ) ) {
10281 return array(
10282 'errors' => array( __( 'A template with that name already exists.', 'ebook-store' ) ),
10283 );
10284 }
10285
10286 $template_dir = ebook_store_ensure_custom_template_directory();
10287 if ( $template_dir === '' ) {
10288 return array(
10289 'errors' => array( __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) ),
10290 );
10291 }
10292
10293 $template_file = $template_dir . $template_key . '.php';
10294 if ( file_exists( $template_file ) ) {
10295 return array(
10296 'errors' => array( __( 'A template file with that slug already exists.', 'ebook-store' ) ),
10297 );
10298 }
10299
10300 $template_code = '';
10301 if ( $template_source !== '' && $template_source !== 'none' && isset( $editable[ $template_source ] ) && file_exists( $editable[ $template_source ]['file'] ) ) {
10302 $template_code = (string) file_get_contents( $editable[ $template_source ]['file'] );
10303 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
10304 $template_code = preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
10305 }
10306 } else {
10307 $template_code = ebook_store_get_empty_template_scaffold( $template_name );
10308 }
10309
10310 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
10311 if ( is_wp_error( $syntax_check ) ) {
10312 return array(
10313 'errors' => array( $syntax_check->get_error_message() ),
10314 );
10315 }
10316
10317 $result = @file_put_contents( $template_file, $template_code );
10318 if ( $result === false ) {
10319 return array(
10320 'errors' => array( __( 'WordPress could not create the new template file.', 'ebook-store' ) ),
10321 );
10322 }
10323
10324 wp_safe_redirect(
10325 ebook_store_get_template_editor_url(
10326 array(
10327 'template' => $template_key,
10328 'created' => 1,
10329 )
10330 )
10331 );
10332 exit;
10333 }
10334
10335 function ebook_store_template_editor_delete_template() {
10336 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
10337 $editable = ebook_store_get_template_editor_files();
10338
10339 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
10340 return array(
10341 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
10342 );
10343 }
10344
10345 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
10346 return array(
10347 'errors' => array( __( 'Built-in templates cannot be deleted.', 'ebook-store' ) ),
10348 'template_key' => $template_key,
10349 );
10350 }
10351
10352 if ( ! file_exists( $editable[ $template_key ]['file'] ) || ! is_writable( $editable[ $template_key ]['file'] ) ) {
10353 return array(
10354 'errors' => array( __( 'That template file could not be deleted because it is missing or not writable.', 'ebook-store' ) ),
10355 'template_key' => $template_key,
10356 );
10357 }
10358
10359 $result = @unlink( $editable[ $template_key ]['file'] );
10360 if ( ! $result ) {
10361 return array(
10362 'errors' => array( __( 'WordPress could not delete that template file.', 'ebook-store' ) ),
10363 'template_key' => $template_key,
10364 );
10365 }
10366
10367 wp_safe_redirect(
10368 ebook_store_get_template_editor_url(
10369 array(
10370 'template' => 'modern',
10371 'deleted' => 1,
10372 )
10373 )
10374 );
10375 exit;
10376 }
10377
10378 function ebook_store_template_editor_save_template() {
10379 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
10380 $editable = ebook_store_get_template_editor_files();
10381
10382 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
10383 return array(
10384 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
10385 );
10386 }
10387
10388 $template_code = isset( $_POST['template_code'] ) ? wp_unslash( $_POST['template_code'] ) : '';
10389
10390 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
10391 if ( is_wp_error( $syntax_check ) ) {
10392 return array(
10393 'errors' => array( $syntax_check->get_error_message() ),
10394 'template_key' => $template_key,
10395 'template_code' => $template_code,
10396 );
10397 }
10398
10399 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
10400 $preferred_name = sprintf( __( '%s Custom', 'ebook-store' ), $editable[ $template_key ]['label'] );
10401 $preferred_key = sanitize_title( $template_key . '-custom' );
10402 $created_template = ebook_store_create_custom_template_from_code( $preferred_name, $template_code, $preferred_key );
10403 if ( is_wp_error( $created_template ) ) {
10404 return array(
10405 'errors' => array( $created_template->get_error_message() ),
10406 'template_key' => $template_key,
10407 'template_code' => $template_code,
10408 );
10409 }
10410
10411 $reassign = ! empty( $_POST['template_editor_reassign_books'] );
10412 $assignment = array(
10413 'ebook_count' => 0,
10414 'default_updated' => false,
10415 );
10416 if ( $reassign ) {
10417 $assignment = ebook_store_reassign_template_usage( $template_key, $created_template['key'] );
10418 }
10419
10420 wp_safe_redirect(
10421 ebook_store_get_template_editor_url(
10422 array(
10423 'template' => $created_template['key'],
10424 'created' => 1,
10425 'forked_from_core' => 1,
10426 'copied_from' => $template_key,
10427 'reassigned' => $reassign ? 1 : 0,
10428 'reassigned_ebooks' => isset( $assignment['ebook_count'] ) ? (int) $assignment['ebook_count'] : 0,
10429 'reassigned_default' => ! empty( $assignment['default_updated'] ) ? 1 : 0,
10430 )
10431 )
10432 );
10433 exit;
10434 }
10435
10436 $template_file = $editable[ $template_key ]['file'];
10437 if ( ! is_writable( $template_file ) ) {
10438 return array(
10439 'errors' => array( __( 'That template file is not writable on the server.', 'ebook-store' ) ),
10440 'template_key' => $template_key,
10441 'template_code' => $template_code,
10442 );
10443 }
10444
10445 $result = @file_put_contents( $template_file, $template_code );
10446
10447 if ( $result === false ) {
10448 return array(
10449 'errors' => array( __( 'WordPress could not save the updated template code.', 'ebook-store' ) ),
10450 'template_key' => $template_key,
10451 'template_code' => $template_code,
10452 );
10453 }
10454
10455 wp_safe_redirect(
10456 ebook_store_get_template_editor_url(
10457 array(
10458 'template' => $template_key,
10459 'saved' => 1,
10460 )
10461 )
10462 );
10463 exit;
10464 }
10465
10466 function ebook_store_template_editor_handle_submission() {
10467 if ( empty( $_POST['ebook_store_template_editor_action'] ) ) {
10468 return null;
10469 }
10470
10471 if ( ! ebook_store_can_access_template_editor() ) {
10472 return array(
10473 'errors' => array( __( 'You do not have permission to edit Ebook Store templates.', 'ebook-store' ) ),
10474 );
10475 }
10476
10477 check_admin_referer( 'ebook_store_template_editor' );
10478
10479 $action_type = isset( $_POST['template_editor_action_type'] ) ? sanitize_key( wp_unslash( $_POST['template_editor_action_type'] ) ) : 'save';
10480
10481 if ( $action_type === 'create' ) {
10482 return ebook_store_template_editor_create_template();
10483 }
10484
10485 if ( $action_type === 'delete' ) {
10486 return ebook_store_template_editor_delete_template();
10487 }
10488
10489 return ebook_store_template_editor_save_template();
10490 }
10491
10492 function ebook_store_template_editor_page_callback() {
10493 if ( ! ebook_store_can_access_template_editor() ) {
10494 wp_die(
10495 esc_html__( 'You do not have permission to access the Ebook Store template editor.', 'ebook-store' ),
10496 esc_html__( 'Access denied', 'ebook-store' ),
10497 array( 'response' => 403 )
10498 );
10499 }
10500
10501 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
10502 $locale = ebook_store_get_locale_strings();
10503 $formats_locale = ebook_store_get_format_labels();
10504 wp_enqueue_style( 'ebookstorestylesheet' );
10505
10506 $code_editor_settings = wp_enqueue_code_editor(
10507 array(
10508 'type' => 'application/x-httpd-php',
10509 'codemirror' => array(
10510 'indentUnit' => 4,
10511 'tabSize' => 4,
10512 'lineNumbers' => true,
10513 'lineWrapping' => false,
10514 'mode' => 'application/x-httpd-php',
10515 ),
10516 )
10517 );
10518
10519 wp_enqueue_script( 'code-editor' );
10520 wp_enqueue_script( 'wp-theme-plugin-editor' );
10521 wp_enqueue_style( 'code-editor' );
10522
10523 $submission = null;
10524 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_template_editor_action'] ) ) {
10525 $submission = ebook_store_template_editor_handle_submission();
10526 }
10527
10528 $editable = ebook_store_get_template_editor_files();
10529 $current_template = ebook_store_get_template_editor_current_key( isset( $_REQUEST['template'] ) ? wp_unslash( $_REQUEST['template'] ) : '' );
10530 if ( $submission && ! empty( $submission['template_key'] ) ) {
10531 $current_template = ebook_store_get_template_editor_current_key( $submission['template_key'] );
10532 }
10533
10534 $current = $current_template !== '' && isset( $editable[ $current_template ] ) ? $editable[ $current_template ] : null;
10535 $current_code = '';
10536
10537 if ( $submission && array_key_exists( 'template_code', (array) $submission ) ) {
10538 $current_code = (string) $submission['template_code'];
10539 } elseif ( $current && file_exists( $current['file'] ) ) {
10540 $current_code = (string) file_get_contents( $current['file'] );
10541 }
10542
10543 $saved = isset( $_GET['saved'] ) && (int) $_GET['saved'] === 1;
10544 $created = isset( $_GET['created'] ) && (int) $_GET['created'] === 1;
10545 $deleted = isset( $_GET['deleted'] ) && (int) $_GET['deleted'] === 1;
10546 $forked_from_core = isset( $_GET['forked_from_core'] ) && (int) $_GET['forked_from_core'] === 1;
10547 $copied_from = isset( $_GET['copied_from'] ) ? sanitize_key( wp_unslash( $_GET['copied_from'] ) ) : '';
10548 $reassigned = isset( $_GET['reassigned'] ) && (int) $_GET['reassigned'] === 1;
10549 $reassigned_ebooks = isset( $_GET['reassigned_ebooks'] ) ? max( 0, (int) $_GET['reassigned_ebooks'] ) : 0;
10550 $reassigned_default = isset( $_GET['reassigned_default'] ) && (int) $_GET['reassigned_default'] === 1;
10551 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
10552 $wordfence_active = ebook_store_is_wordfence_active();
10553 $current_usage = $current ? ebook_store_get_template_usage_stats( $current['key'] ) : array(
10554 'ebooks' => 0,
10555 'default' => false,
10556 );
10557
10558 ob_start();
10559 ?>
10560 <div class="wrap ebook-template-editor">
10561 <div class="ebook-template-editor__hero">
10562 <div class="ebook-template-editor__hero-copy">
10563 <span class="ebook-template-editor__eyebrow"><?php esc_html_e( 'Developer tools', 'ebook-store' ); ?></span>
10564 <h1><?php esc_html_e( 'Button templates', 'ebook-store' ); ?></h1>
10565 <p><?php esc_html_e( 'Edit the PHP templates that drive Ebook Store order-form layouts. Built-in templates are forked into update-safe custom copies when you save them, while custom templates are saved in their own separate files.', 'ebook-store' ); ?></p>
10566 <p><?php esc_html_e( 'If you want a different design, you can copy the template code into ChatGPT and ask for changes that match your site, then paste the updated code back here.', 'ebook-store' ); ?></p>
10567 </div>
10568 <div class="ebook-template-editor__hero-meta">
10569 <span class="ebook-template-editor__pill"><?php esc_html_e( 'PHP-safe editing', 'ebook-store' ); ?></span>
10570 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Update-safe custom copies', 'ebook-store' ); ?></span>
10571 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Template-scoped access only', 'ebook-store' ); ?></span>
10572 </div>
10573 </div>
10574
10575 <?php if ( $saved && $current ) : ?>
10576 <div class="notice notice-success is-dismissible">
10577 <p>
10578 <strong><?php esc_html_e( 'Template saved.', 'ebook-store' ); ?></strong>
10579 <?php
10580 printf(
10581 esc_html__( '%s was updated successfully.', 'ebook-store' ),
10582 esc_html( $current['label'] )
10583 );
10584 ?>
10585 </p>
10586 </div>
10587 <?php endif; ?>
10588
10589 <?php if ( $created && $current && ! $forked_from_core ) : ?>
10590 <div class="notice notice-success is-dismissible">
10591 <p>
10592 <strong><?php esc_html_e( 'Template created.', 'ebook-store' ); ?></strong>
10593 <?php
10594 printf(
10595 esc_html__( '%s is now available in the template selector and stored separately from plugin updates.', 'ebook-store' ),
10596 esc_html( $current['label'] )
10597 );
10598 ?>
10599 </p>
10600 </div>
10601 <?php endif; ?>
10602
10603 <?php if ( $created && $current && $forked_from_core ) : ?>
10604 <div class="notice notice-success is-dismissible">
10605 <p>
10606 <strong><?php esc_html_e( 'Custom template created.', 'ebook-store' ); ?></strong>
10607 <?php
10608 printf(
10609 esc_html__( '%1$s was saved as the new custom template %2$s, so it will survive plugin updates.', 'ebook-store' ),
10610 esc_html( $copied_from !== '' && isset( $editable[ $copied_from ] ) ? $editable[ $copied_from ]['label'] : __( 'Built-in template', 'ebook-store' ) ),
10611 esc_html( $current['label'] )
10612 );
10613 ?>
10614 </p>
10615 <?php if ( $reassigned ) : ?>
10616 <p>
10617 <?php
10618 printf(
10619 esc_html__( '%1$d ebook(s) were reassigned to use this custom template.%2$s', 'ebook-store' ),
10620 (int) $reassigned_ebooks,
10621 $reassigned_default ? ' ' . esc_html__( 'The default template setting was updated too.', 'ebook-store' ) : ''
10622 );
10623 ?>
10624 </p>
10625 <?php endif; ?>
10626 </div>
10627 <?php endif; ?>
10628
10629 <?php if ( $deleted ) : ?>
10630 <div class="notice notice-success is-dismissible">
10631 <p><strong><?php esc_html_e( 'Template deleted.', 'ebook-store' ); ?></strong> <?php esc_html_e( 'The custom template file was removed.', 'ebook-store' ); ?></p>
10632 </div>
10633 <?php endif; ?>
10634
10635 <?php if ( ! empty( $errors ) ) : ?>
10636 <div class="notice notice-error">
10637 <p><strong><?php esc_html_e( 'The template could not be saved yet.', 'ebook-store' ); ?></strong></p>
10638 <ul class="ebook-template-editor__notice-list">
10639 <?php foreach ( $errors as $error ) : ?>
10640 <li><?php echo esc_html( $error ); ?></li>
10641 <?php endforeach; ?>
10642 </ul>
10643 </div>
10644 <?php endif; ?>
10645
10646 <?php if ( $wordfence_active ) : ?>
10647 <div class="notice notice-warning">
10648 <p>
10649 <strong><?php esc_html_e( 'Wordfence note:', 'ebook-store' ); ?></strong>
10650 <?php esc_html_e( 'Saving PHP template files can trigger a Wordfence false positive because this editor writes PHP template files for Ebook Store custom layouts.', 'ebook-store' ); ?>
10651 </p>
10652 <p><?php esc_html_e( 'If Wordfence blocks the save, review the prompt and click “I am certain this is a false positive.” to allow the custom template save.', 'ebook-store' ); ?></p>
10653 </div>
10654 <?php endif; ?>
10655
10656 <div class="ebook-template-editor__layout">
10657 <aside class="ebook-template-editor__sidebar">
10658 <div class="ebook-template-editor__nav-card">
10659 <div class="ebook-template-editor__nav-header">
10660 <h2><?php esc_html_e( 'Templates', 'ebook-store' ); ?></h2>
10661 <button type="button" class="button button-secondary ebook-template-editor__create-trigger" data-template-modal-open="create"><?php esc_html_e( 'New template', 'ebook-store' ); ?></button>
10662 </div>
10663 <div class="ebook-template-editor__nav">
10664 <?php foreach ( $editable as $template ) : ?>
10665 <a class="ebook-template-editor__nav-item<?php echo $current_template === $template['key'] ? ' is-active' : ''; ?>" href="<?php echo esc_url( ebook_store_get_template_editor_url( array( 'template' => $template['key'] ) ) ); ?>">
10666 <strong><?php echo esc_html( $template['label'] ); ?></strong>
10667 <small><?php echo esc_html( $template['filename'] ); ?></small>
10668 </a>
10669 <?php endforeach; ?>
10670 </div>
10671 </div>
10672 </aside>
10673
10674 <div class="ebook-template-editor__main">
10675 <?php if ( $current ) : ?>
10676 <form method="post" class="ebook-template-editor__form">
10677 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10678 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10679 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
10680
10681 <div class="ebook-template-editor__panel">
10682 <div class="ebook-template-editor__panel-header">
10683 <div>
10684 <h2><?php echo esc_html( $current['label'] ); ?></h2>
10685 <p>
10686 <?php
10687 echo ! empty( $current['core'] )
10688 ? esc_html__( 'Built-in templates are update-protected. Saving here creates a new custom template copy instead of overwriting the plugin file.', 'ebook-store' )
10689 : esc_html__( 'Editing the custom PHP template file used by Ebook Store for this layout.', 'ebook-store' );
10690 ?>
10691 </p>
10692 </div>
10693 <div class="ebook-template-editor__header-side">
10694 <div class="ebook-template-editor__file-meta">
10695 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $current['filename'] ); ?></span>
10696 <span><strong><?php esc_html_e( 'Writable:', 'ebook-store' ); ?></strong> <?php echo esc_html( ! empty( $current['core'] ) ? __( 'Copied to custom template on save', 'ebook-store' ) : ( is_writable( $current['file'] ) ? __( 'Yes', 'ebook-store' ) : __( 'No', 'ebook-store' ) ) ); ?></span>
10697 </div>
10698 <?php if ( empty( $current['core'] ) ) : ?>
10699 <button type="button" class="button button-link-delete ebook-template-editor__delete-trigger" data-template-modal-open="delete"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></button>
10700 <?php endif; ?>
10701 </div>
10702 </div>
10703
10704 <div class="ebook-template-editor__editor-wrap">
10705 <textarea id="ebook_store_template_code" name="template_code" class="large-text code" rows="28" spellcheck="false"><?php echo esc_textarea( $current_code ); ?></textarea>
10706 </div>
10707
10708 <?php if ( ! empty( $current['core'] ) ) : ?>
10709 <div class="ebook-template-editor__field">
10710 <label>
10711 <input type="checkbox" name="template_editor_reassign_books" value="1" />
10712 <?php
10713 printf(
10714 esc_html__( 'Also assign the new custom template automatically to ebooks currently using %s.', 'ebook-store' ),
10715 esc_html( $current['label'] )
10716 );
10717 ?>
10718 </label>
10719 <p class="description">
10720 <?php
10721 printf(
10722 esc_html__( 'Right now %1$d ebook(s)%2$s resolve to this template.', 'ebook-store' ),
10723 (int) $current_usage['ebooks'],
10724 ! empty( $current_usage['default'] ) ? ' ' . esc_html__( 'The global default template also points to it.', 'ebook-store' ) : ''
10725 );
10726 ?>
10727 </p>
10728 </div>
10729 <?php endif; ?>
10730
10731 <div class="ebook-template-editor__actions">
10732 <input type="hidden" name="template_editor_action_type" value="save" />
10733 <button type="submit" class="button button-primary button-hero"><?php echo esc_html( ! empty( $current['core'] ) ? __( 'Save as custom template', 'ebook-store' ) : __( 'Save template', 'ebook-store' ) ); ?></button>
10734 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_template_editor_url( array( 'template' => $current['key'] ) ) ); ?>"><?php esc_html_e( 'Reload file', 'ebook-store' ); ?></a>
10735 </div>
10736 <?php if ( $wordfence_active ) : ?>
10737 <p class="description"><?php esc_html_e( 'Wordfence may ask for confirmation when this template is saved. If that happens, choose “I am certain this is a false positive.” and allow the action.', 'ebook-store' ); ?></p>
10738 <?php endif; ?>
10739 </div>
10740 </form>
10741 <?php else : ?>
10742 <div class="ebook-template-editor__panel">
10743 <p><?php esc_html_e( 'No editable templates were found.', 'ebook-store' ); ?></p>
10744 </div>
10745 <?php endif; ?>
10746 </div>
10747 </div>
10748 </div>
10749 <div class="ebook-template-editor__modal" data-template-modal="create" hidden>
10750 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10751 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-create-title">
10752 <form method="post" class="ebook-template-editor__modal-form">
10753 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10754 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10755 <input type="hidden" name="template_editor_action_type" value="create" />
10756 <div class="ebook-template-editor__modal-header">
10757 <h2 id="ebook-template-editor-create-title"><?php esc_html_e( 'Create new template', 'ebook-store' ); ?></h2>
10758 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10759 </div>
10760 <div class="ebook-template-editor__modal-body">
10761 <div class="ebook-template-editor__field">
10762 <label for="new_template_name"><?php esc_html_e( 'Template name', 'ebook-store' ); ?></label>
10763 <input type="text" id="new_template_name" name="new_template_name" required />
10764 </div>
10765 <div class="ebook-template-editor__field">
10766 <label for="new_template_source"><?php esc_html_e( 'Copy code from', 'ebook-store' ); ?></label>
10767 <select id="new_template_source" name="new_template_source">
10768 <option value="none"><?php esc_html_e( 'None (start with an empty scaffold)', 'ebook-store' ); ?></option>
10769 <?php foreach ( $editable as $template ) : ?>
10770 <option value="<?php echo esc_attr( $template['key'] ); ?>"><?php echo esc_html( $template['label'] ); ?></option>
10771 <?php endforeach; ?>
10772 </select>
10773 <p class="description"><?php esc_html_e( 'Choose an existing template to duplicate its code, or start from a clean PHP scaffold.', 'ebook-store' ); ?></p>
10774 </div>
10775 </div>
10776 <div class="ebook-template-editor__modal-actions">
10777 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10778 <button type="submit" class="button button-primary"><?php esc_html_e( 'Create template', 'ebook-store' ); ?></button>
10779 </div>
10780 </form>
10781 </div>
10782 </div>
10783 <?php if ( $current && empty( $current['core'] ) ) : ?>
10784 <div class="ebook-template-editor__modal" data-template-modal="delete" hidden>
10785 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10786 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-delete-title">
10787 <form method="post" class="ebook-template-editor__modal-form">
10788 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10789 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10790 <input type="hidden" name="template_editor_action_type" value="delete" />
10791 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
10792 <div class="ebook-template-editor__modal-header">
10793 <h2 id="ebook-template-editor-delete-title"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></h2>
10794 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10795 </div>
10796 <div class="ebook-template-editor__modal-body">
10797 <p><?php printf( esc_html__( 'Delete the custom template “%s”? This removes the saved custom template file.', 'ebook-store' ), esc_html( $current['label'] ) ); ?></p>
10798 <p class="description"><?php esc_html_e( 'Built-in templates are protected and cannot be removed. This action affects only this custom template file.', 'ebook-store' ); ?></p>
10799 </div>
10800 <div class="ebook-template-editor__modal-actions">
10801 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10802 <button type="submit" class="button button-primary button-link-delete"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></button>
10803 </div>
10804 </form>
10805 </div>
10806 </div>
10807 <?php endif; ?>
10808 <?php
10809
10810 if ( $code_editor_settings && $current ) {
10811 wp_add_inline_script(
10812 'code-editor',
10813 'jQuery(function(){wp.codeEditor.initialize("ebook_store_template_code",' . wp_json_encode( $code_editor_settings ) . ');});'
10814 );
10815 }
10816
10817 wp_add_inline_script(
10818 'code-editor',
10819 'jQuery(function($){$(document).on("click","[data-template-modal-open]",function(){var target=$(this).data("templateModalOpen");$("[data-template-modal=\'"+target+"\']").prop("hidden",false);});$(document).on("click","[data-template-modal-close]",function(){$(this).closest("[data-template-modal]").prop("hidden",true);});$(document).on("keydown",function(event){if(event.key==="Escape"){$("[data-template-modal]").prop("hidden",true);}});});'
10820 );
10821
10822 $output = ob_get_clean();
10823 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
10824 }
10825
10826 function ebook_store_setup_wizard_menu_title() {
10827 return sprintf(
10828 '%s <span class="update-plugins count-1"><span class="plugin-count">%s</span></span>',
10829 esc_html__( 'Setup wizard', 'ebook-store' ),
10830 esc_html__( 'new', 'ebook-store' )
10831 );
10832 }
10833
10834 function ebook_store_setup_wizard_is_pdf( $file ) {
10835 if ( empty( $file['name'] ) || empty( $file['tmp_name'] ) ) {
10836 return false;
10837 }
10838
10839 $filetype = wp_check_filetype( basename( $file['name'] ) );
10840 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
10841 $mime_type = isset( $filetype['type'] ) ? strtolower( (string) $filetype['type'] ) : '';
10842
10843 return $extension === 'pdf' || $mime_type === 'application/pdf';
10844 }
10845
10846 function ebook_store_setup_wizard_human_mode_label( $mode ) {
10847 if ( $mode === 'woocommerce' ) {
10848 return __( 'WooCommerce integration', 'ebook-store' );
10849 }
10850
10851 return __( 'Standalone ebook sales', 'ebook-store' );
10852 }
10853
10854 function ebook_store_is_paypal_enabled() {
10855 return (string) get_option( 'paypal_integration_enabled', '1' ) === '1';
10856 }
10857
10858 function ebook_store_get_standalone_payment_method_state() {
10859 $paypal_ready = ebook_store_is_paypal_enabled() && (
10860 ( function_exists( 'ebook_store_paypal_rest_is_configured' ) && ebook_store_paypal_rest_is_configured() )
10861 || trim( (string) get_option( 'paypal_account', '' ) ) !== ''
10862 );
10863 $stripe_ready = (string) get_option( 'stripe_integration_enabled', '0' ) === '1'
10864 && trim( (string) get_option( 'stripe_publishable_key', '' ) ) !== ''
10865 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== '';
10866 $adyen_environment = (string) get_option( 'adyen_environment', 'test' );
10867 $adyen_ready = (string) get_option( 'adyen_integration_enabled', '0' ) === '1'
10868 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
10869 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
10870 && trim( (string) get_option( 'adyen_hmac_key', '' ) ) !== ''
10871 && ( $adyen_environment !== 'live' || trim( (string) get_option( 'adyen_live_prefix', '' ) ) !== '' );
10872
10873 return array(
10874 'paypal' => array(
10875 'enabled' => ebook_store_is_paypal_enabled(),
10876 'ready' => $paypal_ready,
10877 ),
10878 'stripe' => array(
10879 'enabled' => (string) get_option( 'stripe_integration_enabled', '0' ) === '1',
10880 'ready' => $stripe_ready,
10881 ),
10882 'adyen' => array(
10883 'enabled' => (string) get_option( 'adyen_integration_enabled', '0' ) === '1',
10884 'ready' => $adyen_ready,
10885 ),
10886 );
10887 }
10888
10889 function ebook_store_has_ready_standalone_payment_method() {
10890 $methods = ebook_store_get_standalone_payment_method_state();
10891
10892 foreach ( $methods as $method ) {
10893 if ( ! empty( $method['ready'] ) ) {
10894 return true;
10895 }
10896 }
10897
10898 return false;
10899 }
10900
10901 function ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ) {
10902 if ( ! class_exists( 'Imagick' ) || ! file_exists( $pdf_path ) ) {
10903 return 0;
10904 }
10905
10906 try {
10907 $imagick = new Imagick();
10908 $imagick->pingImage( $pdf_path );
10909 $page_count = (int) $imagick->getNumberImages();
10910 $imagick->clear();
10911 $imagick->destroy();
10912
10913 return max( 0, $page_count );
10914 } catch ( Exception $exception ) {
10915 return 0;
10916 }
10917 }
10918
10919 function ebook_store_setup_wizard_unescape_pdf_literal( $value ) {
10920 $value = preg_replace_callback(
10921 '/\\\\([0-7]{1,3})/',
10922 function ( $matches ) {
10923 return chr( octdec( $matches[1] ) );
10924 },
10925 (string) $value
10926 );
10927
10928 $replacements = array(
10929 '\\n' => "\n",
10930 '\\r' => "\r",
10931 '\\t' => "\t",
10932 '\\b' => "\b",
10933 '\\f' => "\f",
10934 '\\(' => '(',
10935 '\\)' => ')',
10936 '\\\\' => '\\',
10937 );
10938
10939 return strtr( $value, $replacements );
10940 }
10941
10942 function ebook_store_setup_wizard_clean_pdf_text( $text ) {
10943 $text = html_entity_decode( (string) $text, ENT_QUOTES, 'UTF-8' );
10944 $text = preg_replace( "/\r\n?/", "\n", $text );
10945 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', ' ', $text );
10946 $text = preg_replace( '/[ \t]+/', ' ', $text );
10947 $text = preg_replace( "/\n{3,}/", "\n\n", $text );
10948
10949 $lines = preg_split( "/\n+/", trim( $text ) );
10950 $clean_lines = array();
10951
10952 foreach ( $lines as $line ) {
10953 $line = trim( wp_strip_all_tags( $line ) );
10954 $line = preg_replace( '/\s+/', ' ', $line );
10955
10956 if ( strlen( $line ) < 3 ) {
10957 continue;
10958 }
10959
10960 if ( preg_match( '/^[^A-Za-z\\p{L}\\p{N}]+$/u', $line ) ) {
10961 continue;
10962 }
10963
10964 if ( in_array( $line, $clean_lines, true ) ) {
10965 continue;
10966 }
10967
10968 $clean_lines[] = $line;
10969 }
10970
10971 return trim( implode( "\n", $clean_lines ) );
10972 }
10973
10974 function ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path ) {
10975 if ( ! function_exists( 'shell_exec' ) || ! file_exists( $pdf_path ) ) {
10976 return '';
10977 }
10978
10979 $ghostscript_path = trim( (string) @shell_exec( 'command -v gs 2>/dev/null' ) );
10980 if ( $ghostscript_path === '' ) {
10981 return '';
10982 }
10983
10984 $command = escapeshellarg( $ghostscript_path ) . ' -q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=txtwrite -sOutputFile=- ' . escapeshellarg( $pdf_path ) . ' 2>/dev/null';
10985 $output = @shell_exec( $command );
10986
10987 return is_string( $output ) ? ebook_store_setup_wizard_clean_pdf_text( $output ) : '';
10988 }
10989
10990 function ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path ) {
10991 if ( ! file_exists( $pdf_path ) ) {
10992 return '';
10993 }
10994
10995 $pdf_contents = @file_get_contents( $pdf_path );
10996 if ( ! is_string( $pdf_contents ) || $pdf_contents === '' ) {
10997 return '';
10998 }
10999
11000 $matches = array();
11001 preg_match_all( '/stream[\r\n]+(.*?)endstream/s', $pdf_contents, $matches );
11002 if ( empty( $matches[1] ) ) {
11003 return '';
11004 }
11005
11006 $chunks = array();
11007
11008 foreach ( array_slice( $matches[1], 0, 12 ) as $stream ) {
11009 $candidates = array( $stream );
11010
11011 if ( function_exists( 'gzuncompress' ) ) {
11012 $decoded = @gzuncompress( $stream );
11013 if ( is_string( $decoded ) && $decoded !== '' ) {
11014 $candidates[] = $decoded;
11015 }
11016 }
11017
11018 if ( function_exists( 'gzinflate' ) ) {
11019 $decoded = @gzinflate( $stream );
11020 if ( is_string( $decoded ) && $decoded !== '' ) {
11021 $candidates[] = $decoded;
11022 }
11023
11024 $decoded = @gzinflate( substr( $stream, 2 ) );
11025 if ( is_string( $decoded ) && $decoded !== '' ) {
11026 $candidates[] = $decoded;
11027 }
11028 }
11029
11030 foreach ( $candidates as $candidate ) {
11031 $text_matches = array();
11032 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)\s*Tj/s', $candidate, $text_matches );
11033 foreach ( $text_matches[0] as $text_match ) {
11034 if ( preg_match( '/^\((.*)\)\s*Tj$/s', $text_match, $literal_match ) ) {
11035 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( $literal_match[1] );
11036 }
11037 }
11038
11039 $array_matches = array();
11040 preg_match_all( '/\[(.*?)\]\s*TJ/s', $candidate, $array_matches );
11041 foreach ( $array_matches[1] as $array_match ) {
11042 $literal_matches = array();
11043 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)/s', $array_match, $literal_matches );
11044 foreach ( $literal_matches[0] as $literal ) {
11045 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( trim( $literal, '()' ) );
11046 }
11047 }
11048 }
11049 }
11050
11051 return ebook_store_setup_wizard_clean_pdf_text( implode( "\n", $chunks ) );
11052 }
11053
11054 function ebook_store_setup_wizard_extract_pdf_details( $pdf_path, $fallback_title = '' ) {
11055 $details = array(
11056 'title' => '',
11057 'description' => '',
11058 'author' => '',
11059 'pages' => ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ),
11060 );
11061
11062 if ( ! file_exists( $pdf_path ) ) {
11063 return $details;
11064 }
11065
11066 $pdf_contents = @file_get_contents( $pdf_path );
11067 if ( is_string( $pdf_contents ) && $pdf_contents !== '' ) {
11068 if ( preg_match( '/\/Title\s*\((.*?)\)/s', $pdf_contents, $title_match ) ) {
11069 $details['title'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $title_match[1] ) );
11070 }
11071
11072 if ( preg_match( '/\/Subject\s*\((.*?)\)/s', $pdf_contents, $subject_match ) ) {
11073 $details['description'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $subject_match[1] ) );
11074 }
11075
11076 if ( preg_match( '/\/Author\s*\((.*?)\)/s', $pdf_contents, $author_match ) ) {
11077 $details['author'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $author_match[1] ) );
11078 }
11079 }
11080
11081 $text = ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path );
11082 if ( $text === '' ) {
11083 $text = ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path );
11084 }
11085
11086 $lines = $text !== '' ? array_values( array_filter( preg_split( "/\n+/", $text ) ) ) : array();
11087 if ( $details['title'] === '' && ! empty( $lines ) ) {
11088 foreach ( $lines as $line ) {
11089 $line = trim( $line );
11090 if ( strlen( $line ) >= 4 && strlen( $line ) <= 140 ) {
11091 $details['title'] = $line;
11092 break;
11093 }
11094 }
11095 }
11096
11097 if ( $details['description'] === '' && count( $lines ) > 1 ) {
11098 $description_lines = array();
11099 foreach ( $lines as $line ) {
11100 $line = trim( $line );
11101 if ( $line === '' ) {
11102 continue;
11103 }
11104 if ( $details['title'] !== '' && strcasecmp( $line, $details['title'] ) === 0 ) {
11105 continue;
11106 }
11107 $description_lines[] = $line;
11108 if ( strlen( implode( ' ', $description_lines ) ) >= 280 ) {
11109 break;
11110 }
11111 }
11112 $details['description'] = trim( implode( ' ', array_slice( $description_lines, 0, 4 ) ) );
11113 }
11114
11115 if ( $details['title'] === '' ) {
11116 $details['title'] = $fallback_title;
11117 }
11118
11119 if ( $details['description'] !== '' ) {
11120 $details['description'] = wp_trim_words( $details['description'], 48, '...' );
11121 }
11122
11123 return $details;
11124 }
11125
11126 function ebook_store_setup_wizard_upload_pdf( $ebook_id, $file ) {
11127 if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) {
11128 return new WP_Error( 'ebook_store_wizard_upload_missing', __( 'The uploaded PDF file could not be read.', 'ebook-store' ) );
11129 }
11130
11131 // The wizard accepted whatever it was handed. Run the same content check the
11132 // rest of the plugin uses, so a renamed file cannot be parked here.
11133 if ( function_exists( 'ebook_store_validate_uploaded_ebook' ) ) {
11134 $validated = ebook_store_validate_uploaded_ebook( $file['tmp_name'], 'pdf' );
11135
11136 if ( is_wp_error( $validated ) ) {
11137 return $validated;
11138 }
11139 }
11140
11141 // This used to be wp_upload_bits( ..., file_get_contents( $tmp ) ), which
11142 // pulled the whole book into memory — a 60 MB PDF blew memory_limit before it
11143 // ever reached disk. Moving the temp file is constant-memory.
11144 if ( function_exists( 'ebook_store_store_uploaded_ebook' ) ) {
11145 $upload = ebook_store_store_uploaded_ebook( $file['tmp_name'], $file['name'] );
11146 } else {
11147 add_filter( 'upload_dir', 'ebook_set_upload_dir' );
11148 $upload = wp_upload_bits( $file['name'], null, file_get_contents( $file['tmp_name'] ) );
11149 remove_filter( 'upload_dir', 'ebook_set_upload_dir' );
11150 }
11151
11152 if ( is_wp_error( $upload ) ) {
11153 return $upload;
11154 }
11155
11156 if ( isset( $upload['error'] ) && $upload['error'] ) {
11157 return new WP_Error( 'ebook_store_wizard_upload_failed', $upload['error'] );
11158 }
11159
11160 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment', wp_slash( $upload ) );
11161 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment_pdf', wp_slash( $upload ) );
11162 ebook_store_generate_cover_from_pdf( $ebook_id );
11163
11164 return $upload;
11165 }
11166
11167 function ebook_store_setup_wizard_create_ebook( $args ) {
11168 $title = isset( $args['title'] ) ? sanitize_text_field( $args['title'] ) : '';
11169 $description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : '';
11170 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
11171 $currency = isset( $args['currency'] ) && $args['currency'] !== '' ? sanitize_text_field( $args['currency'] ) : ebook_store_get_store_currency();
11172 $pdf_file = isset( $args['pdf_file'] ) ? $args['pdf_file'] : array();
11173 $pdf_pages = isset( $args['pages'] ) ? (int) $args['pages'] : 0;
11174 $author = isset( $args['author'] ) ? sanitize_text_field( $args['author'] ) : '';
11175 $template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( '' ) : 'modern';
11176 $delivery_mode = $price > 0 ? 'paid' : 'free';
11177
11178 if ( $title === '' ) {
11179 $title = __( 'Imported ebook', 'ebook-store' );
11180 }
11181
11182 $ebook_post = array(
11183 'post_title' => $title,
11184 'post_content' => $description,
11185 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $description ), 40, '...' ),
11186 'post_status' => 'draft',
11187 'post_type' => 'ebook',
11188 );
11189
11190 $ebook_id = wp_insert_post( $ebook_post, true );
11191 if ( is_wp_error( $ebook_id ) ) {
11192 return $ebook_id;
11193 }
11194
11195 $ebook_meta = array(
11196 'ebook_price' => number_format( $price, 2, '.', '' ),
11197 'paypal_currency' => $currency,
11198 'donate_or_download' => $delivery_mode,
11199 'form_template' => $template,
11200 );
11201
11202 if ( $pdf_pages > 0 ) {
11203 $ebook_meta['ebook_pages'] = $pdf_pages;
11204 }
11205
11206 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
11207 update_post_meta( $ebook_id, 'ebook_bonus', array() );
11208
11209 if ( $author !== '' ) {
11210 update_post_meta( $ebook_id, 'ebook_store_wizard_author_name', $author );
11211 }
11212
11213 $upload = ebook_store_setup_wizard_upload_pdf( $ebook_id, $pdf_file );
11214 if ( is_wp_error( $upload ) ) {
11215 wp_delete_post( $ebook_id, true );
11216 return $upload;
11217 }
11218
11219 return array(
11220 'ebook_id' => $ebook_id,
11221 'title' => $title,
11222 'description' => $description,
11223 'pages' => $pdf_pages,
11224 );
11225 }
11226
11227 function ebook_store_setup_wizard_create_woocommerce_bundle( $args ) {
11228 if ( ! class_exists( 'WooCommerce' ) ) {
11229 return new WP_Error( 'ebook_store_wizard_woocommerce_missing', __( 'WooCommerce is not active. Activate WooCommerce first, then run this setup path again.', 'ebook-store' ) );
11230 }
11231
11232 $ebook_result = ebook_store_setup_wizard_create_ebook( $args );
11233 if ( is_wp_error( $ebook_result ) ) {
11234 return $ebook_result;
11235 }
11236
11237 $product_name = isset( $args['product_name'] ) ? sanitize_text_field( $args['product_name'] ) : $ebook_result['title'];
11238 $product_description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : $ebook_result['description'];
11239 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
11240
11241 $product_post = array(
11242 'post_title' => $product_name,
11243 'post_content' => $product_description,
11244 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $product_description ), 40, '...' ),
11245 'post_status' => 'draft',
11246 'post_type' => 'product',
11247 );
11248
11249 $product_id = wp_insert_post( $product_post, true );
11250 if ( is_wp_error( $product_id ) ) {
11251 wp_delete_post( $ebook_result['ebook_id'], true );
11252 return $product_id;
11253 }
11254
11255 wp_set_object_terms( $product_id, 'ebookstore', 'product_type' );
11256 update_post_meta( $product_id, '_regular_price', number_format( $price, 2, '.', '' ) );
11257 update_post_meta( $product_id, '_price', number_format( $price, 2, '.', '' ) );
11258 update_post_meta( $product_id, '_virtual', 'yes' );
11259 update_post_meta( $product_id, '_downloadable', 'yes' );
11260 update_post_meta( $product_id, '_enable_ebook_store_option', 'yes' );
11261 update_post_meta( $product_id, 'ebook_store_book_id', $ebook_result['ebook_id'] );
11262 update_post_meta( $ebook_result['ebook_id'], 'woocommerce_product_id', $product_id );
11263
11264 $license_key = trim( (string) get_option( 'ebook_store_license_key', '' ) );
11265 $integration_enabled = (string) get_option( 'ebook_store_woocommerce_integration', '' ) === '1';
11266 $integration_auto_enabled = false;
11267
11268 if ( ! $integration_enabled && $license_key !== '' ) {
11269 update_option( 'ebook_store_woocommerce_integration', '1' );
11270 $integration_enabled = true;
11271 $integration_auto_enabled = true;
11272 }
11273
11274 // Virtual + downloadable orders skip "processing", so delivery has to wait
11275 // for "completed" or it never fires. Only fill the gap; never override a
11276 // status the owner chose.
11277 if ( $integration_enabled && trim( (string) get_option( 'ebook_store_woocommerce_required_order_status', '' ) ) === '' ) {
11278 update_option( 'ebook_store_woocommerce_required_order_status', 'completed' );
11279 }
11280
11281 if ( function_exists( 'wc_delete_product_transients' ) ) {
11282 wc_delete_product_transients( $product_id );
11283 }
11284
11285 return array(
11286 'ebook_id' => $ebook_result['ebook_id'],
11287 'product_id' => $product_id,
11288 'integration_enabled' => $integration_enabled,
11289 'integration_auto_enabled' => $integration_auto_enabled,
11290 );
11291 }
11292
11293 function ebook_store_setup_wizard_handle_submission() {
11294 if ( empty( $_POST['ebook_store_setup_wizard_action'] ) ) {
11295 return null;
11296 }
11297
11298 if ( ! current_user_can( 'manage_options' ) ) {
11299 return array(
11300 'errors' => array( __( 'You do not have permission to run the setup wizard.', 'ebook-store' ) ),
11301 );
11302 }
11303
11304 check_admin_referer( 'ebook_store_setup_wizard' );
11305
11306 $mode = sanitize_key( (string) wp_unslash( $_POST['wizard_mode'] ) );
11307 $price = isset( $_POST['wizard_price'] ) ? (float) wp_unslash( $_POST['wizard_price'] ) : 0.0;
11308 $product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
11309 $wizard_enable_paypal = isset( $_POST['wizard_enable_paypal'] ) && (string) wp_unslash( $_POST['wizard_enable_paypal'] ) === '1' ? '1' : '';
11310 $wizard_enable_stripe = isset( $_POST['wizard_enable_stripe'] ) && (string) wp_unslash( $_POST['wizard_enable_stripe'] ) === '1' ? '1' : '';
11311 $wizard_enable_adyen = isset( $_POST['wizard_enable_adyen'] ) && (string) wp_unslash( $_POST['wizard_enable_adyen'] ) === '1' ? '1' : '';
11312 $wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : '';
11313 $wizard_stripe_publishable_key = isset( $_POST['wizard_stripe_publishable_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_publishable_key'] ) ) : '';
11314 $wizard_stripe_secret_key = isset( $_POST['wizard_stripe_secret_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_secret_key'] ) ) : '';
11315 $wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : 'test';
11316 $wizard_adyen_api_key = isset( $_POST['wizard_adyen_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_api_key'] ) ) : '';
11317 $wizard_adyen_merchant_account = isset( $_POST['wizard_adyen_merchant_account'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_merchant_account'] ) ) : '';
11318 $wizard_adyen_hmac_key = isset( $_POST['wizard_adyen_hmac_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_hmac_key'] ) ) : '';
11319 $wizard_adyen_live_prefix = isset( $_POST['wizard_adyen_live_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_live_prefix'] ) ) : '';
11320 $pdf_file = isset( $_FILES['wizard_pdf_file'] ) ? $_FILES['wizard_pdf_file'] : array();
11321 $errors = array();
11322
11323 if ( ! in_array( $mode, array( 'standalone', 'woocommerce' ), true ) ) {
11324 $errors[] = __( 'Please choose how you want to set up Ebook Store first.', 'ebook-store' );
11325 }
11326
11327 if ( empty( $pdf_file['tmp_name'] ) ) {
11328 $errors[] = __( 'Please upload the PDF file you want to sell.', 'ebook-store' );
11329 } elseif ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
11330 $errors[] = __( 'The uploaded file must be a PDF.', 'ebook-store' );
11331 }
11332
11333 if ( $mode === 'woocommerce' && $product_name === '' ) {
11334 $errors[] = __( 'Please enter the WooCommerce product name.', 'ebook-store' );
11335 }
11336
11337 if ( $mode === 'standalone' ) {
11338 $wizard_ready_payment_methods = 0;
11339
11340 if ( $wizard_enable_paypal === '1' ) {
11341 if ( $wizard_paypal_account === '' ) {
11342 $errors[] = __( 'Enter the PayPal account email or turn off PayPal in Payment method setup.', 'ebook-store' );
11343 } else {
11344 $wizard_ready_payment_methods++;
11345 }
11346 }
11347
11348 if ( $wizard_enable_stripe === '1' ) {
11349 if ( $wizard_stripe_publishable_key === '' || $wizard_stripe_secret_key === '' ) {
11350 $errors[] = __( 'Enter both Stripe API keys or turn off Stripe in Payment method setup.', 'ebook-store' );
11351 } else {
11352 $wizard_ready_payment_methods++;
11353 }
11354 }
11355
11356 if ( $wizard_enable_adyen === '1' ) {
11357 if ( ! in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ) {
11358 $wizard_adyen_environment = 'test';
11359 }
11360 if ( $wizard_adyen_api_key === '' || $wizard_adyen_merchant_account === '' || $wizard_adyen_hmac_key === '' ) {
11361 $errors[] = __( 'Enter the Adyen API key, merchant account, and webhook HMAC key or turn off Adyen in Payment method setup.', 'ebook-store' );
11362 } elseif ( $wizard_adyen_environment === 'live' && $wizard_adyen_live_prefix === '' ) {
11363 $errors[] = __( 'Enter the Adyen live URL prefix or switch Adyen to Test mode in Payment method setup.', 'ebook-store' );
11364 } else {
11365 $wizard_ready_payment_methods++;
11366 }
11367 }
11368
11369 if ( $price > 0 && $wizard_ready_payment_methods < 1 ) {
11370 $errors[] = __( 'Standalone paid ebooks need at least one checkout method that is ready to use. Enable a payment method below and complete its required details before creating the draft.', 'ebook-store' );
11371 }
11372 }
11373
11374 if ( ! empty( $errors ) ) {
11375 return array(
11376 'errors' => $errors,
11377 'mode' => $mode,
11378 );
11379 }
11380
11381 if ( $mode === 'standalone' ) {
11382 update_option( 'paypal_integration_enabled', $wizard_enable_paypal );
11383 update_option( 'paypal_account', $wizard_paypal_account );
11384 update_option( 'stripe_integration_enabled', $wizard_enable_stripe );
11385 update_option( 'stripe_publishable_key', $wizard_stripe_publishable_key );
11386 update_option( 'stripe_secret_key', $wizard_stripe_secret_key );
11387 update_option( 'adyen_integration_enabled', $wizard_enable_adyen );
11388 update_option( 'adyen_environment', in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ? $wizard_adyen_environment : 'test' );
11389 update_option( 'adyen_api_key', $wizard_adyen_api_key );
11390 update_option( 'adyen_merchant_account', $wizard_adyen_merchant_account );
11391 update_option( 'adyen_hmac_key', $wizard_adyen_hmac_key );
11392 update_option( 'adyen_live_prefix', $wizard_adyen_live_prefix );
11393 }
11394
11395 $fallback_title = $mode === 'woocommerce' ? $product_name : preg_replace( '/\.[^.]+$/', '', sanitize_file_name( $pdf_file['name'] ) );
11396 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $pdf_file['tmp_name'], $fallback_title );
11397 $title = $mode === 'woocommerce' ? $product_name : $pdf_details['title'];
11398 $description = $pdf_details['description'];
11399
11400 if ( $title === '' ) {
11401 $title = $fallback_title !== '' ? $fallback_title : __( 'Imported ebook', 'ebook-store' );
11402 }
11403
11404 $shared_args = array(
11405 'title' => $title,
11406 'product_name'=> $product_name,
11407 'description' => $description,
11408 'price' => $price,
11409 'currency' => ebook_store_get_store_currency(),
11410 'pdf_file' => $pdf_file,
11411 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
11412 'author' => isset( $pdf_details['author'] ) ? $pdf_details['author'] : '',
11413 );
11414
11415 $result = $mode === 'woocommerce'
11416 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
11417 : ebook_store_setup_wizard_create_ebook( $shared_args );
11418
11419 if ( is_wp_error( $result ) ) {
11420 return array(
11421 'errors' => array( $result->get_error_message() ),
11422 'mode' => $mode,
11423 );
11424 }
11425
11426 $redirect_args = array(
11427 'wizard_mode' => $mode,
11428 'wizard_created' => 1,
11429 'ebook_id' => (int) $result['ebook_id'],
11430 );
11431
11432 if ( ! empty( $result['product_id'] ) ) {
11433 $redirect_args['product_id'] = (int) $result['product_id'];
11434 }
11435
11436 if ( isset( $result['integration_enabled'] ) ) {
11437 $redirect_args['integration_enabled'] = $result['integration_enabled'] ? 1 : 0;
11438 }
11439
11440 if ( isset( $result['integration_auto_enabled'] ) ) {
11441 $redirect_args['integration_auto_enabled'] = $result['integration_auto_enabled'] ? 1 : 0;
11442 }
11443
11444 wp_safe_redirect( ebook_store_get_setup_wizard_url( $redirect_args ) );
11445 exit;
11446 }
11447
11448 function ebook_store_setup_wizard_page_callback() {
11449 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
11450 $locale = ebook_store_get_locale_strings();
11451 $formats_locale = ebook_store_get_format_labels();
11452 wp_enqueue_style( 'ebookstorestylesheet' );
11453
11454 $submission = null;
11455 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_setup_wizard_action'] ) ) {
11456 $submission = ebook_store_setup_wizard_handle_submission();
11457 }
11458
11459 $wizard_mode = isset( $_REQUEST['wizard_mode'] ) ? sanitize_key( (string) wp_unslash( $_REQUEST['wizard_mode'] ) ) : '';
11460 if ( $submission && ! empty( $submission['mode'] ) ) {
11461 $wizard_mode = $submission['mode'];
11462 }
11463
11464 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
11465 $wizard_created = isset( $_GET['wizard_created'] ) && (int) $_GET['wizard_created'] === 1;
11466 $ebook_id = isset( $_GET['ebook_id'] ) ? (int) $_GET['ebook_id'] : 0;
11467 $product_id = isset( $_GET['product_id'] ) ? (int) $_GET['product_id'] : 0;
11468 $integration_enabled = isset( $_GET['integration_enabled'] ) ? (int) $_GET['integration_enabled'] === 1 : null;
11469 $integration_auto_enabled = isset( $_GET['integration_auto_enabled'] ) ? (int) $_GET['integration_auto_enabled'] === 1 : false;
11470 $current_currency = ebook_store_get_store_currency();
11471 $current_price = isset( $_POST['wizard_price'] ) ? (string) wp_unslash( $_POST['wizard_price'] ) : '5.00';
11472 $current_product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
11473 $payment_method_state = ebook_store_get_standalone_payment_method_state();
11474 $has_ready_standalone_payment = ebook_store_has_ready_standalone_payment_method();
11475 $current_wizard_paypal_enabled = isset( $_POST['wizard_enable_paypal'] ) ? ( (string) wp_unslash( $_POST['wizard_enable_paypal'] ) === '1' ? '1' : '' ) : ( ! empty( $payment_method_state['paypal']['enabled'] ) ? '1' : '' );
11476 $current_wizard_stripe_enabled = isset( $_POST['wizard_enable_stripe'] ) ? ( (string) wp_unslash( $_POST['wizard_enable_stripe'] ) === '1' ? '1' : '' ) : ( ! empty( $payment_method_state['stripe']['enabled'] ) ? '1' : '' );
11477 $current_wizard_adyen_enabled = isset( $_POST['wizard_enable_adyen'] ) ? ( (string) wp_unslash( $_POST['wizard_enable_adyen'] ) === '1' ? '1' : '' ) : ( ! empty( $payment_method_state['adyen']['enabled'] ) ? '1' : '' );
11478 $current_wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : (string) get_option( 'paypal_account', '' );
11479 $current_wizard_stripe_publishable_key = isset( $_POST['wizard_stripe_publishable_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_publishable_key'] ) ) : (string) get_option( 'stripe_publishable_key', '' );
11480 $current_wizard_stripe_secret_key = isset( $_POST['wizard_stripe_secret_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_secret_key'] ) ) : (string) get_option( 'stripe_secret_key', '' );
11481 $current_wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : (string) get_option( 'adyen_environment', 'test' );
11482 $current_wizard_adyen_api_key = isset( $_POST['wizard_adyen_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_api_key'] ) ) : (string) get_option( 'adyen_api_key', '' );
11483 $current_wizard_adyen_merchant_account = isset( $_POST['wizard_adyen_merchant_account'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_merchant_account'] ) ) : (string) get_option( 'adyen_merchant_account', '' );
11484 $current_wizard_adyen_hmac_key = isset( $_POST['wizard_adyen_hmac_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_hmac_key'] ) ) : (string) get_option( 'adyen_hmac_key', '' );
11485 $current_wizard_adyen_live_prefix = isset( $_POST['wizard_adyen_live_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_live_prefix'] ) ) : (string) get_option( 'adyen_live_prefix', '' );
11486
11487 ob_start();
11488 ?>
11489 <div class="wrap ebook-setup-wizard">
11490 <div class="ebook-setup-wizard__hero">
11491 <div class="ebook-setup-wizard__hero-copy">
11492 <span class="ebook-setup-wizard__eyebrow"><?php esc_html_e( 'New onboarding flow', 'ebook-store' ); ?></span>
11493 <h1><?php esc_html_e( 'Setup wizard', 'ebook-store' ); ?></h1>
11494 <p><?php esc_html_e( 'Start with the selling model that matches your store. The wizard can create a ready-to-review ebook draft from your PDF, generate the cover from page one, and optionally set up the linked WooCommerce product for you.', 'ebook-store' ); ?></p>
11495 </div>
11496 <div class="ebook-setup-wizard__hero-meta">
11497 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Creates draft items', 'ebook-store' ); ?></span>
11498 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Extracts title and summary', 'ebook-store' ); ?></span>
11499 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Builds first-page cover', 'ebook-store' ); ?></span>
11500 </div>
11501 </div>
11502
11503 <?php if ( ! empty( $errors ) ) : ?>
11504 <div class="notice notice-error">
11505 <p><strong><?php esc_html_e( 'The wizard could not finish yet.', 'ebook-store' ); ?></strong></p>
11506 <ul class="ebook-setup-wizard__notice-list">
11507 <?php foreach ( $errors as $error ) : ?>
11508 <li><?php echo esc_html( $error ); ?></li>
11509 <?php endforeach; ?>
11510 </ul>
11511 </div>
11512 <?php endif; ?>
11513
11514 <?php if ( $wizard_created && $ebook_id > 0 ) : ?>
11515 <div class="notice notice-success is-dismissible">
11516 <p>
11517 <strong><?php esc_html_e( 'Setup complete.', 'ebook-store' ); ?></strong>
11518 <?php
11519 printf(
11520 esc_html__( '%1$s was created as a draft so you can review it before going live.', 'ebook-store' ),
11521 esc_html( ebook_store_setup_wizard_human_mode_label( $wizard_mode ) )
11522 );
11523 ?>
11524 </p>
11525 <p class="ebook-setup-wizard__success-links">
11526 <a class="button button-primary" href="<?php echo esc_url( get_edit_post_link( $ebook_id, 'raw' ) ); ?>"><?php esc_html_e( 'Review ebook', 'ebook-store' ); ?></a>
11527 <?php if ( $product_id > 0 ) : ?>
11528 <a class="button button-secondary" href="<?php echo esc_url( get_edit_post_link( $product_id, 'raw' ) ); ?>"><?php esc_html_e( 'Review WooCommerce product', 'ebook-store' ); ?></a>
11529 <?php endif; ?>
11530 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Run wizard again', 'ebook-store' ); ?></a>
11531 </p>
11532 <?php if ( $product_id > 0 && $integration_enabled === false ) : ?>
11533 <p class="description"><?php esc_html_e( 'The linked product and ebook were created, but WooCommerce integration is still off. Enable it in Ebook Store > Settings > WooCommerce before sending traffic to this flow.', 'ebook-store' ); ?></p>
11534 <?php elseif ( $product_id > 0 && $integration_auto_enabled ) : ?>
11535 <p class="description"><?php esc_html_e( 'WooCommerce integration was enabled automatically so the linked product can use Ebook Store delivery.', 'ebook-store' ); ?></p>
11536 <?php endif; ?>
11537 </div>
11538 <?php endif; ?>
11539
11540 <?php if ( $wizard_mode === 'standalone' ) : ?>
11541 <div class="ebook-setup-wizard__panel">
11542 <div class="ebook-setup-wizard__panel-header">
11543 <div>
11544 <h2><?php esc_html_e( 'Standalone ebook sales', 'ebook-store' ); ?></h2>
11545 <p><?php esc_html_e( 'Upload the PDF you want to sell. The wizard will extract the title and a summary, build the cover from the first page, and create a draft ebook item with your default modern template.', 'ebook-store' ); ?></p>
11546 </div>
11547 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
11548 </div>
11549 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
11550 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
11551 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
11552 <input type="hidden" name="wizard_mode" value="standalone" />
11553 <div class="ebook-setup-wizard__grid">
11554 <div class="ebook-setup-wizard__field">
11555 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
11556 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
11557 <p class="description"><?php esc_html_e( 'The wizard reads the PDF title and summary and turns page one into the cover automatically.', 'ebook-store' ); ?></p>
11558 </div>
11559 <div class="ebook-setup-wizard__field">
11560 <label for="wizard_price"><?php esc_html_e( 'Selling price', 'ebook-store' ); ?></label>
11561 <div class="ebook-setup-wizard__input-group">
11562 <span><?php echo esc_html( $current_currency ); ?></span>
11563 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
11564 </div>
11565 <p class="description"><?php esc_html_e( 'Set this to 0 if you want the wizard to create the ebook as a free download draft.', 'ebook-store' ); ?></p>
11566 </div>
11567 </div>
11568 <div class="ebook-setup-wizard__gateway-panel">
11569 <div class="ebook-setup-wizard__gateway-header">
11570 <h3><?php esc_html_e( 'Payment method setup', 'ebook-store' ); ?></h3>
11571 <p><?php esc_html_e( 'Choose the checkout methods you want to offer on standalone ebook pages, then enter the credentials each enabled gateway needs so payments can work immediately after launch.', 'ebook-store' ); ?></p>
11572 <?php if ( ! $has_ready_standalone_payment && (float) $current_price > 0 ) : ?>
11573 <p class="ebook-setup-wizard__gateway-note"><?php esc_html_e( 'This store does not have a ready standalone payment method yet. Turn on at least one option below and complete its required details before creating a paid ebook draft.', 'ebook-store' ); ?></p>
11574 <?php endif; ?>
11575 </div>
11576 <div class="ebook-settings-toggle-grid ebook-settings-toggle-grid--wizard">
11577 <label class="ebook-settings-toggle-card">
11578 <input type="hidden" name="wizard_enable_paypal" value="" />
11579 <input type="checkbox" name="wizard_enable_paypal" value="1" <?php checked( $current_wizard_paypal_enabled, '1' ); ?> />
11580 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11581 <span class="ebook-settings-toggle-card__content">
11582 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable PayPal', 'ebook-store' ); ?></span>
11583 <span class="ebook-settings-toggle-card__description"><?php esc_html_e( 'Fastest setup path for standalone Ebook Store checkout. Add the receiving PayPal account email below.', 'ebook-store' ); ?></span>
11584 </span>
11585 </label>
11586 <label class="ebook-settings-toggle-card">
11587 <input type="hidden" name="wizard_enable_stripe" value="" />
11588 <input type="checkbox" name="wizard_enable_stripe" value="1" <?php checked( $current_wizard_stripe_enabled, '1' ); ?> />
11589 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11590 <span class="ebook-settings-toggle-card__content">
11591 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Stripe', 'ebook-store' ); ?></span>
11592 <span class="ebook-settings-toggle-card__description"><?php esc_html_e( 'Accept card payments with Stripe. Add both the publishable key and the secret key below.', 'ebook-store' ); ?></span>
11593 </span>
11594 </label>
11595 <label class="ebook-settings-toggle-card">
11596 <input type="hidden" name="wizard_enable_adyen" value="" />
11597 <input type="checkbox" name="wizard_enable_adyen" value="1" <?php checked( $current_wizard_adyen_enabled, '1' ); ?> />
11598 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11599 <span class="ebook-settings-toggle-card__content">
11600 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Adyen', 'ebook-store' ); ?></span>
11601 <span class="ebook-settings-toggle-card__description"><?php esc_html_e( 'Use Adyen Pay by Link for standalone sales. Add the API key, merchant account, and webhook HMAC key below.', 'ebook-store' ); ?></span>
11602 </span>
11603 </label>
11604 </div>
11605 <div class="ebook-setup-wizard__grid">
11606 <div class="ebook-setup-wizard__field">
11607 <label for="wizard_paypal_account"><?php esc_html_e( 'PayPal account email', 'ebook-store' ); ?></label>
11608 <input type="email" id="wizard_paypal_account" name="wizard_paypal_account" value="<?php echo esc_attr( $current_wizard_paypal_account ); ?>" placeholder="<?php echo esc_attr__( 'your-paypal@example.com', 'ebook-store' ); ?>" />
11609 <p class="description"><?php esc_html_e( 'Required when PayPal is enabled. Payments are sent to this PayPal account.', 'ebook-store' ); ?></p>
11610 </div>
11611 <div class="ebook-setup-wizard__field">
11612 <label for="wizard_stripe_publishable_key"><?php esc_html_e( 'Stripe publishable key', 'ebook-store' ); ?></label>
11613 <input type="text" id="wizard_stripe_publishable_key" name="wizard_stripe_publishable_key" value="<?php echo esc_attr( $current_wizard_stripe_publishable_key ); ?>" placeholder="pk_live_... or pk_test_..." class="ebook-setup-wizard__code-input" />
11614 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
11615 </div>
11616 <div class="ebook-setup-wizard__field">
11617 <label for="wizard_stripe_secret_key"><?php esc_html_e( 'Stripe secret key', 'ebook-store' ); ?></label>
11618 <input type="text" id="wizard_stripe_secret_key" name="wizard_stripe_secret_key" value="<?php echo esc_attr( $current_wizard_stripe_secret_key ); ?>" placeholder="sk_live_... or sk_test_..." class="ebook-setup-wizard__code-input" />
11619 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
11620 </div>
11621 <div class="ebook-setup-wizard__field">
11622 <label for="wizard_adyen_environment"><?php esc_html_e( 'Adyen environment', 'ebook-store' ); ?></label>
11623 <select id="wizard_adyen_environment" name="wizard_adyen_environment">
11624 <option value="test" <?php selected( $current_wizard_adyen_environment, 'test' ); ?>><?php esc_html_e( 'Test', 'ebook-store' ); ?></option>
11625 <option value="live" <?php selected( $current_wizard_adyen_environment, 'live' ); ?>><?php esc_html_e( 'Live', 'ebook-store' ); ?></option>
11626 </select>
11627 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11628 </div>
11629 <div class="ebook-setup-wizard__field">
11630 <label for="wizard_adyen_merchant_account"><?php esc_html_e( 'Adyen merchant account', 'ebook-store' ); ?></label>
11631 <input type="text" id="wizard_adyen_merchant_account" name="wizard_adyen_merchant_account" value="<?php echo esc_attr( $current_wizard_adyen_merchant_account ); ?>" placeholder="YourMerchantAccount" class="ebook-setup-wizard__code-input" />
11632 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11633 </div>
11634 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11635 <label for="wizard_adyen_api_key"><?php esc_html_e( 'Adyen API key', 'ebook-store' ); ?></label>
11636 <input type="text" id="wizard_adyen_api_key" name="wizard_adyen_api_key" value="<?php echo esc_attr( $current_wizard_adyen_api_key ); ?>" placeholder="AQEy..." class="ebook-setup-wizard__code-input" />
11637 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11638 </div>
11639 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11640 <label for="wizard_adyen_hmac_key"><?php esc_html_e( 'Adyen webhook HMAC key', 'ebook-store' ); ?></label>
11641 <input type="text" id="wizard_adyen_hmac_key" name="wizard_adyen_hmac_key" value="<?php echo esc_attr( $current_wizard_adyen_hmac_key ); ?>" placeholder="<?php echo esc_attr__( '64-character hex key from the Adyen webhook settings', 'ebook-store' ); ?>" class="ebook-setup-wizard__code-input" />
11642 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled. Ebook Store uses this to verify webhook events before creating orders.', 'ebook-store' ); ?></p>
11643 </div>
11644 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11645 <label for="wizard_adyen_live_prefix"><?php esc_html_e( 'Adyen live URL prefix', 'ebook-store' ); ?></label>
11646 <input type="text" id="wizard_adyen_live_prefix" name="wizard_adyen_live_prefix" value="<?php echo esc_attr( $current_wizard_adyen_live_prefix ); ?>" placeholder="1797a841fbb37ca7-AdyenDemo" class="ebook-setup-wizard__code-input" />
11647 <p class="description"><?php esc_html_e( 'Required only when Adyen is enabled in Live mode.', 'ebook-store' ); ?></p>
11648 </div>
11649 </div>
11650 </div>
11651 <div class="ebook-setup-wizard__actions">
11652 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create ebook draft', 'ebook-store' ); ?></button>
11653 </div>
11654 </form>
11655 </div>
11656 <?php elseif ( $wizard_mode === 'woocommerce' ) : ?>
11657 <div class="ebook-setup-wizard__panel">
11658 <div class="ebook-setup-wizard__panel-header">
11659 <div>
11660 <h2><?php esc_html_e( 'WooCommerce integration', 'ebook-store' ); ?></h2>
11661 <p><?php esc_html_e( 'Create the ebook item and the WooCommerce product in one pass. The wizard links them together, marks the product as virtual, and uses the same title on both sides.', 'ebook-store' ); ?></p>
11662 </div>
11663 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
11664 </div>
11665 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
11666 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
11667 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
11668 <input type="hidden" name="wizard_mode" value="woocommerce" />
11669 <div class="ebook-setup-wizard__grid">
11670 <div class="ebook-setup-wizard__field">
11671 <label for="wizard_product_name"><?php esc_html_e( 'Product name', 'ebook-store' ); ?></label>
11672 <input type="text" id="wizard_product_name" name="wizard_product_name" value="<?php echo esc_attr( $current_product_name ); ?>" required />
11673 <p class="description"><?php esc_html_e( 'This name is used for both the WooCommerce product and the linked Ebook Store item.', 'ebook-store' ); ?></p>
11674 </div>
11675 <div class="ebook-setup-wizard__field">
11676 <label for="wizard_price"><?php esc_html_e( 'Product price', 'ebook-store' ); ?></label>
11677 <div class="ebook-setup-wizard__input-group">
11678 <span><?php echo esc_html( $current_currency ); ?></span>
11679 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
11680 </div>
11681 <p class="description"><?php esc_html_e( 'The same amount is written to the Ebook Store item and the WooCommerce product.', 'ebook-store' ); ?></p>
11682 </div>
11683 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11684 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
11685 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
11686 <p class="description"><?php esc_html_e( 'The uploaded PDF becomes the secured file in Ebook Store. Its text is used to build a summary, and the first page becomes the generated cover.', 'ebook-store' ); ?></p>
11687 </div>
11688 </div>
11689 <div class="ebook-setup-wizard__actions">
11690 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create linked draft items', 'ebook-store' ); ?></button>
11691 </div>
11692 </form>
11693 </div>
11694 <?php else : ?>
11695 <div class="ebook-setup-wizard__chooser">
11696 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'standalone' ) ) ); ?>">
11697 <span class="dashicons dashicons-book-alt"></span>
11698 <h2><?php esc_html_e( 'Sell with Ebook Store', 'ebook-store' ); ?></h2>
11699 <p><?php esc_html_e( 'Use Ebook Store as the primary checkout and delivery flow. Best when you want the standalone order form, watermarking, and direct secure fulfillment without a separate product catalog.', 'ebook-store' ); ?></p>
11700 <strong><?php esc_html_e( 'Upload a PDF and generate the ebook item automatically', 'ebook-store' ); ?></strong>
11701 </a>
11702 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'woocommerce' ) ) ); ?>">
11703 <span class="dashicons dashicons-cart"></span>
11704 <h2><?php esc_html_e( 'Use WooCommerce integration', 'ebook-store' ); ?></h2>
11705 <p><?php esc_html_e( 'Create the secured ebook item behind the scenes, then pair it with a WooCommerce product so checkout happens in WooCommerce while Ebook Store still handles delivery and protection.', 'ebook-store' ); ?></p>
11706 <strong><?php esc_html_e( 'Create and link both items in one guided flow', 'ebook-store' ); ?></strong>
11707 </a>
11708 </div>
11709 <?php endif; ?>
11710 </div>
11711 <?php
11712 $output = ob_get_clean();
11713 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
11714 }
11715
11716 function ebook_store_get_import_books_url() {
11717 return admin_url( 'edit.php?post_type=ebook&page=ebook-store-import-books' );
11718 }
11719
11720 function ebook_store_import_books_has_pro_access() {
11721 if ( function_exists( 'ebook_store_has_pro_license' ) ) {
11722 return ebook_store_has_pro_license();
11723 }
11724
11725 return trim( (string) get_option( 'ebook_store_license_key', '' ) ) !== '';
11726 }
11727
11728 function ebook_store_import_books_admin_assets() {
11729 if ( ! is_admin() ) {
11730 return;
11731 }
11732
11733 $page = isset( $_GET['page'] ) ? sanitize_key( (string) wp_unslash( $_GET['page'] ) ) : '';
11734 if ( $page !== 'ebook-store-import-books' ) {
11735 return;
11736 }
11737
11738 wp_enqueue_style( 'ebookstorestylesheet' );
11739 wp_enqueue_script( 'plupload-all' );
11740 wp_enqueue_script(
11741 'ebook-store-import-books',
11742 plugins_url( 'js/ebook_store_import_books.js', __FILE__ ),
11743 array( 'jquery', 'plupload-all' ),
11744 filemtime( plugin_dir_path( __FILE__ ) . 'js/ebook_store_import_books.js' ),
11745 true
11746 );
11747 wp_localize_script(
11748 'ebook-store-import-books',
11749 'ebookStoreImportBooks',
11750 array(
11751 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
11752 'nonce' => wp_create_nonce( 'ebook_store_import_books' ),
11753 'sessionId' => wp_generate_uuid4(),
11754 'hasProLicense' => ebook_store_import_books_has_pro_access(),
11755 'woocommerceAvailable' => class_exists( 'WooCommerce' ),
11756 'maxFileSize' => (string) wp_max_upload_size() . 'b',
11757 'strings' => array(
11758 'dropActive' => __( 'Drop PDFs to add them to the import queue', 'ebook-store' ),
11759 'dropIdle' => __( 'Drop PDF files here or use the button below', 'ebook-store' ),
11760 'uploading' => __( 'Uploading...', 'ebook-store' ),
11761 'uploaded' => __( 'Uploaded', 'ebook-store' ),
11762 'queued' => __( 'Queued for import', 'ebook-store' ),
11763 'importing' => __( 'Creating draft items...', 'ebook-store' ),
11764 'imported' => __( 'Imported', 'ebook-store' ),
11765 'failed' => __( 'Failed', 'ebook-store' ),
11766 'invalidMode' => __( 'Choose how imported books should be created before starting the import.', 'ebook-store' ),
11767 'invalidPrice' => __( 'Enter a valid default price before starting the import.', 'ebook-store' ),
11768 'noUploadedFiles' => __( 'Upload at least one PDF before starting the import.', 'ebook-store' ),
11769 'proRequired' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ),
11770 'woocommerceMissing' => __( 'WooCommerce is not active, so the connected product option is not available.', 'ebook-store' ),
11771 'titleFallback' => __( 'Imported ebook', 'ebook-store' ),
11772 'descriptionFallback' => __( 'No description could be extracted from the PDF.', 'ebook-store' ),
11773 'ebookLink' => __( 'Edit ebook', 'ebook-store' ),
11774 'productLink' => __( 'Edit product', 'ebook-store' ),
11775 'completedSummary' => __( 'Import finished.', 'ebook-store' ),
11776 'processingSummary' => __( 'Importing uploaded books...', 'ebook-store' ),
11777 ),
11778 )
11779 );
11780 }
11781
11782 function ebook_store_import_books_get_temp_root() {
11783 return trailingslashit( get_temp_dir() ) . 'ebook-store-imports';
11784 }
11785
11786 function ebook_store_import_books_get_session_dir( $user_id, $session_id ) {
11787 $user_id = max( 0, (int) $user_id );
11788 $session_id = preg_replace( '/[^a-z0-9_-]/i', '', (string) $session_id );
11789
11790 return trailingslashit( ebook_store_import_books_get_temp_root() ) . $user_id . '/' . $session_id;
11791 }
11792
11793 function ebook_store_import_books_ensure_directory( $dir ) {
11794 if ( $dir === '' ) {
11795 return false;
11796 }
11797
11798 if ( file_exists( $dir ) ) {
11799 return is_dir( $dir );
11800 }
11801
11802 return wp_mkdir_p( $dir );
11803 }
11804
11805 function ebook_store_import_books_cleanup_empty_dirs( $dir, $stop_dir ) {
11806 $dir = untrailingslashit( (string) $dir );
11807 $stop_dir = untrailingslashit( (string) $stop_dir );
11808
11809 while ( $dir !== '' && strpos( $dir, $stop_dir ) === 0 && $dir !== $stop_dir ) {
11810 $entries = @scandir( $dir );
11811 if ( ! is_array( $entries ) || count( $entries ) > 2 ) {
11812 break;
11813 }
11814 @rmdir( $dir );
11815 $dir = dirname( $dir );
11816 }
11817 }
11818
11819 function ebook_store_import_books_store_queue_item( $token, $data ) {
11820 $user_id = get_current_user_id();
11821 set_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token, $data, DAY_IN_SECONDS );
11822 }
11823
11824 function ebook_store_import_books_get_queue_item( $token ) {
11825 $user_id = get_current_user_id();
11826 return get_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11827 }
11828
11829 function ebook_store_import_books_delete_queue_item( $token ) {
11830 $user_id = get_current_user_id();
11831 delete_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11832 }
11833
11834 function ebook_store_import_books_ajax_upload() {
11835 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11836
11837 if ( ! current_user_can( 'manage_options' ) ) {
11838 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11839 }
11840
11841 if ( ! ebook_store_import_books_has_pro_access() ) {
11842 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11843 }
11844
11845 if ( empty( $_FILES['file']['tmp_name'] ) ) {
11846 wp_send_json_error( array( 'message' => __( 'No upload chunk was received.', 'ebook-store' ) ), 400 );
11847 }
11848
11849 $session_id = isset( $_REQUEST['session'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['session'] ) ) : '';
11850 $file_id = isset( $_REQUEST['file_id'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['file_id'] ) ) : '';
11851 $original_name = isset( $_REQUEST['name'] ) ? sanitize_file_name( (string) wp_unslash( $_REQUEST['name'] ) ) : sanitize_file_name( (string) $_FILES['file']['name'] );
11852 $chunk = isset( $_REQUEST['chunk'] ) ? max( 0, (int) $_REQUEST['chunk'] ) : 0;
11853 $chunks = isset( $_REQUEST['chunks'] ) ? max( 0, (int) $_REQUEST['chunks'] ) : 0;
11854
11855 if ( $session_id === '' || $file_id === '' || $original_name === '' ) {
11856 wp_send_json_error( array( 'message' => __( 'The upload request is missing required file information.', 'ebook-store' ) ), 400 );
11857 }
11858
11859 $filetype = wp_check_filetype( $original_name );
11860 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
11861 if ( $extension !== 'pdf' ) {
11862 wp_send_json_error( array( 'message' => __( 'Only PDF files can be imported here.', 'ebook-store' ) ), 400 );
11863 }
11864
11865 $session_dir = ebook_store_import_books_get_session_dir( get_current_user_id(), $session_id );
11866 if ( ! ebook_store_import_books_ensure_directory( $session_dir ) ) {
11867 wp_send_json_error( array( 'message' => __( 'The temporary upload directory could not be created.', 'ebook-store' ) ), 500 );
11868 }
11869
11870 $stored_name = sanitize_file_name( $file_id . '-' . $original_name );
11871 $partial_path = trailingslashit( $session_dir ) . $stored_name . '.part';
11872 $final_path = trailingslashit( $session_dir ) . $stored_name;
11873 $chunk_path = $_FILES['file']['tmp_name'];
11874
11875 $input_handle = @fopen( $chunk_path, 'rb' );
11876 if ( ! $input_handle ) {
11877 wp_send_json_error( array( 'message' => __( 'The uploaded file chunk could not be read.', 'ebook-store' ) ), 500 );
11878 }
11879
11880 $output_handle = @fopen( $partial_path, $chunk === 0 ? 'wb' : 'ab' );
11881 if ( ! $output_handle ) {
11882 @fclose( $input_handle );
11883 wp_send_json_error( array( 'message' => __( 'The temporary upload file could not be written.', 'ebook-store' ) ), 500 );
11884 }
11885
11886 while ( ! feof( $input_handle ) ) {
11887 $buffer = fread( $input_handle, 1048576 );
11888 if ( $buffer === false ) {
11889 break;
11890 }
11891 fwrite( $output_handle, $buffer );
11892 }
11893
11894 @fclose( $input_handle );
11895 @fclose( $output_handle );
11896
11897 $is_last_chunk = $chunks < 2 || ( $chunk + 1 ) >= $chunks;
11898 if ( ! $is_last_chunk ) {
11899 wp_send_json_success(
11900 array(
11901 'fileId' => $file_id,
11902 'status' => 'chunked',
11903 )
11904 );
11905 }
11906
11907 @unlink( $final_path );
11908 if ( ! @rename( $partial_path, $final_path ) ) {
11909 wp_send_json_error( array( 'message' => __( 'The uploaded PDF could not be finalized.', 'ebook-store' ) ), 500 );
11910 }
11911
11912 $pdf_file = array(
11913 'name' => $original_name,
11914 'tmp_name' => $final_path,
11915 );
11916 if ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
11917 @unlink( $final_path );
11918 wp_send_json_error( array( 'message' => __( 'The uploaded file is not a valid PDF.', 'ebook-store' ) ), 400 );
11919 }
11920
11921 $fallback_title = preg_replace( '/\.[^.]+$/', '', $original_name );
11922 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $final_path, $fallback_title );
11923 $token = str_replace( '-', '', wp_generate_uuid4() );
11924 $filesize = file_exists( $final_path ) ? (int) filesize( $final_path ) : 0;
11925
11926 ebook_store_import_books_store_queue_item(
11927 $token,
11928 array(
11929 'token' => $token,
11930 'file_id' => $file_id,
11931 'session_id' => $session_id,
11932 'file_path' => $final_path,
11933 'file_name' => $original_name,
11934 'file_size' => $filesize,
11935 'details' => $pdf_details,
11936 'uploaded_at' => time(),
11937 )
11938 );
11939
11940 wp_send_json_success(
11941 array(
11942 'fileId' => $file_id,
11943 'token' => $token,
11944 'fileName' => $original_name,
11945 'fileSize' => $filesize,
11946 'title' => isset( $pdf_details['title'] ) ? (string) $pdf_details['title'] : '',
11947 'description' => isset( $pdf_details['description'] ) ? (string) $pdf_details['description'] : '',
11948 'author' => isset( $pdf_details['author'] ) ? (string) $pdf_details['author'] : '',
11949 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
11950 )
11951 );
11952 }
11953
11954 function ebook_store_import_books_ajax_process() {
11955 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11956
11957 if ( ! current_user_can( 'manage_options' ) ) {
11958 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11959 }
11960
11961 if ( ! ebook_store_import_books_has_pro_access() ) {
11962 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11963 }
11964
11965 $token = isset( $_POST['token'] ) ? sanitize_key( (string) wp_unslash( $_POST['token'] ) ) : '';
11966 $mode = isset( $_POST['import_mode'] ) ? sanitize_key( (string) wp_unslash( $_POST['import_mode'] ) ) : 'ebook';
11967 $import_price = isset( $_POST['import_price'] ) && is_scalar( $_POST['import_price'] ) ? (float) str_replace( ',', '.', (string) wp_unslash( $_POST['import_price'] ) ) : 0.0;
11968 $import_price = max( 0, $import_price );
11969
11970 if ( $token === '' ) {
11971 wp_send_json_error( array( 'message' => __( 'The uploaded file token is missing.', 'ebook-store' ) ), 400 );
11972 }
11973
11974 if ( ! in_array( $mode, array( 'ebook', 'woocommerce' ), true ) ) {
11975 wp_send_json_error( array( 'message' => __( 'The selected import mode is invalid.', 'ebook-store' ) ), 400 );
11976 }
11977
11978 $queue_item = ebook_store_import_books_get_queue_item( $token );
11979 if ( ! is_array( $queue_item ) || empty( $queue_item['file_path'] ) ) {
11980 wp_send_json_error( array( 'message' => __( 'The uploaded file could not be found. Please upload it again.', 'ebook-store' ) ), 404 );
11981 }
11982
11983 $file_path = (string) $queue_item['file_path'];
11984 if ( ! file_exists( $file_path ) ) {
11985 ebook_store_import_books_delete_queue_item( $token );
11986 wp_send_json_error( array( 'message' => __( 'The uploaded PDF is no longer available. Please upload it again.', 'ebook-store' ) ), 404 );
11987 }
11988
11989 $details = isset( $queue_item['details'] ) && is_array( $queue_item['details'] ) ? $queue_item['details'] : array();
11990 $file_name = isset( $queue_item['file_name'] ) ? (string) $queue_item['file_name'] : __( 'uploaded PDF', 'ebook-store' );
11991 $fallback_title = preg_replace( '/\.[^.]+$/', '', (string) $queue_item['file_name'] );
11992 $title = isset( $details['title'] ) && $details['title'] !== '' ? (string) $details['title'] : $fallback_title;
11993 $description = isset( $details['description'] ) ? (string) $details['description'] : '';
11994
11995 $shared_args = array(
11996 'title' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11997 'product_name' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11998 'description' => $description,
11999 'price' => $import_price,
12000 'currency' => ebook_store_get_store_currency(),
12001 'pdf_file' => array(
12002 'name' => $file_name,
12003 'tmp_name' => $file_path,
12004 ),
12005 'pages' => isset( $details['pages'] ) ? (int) $details['pages'] : 0,
12006 'author' => isset( $details['author'] ) ? (string) $details['author'] : '',
12007 );
12008
12009 try {
12010 $result = $mode === 'woocommerce'
12011 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
12012 : ebook_store_setup_wizard_create_ebook( $shared_args );
12013 } catch ( Throwable $throwable ) {
12014 wp_send_json_error(
12015 array(
12016 'message' => sprintf(
12017 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
12018 $file_name,
12019 $throwable->getMessage()
12020 ),
12021 ),
12022 500
12023 );
12024 }
12025
12026 if ( is_wp_error( $result ) ) {
12027 wp_send_json_error(
12028 array(
12029 'message' => sprintf(
12030 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
12031 $file_name,
12032 $result->get_error_message()
12033 ),
12034 ),
12035 400
12036 );
12037 }
12038
12039 ebook_store_import_books_delete_queue_item( $token );
12040 @unlink( $file_path );
12041 ebook_store_import_books_cleanup_empty_dirs( dirname( $file_path ), ebook_store_import_books_get_temp_root() );
12042
12043 wp_send_json_success(
12044 array(
12045 'title' => $shared_args['title'],
12046 'ebookId' => isset( $result['ebook_id'] ) ? (int) $result['ebook_id'] : 0,
12047 'ebookEditUrl' => ! empty( $result['ebook_id'] ) ? get_edit_post_link( (int) $result['ebook_id'], 'raw' ) : '',
12048 'productId' => ! empty( $result['product_id'] ) ? (int) $result['product_id'] : 0,
12049 'productEditUrl' => ! empty( $result['product_id'] ) ? get_edit_post_link( (int) $result['product_id'], 'raw' ) : '',
12050 )
12051 );
12052 }
12053
12054 function ebook_store_register_my_custom_submenu_page() {
12055
12056 add_submenu_page(
12057 'edit.php?post_type=ebook' //or 'options.php'
12058 , __('Orders - Add New', 'ebook-store')
12059 , __('Orders - Add New', 'ebook-store')
12060 , 'manage_options'
12061 , 'ebook-store-add-order-page'
12062 , 'ebook_store_add_order_page_callback'
12063 );
12064
12065 add_submenu_page(
12066 'edit.php?post_type=ebook' //or 'options.php'
12067 , __('Orders - Reports', 'ebook-store')
12068 , __('Orders - Reports', 'ebook-store')
12069 , 'manage_options'
12070 , 'ebook-store-order_reports'
12071 , 'ebook_store_order_reports_callback'
12072 );
12073
12074 add_submenu_page(
12075 'edit.php?post_type=ebook' //or 'options.php'
12076 , __('Orders - New Issue', 'ebook-store')
12077 , __('Orders - New Issue', 'ebook-store')
12078 , 'manage_options'
12079 , 'ebook-store-add-issue-page'
12080 , 'ebook_store_add_issue_page_callback'
12081 );
12082
12083 add_submenu_page(
12084 'edit.php?post_type=ebook',
12085 __( 'Setup wizard', 'ebook-store' ),
12086 ebook_store_setup_wizard_menu_title(),
12087 'manage_options',
12088 'ebook-store-setup-wizard',
12089 'ebook_store_setup_wizard_page_callback'
12090 );
12091
12092 add_submenu_page(
12093 'edit.php?post_type=ebook',
12094 __( 'Import books', 'ebook-store' ),
12095 __( 'Import books', 'ebook-store' ),
12096 'manage_options',
12097 'ebook-store-import-books',
12098 'ebook_store_import_books_page_callback'
12099 );
12100
12101 add_submenu_page(
12102 'edit.php?post_type=ebook',
12103 __( 'Button templates', 'ebook-store' ),
12104 __( 'Button templates', 'ebook-store' ),
12105 'manage_options',
12106 'ebook-store-template-editor',
12107 'ebook_store_template_editor_page_callback'
12108 );
12109
12110 if ( ! ebook_store_can_access_template_editor() ) {
12111 remove_submenu_page( 'edit.php?post_type=ebook', 'ebook-store-template-editor' );
12112 }
12113
12114 add_submenu_page(
12115 'edit.php?post_type=ebook',
12116 'Settings',
12117 __('Settings', 'ebook-store'),
12118 'manage_options',
12119 'settings-page',
12120 'ebook_store_settings_callback' );
12121
12122 }
12123 function ebook_store_settings_callback() {
12124 echo '<script>window.location = "options-general.php?page=ebook_options.php"; </script>';
12125 }
12126 function ebook_store_add_query_vars_filter( $vars ){
12127 $vars[] = "new_issue_purchase_period";
12128 $vars[] = "new_issue_ebook_id";
12129 $vars[] = "woocommerce_product_id";
12130 return $vars;
12131 }
12132
12133 //Add custom query vars
12134
12135 function ebook_store_add_issue_page_callback() {
12136 require_once 'ebook_store_add_issue_page_callback.php';
12137
12138 }
12139
12140 function ebook_store_import_books_page_callback() {
12141 require_once 'ebook_store_import_books_page_callback.php';
12142 }
12143
12144 function ebook_store_add_order_page_callback() {
12145 require_once 'ebook_store_add_order_page_callback.php';
12146 }
12147 function ebook_store_add_order($data, $silent = false, $thankYouRedirect = false, $ebook_key = false) {
12148 $QSWPOptions = new QSWPOptions();
12149
12150 if (@$ebook_key == '') {
12151 $ebook_key = md5(microtime() . rand(1,10000000));
12152 } else {
12153 // An explicit key was supplied — this is a payment webhook (Stripe/Adyen)
12154 // or a recovery call. If an order already exists for this key, a retried
12155 // or duplicated webhook is calling again: return the existing order rather
12156 // than creating a duplicate order and sending a second delivery email.
12157 $existing_order = ebook_get_order( 'ebook_key', $ebook_key );
12158 if ( ! empty( $existing_order['order_id'][0] ) ) {
12159 return $silent ? (int) $existing_order['order_id'][0] : null;
12160 }
12161 }
12162
12163 $data['md5_nonce'] = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
12164 if ( ! isset( $data['mc_fee'] ) ) {
12165 $data['mc_fee'] = 0;
12166 }
12167 $data['item_name'] = get_the_title($data['ebook']);
12168 $data['payment_date'] = date("m/d/Y H:i:s");
12169 @$data['txn_id'] = (@$data['txn_id'] != '' ? @$data['txn_id'] : 'Manual Payment');
12170 $data['mc_currency'] = ebook_store_get_store_currency();
12171 if ( ! isset( $data['tax'] ) || ! is_numeric( $data['tax'] ) ) {
12172 $data['tax'] = 0;
12173 }
12174 $data['residence_country'] = 'n/a';
12175
12176 ebook_store_subscribe_buyer_to_marketing_lists($data['payer_email']);
12177 if (@$data['user_id'] == 0) {
12178 $data['user_id'] = ebook_store_silent_registration($data);
12179 }
12180
12181
12182 $my_post = array(
12183 'post_title' => $data['first_name'] . ' ' . $data['last_name'],
12184 'post_type' => 'ebook_order',
12185 'post_status' => 'publish',
12186 'post_author' => 1,
12187 //'post_category' => array(8,39),
12188 );
12189 if ($data['mc_gross'] == false) {
12190 $data['mc_gross'] = 0;
12191 }
12192 // Store a machine-readable amount with NO thousands separator. It used
12193 // to be number_format()'d twice with a ',' separator: the second pass
12194 // re-cast "1,500.00" to a float, which PHP truncates at the comma to
12195 // 1.0 — so every order of 1000+ was recorded as 1.00. Thousands
12196 // separators belong at display time only.
12197 $mc_gross = number_format( (float) $data['mc_gross'], 2, '.', '' );
12198 $ebook = get_post_meta($data['ebook'], 'ebook', true);
12199 $ebookObj = new EbookStoreEbook($data['ebook']);
12200
12201 // Note: unlike the PayPal IPN path, every caller of this function has
12202 // already verified the payment (admin-entered order, or a signed
12203 // Stripe/Adyen webhook), so there is no untrusted amount to check here.
12204 $post_id = wp_insert_post( $my_post, true );
12205 if ( ! is_wp_error( $post_id ) && $post_id ) {
12206 foreach ($data as $k => $v) {
12207 update_post_meta($post_id, $k, $v);
12208 @$order[$k] = $v;
12209 }
12210
12211 $order['order_id'] = $post_id;
12212 $order['password'] = ebook_store_get_password_for_order($post_id, array(
12213 'password' => isset($data['password']) ? $data['password'] : '',
12214 'payer_email' => isset($data['payer_email']) ? $data['payer_email'] : '',
12215 'md5_nonce' => isset($data['md5_nonce']) ? $data['md5_nonce'] : '',
12216 'ebook_key' => $ebook_key,
12217 ));
12218 $data['password'] = $order['password'];
12219
12220 global $attachment, $ebook_email_delivery, $formData;
12221 $attachment = ebook_attachment($data['ebook'],true);
12222
12223 $formData = ebook_store_get_form($data['md5_nonce']);
12224 $formData = json_encode($formData);
12225 $ebook_order['ebook_key'][0] = $ebook_key;
12226 $ebook_order['ebook'][0] = $data['ebook'];
12227 $ebook_order['order_id'][0] = $post_id;
12228 $ebook_order['md5_nonce'][0] = isset( $data['md5_nonce'] ) ? $data['md5_nonce'] : '';
12229 $order['downloadlink'] = ebook_download_link($ebook_order);
12230
12231 $ebookObj->order_id = $post_id;
12232 // Plugin order: link() needs the ebook_key credentials or the
12233 // non-PDF format links point at the WooCommerce-only endpoint.
12234 $ebookObj->ebook_key = $ebook_key;
12235 $ebookObj->md5_nonce = isset( $data['md5_nonce'] ) ? (string) $data['md5_nonce'] : '';
12236 $ebookObj->setLink['pdf'] = $order['downloadlink'];
12237
12238 $order['downloadlink_html'] = $ebookObj->format_links();
12239 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
12240
12241 $order['ebook_key'] = $ebook_key;
12242 $order['order_id'] = $post_id;
12243 $order['md5_nonce'] = $data['md5_nonce'];
12244 update_post_meta($post_id,'ebook_key',$ebook_key);
12245 update_post_meta($post_id,'downloads',0);
12246 update_post_meta($post_id,'mc_gross',$mc_gross);
12247 update_post_meta($post_id,'formData',wp_slash($formData));
12248 update_post_meta($post_id,'downloadlink',$order['downloadlink']);
12249 update_post_meta($post_id,'ebook',$data['ebook']);
12250 //error_log('ebook_store_add_order - ' . print_r($order,true));
12251
12252 $email_template = ebook_store_get_effective_delivery_email_template( absint( $data['ebook'] ) );
12253 $ebook_email_delivery = array(
12254 'to' => $data['payer_email'],
12255 'subject' => $email_template['subject'],
12256 'text' => $email_template['text'],
12257 'attachment' => $attachment,
12258 'order' => $order,
12259 'source' => 'standalone',
12260 'related_order_type' => 'ebook_order',
12261 );
12262 //mail(get_option( 'admin_email' ), 'Ebook store for WordPress - Verified Order Received', print_r($ebook_email_delivery,true));
12263 @$fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
12264 //wp_mail('deian@motov.net','test','test');
12265 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
12266 $data['ebook_key'] = $ebook_key;
12267 $data['order_id'] = $post_id;
12268 ebook_encrypt_pdf($data, null, $data);
12269 }
12270 if ($silent == false) {
12271 ?>
12272 <div class="updated">
12273 <p><?php _e( 'Order has been generated and email was sent to the buyer!<br /><small>If your email has not arrived, we strongly recommend using Easy WP SMTP to send emails trough a service like Gmail to ensure inbox delivery.</small>', 'ebook-store' ); ?></p>
12274 </div><?php
12275 } ebook_email_delivery($post_id);
12276
12277 add_action( 'init', 'ebook_email_delivery', 100);
12278 if ($thankYouRedirect) {
12279 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
12280 $link = add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage));
12281 header("Location: $link");
12282 }
12283 if ($silent) {
12284 return $post_id;
12285 }
12286 //error_log('ebook_email_delivery added to plugins_loaded');
12287 }
12288 }
12289
12290
12291
12292
12293
12294 function erl($var) {
12295 if (is_array($var)) {
12296 $varOut = print_r($varOut,true);
12297 } else {
12298 $varOut = $var;
12299 }
12300 error_log(debug_backtrace()[1]['function']);
12301 error_log($varOut);
12302 }
12303
12304
12305 function file_upload_max_size() {
12306 static $max_size = -1;
12307
12308 if ($max_size < 0) {
12309 // Start with post_max_size.
12310 $max_size = parse_size(ini_get('post_max_size'));
12311
12312 // If upload_max_size is less, then reduce. Except if upload_max_size is
12313 // zero, which indicates no limit.
12314 $upload_max = parse_size(ini_get('upload_max_filesize'));
12315 if ($upload_max > 0 && $upload_max < $max_size) {
12316 $max_size = $upload_max;
12317 }
12318 }
12319 return $max_size;
12320 }
12321
12322 function parse_size($size) {
12323 $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
12324 $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
12325 if ($unit) {
12326 // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
12327 return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
12328 }
12329 else {
12330 return round($size);
12331 }
12332 }
12333
12334
12335 function register_ebook_store_woocommerce_type() {
12336 /**
12337 * This should be in its own separate file.
12338 */
12339 // if (class_exists('WC_Product_Simple')) {
12340 // class WC_Product_Ebook_Store extends WC_Product_Simple {
12341
12342 // public function __construct( $product ) {
12343
12344 // $this->product_type = 'ebook_store';
12345
12346 // parent::__construct( $product );
12347
12348 // }
12349 // public function get_type() {
12350 // return 'ebook_store';
12351 // }
12352
12353 // }
12354 // }
12355 if (class_exists('WC_Product')) {
12356 class WC_Product_ebookstore extends WC_Product {
12357
12358 function __construct($product) {
12359 $this->supports[] = 'ajax_add_to_cart';
12360 parent::__construct($product);
12361 $this->product_type = 'ebookstore';
12362 }
12363
12364 /**
12365 * Method to read a product from the database.
12366 * @param WC_Product
12367 */
12368
12369 public function get_product_type( $product_id ) {
12370 return 'ebookstore';
12371
12372 }
12373 public function get_type() {
12374 return 'ebookstore';
12375 }
12376 }
12377
12378
12379 }
12380
12381 }
12382 function woocommerce_custom_product_tabs_for_ebook_store( $tabs) {
12383 $tabs['ebookstore'] = array(
12384 'label' => __( 'Ebook Store', 'woocommerce' ),
12385 'target' => 'ebook_store_options',
12386 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_ebookstore' ),
12387 );
12388 return $tabs;
12389 }
12390
12391
12392
12393
12394 function add_ebook_store_item( $types ){
12395
12396 // Key should be exactly the same as in the class product_type parameter
12397 //$types[ 'ebookstore' ] = __( 'Ebook Store', 'ebook-store' );
12398
12399 return $types;
12400
12401 }
12402 add_filter( 'product_type_selector', 'add_ebook_store_item' );
12403
12404 function woocommerce_ebook_store_price_field() {
12405
12406 if ( 'product' != get_post_type() ) :
12407 //return;
12408 endif;
12409
12410 ?><script type='text/javascript'>
12411 jQuery( document ).ready( function() {
12412 jQuery( '.options_group.pricing' ).addClass( 'show_if_ebook_store' ).show();
12413 jQuery( '.general_options' ).show();
12414 jQuery('label[for="_virtual"]').show().addClass('show_if_ebook_store');
12415 jQuery('label[for="_downloadable"]').show().addClass('show_if_ebook_store');
12416
12417 });
12418 jQuery('#product-type').on('change', function () {
12419 jQuery('.general_options').show();
12420 setTimeout(function () {
12421 jQuery('label[for="_virtual"], label[for="_downloadable"]').show();
12422 }, 500);
12423 });
12424
12425 </script><?php
12426 }
12427
12428
12429
12430 /*-----------------------------------*/
12431 /**
12432 * Add a custom product tab.
12433 */
12434
12435 /**
12436 * Contents of the rental options product tab.
12437 */
12438 function ebook_store_woocommerce_tab_content() {
12439 global $post;
12440 $postOrig = $post;
12441 ?><div id='ebook_store_options' class='panel woocommerce_options_panel' style=""><?php
12442 ?><div class='options_group'>
12443 <?php
12444 if (get_option('ebook_store_woocommerce_integration') == 0) {
12445 ?>
12446 <h4>To use this feature please get the full version of the Ebook Store plugin by clicking <a href="https://www.shopfiles.com/index.php/products/wordpress-ebook-store" target="_blank">here</a>, and enable WooCommerce integration in <a href="options-general.php?page=ebook_options.php&tab=WooCommerce">Settings > Ebook Store > WooCommerce</a>.</h4>
12447 <?php
12448 return true;
12449 }
12450
12451 $product = function_exists( 'wc_get_product' ) ? wc_get_product( $post->ID ) : null;
12452 if ( $product && $product->is_type( 'variable' ) ) {
12453 ?>
12454 <p class="form-field">
12455 <strong><?php esc_html_e( 'Variable product note:', 'ebook-store' ); ?></strong>
12456 <?php esc_html_e( 'Assign the ebook on each variation below. Leaving a variation empty means that variation will not trigger Ebook Store delivery.', 'ebook-store' ); ?>
12457 </p>
12458 <?php
12459 }
12460
12461 woocommerce_wp_select(array(
12462 'id' => 'ebook_store_book_id',
12463 'label' => __( 'Select Ebook', 'woocommerce' ),
12464 'desc_tip' => 'true',
12465 'description' => __( 'The selected ebook will be delivered to your customer. Encryption and watermarking will be handled by ebook store plugin.' ),
12466 'type' => 'text',
12467 'value' => get_post_meta(get_the_ID(), 'ebook_store_book_id', true),
12468 'options' => ebook_store_get_woocommerce_options(),
12469 ) );
12470
12471 $selected_ebook_id = (int) get_post_meta( get_the_ID(), 'ebook_store_book_id', true );
12472 if ( $selected_ebook_id > 0 ) {
12473 $selected_ebook = get_post( $selected_ebook_id );
12474 if ( $selected_ebook && $selected_ebook->post_type === 'ebook' ) {
12475 $ebook_meta = get_post_meta( $selected_ebook_id, 'ebook', true );
12476 $ebook_meta = is_array( $ebook_meta ) ? $ebook_meta : array();
12477 $ebook_cover = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_cover', true );
12478 $ebook_cover = is_array( $ebook_cover ) ? $ebook_cover : array();
12479 $ebook_pdf = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_pdf', true );
12480 $ebook_pdf = is_array( $ebook_pdf ) ? $ebook_pdf : array();
12481 $ebook_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( isset( $ebook_meta['form_template'] ) ? $ebook_meta['form_template'] : '' ) : 'modern';
12482 $form_templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
12483 $template_label = isset( $form_templates[ $ebook_template ]['label'] ) ? $form_templates[ $ebook_template ]['label'] : ucfirst( $ebook_template );
12484 $delivery_mode = isset( $ebook_meta['donate_or_download'] ) ? $ebook_meta['donate_or_download'] : 'paid';
12485 $delivery_label = __( 'Paid download', 'ebook-store' );
12486 if ( $delivery_mode === 'free' ) {
12487 $delivery_label = __( 'Free download', 'ebook-store' );
12488 } elseif ( $delivery_mode === 'donate' ) {
12489 $delivery_label = __( 'Donate to download', 'ebook-store' );
12490 }
12491 $currency = isset( $ebook_meta['paypal_currency'] ) && $ebook_meta['paypal_currency'] !== '' ? $ebook_meta['paypal_currency'] : ebook_store_get_store_currency();
12492 $price = isset( $ebook_meta['ebook_price'] ) ? (float) $ebook_meta['ebook_price'] : 0.0;
12493 $pages = isset( $ebook_meta['ebook_pages'] ) ? (int) $ebook_meta['ebook_pages'] : 0;
12494 $cover_url = ! empty( $ebook_cover['url'] ) ? $ebook_cover['url'] : '';
12495 $pdf_name = ! empty( $ebook_pdf['file'] ) ? basename( $ebook_pdf['file'] ) : '';
12496 $edit_ebook_link = get_edit_post_link( $selected_ebook_id, 'raw' );
12497 ?>
12498 <div class="ebook-woocommerce-linked-ebook">
12499 <div class="ebook-woocommerce-linked-ebook__media">
12500 <div class="ebook-woocommerce-linked-ebook__cover<?php echo $cover_url ? ' has-image' : ' is-empty'; ?>">
12501 <?php if ( $cover_url ) : ?>
12502 <img src="<?php echo esc_url( $cover_url ); ?>" alt="<?php echo esc_attr( get_the_title( $selected_ebook_id ) ); ?>" />
12503 <?php else : ?>
12504 <span><?php esc_html_e( 'No cover', 'ebook-store' ); ?></span>
12505 <?php endif; ?>
12506 </div>
12507 </div>
12508 <div class="ebook-woocommerce-linked-ebook__content">
12509 <div class="ebook-woocommerce-linked-ebook__header">
12510 <div>
12511 <h4><?php echo esc_html( get_the_title( $selected_ebook_id ) ); ?></h4>
12512 <p><?php echo esc_html( wp_trim_words( wp_strip_all_tags( $selected_ebook->post_excerpt ? $selected_ebook->post_excerpt : $selected_ebook->post_content ), 24, '...' ) ); ?></p>
12513 </div>
12514 <?php if ( $edit_ebook_link ) : ?>
12515 <a class="button button-secondary" href="<?php echo esc_url( $edit_ebook_link ); ?>"><?php esc_html_e( 'Edit ebook', 'ebook-store' ); ?></a>
12516 <?php endif; ?>
12517 </div>
12518 <div class="ebook-woocommerce-linked-ebook__meta">
12519 <span><strong><?php esc_html_e( 'Status:', 'ebook-store' ); ?></strong> <?php echo esc_html( ucfirst( get_post_status( $selected_ebook_id ) ) ); ?></span>
12520 <span><strong><?php esc_html_e( 'Price:', 'ebook-store' ); ?></strong> <?php echo esc_html( ebook_store_order_money_display( $price, $currency ) ); ?></span>
12521 <span><strong><?php esc_html_e( 'Delivery:', 'ebook-store' ); ?></strong> <?php echo esc_html( $delivery_label ); ?></span>
12522 <span><strong><?php esc_html_e( 'Template:', 'ebook-store' ); ?></strong> <?php echo esc_html( $template_label ); ?></span>
12523 <?php if ( $pages > 0 ) : ?>
12524 <span><strong><?php esc_html_e( 'Pages:', 'ebook-store' ); ?></strong> <?php echo esc_html( number_format_i18n( $pages ) ); ?></span>
12525 <?php endif; ?>
12526 <?php if ( $pdf_name !== '' ) : ?>
12527 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $pdf_name ); ?></span>
12528 <?php endif; ?>
12529 </div>
12530 <p class="description"><?php esc_html_e( 'This is the ebook that Ebook Store will protect and deliver when this WooCommerce product is purchased.', 'ebook-store' ); ?></p>
12531 </div>
12532 </div>
12533 <?php
12534 }
12535 }
12536
12537 ?></div>
12538
12539 </div><?php
12540 $post = $postOrig;
12541 }
12542 /**
12543 * Save the custom fields.
12544 */
12545 function save_woocommerce_ebook_store_data( $post_id ) {
12546
12547 $ebook_store_ebook_id = isset( $_POST['_enable_ebook_store_option'] ) ? 'yes' : 'no';
12548 update_post_meta( $post_id, '_enable_ebook_store_option', $ebook_store_ebook_id );
12549
12550 if ( isset( $_POST['ebook_store_book_id'] ) ) {
12551 //first reset the old ebook store item to 0 in case id of the assigned woocommerce prduct is changed
12552 $ebook_store_book_id = get_post_meta($post_id, 'ebook_store_book_id', true);
12553 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', 0 );
12554 //then update to the currently set it.
12555 $ebook_store_book_id = sanitize_text_field( $_POST['ebook_store_book_id'] );
12556 update_post_meta( $post_id, 'ebook_store_book_id', $ebook_store_book_id );
12557 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', $post_id );
12558 //wp_die('woocommerce_product_id - ' . get_post_meta($ebook_store_book_id, 'woocommerce_product_id', true));
12559 }
12560
12561 }
12562 function ebook_store_get_woocommerce_options() {
12563 $new = new WP_Query(
12564 array(
12565 'posts_per_page' => -1,
12566 'post_type' => 'ebook',
12567 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
12568 'orderby' => 'title',
12569 'order' => 'ASC',
12570 'no_found_rows' => true,
12571 )
12572 );
12573 $ebooks = array();
12574 $ebooks[0] = '-- Please select --';
12575 while ($new->have_posts()) : $new->the_post();
12576 $ebook_title = get_the_title(get_the_ID());
12577 if ($ebook_title == '') {
12578 $ebook_title = '(no title)';
12579 }
12580 $ebook_status = get_post_status( get_the_ID() );
12581 if ( $ebook_status && $ebook_status !== 'publish' ) {
12582 $ebook_title .= ' [' . ucfirst( $ebook_status ) . ']';
12583 }
12584 $ebooks[get_the_ID()] = $ebook_title;
12585 endwhile;
12586 wp_reset_postdata();
12587
12588 return $ebooks;
12589 }
12590
12591 /**
12592 * The WooCommerce order status that releases the ebook.
12593 *
12594 * Sites set up through the wizard never had the option written, and older
12595 * installs may hold the legacy '0' ("any status"); both mean "completed".
12596 *
12597 * @return string Status slug without the 'wc-' prefix.
12598 */
12599 function ebook_store_get_wc_required_status() {
12600 $status = trim( (string) get_option( 'ebook_store_woocommerce_required_order_status', 'completed' ) );
12601 if ( $status === '' || $status === '0' ) {
12602 return 'completed';
12603 }
12604
12605 return sanitize_key( $status );
12606 }
12607
12608 /**
12609 * May the files for this WooCommerce order be handed out?
12610 *
12611 * The order key only proves the visitor saw the checkout page; this is the
12612 * payment gate shared by the download endpoint and the order-details table.
12613 *
12614 * @param int $order_id WooCommerce order ID.
12615 * @return bool
12616 */
12617 function ebook_store_wc_order_is_deliverable( $order_id ) {
12618 if ( ! function_exists( 'wc_get_order' ) ) {
12619 return false;
12620 }
12621
12622 $order = wc_get_order( absint( $order_id ) );
12623 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
12624 return false;
12625 }
12626
12627 if ( $order->has_status( array( 'cancelled', 'refunded', 'failed', 'pending', 'on-hold', 'checkout-draft', 'trash' ) ) ) {
12628 return false;
12629 }
12630
12631 return $order->is_paid() || $order->has_status( ebook_store_get_wc_required_status() );
12632 }
12633
12634 function ebook_store_woocommerce_email_delivery_pending($order_id) {
12635 if ('pending' != ebook_store_get_wc_required_status()) return;
12636 ebook_store_woocommerce_email_delivery($order_id);
12637 }
12638
12639 function ebook_store_woocommerce_email_delivery_processing($order_id) {
12640 if ('processing' != ebook_store_get_wc_required_status()) return;
12641 ebook_store_woocommerce_email_delivery($order_id);
12642 }
12643
12644 function ebook_store_woocommerce_email_delivery_on_hold($order_id) {
12645 if ('on-hold' != ebook_store_get_wc_required_status()) return;
12646 ebook_store_woocommerce_email_delivery($order_id);
12647 }
12648
12649 function ebook_store_woocommerce_email_delivery_cancelled($order_id) {
12650 if ('cancelled' != ebook_store_get_wc_required_status()) return;
12651 ebook_store_woocommerce_email_delivery($order_id);
12652 }
12653 function ebook_store_woocommerce_email_delivery_completed($order_id) {
12654 // WooCommerce skips "processing" for all-virtual, all-downloadable orders and
12655 // goes straight to "completed", so a "processing" setting must fire here too.
12656 $required_status = ebook_store_get_wc_required_status();
12657 if ('completed' != $required_status && 'processing' != $required_status) return;
12658 ebook_store_woocommerce_email_delivery($order_id);
12659 }
12660
12661 function ebook_store_woocommerce_email_delivery($order_id = 0, $event = 'completed' ) {
12662
12663 global $ebook_email_delivery;
12664 // erl('triggered completed');
12665 //error_reporting(E_ALL);
12666 $ebook_email_delivery = array();
12667 $order = new WC_Order( $order_id );
12668 if ( (string) $order->get_meta( '_ebook_store_delivered_at', true ) !== '' ) {
12669 return true;
12670 }
12671 $delivered = false;
12672 $order_items = $order->get_items();
12673 $order->address = $order->get_address();
12674 // error_log('WC ORDER' . print_r($order, true));
12675
12676 foreach ($order_items as $id => $item) {
12677 // error_log(print_r($item,true));
12678 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
12679 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
12680 if ($ebook_store_book_id == 0) {
12681 continue;
12682 }
12683 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_store_book_id );
12684 $ebook_email_delivery = array(
12685 'to' => $order->address['email'],
12686 'subject' => $email_template['subject'],
12687 'text' => $email_template['text'],
12688 'source' => 'woocommerce',
12689 'related_order_type' => 'shop_order',
12690 'woocommerce_event' => sanitize_key( (string) $event ),
12691 );
12692 //create new order array to make email delivery work
12693 $ebook = new EbookStoreEbook($ebook_store_book_id);
12694 $ebook->order_id = $order_id;
12695 $ebook->wc_order = ebook_store_get_wc_order_key($order_id); // HPOS-safe; raw post meta is empty when orders live in the orders table.
12696 $taxes = $order->get_taxes();
12697
12698 // error_log('taxes' . print_r($taxes,true));
12699 // foreach ($taxes as $tax) {
12700 // $total_taxes = $tax->get_tax_total();
12701 // error_log('total_taxes' . print_r($total_taxes,true));
12702 // //break;
12703 // }
12704 $vat = ebook_store_get_vat_percent();
12705 if ($vat > 0) {
12706 $tax = $ebook->price * ($vat / 100);
12707 } else {
12708 $tax = 0;
12709 }
12710 // Reuse the key already stored on the order so the QR code and links keep
12711 // pointing at the same record across status changes; generate it only once.
12712 $md5_nonce = (string) $order->get_meta( 'md5_nonce', true );
12713 $ebook_key = (string) $order->get_meta( 'ebook_key', true );
12714 if ( $md5_nonce === '' || $ebook_key === '' ) {
12715 $md5_nonce = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
12716 $ebook_key = md5($md5_nonce);
12717 }
12718 $payment_date = $order->get_date_created() ? $order->get_date_created()->__toString() : '';
12719 // Stored through the CRUD layer so HPOS sites can find the buyer later for
12720 // watermarks, passwords and the verification page.
12721 $order->update_meta_data( 'md5_nonce', $md5_nonce );
12722 $order->update_meta_data( 'ebook_key', $ebook_key );
12723 $order->update_meta_data( 'payer_email', (string) $order->address['email'] );
12724 $order->update_meta_data( 'first_name', (string) $order->address['first_name'] );
12725 $order->update_meta_data( 'last_name', (string) $order->address['last_name'] );
12726 $order->update_meta_data( 'txn_id', (string) $order->get_transaction_id() );
12727 $order->update_meta_data( 'payment_date', $payment_date );
12728 $order->update_meta_data( 'residence_country', (string) $order->address['country'] );
12729 $order->save();
12730 // Post-meta copies keep the older get_post_meta() lookups working when orders live in wp_posts.
12731 update_post_meta( $order_id, 'md5_nonce', $md5_nonce );
12732 update_post_meta( $order_id, 'ebook_key', $ebook_key );
12733 $pdf_download_link = esc_url_raw( add_query_arg( array( 'action' => 'wc_order_download', 'ebook_id' => $ebook_store_book_id, 'format' => 'pdf', 'wc_order' => $ebook->wc_order, 'order_id' => $order_id ), get_permalink( $ebook_store_book_id ) ) );
12734
12735 $ebook_email_delivery['order'] = array(
12736 'first_name' => $order->address['first_name'],
12737 'last_name' => $order->address['last_name'],
12738 'mc_gross' => $item['line_subtotal'],
12739 'ebook' => $ebook_store_book_id,
12740 'payer_email' => $order->address['email'],
12741 'md5_nonce' => $md5_nonce,
12742 'ebook_key' => $ebook_key,
12743 'mc_fee' => '',
12744 'item_name' => get_the_title($ebook_store_book_id),
12745 'payment_date' => $payment_date,
12746 'mc_currency' => $order->get_currency(),
12747 'residence_country' => $order->address['country'],
12748 'order_id' => $order_id,
12749 'password' => $ebook->password(),
12750 'txn_id' => $order->get_transaction_id(),
12751 'downloadlink' => $pdf_download_link,
12752 'downloadlink_html' => $ebook->format_links(),
12753 'downloadlinks' => $ebook->format_links(),
12754 'tax' => $tax,
12755 'ip' => $_SERVER['REMOTE_ADDR'],
12756 );
12757
12758
12759 $ebook_email_delivery['attachment'][0]['file'] = $ebook->file;
12760 $ebook_email_delivery['attachment'][0]['file'] = $ebook_email_delivery['attachment'][0]['file']['file'];
12761
12762 @$fileExt = pathinfo($ebook_email_delivery['attachment'][0]['file'],PATHINFO_EXTENSION);
12763
12764 //wp_mail('deian@motov.net','test','test');
12765 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
12766 $ebook_email_delivery['attachment'][0]['file'] = ebook_encrypt_pdf($ebook_email_delivery['order']);
12767 }
12768 if ( $fileExt == 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
12769 $ebook_email_delivery['attachment'][0]['file'] = $ebook->encrypted( $order_id, 'epub' );
12770 }
12771
12772 $ebook_store_woocommerce_required_order_status = esc_attr(get_option('ebook_store_woocommerce_required_order_status', 0));
12773 // if ($order_status != $ebook_store_woocommerce_required_order_status && $ebook_store_woocommerce_required_order_status != '0') {
12774 // error_log('ORDER STATUS ' . $ebook_store_woocommerce_required_order_status . ' VS ' . $order_status);
12775 // erl('will return now');
12776 // return $order_status;
12777 // }
12778
12779 ebook_email_delivery();
12780 $delivered = true;
12781 // error_log('ebook id ' . $ebook_store_book_id);
12782 # code...
12783 }
12784 if ( $delivered ) {
12785 $order->update_meta_data( '_ebook_store_delivered_at', current_time( 'mysql', true ) );
12786 $order->save();
12787 }
12788 // if ( 'processing' == $order_status && ( 'on-hold' == $order->status || 'pending' == $order->status || 'failed' == $order->status ) ) {
12789 // return 'completed';
12790 // }
12791 return true;
12792 }
12793
12794 function ebook_store_woocommerce_order_details($order, $skipHeaders = false){
12795 $order_items = $order->get_items();
12796 $order_status = $order->get_status();
12797 $order_id = 0;
12798 if (is_object($order)) {
12799 if (method_exists($order, 'get_id')) {
12800 $order_id = $order->get_id();
12801 } elseif (isset($order->id)) {
12802 $order_id = $order->id;
12803 }
12804 }
12805 if ( ! ebook_store_wc_order_is_deliverable( $order_id ) ) {
12806 return true;
12807 }
12808 if ($skipHeaders == false) {
12809 @$out .= "<h2>". __('Downloads', 'ebook-store') . "</h2>";
12810 @$out .= '<table class="shop_table order_downloads_ebook_store">
12811 <thead>
12812 <tr>
12813 <th class="product-name">'. __('Ebook (digital copy)', 'ebook-store') . '</th>
12814 <th class="product-total">'. __('Download', 'ebook-store') . '</th>
12815 </tr>
12816 </thead>
12817 <tbody>';
12818
12819 }
12820 // echo "<pre>";
12821 // print_r($order_items);
12822 // echo "<pre>";
12823 foreach ($order_items as $item_id => $item) {
12824 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
12825 $woocommerce_product_id = $item->get_product_id();
12826 $product = $item->get_product();
12827 $title = $product ? $product->get_title() : $item->get_name();
12828
12829 //wp_die("ID is $woocommerce_product_id");
12830 //$woocommerce_product_id = 100;
12831 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
12832
12833 if ($ebook_store_book_id != 0) {
12834 @$ebooks++;
12835 }
12836
12837 $ebook = new EbookStoreEbook($ebook_store_book_id);
12838 $ebook->order_id = $order_id;
12839 $ebook->wc_order = ebook_store_get_wc_order_key($order_id); // HPOS-safe; raw post meta is empty when orders live in the orders table.
12840 $cell = '<span class="amount">' . $ebook->format_links() . '</span><br /><small>' . $ebook->password_desc() . '</small>';
12841 // The reader is only offered when the main file really is a PDF: for an
12842 // ePub- or ZIP-only title the viewer cannot show anything, and its probe
12843 // of the download URL would count as a download.
12844 $main_file_is_pdf = ! empty( $ebook->files['pdf'] ) && strtolower( pathinfo( (string) $ebook->files['pdf'], PATHINFO_EXTENSION ) ) === 'pdf';
12845 if ( get_option( 'ebook_store_woocommerce_pdf_reader', false ) && $main_file_is_pdf ) {
12846 // Read the buyer's own protected copy through the download endpoint
12847 // (inline=1 shows it in the viewer without spending a download), and
12848 // keep the download links: the reader used to replace them entirely.
12849 $reader_url = add_query_arg(
12850 array(
12851 'action' => 'wc_order_download',
12852 'ebook_id' => $ebook_store_book_id,
12853 'format' => 'pdf',
12854 'wc_order' => $ebook->wc_order,
12855 'order_id' => $order_id,
12856 'inline' => 1,
12857 ),
12858 get_permalink( $ebook_store_book_id )
12859 );
12860 $cell = ebook_pdf_reader(false, $ebook_store_book_id, $reader_url) . '<div style="clear:both;"></div>' . $cell;
12861 }
12862
12863 if ($ebook_store_book_id > 0) {
12864 @$out .= '
12865 <tr class="order_item">
12866 <td class="product-name">
12867 ' . $title . '</td>
12868 <td class="product-total">
12869 ' . $cell . '
12870 </td>
12871 </tr>';
12872
12873 }
12874 }
12875 if ($skipHeaders == false) {
12876 @$out .= '
12877 </tbody>
12878 </table>';
12879 }
12880
12881 if (@$ebooks > 0) {
12882 echo $out;
12883 }
12884
12885 // echo '<pre>' . print_r($order,true) . '</pre>';
12886 // echo '<pre>' . print_r($ord,true) . '</pre>';
12887
12888 // echo '<p><strong>'.__('Pickup Location').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12889 // echo '<p><strong>'.__('Pickup Date').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12890 }
12891 /**
12892 * The order key stored against a WooCommerce order.
12893 *
12894 * Reads through the WooCommerce CRUD layer so the lookup keeps working under
12895 * High-Performance Order Storage, where orders no longer live in wp_posts and
12896 * get_post_meta() returns nothing.
12897 *
12898 * @param int $order_id WooCommerce order ID.
12899 * @return string Order key, or '' when the order does not exist.
12900 */
12901 function ebook_store_get_wc_order_key( $order_id ) {
12902 $order_id = absint( $order_id );
12903 if ( $order_id < 1 ) {
12904 return '';
12905 }
12906
12907 if ( function_exists( 'wc_get_order' ) ) {
12908 $order = wc_get_order( $order_id );
12909 if ( $order ) {
12910 return (string) $order->get_order_key();
12911 }
12912
12913 return '';
12914 }
12915
12916 return (string) get_post_meta( $order_id, '_order_key', true );
12917 }
12918
12919 /**
12920 * Does the supplied key authorise access to this WooCommerce order?
12921 *
12922 * @param int $order_id WooCommerce order ID.
12923 * @param string $order_key Key supplied in the request.
12924 * @return bool
12925 */
12926 function ebook_store_wc_order_key_matches( $order_id, $order_key ) {
12927 $order_key = sanitize_text_field( (string) $order_key );
12928 if ( $order_key === '' ) {
12929 return false;
12930 }
12931
12932 $stored = ebook_store_get_wc_order_key( $order_id );
12933
12934 return $stored !== '' && hash_equals( $stored, $order_key );
12935 }
12936
12937 /**
12938 * Is this ebook actually one of the items in that WooCommerce order?
12939 *
12940 * Without this check a valid order key for a $1 book would unlock every other
12941 * ebook in the catalogue.
12942 *
12943 * @param int $order_id WooCommerce order ID.
12944 * @param int $ebook_id Ebook post ID.
12945 * @return bool
12946 */
12947 function ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) {
12948 $ebook_id = absint( $ebook_id );
12949 if ( $ebook_id < 1 ) {
12950 return false;
12951 }
12952
12953 if ( ! function_exists( 'wc_get_order' ) ) {
12954 // Without WooCommerce there is nothing to cross-check against; the order
12955 // key match above is the only gate.
12956 return true;
12957 }
12958
12959 $order = wc_get_order( absint( $order_id ) );
12960 if ( ! $order ) {
12961 return false;
12962 }
12963
12964 foreach ( $order->get_items() as $item ) {
12965 $product_id = (int) $item->get_product_id();
12966 $variation_id = (int) $item->get_variation_id();
12967
12968 foreach ( array( $variation_id, $product_id ) as $candidate ) {
12969 if ( $candidate < 1 ) {
12970 continue;
12971 }
12972 if ( (int) get_post_meta( $candidate, 'ebook_store_book_id', true ) === $ebook_id ) {
12973 return true;
12974 }
12975 }
12976 }
12977
12978 return (bool) apply_filters( 'ebook_store_wc_order_contains_ebook', false, $order_id, $ebook_id );
12979 }
12980
12981 function ebook_process_download_woocomerce() {
12982 if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'wc_order_download' ) {
12983 return false;
12984 }
12985
12986 // ebook_process_download() now performs the order-key and order-contents
12987 // checks itself, so this wrapper only forwards the request.
12988 ebook_process_download( isset( $_GET['wc_order'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_order'] ) ) : '' );
12989 die();
12990 }
12991
12992 function ebook_store_add_to_cart() {
12993 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
12994 $locale = ebook_store_get_locale_strings();
12995 $formats_locale = ebook_store_get_format_labels();
12996
12997 if ($_GET['woocommerce_product_id'] > 0) {
12998 global $woocommerce;
12999 $woocommerce_product_id = (int)$_GET['woocommerce_product_id'];
13000 $cart_config = ebook_store_get_wc_add_to_cart_config( $woocommerce_product_id );
13001 if ( ! $cart_config ) {
13002 return;
13003 }
13004
13005 if ( ! ebook_store_cart_contains_wc_product( $cart_config ) ) {
13006 WC()->cart->add_to_cart(
13007 $cart_config['product_id'],
13008 1,
13009 $cart_config['variation_id'],
13010 $cart_config['variation']
13011 );
13012 }
13013
13014 global $ebook_store_messages;
13015
13016 if (get_option('ebook_store_woocommerce_integration_no_added_to_cart') < 1) {
13017 $ebook_store_messages[$woocommerce_product_id] = '<div class="ebook-store-alert ebook-store-alert-success">"' . esc_html( $cart_config['title'] ) . '" ' . $locale['addedToCart'] . '. <a href="' . wc_get_cart_url() . '" class="ebook_store_button">View Cart</a></div>';
13018 }
13019 }
13020 }
13021
13022
13023
13024 function ebook_store_action_links ( $links ) {
13025 $mylinks = array(
13026 '<a href="' . admin_url( 'options-general.php?page=ebook_options.php' ) . '">Settings</a>',
13027 '<a href="https://www.youtube.com/watch?v=T0mrdvrbmbc&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-" target="_blank">Video Tutorials</a>',
13028 '<a href="https://shopfiles.com/index.php/products/wordpress-ebook-store" target="_blank">Upgrade</a>',
13029 );
13030 return array_merge( $links, $mylinks );
13031 }
13032
13033 function ebook_store_payment_completed_wpam($ipn_data, $order_id)
13034 {
13035 if ( ! class_exists( 'WPAM_Logger' ) || ! class_exists( 'WPAM_Tracking_RequestTracker' ) ) {
13036 return;
13037 }
13038 $custom_data = isset( $ipn_data['custom'] ) ? (string) $ipn_data['custom'] : '';
13039 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - payment completed hook fired. Custom field value: '.$custom_data);
13040 $custom_values = array();
13041 parse_str($custom_data, $custom_values);
13042 if(isset($custom_values['wpam_tracking']) && !empty($custom_values['wpam_tracking']))
13043 {
13044 $tracking_value = $custom_values['wpam_tracking'];
13045 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Tracking data present. Need to track affiliate commission. Tracking value: '.$tracking_value);
13046 $purchaseLogId = $ipn_data['txn_id'];
13047 $purchaseAmount = $ipn_data['mc_gross']; //TODO - later calculate sub-total only
13048 $strRefKey = $tracking_value;
13049 $requestTracker = new WPAM_Tracking_RequestTracker();
13050 $requestTracker->handleCheckoutWithRefKey( $purchaseLogId, $purchaseAmount, $strRefKey);
13051 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Commission tracked for transaction ID: '.$purchaseLogId.'. Purchase amt: '.$purchaseAmount);
13052 }
13053 }
13054
13055 function ebook_store_payment_gateway_parameters_wpam($parameters)
13056 {
13057 if (class_exists('WPAM_PluginConfig') == false) {
13058 return;
13059 }
13060 if(isset($_COOKIE['wpam_id']))
13061 {
13062 $name = 'wpam_tracking';
13063 $value = $_COOKIE['wpam_id'];
13064 $new_val = $name.'='.$value;
13065 $current_val = $parameters['custom'];
13066 if(empty($current_val)){
13067 $parameters['custom'] = $new_val;
13068 }
13069 else{
13070 $parameters['custom'] = $current_val.'&'.$new_val;
13071 }
13072 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
13073 }
13074
13075 else if(isset($_COOKIE[WPAM_PluginConfig::$RefKey]))
13076 {
13077 $name = 'wpam_tracking';
13078 $value = $_COOKIE[WPAM_PluginConfig::$RefKey];
13079 $new_val = $name.'='.$value;
13080 $current_val = $parameters['custom'];
13081 if(empty($current_val)){
13082 $parameters['custom'] = $new_val;
13083 }
13084 else{
13085 $parameters['custom'] = $current_val.'&'.$new_val;
13086 }
13087 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
13088 }
13089
13090 return $parameters;
13091 }
13092
13093 function ebook_store_my_taxonomies_product() {
13094 $labels = array(
13095 'name' => _x( 'Ebook Categories', 'Ebook Categories', 'ebook-store' ),
13096 'singular_name' => _x( 'Ebook Category', 'Ebook Category', 'ebook-store' ),
13097 'search_items' => __( 'Search Ebook Categories', 'ebook-store' ),
13098 'all_items' => __( 'All Ebook Categories', 'ebook-store' ),
13099 'parent_item' => __( 'Parent Ebook Category', 'ebook-store' ),
13100 'parent_item_colon' => __( 'Parent Ebook Category:', 'ebook-store' ),
13101 'edit_item' => __( 'Edit Ebook Category', 'ebook-store' ),
13102 'update_item' => __( 'Update Ebook Category', 'ebook-store' ),
13103 'add_new_item' => __( 'Add New Ebook Category', 'ebook-store' ),
13104 'new_item_name' => __( 'New Ebook Category', 'ebook-store' ),
13105 'menu_name' => __( 'Ebook Categories', 'ebook-store' ),
13106 );
13107 $args = array(
13108 'labels' => $labels,
13109 'hierarchical' => true,
13110 );
13111 register_taxonomy( 'ebook_category', 'ebook', $args );
13112 }
13113
13114 function ebook_store_vc_before_init_actions() {
13115 class vcInfoBox extends WPBakeryShortCode {
13116
13117 // Element Init
13118 function __construct() {
13119 add_action( 'init', array( $this, 'vc_infobox_mapping' ) );
13120 add_shortcode( 'vc_infobox', array( $this, 'vc_infobox_html' ) );
13121 }
13122
13123 // Element Mapping
13124 public function vc_infobox_mapping() {
13125
13126 // Stop all if VC is not enabled
13127 if ( !defined( 'WPB_VC_VERSION' ) ) {
13128 return;
13129 }
13130
13131 // Map the block with vc_map()
13132 vc_map(
13133 array(
13134 'name' => __('Ebook Order Form', 'text-domain'),
13135 'base' => 'ebook_store',
13136 'description' => __('Inserts shortcode that will call the Ebook Order form.', 'text-domain'),
13137 'category' => __('Ebook Store', 'ebook-store'),
13138 'icon' => 'dashicons-cart', //get_template_directory_uri().'/assets/img/vc-icon.png',
13139 'params' => array(
13140
13141 array(
13142 'type' => 'dropdown',
13143 'holder' => 'h3',
13144 'class' => 'title-class',
13145 'heading' => __( 'Select Ebook', 'text-domain' ),
13146 'param_name' => 'ebook_id',
13147 'value' => vc_ebook_store_array(),
13148 'description' => __( 'You are embedding a direct order form for the Ebook via Ebook Store plugin.', 'text-domain' ),
13149 'admin_label' => false,
13150 'weight' => 0,
13151 'group' => 'Ebook Store',
13152 ),
13153
13154
13155 ),
13156 )
13157 );
13158
13159 }
13160
13161
13162 // Element HTML
13163 public function vc_infobox_html( $atts ) {
13164
13165 // Params extraction
13166 extract(
13167 shortcode_atts(
13168 array(
13169 'title' => '',
13170 'text' => '',
13171 'ebook_id' => '',
13172 ),
13173 $atts
13174 )
13175 );
13176
13177 // Fill $html var with data
13178 $html = '
13179 <div class="vc-infobox-wrap">
13180
13181 <h2 class="vc-infobox-title">' . $title . '</h2>
13182
13183 <div class="vc-infobox-text"></div>
13184
13185
13186
13187 </div>';
13188
13189 return $html;
13190
13191 }
13192
13193 } // End Element Class
13194
13195
13196 // Element Class Init
13197 new vcInfoBox();
13198 }
13199 function vc_ebook_store_array() {
13200 $args = array(
13201 'post_type' => 'ebook',
13202 );
13203 $out = array();
13204 $query = new WP_Query($args);
13205 while ($query->have_posts()) {
13206 $query->the_post();
13207 $out[get_the_title()] = get_the_ID();
13208 }
13209 return $out;
13210 }
13211 function ebook_store_order_reports_callback() {
13212 include_once('ebook_store_order_reports_callback.php');
13213 }
13214 /**
13215 * Stream the orders list as a CSV download.
13216 *
13217 * The previous version loaded every order at once, called get_post_meta() per
13218 * row, buffered the whole file in a PHP array and then die()'d with an implode.
13219 * At 10k orders that peaked several hundred megabytes and never finished. It
13220 * also took the column list from the FIRST row only, so every order with a
13221 * different meta shape emitted an undefined-index warning per missing field.
13222 *
13223 * This walks the orders in pages of 200, writes straight to the output stream,
13224 * and uses the union of all meta keys as the header row.
13225 */
13226 function ebook_store_export_orders() {
13227 if ( ! is_admin() ) {
13228 return;
13229 }
13230
13231 if ( ! isset( $_GET['export'] ) || (int) $_GET['export'] !== 1 ) {
13232 return;
13233 }
13234
13235 if ( ! current_user_can( 'manage_options' ) ) {
13236 wp_die( esc_html__( 'You are not allowed to export orders.', 'ebook-store' ) );
13237 }
13238
13239 check_admin_referer( 'ebook_store_export_orders' );
13240
13241 $selected_ebook_id = isset( $_GET['ebook_id'] ) ? absint( wp_unslash( $_GET['ebook_id'] ) ) : 0;
13242
13243 $base_args = array(
13244 'post_type' => 'ebook_order',
13245 'post_status' => 'any',
13246 'posts_per_page' => 200,
13247 'fields' => 'ids',
13248 'no_found_rows' => true,
13249 'update_post_term_cache' => false,
13250 'orderby' => 'ID',
13251 'order' => 'ASC',
13252 );
13253
13254 if ( $selected_ebook_id > 0 ) {
13255 $base_args['meta_key'] = 'ebook';
13256 $base_args['meta_value'] = $selected_ebook_id;
13257 }
13258
13259 // First pass: collect the union of meta keys so no row is missing a column.
13260 $keys = array();
13261 $page = 1;
13262 do {
13263 $args = $base_args;
13264 $args['paged'] = $page;
13265 $ids = get_posts( $args );
13266
13267 foreach ( $ids as $id ) {
13268 foreach ( array_keys( (array) get_post_meta( $id ) ) as $key ) {
13269 $keys[ $key ] = true;
13270 }
13271 }
13272
13273 $page++;
13274 } while ( count( $ids ) === $base_args['posts_per_page'] );
13275
13276 if ( empty( $keys ) ) {
13277 wp_die( esc_html__( 'There are no orders to export yet.', 'ebook-store' ) );
13278 }
13279
13280 $keys = array_keys( $keys );
13281 sort( $keys );
13282
13283 nocache_headers();
13284 header( 'Content-Type: text/csv; charset=utf-8' );
13285 header( 'Content-Disposition: attachment; filename="ebook-store-orders-' . gmdate( 'Y-m-d' ) . '.csv"' );
13286
13287 $out = fopen( 'php://output', 'w' );
13288
13289 // A UTF-8 BOM so Excel opens accented buyer names correctly.
13290 fwrite( $out, "\xEF\xBB\xBF" );
13291 fputcsv( $out, array_merge( array( 'order_id', 'order_date' ), $keys ) );
13292
13293 $page = 1;
13294 do {
13295 $args = $base_args;
13296 $args['paged'] = $page;
13297 $ids = get_posts( $args );
13298
13299 foreach ( $ids as $id ) {
13300 $meta = (array) get_post_meta( $id );
13301 $row = array( $id, get_post_field( 'post_date_gmt', $id ) );
13302
13303 foreach ( $keys as $key ) {
13304 $row[] = isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
13305 }
13306
13307 fputcsv( $out, $row );
13308 }
13309
13310 // Release the meta cache for the page we just wrote, otherwise the
13311 // pagination saves nothing.
13312 foreach ( $ids as $id ) {
13313 wp_cache_delete( $id, 'post_meta' );
13314 }
13315
13316 flush();
13317 $page++;
13318 } while ( count( $ids ) === $base_args['posts_per_page'] );
13319
13320 fclose( $out );
13321 exit;
13322 }
13323
13324 function ebook_store_session_end() {
13325 session_destroy ();
13326 }
13327 function ebook_store_encryptable($file) {
13328 require_once __DIR__ . '/fpdi/bootstrap.php';
13329 try {
13330 $pdf = new \setasign\Fpdi\Fpdi();
13331 $pdf->setSourceFile($file);
13332 return true;
13333 } catch (\Throwable $e) {
13334 return false;
13335 }
13336 }
13337
13338 function ebook_store_offer_tutorial() {
13339 if (ebook_store_endsWith(esc_url_raw($_SERVER['REQUEST_URI']), 'edit.php?post_type=ebook')) {
13340 $args = array(
13341 'post_type' => 'ebook',
13342 );
13343 $myquery = new WP_Query($args);
13344 if ($myquery->found_posts == 0) {
13345 ?>
13346 <div class="updated">
13347 <p><?php _e( 'You have not created any ebooks yet, do you want to see the sample videos how to help you get started with it?<br />
13348 <a href="https://www.youtube.com/watch?v=HxYzlaEHPU4&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-&index=1" target="_blank">Watch Tutorials on YouTube</a>', 'ebooks-store' ); ?></p>
13349 </div>
13350 <?php
13351 }
13352 }
13353 }
13354 function ebook_store_not_encryptable() {
13355 $class = 'notice notice-error';
13356 $message = __( 'This file is can not be encrypted. Please save it in Adobe Acrobat 5.0 PDF 1.5 compatibility format. See video tutorial here: <a href="https://youtu.be/-yDr-76W4gk" target="_blank">https://youtu.be/-yDr-76W4gk</a>', 'ebook-store' );
13357
13358 printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), $message );
13359 }
13360
13361 function ebook_store_get_password_for_order($order_id = 0, $context = array()) {
13362 static $runtime_password_cache = array();
13363
13364 $random_enabled = intval(get_option('ebook_store_random_password')) === 1;
13365 $blank_enabled = intval(get_option('ebook_store_blank_password')) === 1;
13366
13367 $cache_key = '';
13368 if ($order_id > 0) {
13369 $cache_key = 'order_' . $order_id;
13370 } elseif (!empty($context['md5_nonce'])) {
13371 $cache_key = 'nonce_' . $context['md5_nonce'];
13372 } elseif (!empty($context['ebook_key'])) {
13373 $cache_key = 'ebook_key_' . $context['ebook_key'];
13374 }
13375
13376 if ($cache_key && isset($runtime_password_cache[$cache_key])) {
13377 return $runtime_password_cache[$cache_key];
13378 }
13379
13380 $stored_exists = ($order_id > 0 && function_exists('metadata_exists')) ? metadata_exists('post', $order_id, 'password') : false;
13381 if ($stored_exists) {
13382 $stored_password = get_post_meta($order_id,'password',true);
13383 if ($cache_key) {
13384 $runtime_password_cache[$cache_key] = $stored_password;
13385 }
13386 return $stored_password;
13387 }
13388
13389 $payer_email = isset($context['payer_email']) ? trim($context['payer_email']) : '';
13390 if (!$payer_email && $order_id > 0) {
13391 $payer_email = get_post_meta($order_id,'payer_email',true);
13392 }
13393 $provided_password = isset($context['password']) ? (string)$context['password'] : '';
13394 $billing_email = isset($context['billing_email']) ? trim($context['billing_email']) : '';
13395 if ($billing_email === '' && $order_id > 0) {
13396 $billing_email = get_post_meta($order_id,'_billing_email',true);
13397 $billing_email = is_string($billing_email) ? trim($billing_email) : '';
13398 }
13399
13400 if ($blank_enabled) {
13401 $password = '';
13402 } elseif ($random_enabled) {
13403 $password = $provided_password;
13404 if ($password === '') {
13405 $password = substr(md5(uniqid('ebook_pw_', true)), 0, 8);
13406 }
13407 } else {
13408 $password = $provided_password ?: $payer_email;
13409 if ($password === '') {
13410 $password = $billing_email;
13411 }
13412 if ($password === '' && !empty($context['fallback_email'])) {
13413 $password = $context['fallback_email'];
13414 }
13415 }
13416
13417 if ($order_id > 0) {
13418 update_post_meta($order_id,'password',$password);
13419 }
13420
13421 if ($cache_key) {
13422 $runtime_password_cache[$cache_key] = $password;
13423 }
13424
13425 return $password;
13426 }
13427
13428 /**
13429 * The order-detail block shared by the confirmation page and the delivery email.
13430 *
13431 * @param bool $with_vat Append the VAT figure to the total line.
13432 * @return string
13433 */
13434 function ebook_store_default_order_details_block( $with_vat = false ) {
13435 $total = $with_vat
13436 /* translators: %%mc_currency%%, %%total%% and %%tax%% are merge codes — keep them exactly as they are. */
13437 ? __( 'Total: %%mc_currency%% %%total%% (VAT %%tax%%)', 'ebook-store' )
13438 /* translators: %%mc_currency%% and %%total%% are merge codes — keep them exactly as they are. */
13439 : __( 'Total: %%mc_currency%% %%total%%', 'ebook-store' );
13440
13441 return '<h3>' . __( 'Details for your order:', 'ebook-store' ) . '</h3>' . "\n"
13442 /* translators: %%order_id%% is a merge code — keep it exactly as it is. */
13443 . __( 'Order #: %%order_id%%', 'ebook-store' ) . "\n"
13444 /* translators: %%txn_id%% is a merge code — keep it exactly as it is. */
13445 . __( 'Transaction: %%txn_id%%', 'ebook-store' ) . "\n"
13446 /* translators: %%mc_currency%% and %%mc_gross%% are merge codes — keep them exactly as they are. */
13447 . __( 'Amount: %%mc_currency%% %%mc_gross%%', 'ebook-store' ) . "\n"
13448 . $total . "\n\n"
13449 /* translators: %%password%% is a merge code — keep it exactly as it is. */
13450 . '<strong>' . __( 'Your password is: %%password%%', 'ebook-store' ) . '</strong>' . "\n\n"
13451 . __( 'Thank you!', 'ebook-store' );
13452 }
13453
13454 /**
13455 * Default content of the order confirmation ("thank you") page.
13456 *
13457 * This used to be a hardcoded English string on QSWPOptions, so a shop selling
13458 * in German or Arabic showed its buyers an English confirmation page until
13459 * somebody rewrote it by hand. Built from translated pieces instead, so a store
13460 * gets the page in its own language out of the box.
13461 *
13462 * The %%...%% merge codes are substituted later and must survive translation.
13463 *
13464 * @return string
13465 */
13466 function ebook_store_default_thankyou_page() {
13467 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
13468 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
13469 . __( 'You have successfully completed the order process! Please use the link(s) below to download your copy:', 'ebook-store' ) . "\n\n"
13470 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
13471 . '<strong>' . __( 'Use the link(s) to start the download:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
13472 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
13473 . __( 'Bonus ebook download links:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n"
13474 . ebook_store_default_order_details_block( false );
13475 }
13476
13477 /**
13478 * Default body of the delivery email.
13479 *
13480 * Same reasoning as the confirmation page: it shipped in English only.
13481 *
13482 * @return string
13483 */
13484 function ebook_store_default_email_delivery_text() {
13485 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
13486 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
13487 . __( 'You have successfully completed the order process!', 'ebook-store' ) . "\n\n"
13488 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
13489 . '<strong>' . __( 'Please use the link(s) below to download your copy:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
13490 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
13491 . __( 'Bonus ebooks:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n\n"
13492 . ebook_store_default_order_details_block( true );
13493 }
13494
13495 /**
13496 * Default subject line of the delivery email.
13497 *
13498 * @return string
13499 */
13500 function ebook_store_default_email_delivery_subject() {
13501 return __( 'Email Delivery - Order complete!', 'ebook-store' );
13502 }
13503
13504 /**
13505 * Mark the admin body when this site holds a valid Pro licence.
13506 *
13507 * The "Pro" badge is drawn by CSS on any element carrying .goPro2, and several
13508 * of those classes are printed unconditionally — the delivery options, the bonus
13509 * books picker, the importer. A paying customer was still shown a locked-looking
13510 * screen covered in upsell badges. One body class lets the stylesheet stand every
13511 * one of them down, including any added later.
13512 *
13513 * @param string $classes Existing body classes.
13514 * @return string
13515 */
13516 function ebook_store_admin_body_class( $classes ) {
13517 if ( function_exists( 'ebook_store_has_pro_license' ) && ebook_store_has_pro_license() ) {
13518 $classes .= ' ebook-store-pro-active';
13519 }
13520
13521 return $classes;
13522 }
13523 add_filter( 'admin_body_class', 'ebook_store_admin_body_class' );
13524
13525 function ebook_store_file_formats_form($ebook_id) {
13526 $formats = array('pdf','zip','mobi','txt','mp3','epub','mp4');
13527 $ebook = new EbookStoreEbook($ebook_id);
13528 $ebook->formats = is_array($ebook->formats) ? $ebook->formats : array();
13529
13530 // Everything but PDF is a Pro format, but the "Pro" badge should only appear
13531 // to someone who does not have Pro. It used to be keyed on the format index
13532 // alone, so a paying licence holder still saw every format tab stamped and
13533 // styled as locked.
13534 $pro_unlocked = function_exists('ebook_store_has_pro_license') ? ebook_store_has_pro_license() : false;
13535
13536 $entries = array();
13537
13538 foreach ($formats as $index => $format) {
13539 if ($format == 'pdf' && get_option('encrypt_pdf')) {
13540 if (!ebook_store_encryptable(@$ebook->files[$format]) && @$ebook->files[$format] != '') {
13541 ebook_store_not_encryptable();
13542 }
13543 }
13544
13545 $has_file = @in_array($format, $ebook->formats);
13546 $filename = ($has_file && !empty($ebook->files[$format])) ? basename($ebook->files[$format]) : '';
13547 $size_label = @$ebook->human_filesize(@$ebook->files[$format . '_size']);
13548 $entries[] = array(
13549 'slug' => $format,
13550 'label' => strtoupper($format),
13551 'has_file' => $has_file,
13552 'filename' => $filename,
13553 'size_label' => $size_label,
13554 'exists_class'=> $has_file ? '' : 'hiddenEbookStore',
13555 'is_pro' => ($index > 0) && ! $pro_unlocked,
13556 );
13557 }
13558 ?>
13559 <div class="ebook_store_file_formats_form">
13560 <h3><?php _e('Upload eBook Files', 'ebook-store'); ?></h3>
13561 <p class="description"><?php _e('Upload your eBook in different formats. Supported formats are grouped by format tabs below.', 'ebook-store'); ?></p>
13562
13563 <div class="ebook-format-tabs" id="ebook-format-tabs-<?php echo esc_attr($ebook_id); ?>">
13564 <div class="ebook-format-tabs__nav" role="tablist">
13565 <?php foreach ($entries as $idx => $entry) :
13566 $panel_id = 'ebook-format-panel-' . $entry['slug'];
13567 $status_icon = $entry['has_file'] ? 'dashicons-yes-alt' : 'dashicons-minus';
13568 $nav_classes = array('ebook-format-tabs__nav-item');
13569 if ($entry['is_pro']) {
13570 $nav_classes[] = 'goPro2';
13571 }
13572 if ($entry['has_file']) {
13573 $nav_classes[] = 'has-file';
13574 }
13575 if ($idx === 0) {
13576 $nav_classes[] = 'is-active';
13577 }
13578 ?>
13579 <button type="button"
13580 class="<?php echo esc_attr(implode(' ', $nav_classes)); ?>"
13581 data-format="<?php echo esc_attr($entry['slug']); ?>"
13582 data-target="#<?php echo esc_attr($panel_id); ?>"
13583 role="tab"
13584 aria-controls="<?php echo esc_attr($panel_id); ?>"
13585 aria-selected="<?php echo $idx === 0 ? 'true' : 'false'; ?>">
13586 <span class="ebook-format-tab__icon">
13587 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?>" />
13588 </span>
13589 <span class="ebook-format-tab__label"><?php echo esc_html($entry['label']); ?></span>
13590 <span class="ebook-format-tab__status dashicons <?php echo esc_attr($status_icon); ?>"></span>
13591 </button>
13592 <?php endforeach; ?>
13593 </div>
13594 <div class="ebook-format-tabs__panels">
13595 <?php foreach ($entries as $idx => $entry) :
13596 $panel_id = 'ebook-format-panel-' . $entry['slug'];
13597 $panel_classes = array('ebook-format-tab-panel');
13598 if ($entry['is_pro']) {
13599 $panel_classes[] = 'goPro2';
13600 }
13601 if ($entry['has_file']) {
13602 $panel_classes[] = 'has-file';
13603 }
13604 if ($idx === 0) {
13605 $panel_classes[] = 'is-active';
13606 }
13607 ?>
13608 <div
13609 id="<?php echo esc_attr($panel_id); ?>"
13610 class="<?php echo esc_attr(implode(' ', $panel_classes)); ?>"
13611 data-tab-panel="<?php echo esc_attr($entry['slug']); ?>"
13612 <?php echo $idx === 0 ? '' : 'hidden'; ?>>
13613 <div class="ebook_store_file_format_item<?php echo $entry['is_pro'] ? ' goPro2' : ''; ?>">
13614 <div class="ebook_store_file_format_item_left">
13615 <span class="ebook_store_icon">
13616 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?> format" width="64" />
13617 </span>
13618 <span class="ebook_store_size <?php echo esc_attr($entry['exists_class']); ?>"><?php echo esc_html($entry['size_label']); ?></span>
13619 </div>
13620 <div class="ebook_store_file_format_item_right">
13621 <div class="ebook_store_filename">
13622 <?php if ($entry['has_file']) : ?>
13623 <a href="#" title="<?php _e('Current file', 'ebook-store'); ?>"><?php echo esc_html($entry['filename']); ?></a>
13624 <?php else : ?>
13625 <span><?php _e('No file uploaded yet', 'ebook-store'); ?></span>
13626 <?php endif; ?>
13627 </div>
13628 <div class="ebook_store_upload">
13629 <input type="file"
13630 accept=".<?php echo esc_attr($entry['slug']); ?>"
13631 name="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
13632 id="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
13633 data-format="<?php echo esc_attr($entry['slug']); ?>"
13634 title="<?php echo esc_attr( sprintf(__('Choose %s file', 'ebook-store'), $entry['label']) ); ?>"
13635 />
13636 </div>
13637 <div class="ebook_store_control <?php echo esc_attr($entry['exists_class']); ?>">
13638 <label title="<?php _e('Delete file', 'ebook-store'); ?>">
13639 <input type="checkbox" name="ebook_store_delete_<?php echo esc_attr($entry['slug']); ?>" />
13640 <i class="dashicons dashicons-trash"></i>
13641 <span class="screen-reader-text"><?php _e('Delete file', 'ebook-store'); ?></span>
13642 </label>
13643 </div>
13644 </div>
13645 </div>
13646 </div>
13647 <?php endforeach; ?>
13648 </div>
13649 </div>
13650 <?php
13651 // Files are sliced in the browser and streamed past the server's upload
13652 // limits, so quoting upload_max_filesize here only scared authors off
13653 // books that upload perfectly well.
13654 if ( function_exists( 'ebook_store_max_upload_bytes' ) ) :
13655 ?>
13656 <p class="description">
13657 <?php
13658 printf(
13659 /* translators: %s: maximum file size, already formatted. */
13660 esc_html__( 'Files are uploaded in small pieces, so the server upload limit does not apply. Maximum size per file: %s.', 'ebook-store' ),
13661 esc_html( size_format( ebook_store_max_upload_bytes() ) )
13662 );
13663 ?>
13664 </p>
13665 <?php else : ?>
13666 <p class="description"><?php _e('Maximum upload file size:', 'ebook-store'); ?> <?php echo ini_get('upload_max_filesize'); ?>B</p>
13667 <?php endif; ?>
13668 </div>
13669 <?php
13670 }
13671
13672 /**
13673 * Whether an ebook is delivered for free.
13674 *
13675 * A book is free when its delivery mode says so, or when it has no price and
13676 * its mode was never set (books from before the mode existed). A book that
13677 * was explicitly saved as paid or donate is not free, whatever its price field
13678 * holds — a paid book with a blank price is a mistake, not a giveaway.
13679 *
13680 * @param mixed $ebook The 'ebook' post meta array.
13681 * @return bool
13682 */
13683 function ebook_store_is_free_ebook( $ebook ) {
13684 if ( ! is_array( $ebook ) ) {
13685 return false;
13686 }
13687
13688 $mode = isset( $ebook['donate_or_download'] ) ? (string) $ebook['donate_or_download'] : '';
13689 $price = ( isset( $ebook['ebook_price'] ) && is_numeric( $ebook['ebook_price'] ) ) ? (float) $ebook['ebook_price'] : 0.0;
13690
13691 if ( $mode === 'free' ) {
13692 return true;
13693 }
13694
13695 return $mode !== 'paid' && $mode !== 'donate' && $price == 0;
13696 }
13697
13698 function ebook_store_price_plus_vat($price) {
13699 $vat = ebook_store_get_vat_percent();
13700 $price = (float) $price;
13701
13702 if ( $vat <= 0 ) {
13703 return $price;
13704 }
13705
13706 return $price + ( $price * ( $vat / 100 ) );
13707 }
13708
13709 /**
13710 * Signature binding an ebook to the exact amount AND currency it is sold for.
13711 *
13712 * The buy button's original signature covered only the ebook id and its net
13713 * price, so a buyer could edit the posted form to pay in a cheaper currency, or
13714 * drop the tax line, and PayPal would still verify the payment. This is carried
13715 * as a third segment of the `custom` field and re-checked against the values
13716 * PayPal itself reports.
13717 *
13718 * @param int $ebook_id Ebook post ID.
13719 * @param mixed $gross Amount the buyer is expected to pay, tax included.
13720 * @param string $currency ISO currency the store sells in.
13721 * @return string
13722 */
13723 function ebook_store_paypal_amount_signature( $ebook_id, $gross, $currency ) {
13724 $ebook_id = absint( $ebook_id );
13725 $gross = number_format( (float) $gross, 2, '.', '' );
13726 $currency = strtoupper( trim( (string) $currency ) );
13727
13728 return md5( NONCE_KEY . '|es-v2|' . $ebook_id . '|' . $gross . '|' . $currency );
13729 }
13730
13731 function ebook_store_get_store_currency() {
13732 $currency = strtoupper( trim( (string) get_option( 'paypal_currency', 'USD' ) ) );
13733
13734 return preg_match( '/^[A-Z]{3}$/', $currency ) ? $currency : 'USD';
13735 }
13736
13737 function ebook_store_get_vat_percent() {
13738 $vat = trim( (string) get_option( 'vat_percent', '0' ) );
13739
13740 if ( $vat === '' || ! is_numeric( $vat ) ) {
13741 return 0.0;
13742 }
13743
13744 return max( 0, (float) $vat );
13745 }
13746
13747 function ebook_store_get_download_limit() {
13748 $limit = trim( (string) get_option( 'downloads_limit', '5' ) );
13749
13750 if ( $limit === '' || ! is_numeric( $limit ) ) {
13751 return 5;
13752 }
13753
13754 return max( 1, (int) $limit );
13755 }
13756
13757
13758
13759 function ebook_store_row( $atts, $content = "") {
13760 return "<div class=\"ebook_store_row ebook_store_row_".intval($atts['col'])."\">" . do_shortcode($content) . "</div>";
13761 }
13762
13763 function ebook_store_silent_registration($data) {
13764 if (get_option('ebook_store_silent_registration') != 1) {
13765 return get_current_user_id();
13766 }
13767
13768 $user_email = isset( $data['payer_email'] ) ? sanitize_email( $data['payer_email'] ) : '';
13769 if ( $user_email === '' || ! is_email( $user_email ) ) {
13770 return get_current_user_id();
13771 }
13772
13773 // The email address is the buyer's identity. If an account already uses it,
13774 // reuse that account. The username must NOT be the key: the old code keyed on
13775 // "first_last", so a second buyer who happened to share a name with an
13776 // existing customer was silently attached to that customer's account.
13777 $existing = email_exists( $user_email );
13778 if ( $existing ) {
13779 return $existing;
13780 }
13781
13782 // Build a username that does not collide with any existing account.
13783 $base = sanitize_user( trim( (string) $data['first_name'] . '_' . (string) $data['last_name'] ), true );
13784 if ( $base === '' || $base === '_' ) {
13785 $parts = explode( '@', $user_email );
13786 $base = sanitize_user( $parts[0], true );
13787 }
13788 if ( $base === '' ) {
13789 $base = 'customer';
13790 }
13791
13792 $user_name = $base;
13793 $suffix = 1;
13794 while ( username_exists( $user_name ) ) {
13795 $suffix++;
13796 $user_name = $base . '_' . $suffix;
13797 }
13798
13799 $user_id = wp_create_user( $user_name, wp_generate_password( 16, true ), $user_email );
13800
13801 // A WP_Error must never be stored as the order's user_id.
13802 if ( is_wp_error( $user_id ) ) {
13803 return get_current_user_id();
13804 }
13805
13806 wp_new_user_notification( $user_id, null, 'both' );
13807
13808 return $user_id;
13809 }
13810
13811
13812
13813 function ebook_store_downloads($atts) {
13814 // Webhook-created orders are stored with user_id 0; never match an
13815 // anonymous visitor (also id 0) against them.
13816 if ( ! is_user_logged_in() || get_current_user_id() < 1 ) {
13817 return '<p>' . esc_html__( 'Please log in to see your orders.', 'ebook-store' ) . '</p>';
13818 }
13819 $args = array(
13820 'post_type' => 'ebook_order',
13821 'posts_per_page' => -1,
13822 'orderby' => 'date',
13823 'order' => 'DESC',
13824 'meta_query' => array(
13825 array(
13826 'key' => 'user_id',
13827 'value' => get_current_user_id(),
13828 'compare' => '=',
13829 )
13830 ));
13831 // The Query
13832 $the_query = new WP_Query( $args );
13833 $out = '';
13834 // The Loop
13835 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
13836 if ( $the_query->have_posts() ) {
13837 while ( $the_query->have_posts() ) {
13838 $the_query->the_post();
13839 $link = add_query_arg(array('ebook_key' => get_post_meta(get_the_ID(),'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
13840 $out .= '<h4><a href="' . esc_url( $link ) . '">' . esc_html__('Order','ebook-store') . ' #' . get_the_ID() . '</a></h4>';
13841 }
13842 } else {
13843 $out = '<p>' . esc_html__('No orders found.','ebook-store') . '</p>';
13844 }
13845 wp_reset_postdata();
13846 return $out;
13847 }
13848 function ebook_pdf_reader($ebook_order = false, $ebook_post_id = false, $source_url = '') {
13849 $src = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html';
13850 // /return $src;
13851 $src = add_query_arg('title',__('Online Viewer','ebook-store'),$src);
13852 // An explicit source wins: the WooCommerce order screen passes the buyer's
13853 // own authenticated download URL, because the raw uploads path the reader
13854 // used to be pointed at is denied by the plugin's own .htaccess (the reader
13855 // showed a 403 instead of the book) and would be public where it is not.
13856 if ( $source_url !== '' ) {
13857 return '<iframe style="float:left;" src="' . esc_url( $src ) . '#' . esc_url( $source_url ) . '" width=\'100%\' height=\'' . esc_attr( get_option('pdf_viewer_height',600) ) . '\' allowfullscreen webkitallowfullscreen></iframe>';
13858 }
13859 if ($ebook_order) {
13860 return '<iframe style="float:left;" src="'.$src.'#' . $ebook_order['downloadlink'][0] . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
13861 } else {
13862 $meta = get_post_meta($ebook_post_id, 'ebook_wp_custom_attachment', true);
13863
13864 if (is_array($meta)) {
13865 $pdf_url = $meta['url'];
13866 } else {
13867 $pdf_url = '';
13868 }
13869
13870 //wp_die('URL ' . $pdf_url);
13871 if (is_ssl()) {
13872 $pdf_url = str_replace('http://', 'https://', $pdf_url);
13873 }
13874 return '<iframe style="float:left;" src="'.$src.'#' . $pdf_url . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
13875 }
13876
13877 }
13878 function pr_die($array) {
13879 wp_die('<pre>'.print_r($array,true).'</pre>');
13880 }
13881 function ebook_store_redirect_add_order() {
13882 if (endswith(esc_url_raw($_SERVER['REQUEST_URI']), 'post-new.php?post_type=ebook_order')) {
13883 header("Location: edit.php?post_type=ebook&page=ebook-store-add-order-page");
13884 }
13885 }
13886
13887 if (!function_exists('endswith')) {
13888 function endswith($string, $test) {
13889 $strlen = strlen($string);
13890 $testlen = strlen($test);
13891 if ($testlen > $strlen) return false;
13892 return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
13893 }
13894 }
13895 add_action( 'woocommerce_after_account_downloads', 'ebook_store_woocommerce_after_account_downloads', 10, 1 );
13896 function ebook_store_woocommerce_after_account_downloads() {
13897
13898 echo '<h3>Ebook Downloads</h3>';
13899 //echo '<p>You can access your ebook downloads by going to the Orders section and clicking on an order number that will open the order confirmation page containing the download links.</p>';
13900 echo '<table class="woocommerce-MyAccount-downloads shop_table shop_table_responsive">
13901 <thead>
13902 <tr>
13903 <th class="download-product"><span class="nobr">'.__('Product','ebook-store').'</span></th>
13904 <th class="download-remaining"><span class="nobr">'.__('Download Links','ebook-store').'</span></th>
13905 <!-- <th class="download-expires"><span class="nobr">'.__('Expires','ebook-store').'</span></th>
13906 <th class="download-file"><span class="nobr">'.__('File','ebook-store').'</span></th>-->
13907 </tr>
13908 </thead>
13909 <tbody>';
13910 $customer_user_id = get_current_user_id();
13911
13912 $customer_orders = $customer_user_id > 0 ? wc_get_orders( array(
13913 'customer_id' => $customer_user_id,
13914 'limit' => -1,
13915 ) ) : array();
13916 foreach ( $customer_orders as $order ) {
13917 try {
13918 ebook_store_woocommerce_order_details($order, true);
13919 } catch ( \Throwable $e ) {
13920 continue;
13921 }
13922 }
13923 echo '
13924 </tbody></table>';
13925 }
13926
13927 function ebook_store_wp_super_cache_warning() {
13928 if (!file_exists(WP_PLUGIN_DIR . '/wp-super-cache/wp-cache.php')) {
13929 return true;
13930 }
13931
13932 if ( isset( $_GET['dismiss'] ) && $_GET['dismiss'] === 'wpsc' ) {
13933 update_option( 'ebookstorewpsc', 1 );
13934 return true;
13935 }
13936 if (get_option( 'ebookstorewpsc') < 1) {
13937 ?>
13938 <div id="wpsc-index-warning" class="error notice" style="padding: 10px 10px 50px 10px"><h1>Warning!</h1><p>You are using WP Super Cache, in order to have <b>ebook store</b> working properly with it, you must add these lines to the "Ignore strings" section <a href="options-general.php?page=wpsupercache&tab=settings">here</a>. Exactly where it says "Add here strings (not a filename) that forces a page not to be cached.". The lines you must add are:<br /><textarea>ipn
13939 ebook
13940 </textarea></p><a id="wpsc-dismiss" href="<?php echo esc_url($_SERVER['REQUEST_URI']); ?>&dismiss=wpsc">Dismiss</a></div>
13941 <?php
13942
13943 }
13944 }
13945
13946 function ebook_store_woocommerce_variation_fields( $loop, $variation_data, $variation ) {
13947 if ( get_option( 'ebook_store_woocommerce_integration' ) == 0 ) {
13948 return;
13949 }
13950
13951 $variation_id = isset( $variation->ID ) ? (int) $variation->ID : 0;
13952 $selected_ebook_id = $variation_id > 0 ? (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) : 0;
13953 $options = ebook_store_get_woocommerce_options();
13954 ?>
13955 <div class="form-row form-row-full">
13956 <label for="ebook_store_book_id_variation_<?php echo esc_attr( $variation_id ); ?>"><?php esc_html_e( 'Ebook Store ebook for this variation', 'ebook-store' ); ?></label>
13957 <select id="ebook_store_book_id_variation_<?php echo esc_attr( $variation_id ); ?>" name="ebook_store_book_id_variation[<?php echo esc_attr( $variation_id ); ?>]" class="select short">
13958 <?php foreach ( $options as $ebook_id => $ebook_title ) : ?>
13959 <option value="<?php echo esc_attr( $ebook_id ); ?>" <?php selected( $selected_ebook_id, (int) $ebook_id ); ?>>
13960 <?php echo esc_html( $ebook_title ); ?>
13961 </option>
13962 <?php endforeach; ?>
13963 </select>
13964 <p class="description"><?php esc_html_e( 'Choose which ebook this variation should deliver. Leave it empty for physical-only or non-ebook variations.', 'ebook-store' ); ?></p>
13965 </div>
13966 <?php
13967 }
13968
13969 function ebook_store_save_woocommerce_variation_data( $variation_id, $loop ) {
13970 $variation_id = (int) $variation_id;
13971 $old_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
13972 if ( $old_ebook_id > 0 ) {
13973 update_post_meta( $old_ebook_id, 'woocommerce_product_id', 0 );
13974 }
13975
13976 $new_ebook_id = 0;
13977 if ( isset( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) ) {
13978 $new_ebook_id = absint( wp_unslash( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) );
13979 }
13980
13981 if ( $new_ebook_id > 0 ) {
13982 update_post_meta( $variation_id, 'ebook_store_book_id', $new_ebook_id );
13983 update_post_meta( $variation_id, '_enable_ebook_store_option', 'yes' );
13984 update_post_meta( $new_ebook_id, 'woocommerce_product_id', $variation_id );
13985 } else {
13986 delete_post_meta( $variation_id, 'ebook_store_book_id' );
13987 update_post_meta( $variation_id, '_enable_ebook_store_option', 'no' );
13988 }
13989 }
13990
13991 function ebook_store_get_wc_item_product_ids( $item ) {
13992 $product_id = 0;
13993 $variation_id = 0;
13994
13995 if ( is_object( $item ) ) {
13996 if ( method_exists( $item, 'get_product_id' ) ) {
13997 $product_id = (int) $item->get_product_id();
13998 }
13999 if ( method_exists( $item, 'get_variation_id' ) ) {
14000 $variation_id = (int) $item->get_variation_id();
14001 }
14002 } elseif ( is_array( $item ) ) {
14003 $product_id = isset( $item['product_id'] ) ? (int) $item['product_id'] : 0;
14004 $variation_id = isset( $item['variation_id'] ) ? (int) $item['variation_id'] : 0;
14005 }
14006
14007 return array(
14008 'product_id' => $product_id,
14009 'variation_id' => $variation_id,
14010 );
14011 }
14012
14013 function ebook_store_product_has_variation_ebook_mappings( $product_id ) {
14014 $product_id = (int) $product_id;
14015 if ( $product_id < 1 ) {
14016 return false;
14017 }
14018
14019 $variation_ids = get_posts(
14020 array(
14021 'post_parent' => $product_id,
14022 'post_type' => 'product_variation',
14023 'post_status' => array( 'publish', 'private' ),
14024 'posts_per_page' => -1,
14025 'fields' => 'ids',
14026 'no_found_rows' => true,
14027 )
14028 );
14029
14030 if ( ! is_array( $variation_ids ) || empty( $variation_ids ) ) {
14031 return false;
14032 }
14033
14034 foreach ( $variation_ids as $variation_id ) {
14035 if ( (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) > 0 ) {
14036 return true;
14037 }
14038 }
14039
14040 return false;
14041 }
14042
14043 function ebook_store_get_wc_product_ebook_mapping( $product_id, $variation_id = 0 ) {
14044 $product_id = (int) $product_id;
14045 $variation_id = (int) $variation_id;
14046
14047 if ( $variation_id > 0 ) {
14048 $variation_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
14049 if ( $variation_ebook_id > 0 ) {
14050 return array(
14051 'ebook_id' => $variation_ebook_id,
14052 'product_id' => $product_id,
14053 'variation_id' => $variation_id,
14054 'mapped_post_id' => $variation_id,
14055 );
14056 }
14057
14058 if ( ebook_store_product_has_variation_ebook_mappings( $product_id ) ) {
14059 return array(
14060 'ebook_id' => 0,
14061 'product_id' => $product_id,
14062 'variation_id' => $variation_id,
14063 'mapped_post_id' => $variation_id,
14064 );
14065 }
14066
14067 return array(
14068 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
14069 'product_id' => $product_id,
14070 'variation_id' => $variation_id,
14071 'mapped_post_id' => $product_id,
14072 );
14073 }
14074
14075 return array(
14076 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
14077 'product_id' => $product_id,
14078 'variation_id' => 0,
14079 'mapped_post_id' => $product_id,
14080 );
14081 }
14082
14083 function ebook_store_get_wc_item_ebook_mapping( $item ) {
14084 $ids = ebook_store_get_wc_item_product_ids( $item );
14085 return ebook_store_get_wc_product_ebook_mapping( $ids['product_id'], $ids['variation_id'] );
14086 }
14087
14088 function ebook_store_get_wc_product_reference_for_ebook( $ebook_id ) {
14089 global $wpdb;
14090
14091 $ebook_id = (int) $ebook_id;
14092 if ( $ebook_id < 1 || ! function_exists( 'wc_get_product' ) ) {
14093 return 0;
14094 }
14095
14096 $stored_reference_id = (int) get_post_meta( $ebook_id, 'woocommerce_product_id', true );
14097 $allowed_statuses = array( 'publish', 'private' );
14098 if ( $stored_reference_id > 0 && in_array( get_post_status( $stored_reference_id ), $allowed_statuses, true ) ) {
14099 $stored_mapping = (int) get_post_meta( $stored_reference_id, 'ebook_store_book_id', true );
14100 if ( $stored_mapping === $ebook_id ) {
14101 return $stored_reference_id;
14102 }
14103 }
14104
14105 $product_reference_id = 0;
14106 $status_placeholders = implode( ',', array_fill( 0, count( $allowed_statuses ), '%s' ) );
14107 $query_params = array_merge(
14108 array( 'ebook_store_book_id', (string) $ebook_id, 'product', 'product_variation' ),
14109 $allowed_statuses
14110 );
14111
14112 $product_reference_id = (int) $wpdb->get_var(
14113 $wpdb->prepare(
14114 "
14115 SELECT pm.post_id
14116 FROM {$wpdb->postmeta} pm
14117 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
14118 WHERE pm.meta_key = %s
14119 AND pm.meta_value = %s
14120 AND p.post_type IN (%s, %s)
14121 AND p.post_status IN ($status_placeholders)
14122 ORDER BY pm.post_id DESC
14123 LIMIT 1
14124 ",
14125 $query_params
14126 )
14127 );
14128
14129 if ( $product_reference_id > 0 && $product_reference_id !== $stored_reference_id && ! wp_doing_ajax() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
14130 update_post_meta( $ebook_id, 'woocommerce_product_id', $product_reference_id );
14131 }
14132
14133 return $product_reference_id;
14134 }
14135
14136 function ebook_store_get_wc_add_to_cart_config( $product_reference_id ) {
14137 $product_reference_id = (int) $product_reference_id;
14138 if ( $product_reference_id < 1 || ! function_exists( 'wc_get_product' ) ) {
14139 return false;
14140 }
14141
14142 $product = wc_get_product( $product_reference_id );
14143 if ( ! $product ) {
14144 return false;
14145 }
14146
14147 if ( $product->is_type( 'variation' ) ) {
14148 return array(
14149 'product_id' => (int) $product->get_parent_id(),
14150 'variation_id' => (int) $product->get_id(),
14151 'variation' => $product->get_variation_attributes(),
14152 'title' => $product->get_name(),
14153 );
14154 }
14155
14156 return array(
14157 'product_id' => (int) $product->get_id(),
14158 'variation_id' => 0,
14159 'variation' => array(),
14160 'title' => $product->get_name(),
14161 );
14162 }
14163
14164 function ebook_store_cart_contains_wc_product( $cart_config ) {
14165 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
14166 return false;
14167 }
14168
14169 foreach ( WC()->cart->get_cart() as $cart_item ) {
14170 $cart_product_id = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
14171 $cart_variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0;
14172 if ( $cart_product_id === (int) $cart_config['product_id'] && $cart_variation_id === (int) $cart_config['variation_id'] ) {
14173 return true;
14174 }
14175 }
14176
14177 return false;
14178 }
14179
14180 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
14181 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
14182 function ebook_store_test_encryption() {
14183 if (!isset($_GET['ebook_id'])) {
14184 wp_die(esc_html__('Missing ebook ID.', 'ebook-store'));
14185 }
14186 error_log('Ebook Store Test Encryption: request=' . print_r($_REQUEST, true));
14187 $ebook_id = absint($_GET['ebook_id']);
14188 if (!$ebook_id || get_post_type($ebook_id) !== 'ebook') {
14189 wp_die(esc_html__('Invalid ebook.', 'ebook-store'));
14190 }
14191 if (!current_user_can('edit_post', $ebook_id)) {
14192 wp_die(esc_html__('You are not allowed to perform this test.', 'ebook-store'));
14193 }
14194 check_admin_referer('ebook_store_test_encryption_' . $ebook_id);
14195
14196 $test_format = isset($_GET['format']) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : 'pdf';
14197 if ( $test_format === 'epub' ) {
14198 ebook_store_test_epub_watermark( $ebook_id );
14199 exit;
14200 }
14201
14202 $pdf_meta = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
14203 if (!is_array($pdf_meta) || empty($pdf_meta['file']) || !file_exists($pdf_meta['file'])) {
14204 error_log('Ebook Store Test Encryption: PDF missing for ebook ' . $ebook_id);
14205 wp_die(esc_html__('No PDF found for this ebook.', 'ebook-store'));
14206 }
14207 if (!ebook_store_encryptable($pdf_meta['file'])) {
14208 error_log('Ebook Store Test Encryption: PDF not encryptable');
14209 wp_die(esc_html__('The PDF is not compatible with encryption. Please re-upload it in Acrobat 5.0 PDF 1.5 format.', 'ebook-store'));
14210 }
14211
14212 global $attachment, $ebook_email_delivery;
14213 $original_attachment = isset($attachment) ? $attachment : null;
14214 $original_email_delivery = isset($ebook_email_delivery) ? $ebook_email_delivery : null;
14215
14216 $attachment = array(
14217 array('file' => $pdf_meta['file'])
14218 );
14219 $ebook_email_delivery = array(
14220 'order' => array(
14221 'order_id' => 0,
14222 'ebook' => $ebook_id,
14223 'payer_email' => 'test@example.com',
14224 )
14225 );
14226
14227 $data = array(
14228 'txn_id' => 'encryption-test',
14229 'payment_date' => current_time('mysql'),
14230 'payer_email' => 'test@example.com',
14231 'password' => 'test',
14232 'md5_nonce' => 'encryption_test_' . $ebook_id,
14233 'ebook_key' => ebook_store_test_verification_key( $ebook_id ),
14234 );
14235
14236 try {
14237 $encrypted_file = ebook_encrypt_pdf($data, null);
14238 } catch (Throwable $e) {
14239 error_log('Ebook Store Test Encryption: exception ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
14240 $attachment = $original_attachment;
14241 $ebook_email_delivery = $original_email_delivery;
14242 wp_die(sprintf(
14243 '%s %s',
14244 esc_html__('Encryption test failed with an exception.', 'ebook-store'),
14245 esc_html($e->getMessage())
14246 ));
14247 }
14248 if (!$encrypted_file || !file_exists($encrypted_file)) {
14249 error_log('Ebook Store Test Encryption: encryption failed, file missing.');
14250 $attachment = $original_attachment;
14251 $ebook_email_delivery = $original_email_delivery;
14252 wp_die(esc_html__('Encryption test failed. Please check your PHP error log for details.', 'ebook-store'));
14253 }
14254
14255 error_log('Ebook Store Test Encryption: success, streaming file ' . $encrypted_file);
14256 header('Content-Type: application/pdf');
14257 header('Content-Disposition: inline; filename="' . basename($pdf_meta['file']) . '"');
14258 header('Content-Length: ' . filesize($encrypted_file));
14259 ebook_readfile_chunked($encrypted_file);
14260 @unlink($encrypted_file);
14261
14262 $attachment = $original_attachment;
14263 $ebook_email_delivery = $original_email_delivery;
14264 exit;
14265 }
14266
14267 /**
14268 * Stream a watermarked copy of an ebook's ePub so the owner can preview exactly
14269 * what a buyer receives. Uses the configured watermark settings with sample buyer
14270 * values, and never touches the stored upload.
14271 *
14272 * @param int $ebook_id Ebook post ID (already permission- and nonce-checked).
14273 */
14274 function ebook_store_test_epub_watermark( $ebook_id ) {
14275 $epub_meta = get_post_meta( $ebook_id, 'ebook_wp_custom_attachment_epub', true );
14276 if ( ! is_array( $epub_meta ) || empty( $epub_meta['file'] ) || ! file_exists( $epub_meta['file'] ) ) {
14277 wp_die( esc_html__( 'No ePub found for this ebook.', 'ebook-store' ) );
14278 }
14279 if ( ! function_exists( 'ebook_store_watermark_epub' ) ) {
14280 wp_die( esc_html__( 'The ePub watermarking engine is not available on this server.', 'ebook-store' ) );
14281 }
14282
14283 $src = $epub_meta['file'];
14284
14285 // Build a representative watermark from the configured template, standing in
14286 // sample buyer values for a real order.
14287 $template = (string) get_option( 'ebook_store_epub_watermark_text', 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%. Please do not share.' );
14288 if ( trim( $template ) === '' ) {
14289 $template = 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%';
14290 }
14291 $sample = str_replace(
14292 array( '%%first_name%%', '%%last_name%%', '%%payer_email%%', '%%residence_country%%', '%%payment_date%%', '%%txn_id%%' ),
14293 array( 'Test', 'Buyer', 'test@example.com', 'US', date_i18n( 'Y-m-d' ), 'watermark-test' ),
14294 $template
14295 );
14296
14297 $dir = untrailingslashit( get_temp_dir() );
14298 $dest = $dir . '/ebook-store-epub-test-' . $ebook_id . '-' . wp_generate_password( 8, false ) . '.epub';
14299
14300 $qr_text = ( get_option( 'ebook_store_epub_qr' ) && function_exists( 'ebook_store_get_order_verification_url' ) )
14301 ? ebook_store_get_order_verification_url( ebook_store_test_verification_key( $ebook_id ) )
14302 : '';
14303
14304 $out = ebook_store_watermark_epub(
14305 $src,
14306 $dest,
14307 $sample,
14308 array(
14309 'scope' => get_option( 'ebook_store_epub_watermark_scope', 'first' ),
14310 'position' => get_option( 'ebook_store_epub_watermark_position', 'bottom' ),
14311 'style' => get_option( 'ebook_store_epub_watermark_style', 'subtle' ),
14312 'stamp_metadata' => get_option( 'ebook_store_epub_stamp_metadata' ),
14313 'qr' => get_option( 'ebook_store_epub_qr' ),
14314 'qr_scope' => get_option( 'ebook_store_epub_qr_scope', 'first' ),
14315 'qr_position' => get_option( 'ebook_store_epub_qr_position', 'bottom' ),
14316 'qr_text' => $qr_text,
14317 'order_id' => 0,
14318 )
14319 );
14320
14321 if ( $out !== $dest || ! file_exists( $dest ) ) {
14322 wp_die( esc_html__( 'This ePub could not be watermarked for testing. It may be DRM-protected, digitally signed, corrupt, or too large — in which case buyers receive the original file unchanged.', 'ebook-store' ) );
14323 }
14324
14325 nocache_headers();
14326 header( 'Content-Type: application/epub+zip' );
14327 header( 'Content-Disposition: attachment; filename="watermark-test-' . basename( $src ) . '"' );
14328 header( 'Content-Length: ' . filesize( $dest ) );
14329 ebook_readfile_chunked( $dest );
14330 @unlink( $dest );
14331 exit;
14332 }
14333
14334 function ebook_store_deregister_embeds(){
14335 wp_dequeue_script( 'wp-embed' );
14336 }
14337
14338 /**
14339 * Log a adyen webhook event.
14340 *
14341 * Writes through ebook_store_log(), which keeps the file outside the web root,
14342 * caps its size and stays silent unless logging is switched on. The old version
14343 * appended to an uncapped .txt inside the plugin folder that any visitor could
14344 * fetch; one of those files reached 118 MB and contained live order tokens.
14345 *
14346 * @param string $message Message.
14347 * @param array $context Context.
14348 */
14349 function ebook_store_log_adyen_event( $message, $context = array() ) {
14350 ebook_store_log( 'adyen', $message, $context );
14351 }
14352
14353 /**
14354 * Number of minor units in one unit of a currency: 1 for JPY-style zero-decimal
14355 * currencies, 1000 for the three-decimal ones, otherwise 100. Adyen and Stripe
14356 * both express amounts in minor units.
14357 *
14358 * @param string $currency ISO 4217 code.
14359 * @return int
14360 */
14361 function ebook_store_currency_minor_unit_factor( $currency ) {
14362 $currency = strtoupper( trim( (string) $currency ) );
14363 $zero_decimal = array( 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' );
14364 $three_decimal = array( 'BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND' );
14365
14366 $factor = 100;
14367 if ( in_array( $currency, $zero_decimal, true ) ) {
14368 $factor = 1;
14369 } elseif ( in_array( $currency, $three_decimal, true ) ) {
14370 $factor = 1000;
14371 }
14372
14373 $factor = (int) apply_filters( 'ebook_store_currency_minor_unit_factor', $factor, $currency );
14374
14375 return $factor > 0 ? $factor : 100;
14376 }
14377
14378 function ebook_store_get_adyen_api_base_url() {
14379 $environment = (string) get_option( 'adyen_environment', 'test' );
14380 $version = 'v71';
14381
14382 if ( $environment === 'live' ) {
14383 $prefix = trim( (string) get_option( 'adyen_live_prefix', '' ) );
14384 // Older versions ran the prefix through esc_url_raw() ("http://<prefix>"), and
14385 // owners sometimes paste the whole live endpoint. Keep only the prefix itself.
14386 $prefix = preg_replace( '#^https?://#i', '', $prefix );
14387 $prefix = preg_replace( '/-checkout-live\.adyenpayments\.com.*$/i', '', $prefix );
14388 $prefix = trim( $prefix, "/ \t\n\r" );
14389 if ( $prefix === '' ) {
14390 return new WP_Error( 'adyen_missing_live_prefix', 'Adyen live URL prefix not configured' );
14391 }
14392
14393 return 'https://' . rawurlencode( $prefix ) . '-checkout-live.adyenpayments.com/checkout/' . $version;
14394 }
14395
14396 return 'https://checkout-test.adyen.com/' . $version;
14397 }
14398
14399 function ebook_store_get_adyen_webhook_url() {
14400 return site_url( 'wp-json/ebook-store/v1/adyen-webhook' );
14401 }
14402
14403 function ebook_store_adyen_request( $method, $path, $body = null, $headers = array() ) {
14404 $api_key = trim( (string) get_option( 'adyen_api_key', '' ) );
14405 if ( $api_key === '' ) {
14406 return new WP_Error( 'adyen_missing_api_key', 'Adyen API key not configured' );
14407 }
14408
14409 $base_url = ebook_store_get_adyen_api_base_url();
14410 if ( is_wp_error( $base_url ) ) {
14411 return $base_url;
14412 }
14413
14414 $request_headers = array_merge(
14415 array(
14416 'Content-Type' => 'application/json',
14417 'X-API-Key' => $api_key,
14418 ),
14419 (array) $headers
14420 );
14421
14422 $args = array(
14423 'method' => strtoupper( (string) $method ),
14424 'timeout' => 20,
14425 'headers' => $request_headers,
14426 'user-agent' => 'Ebook-Store/' . ( defined( 'EBOOK_STORE_VERSION' ) ? EBOOK_STORE_VERSION : '1.0' ),
14427 );
14428
14429 if ( null !== $body ) {
14430 $args['body'] = wp_json_encode( $body );
14431 }
14432
14433 $response = wp_remote_request( rtrim( $base_url, '/' ) . '/' . ltrim( (string) $path, '/' ), $args );
14434 if ( is_wp_error( $response ) ) {
14435 return $response;
14436 }
14437
14438 $status_code = (int) wp_remote_retrieve_response_code( $response );
14439 $response_body = wp_remote_retrieve_body( $response );
14440 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
14441
14442 if ( $status_code < 200 || $status_code >= 300 ) {
14443 $message = is_array( $data ) && ! empty( $data['message'] ) ? (string) $data['message'] : 'Adyen request failed.';
14444 return new WP_Error(
14445 'adyen_request_failed',
14446 $message,
14447 array(
14448 'status_code' => $status_code,
14449 'body' => $data,
14450 )
14451 );
14452 }
14453
14454 return is_array( $data ) ? $data : array();
14455 }
14456
14457 function ebook_store_get_adyen_shopper_locale() {
14458 $locale = get_locale();
14459 if ( function_exists( 'ebook_store_get_selected_language_code' ) ) {
14460 $language_code = (string) ebook_store_get_selected_language_code();
14461 if ( $language_code !== '' && $language_code !== '0' ) {
14462 $locale = $language_code . '_' . strtoupper( $language_code );
14463 }
14464 }
14465
14466 $locale = str_replace( '-', '_', (string) $locale );
14467 if ( preg_match( '/^[a-z]{2}_[A-Z]{2}$/', $locale ) ) {
14468 return str_replace( '_', '-', $locale );
14469 }
14470
14471 $language = strtolower( substr( $locale, 0, 2 ) );
14472 if ( preg_match( '/^[a-z]{2}$/', $language ) ) {
14473 return $language . '-' . strtoupper( $language );
14474 }
14475
14476 return 'en-US';
14477 }
14478
14479 function ebook_store_get_adyen_country_code() {
14480 $locale = str_replace( '-', '_', (string) get_locale() );
14481 if ( preg_match( '/^[a-z]{2}_([A-Z]{2})$/', $locale, $matches ) ) {
14482 return strtoupper( $matches[1] );
14483 }
14484
14485 return '';
14486 }
14487
14488 function ebook_store_store_adyen_checkout_state( $state ) {
14489 if ( ! is_array( $state ) ) {
14490 return;
14491 }
14492
14493 $state['created_at'] = isset( $state['created_at'] ) ? (int) $state['created_at'] : time();
14494 // An Adyen payment link stays payable for up to 30 days, and this state is
14495 // what maps the payment back to the ebook. At 7 days a link paid in week two
14496 // arrived with nothing to match it to. 30 days is also the longest expiry
14497 // a memcached object cache accepts; anything longer is read as a date in
14498 // 1970 and the entry is dropped on write.
14499 $ttl = MONTH_IN_SECONDS;
14500
14501 if ( ! empty( $state['payment_link_id'] ) ) {
14502 set_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ), $state, $ttl );
14503 }
14504
14505 if ( ! empty( $state['ebook_key'] ) ) {
14506 set_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ), $state, $ttl );
14507 }
14508 }
14509
14510 function ebook_store_get_adyen_checkout_state( $context = array() ) {
14511 $context = is_array( $context ) ? $context : array();
14512 $keys = array();
14513
14514 if ( ! empty( $context['payment_link_id'] ) ) {
14515 $keys[] = 'ebook_store_adyen_link_' . sanitize_key( (string) $context['payment_link_id'] );
14516 }
14517
14518 if ( ! empty( $context['ebook_key'] ) ) {
14519 $keys[] = 'ebook_store_adyen_ebook_' . sanitize_key( (string) $context['ebook_key'] );
14520 }
14521
14522 foreach ( $keys as $key ) {
14523 $state = get_transient( $key );
14524 if ( is_array( $state ) ) {
14525 return $state;
14526 }
14527 }
14528
14529 return false;
14530 }
14531
14532 function ebook_store_clear_adyen_checkout_state( $state ) {
14533 if ( ! is_array( $state ) ) {
14534 return;
14535 }
14536
14537 if ( ! empty( $state['payment_link_id'] ) ) {
14538 delete_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ) );
14539 }
14540
14541 if ( ! empty( $state['ebook_key'] ) ) {
14542 delete_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ) );
14543 }
14544 }
14545
14546 function ebook_store_fetch_adyen_payment_link( $payment_link_id ) {
14547 $payment_link_id = trim( (string) $payment_link_id );
14548 if ( $payment_link_id === '' ) {
14549 return new WP_Error( 'adyen_missing_payment_link_id', 'Adyen payment link ID is missing' );
14550 }
14551
14552 return ebook_store_adyen_request( 'GET', 'paymentLinks/' . rawurlencode( $payment_link_id ) );
14553 }
14554
14555 function ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) {
14556 return 'ebook_store_adyen_lock_' . sanitize_key( (string) $psp_reference );
14557 }
14558
14559 function ebook_store_acquire_adyen_order_creation_lock( $psp_reference ) {
14560 $psp_reference = (string) $psp_reference;
14561 if ( $psp_reference === '' ) {
14562 return false;
14563 }
14564
14565 $lock_key = ebook_store_get_adyen_order_creation_lock_key( $psp_reference );
14566
14567 if ( add_option( $lock_key, time(), '', 'no' ) ) {
14568 return true;
14569 }
14570
14571 // The lock has no expiry of its own, so a request that died between taking it
14572 // and the finally that releases it used to wedge this payment forever: every
14573 // retry then answered "creation already in progress" and no order was made.
14574 $held_since = (int) get_option( $lock_key, 0 );
14575 if ( $held_since > 0 && ( time() - $held_since ) > 60 ) {
14576 delete_option( $lock_key );
14577
14578 return add_option( $lock_key, time(), '', 'no' );
14579 }
14580
14581 return false;
14582 }
14583
14584 function ebook_store_release_adyen_order_creation_lock( $psp_reference ) {
14585 $psp_reference = (string) $psp_reference;
14586 if ( $psp_reference === '' ) {
14587 return;
14588 }
14589
14590 delete_option( ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) );
14591 }
14592
14593 function ebook_store_wait_for_adyen_order( $psp_reference, $ebook_key = '' ) {
14594 $psp_reference = (string) $psp_reference;
14595 $ebook_key = (string) $ebook_key;
14596
14597 for ( $attempt = 0; $attempt < 10; $attempt++ ) {
14598 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
14599 if ( $existing_order_id ) {
14600 return $existing_order_id;
14601 }
14602
14603 if ( $ebook_key !== '' ) {
14604 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
14605 if ( $existing_order_id ) {
14606 return $existing_order_id;
14607 }
14608 }
14609
14610 usleep( 200000 );
14611 }
14612
14613 return 0;
14614 }
14615
14616 function ebook_store_create_order_from_adyen_notification( $notification, $source = 'webhook' ) {
14617 $notification = is_array( $notification ) ? $notification : array();
14618 $psp_reference = isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '';
14619 $merchant_reference = isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '';
14620 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
14621 $payment_link_id = isset( $additional_data['paymentLinkId'] ) ? (string) $additional_data['paymentLinkId'] : '';
14622 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
14623 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
14624
14625 if ( $psp_reference === '' ) {
14626 return new WP_Error( 'adyen_missing_psp_reference', 'Adyen webhook is missing the PSP reference' );
14627 }
14628
14629 if ( $event_code !== 'AUTHORISATION' ) {
14630 return new WP_Error( 'adyen_invalid_event_code', 'Adyen notification is not an AUTHORISATION event' );
14631 }
14632
14633 if ( ! $success ) {
14634 return new WP_Error( 'adyen_payment_not_authorised', 'Adyen payment was not authorised successfully' );
14635 }
14636
14637 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
14638 if ( $existing_order_id ) {
14639 return $existing_order_id;
14640 }
14641
14642 if ( $merchant_reference !== '' ) {
14643 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $merchant_reference );
14644 if ( $existing_order_id ) {
14645 return $existing_order_id;
14646 }
14647 }
14648
14649 $has_lock = ebook_store_acquire_adyen_order_creation_lock( $psp_reference );
14650 if ( ! $has_lock ) {
14651 $existing_order_id = ebook_store_wait_for_adyen_order( $psp_reference, $merchant_reference );
14652 if ( $existing_order_id ) {
14653 return $existing_order_id;
14654 }
14655
14656 return new WP_Error( 'adyen_order_creation_in_progress', 'Adyen order creation is already in progress' );
14657 }
14658
14659 try {
14660 $state = ebook_store_get_adyen_checkout_state(
14661 array(
14662 'payment_link_id' => $payment_link_id,
14663 'ebook_key' => $merchant_reference,
14664 )
14665 );
14666 $state = is_array( $state ) ? $state : array();
14667
14668 $payment_link = $payment_link_id !== '' ? ebook_store_fetch_adyen_payment_link( $payment_link_id ) : new WP_Error( 'adyen_missing_payment_link_id', 'Adyen payment link ID is missing' );
14669 // Only a link we HAD an id for but could not read is a transient failure.
14670 // A notification carrying no id at all can never be matched, however many
14671 // times Adyen sends it.
14672 $payment_link_error = '';
14673 if ( is_wp_error( $payment_link ) ) {
14674 if ( $payment_link_id !== '' ) {
14675 $payment_link_error = $payment_link->get_error_message();
14676 }
14677 $payment_link = array();
14678 }
14679
14680 $ebook_id = ! empty( $state['ebook_id'] ) ? absint( $state['ebook_id'] ) : 0;
14681 if ( ! $ebook_id && ! empty( $payment_link['metadata']['ebook_id'] ) ) {
14682 $ebook_id = absint( $payment_link['metadata']['ebook_id'] );
14683 }
14684
14685 if ( ! $ebook_id ) {
14686 // Two very different failures used to share one error code. If the
14687 // lookup itself failed (Adyen unreachable, credentials rejected) the
14688 // payment is fine and the webhook must be retried; only a payment we
14689 // genuinely cannot match to an ebook is permanent.
14690 if ( $payment_link_error !== '' ) {
14691 return new WP_Error(
14692 'adyen_payment_link_unavailable',
14693 sprintf( 'Adyen payment link could not be read, so the ebook is unknown: %s', $payment_link_error )
14694 );
14695 }
14696
14697 return new WP_Error( 'adyen_missing_ebook', 'Adyen payment could not be matched to an ebook' );
14698 }
14699
14700 $ebook_post = get_post( $ebook_id );
14701 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
14702 return new WP_Error( 'adyen_invalid_ebook', 'Adyen payment points to an invalid ebook' );
14703 }
14704
14705 $shopper_email = '';
14706 if ( ! empty( $payment_link['shopperEmail'] ) ) {
14707 $shopper_email = sanitize_email( (string) $payment_link['shopperEmail'] );
14708 }
14709 if ( $shopper_email === '' && ! empty( $additional_data['shopperEmail'] ) ) {
14710 $shopper_email = sanitize_email( (string) $additional_data['shopperEmail'] );
14711 }
14712 if ( $shopper_email === '' && ! empty( $state['buyer_email'] ) ) {
14713 $shopper_email = sanitize_email( (string) $state['buyer_email'] );
14714 }
14715 if ( $shopper_email === '' ) {
14716 $shopper_email = 'adyen-buyer-' . strtolower( substr( preg_replace( '/[^a-zA-Z0-9]+/', '', $psp_reference ), 0, 12 ) ) . '@' . preg_replace( '/^www\./', '', (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
14717 }
14718
14719 $shopper_name = '';
14720 if ( ! empty( $payment_link['shopperName'] ) && is_array( $payment_link['shopperName'] ) ) {
14721 $shopper_name = trim(
14722 ( isset( $payment_link['shopperName']['firstName'] ) ? (string) $payment_link['shopperName']['firstName'] : '' ) . ' ' .
14723 ( isset( $payment_link['shopperName']['lastName'] ) ? (string) $payment_link['shopperName']['lastName'] : '' )
14724 );
14725 }
14726 $name_parts = ebook_store_parse_customer_name( $shopper_name );
14727 if ( $name_parts[0] === '' ) {
14728 $name_parts[0] = __( 'Adyen', 'ebook-store' );
14729 $name_parts[1] = __( 'Buyer', 'ebook-store' );
14730 }
14731
14732 $currency = isset( $notification['amount']['currency'] ) ? strtoupper( (string) $notification['amount']['currency'] ) : ebook_store_get_store_currency();
14733 $amount_value = isset( $notification['amount']['value'] ) ? ((float) $notification['amount']['value'] / ebook_store_currency_minor_unit_factor( $currency )) : 0.0;
14734 $country_code = ! empty( $payment_link['countryCode'] ) ? strtoupper( (string) $payment_link['countryCode'] ) : '';
14735
14736 $order_data = array(
14737 'first_name' => $name_parts[0],
14738 'last_name' => $name_parts[1],
14739 'payer_email' => $shopper_email,
14740 'payment_status' => 'Completed',
14741 'txn_id' => $psp_reference,
14742 'payment_type' => 'adyen',
14743 'mc_gross' => $amount_value,
14744 'mc_fee' => 0,
14745 'ebook_key' => $merchant_reference,
14746 'ebook' => $ebook_id,
14747 'post_id' => $ebook_id,
14748 'mc_currency' => $currency,
14749 'residence_country' => $country_code,
14750 'downloads' => 0,
14751 'adyen_psp_reference' => $psp_reference,
14752 'adyen_payment_link_id' => $payment_link_id,
14753 'adyen_event_code' => $event_code,
14754 'adyen_order_source' => $source,
14755 );
14756
14757 $order_id = ebook_store_add_order( $order_data, true, false, $merchant_reference );
14758 if ( ! $order_id ) {
14759 return new WP_Error( 'adyen_order_create_failed', 'Failed to create Ebook Store order from Adyen payment' );
14760 }
14761
14762 ebook_store_clear_adyen_checkout_state( $state );
14763 ebook_store_ga4_track_purchase_for_order( $order_id );
14764
14765 return (int) $order_id;
14766 } finally {
14767 ebook_store_release_adyen_order_creation_lock( $psp_reference );
14768 }
14769 }
14770
14771 function ebook_store_recover_adyen_order_by_ebook_key( $ebook_key ) {
14772 $ebook_key = (string) $ebook_key;
14773 if ( $ebook_key === '' ) {
14774 return new WP_Error( 'adyen_missing_ebook_key', 'Ebook key is missing for Adyen recovery' );
14775 }
14776
14777 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
14778 if ( $existing_order_id ) {
14779 return $existing_order_id;
14780 }
14781
14782 $state = ebook_store_get_adyen_checkout_state(
14783 array(
14784 'ebook_key' => $ebook_key,
14785 )
14786 );
14787 if ( ! is_array( $state ) || empty( $state['payment_link_id'] ) ) {
14788 return new WP_Error( 'adyen_checkout_state_missing', 'No Adyen checkout state found for this ebook key' );
14789 }
14790
14791 $payment_link = ebook_store_fetch_adyen_payment_link( (string) $state['payment_link_id'] );
14792 if ( is_wp_error( $payment_link ) ) {
14793 return $payment_link;
14794 }
14795
14796 if ( ! empty( $payment_link['status'] ) && strtolower( (string) $payment_link['status'] ) === 'completed' ) {
14797 return new WP_Error( 'adyen_waiting_for_webhook', 'Adyen payment is completed but Ebook Store is still waiting for the webhook confirmation.' );
14798 }
14799
14800 return 0;
14801 }
14802
14803 function ebook_store_create_adyen_payment_link( $ebook_id, $price, $title, $ebook_key ) {
14804 $ebook_id = absint( $ebook_id );
14805 if ( ! $ebook_id ) {
14806 $ebook_id = get_the_ID();
14807 }
14808
14809 $checkout_page = get_option( 'ebook_store_checkout_page' );
14810 if ( ! $checkout_page ) {
14811 $checkout_page = $ebook_id;
14812 }
14813
14814 $return_url = add_query_arg(
14815 array(
14816 'ebook_key' => $ebook_key,
14817 'action' => 'thank_you',
14818 'gateway' => 'adyen',
14819 ),
14820 get_permalink( $checkout_page )
14821 );
14822
14823 $country_code = ebook_store_get_adyen_country_code();
14824 $shopper_locale = ebook_store_get_adyen_shopper_locale();
14825 $merchant_account = trim( (string) get_option( 'adyen_merchant_account', '' ) );
14826
14827 if ( $merchant_account === '' ) {
14828 ebook_store_log_adyen_event(
14829 'Adyen payment link creation failed',
14830 array(
14831 'ebook_id' => $ebook_id,
14832 'error' => 'Adyen merchant account not configured',
14833 )
14834 );
14835 return false;
14836 }
14837
14838 $payload = array(
14839 'merchantAccount' => $merchant_account,
14840 'reference' => (string) $ebook_key,
14841 'amount' => array(
14842 'currency' => ebook_store_get_store_currency(),
14843 'value' => (int) round( (float) $price * ebook_store_currency_minor_unit_factor( ebook_store_get_store_currency() ) ),
14844 ),
14845 'description' => sprintf( 'Ebook Store order for %s', $title ),
14846 'shopperLocale' => $shopper_locale,
14847 'returnUrl' => $return_url,
14848 'requiredShopperFields'=> array( 'shopperEmail', 'shopperName' ),
14849 'expiresAt' => gmdate( 'Y-m-d\TH:i:s+00:00', time() + ( 30 * DAY_IN_SECONDS ) ),
14850 'metadata' => array(
14851 'ebook_id' => (string) $ebook_id,
14852 'ebook_key' => (string) $ebook_key,
14853 ),
14854 );
14855
14856 if ( $country_code !== '' ) {
14857 $payload['countryCode'] = $country_code;
14858 }
14859
14860 $response = ebook_store_adyen_request(
14861 'POST',
14862 'paymentLinks',
14863 $payload,
14864 array(
14865 'Idempotency-Key' => wp_generate_uuid4(),
14866 )
14867 );
14868
14869 if ( is_wp_error( $response ) ) {
14870 ebook_store_log_adyen_event(
14871 'Adyen payment link creation failed',
14872 array(
14873 'ebook_id' => $ebook_id,
14874 'error' => $response->get_error_message(),
14875 'data' => $response->get_error_data(),
14876 )
14877 );
14878 return false;
14879 }
14880
14881 if ( empty( $response['url'] ) || empty( $response['id'] ) ) {
14882 ebook_store_log_adyen_event(
14883 'Adyen payment link response missing data',
14884 array(
14885 'ebook_id' => $ebook_id,
14886 'response' => $response,
14887 )
14888 );
14889 return false;
14890 }
14891
14892 $state = array(
14893 'ebook_id' => $ebook_id,
14894 'ebook_key' => (string) $ebook_key,
14895 'title' => (string) $title,
14896 'payment_link_id' => (string) $response['id'],
14897 'store_amount' => (float) $price,
14898 'store_currency' => ebook_store_get_store_currency(),
14899 'country_code' => $country_code,
14900 );
14901 ebook_store_store_adyen_checkout_state( $state );
14902
14903 return (string) $response['url'];
14904 }
14905
14906 function ebook_store_verify_adyen_hmac( $notification ) {
14907 $notification = is_array( $notification ) ? $notification : array();
14908 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
14909 $provided_signature = isset( $additional_data['hmacSignature'] ) ? trim( (string) $additional_data['hmacSignature'] ) : '';
14910 $hmac_key = trim( (string) get_option( 'adyen_hmac_key', '' ) );
14911
14912 if ( $provided_signature === '' || $hmac_key === '' ) {
14913 return false;
14914 }
14915
14916 $signing_string = implode(
14917 ':',
14918 array(
14919 isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '',
14920 isset( $notification['originalReference'] ) ? (string) $notification['originalReference'] : '',
14921 isset( $notification['merchantAccountCode'] ) ? (string) $notification['merchantAccountCode'] : '',
14922 isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '',
14923 isset( $notification['amount']['value'] ) ? (string) $notification['amount']['value'] : '',
14924 isset( $notification['amount']['currency'] ) ? (string) $notification['amount']['currency'] : '',
14925 isset( $notification['eventCode'] ) ? (string) $notification['eventCode'] : '',
14926 isset( $notification['success'] ) ? (string) $notification['success'] : '',
14927 )
14928 );
14929
14930 // Adyen issues webhook HMAC keys as hex; keep base64 for keys pasted in that form.
14931 $decoded_key = ( strlen( $hmac_key ) % 2 === 0 && preg_match( '/^[0-9a-fA-F]+$/', $hmac_key ) ) ? hex2bin( $hmac_key ) : base64_decode( $hmac_key, true );
14932 if ( $decoded_key === false ) {
14933 return false;
14934 }
14935
14936 $calculated_signature = base64_encode( hash_hmac( 'sha256', $signing_string, $decoded_key, true ) );
14937
14938 return hash_equals( $calculated_signature, $provided_signature );
14939 }
14940
14941 function ebook_store_register_adyen_webhook() {
14942 register_rest_route(
14943 'ebook-store/v1',
14944 '/adyen-webhook',
14945 array(
14946 'methods' => 'POST',
14947 'callback' => 'ebook_store_handle_adyen_webhook',
14948 'permission_callback' => '__return_true',
14949 )
14950 );
14951 }
14952 add_action( 'rest_api_init', 'ebook_store_register_adyen_webhook' );
14953
14954 function ebook_store_handle_adyen_webhook( WP_REST_Request $request ) {
14955 $payload = $request->get_body();
14956 $data = json_decode( $payload, true );
14957 $items = isset( $data['notificationItems'] ) && is_array( $data['notificationItems'] ) ? $data['notificationItems'] : array();
14958
14959 if ( empty( $items ) ) {
14960 ebook_store_log_adyen_event(
14961 'Adyen webhook ignored empty payload',
14962 array(
14963 'payload' => $payload,
14964 )
14965 );
14966 return new WP_REST_Response( '[accepted]', 200 );
14967 }
14968
14969 foreach ( $items as $entry ) {
14970 $notification = isset( $entry['NotificationRequestItem'] ) && is_array( $entry['NotificationRequestItem'] )
14971 ? $entry['NotificationRequestItem']
14972 : array();
14973 if ( empty( $notification ) ) {
14974 continue;
14975 }
14976
14977 if ( ! ebook_store_verify_adyen_hmac( $notification ) ) {
14978 ebook_store_log_adyen_event(
14979 'Adyen webhook HMAC verification failed',
14980 array(
14981 'notification' => $notification,
14982 )
14983 );
14984 return new WP_REST_Response( 'invalid hmac', 400 );
14985 }
14986
14987 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
14988 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
14989
14990 switch ( $event_code ) {
14991 case 'AUTHORISATION':
14992 $result = ebook_store_create_order_from_adyen_notification( $notification, 'webhook' );
14993 if ( is_wp_error( $result ) ) {
14994 // A failure we could recover from must NOT be acknowledged:
14995 // answering 200 told Adyen the payment was handled, so it
14996 // never redelivered and the buyer's paid order was lost.
14997 $retryable = array(
14998 'adyen_payment_link_unavailable',
14999 'adyen_order_creation_in_progress',
15000 'adyen_order_create_failed',
15001 );
15002
15003 if ( in_array( $result->get_error_code(), $retryable, true ) ) {
15004 ebook_store_log_adyen_event(
15005 'Adyen AUTHORISATION failed, asking Adyen to retry',
15006 array(
15007 'error' => $result->get_error_message(),
15008 'code' => $result->get_error_code(),
15009 'notification' => $notification,
15010 )
15011 );
15012
15013 return new WP_REST_Response( 'retry', 500 );
15014 }
15015
15016 ebook_store_log_adyen_event(
15017 'Adyen AUTHORISATION processing skipped (not retrying)',
15018 array(
15019 'error' => $result->get_error_message(),
15020 'code' => $result->get_error_code(),
15021 'notification' => $notification,
15022 )
15023 );
15024 } else {
15025 ebook_store_log_adyen_event(
15026 'Adyen order created from AUTHORISATION',
15027 array(
15028 'order_id' => $result,
15029 'pspReference' => isset( $notification['pspReference'] ) ? $notification['pspReference'] : '',
15030 )
15031 );
15032 }
15033 break;
15034
15035 case 'REFUND':
15036 if ( $success && ! empty( $notification['originalReference'] ) ) {
15037 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
15038 if ( $order_id ) {
15039 update_post_meta( $order_id, 'payment_status', 'Refunded' );
15040 update_post_meta( $order_id, 'refund_gateway', 'adyen' );
15041 update_post_meta( $order_id, 'refund_gateway_id', isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '' );
15042 update_post_meta( $order_id, 'refund_gateway_status', 'received' );
15043 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
15044 }
15045 }
15046 break;
15047
15048 case 'REFUND_FAILED':
15049 case 'REFUNDED_REVERSED':
15050 if ( ! empty( $notification['originalReference'] ) ) {
15051 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
15052 if ( $order_id ) {
15053 update_post_meta( $order_id, 'payment_status', 'Completed' );
15054 update_post_meta( $order_id, 'refund_gateway_status', strtolower( $event_code ) );
15055 delete_post_meta( $order_id, 'refund_gateway_id' );
15056 }
15057 }
15058 break;
15059 }
15060 }
15061
15062 return new WP_REST_Response( '[accepted]', 200 );
15063 }
15064
15065 /**
15066 * Log a stripe webhook event.
15067 *
15068 * Writes through ebook_store_log(), which keeps the file outside the web root,
15069 * caps its size and stays silent unless logging is switched on. The old version
15070 * appended to an uncapped .txt inside the plugin folder that any visitor could
15071 * fetch; one of those files reached 118 MB and contained live order tokens.
15072 *
15073 * @param string $message Message.
15074 * @param array $context Context.
15075 */
15076 function ebook_store_log_stripe_event( $message, $context = array() ) {
15077 ebook_store_log( 'stripe', $message, $context );
15078 }
15079
15080 function ebook_store_bootstrap_stripe_sdk() {
15081 require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
15082
15083 $stripe_secret_key = (string) get_option( 'stripe_secret_key' );
15084 if ( $stripe_secret_key === '' ) {
15085 return new WP_Error( 'stripe_missing_secret_key', 'Stripe secret key not configured' );
15086 }
15087
15088 \Stripe\Stripe::setApiKey( $stripe_secret_key );
15089
15090 return true;
15091 }
15092
15093 function ebook_store_get_order_id_by_meta( $meta_key, $meta_value ) {
15094 if ( $meta_value === '' || $meta_value === null ) {
15095 return 0;
15096 }
15097
15098 $orders = get_posts(
15099 array(
15100 'post_type' => 'ebook_order',
15101 'post_status' => 'any',
15102 'posts_per_page' => 1,
15103 'fields' => 'ids',
15104 'meta_key' => $meta_key,
15105 'meta_value' => (string) $meta_value,
15106 // Never served from the request's query cache: this lookup is used to
15107 // spot an order that another request created a moment ago, and the
15108 // same query may already have run (and cached "nothing") earlier in
15109 // this request.
15110 'cache_results' => false,
15111 'update_post_meta_cache' => false,
15112 'update_post_term_cache' => false,
15113 )
15114 );
15115
15116 return empty( $orders ) ? 0 : (int) $orders[0];
15117 }
15118
15119 function ebook_store_parse_customer_name( $name ) {
15120 $name = trim( (string) $name );
15121 if ( $name === '' ) {
15122 return array( '', '' );
15123 }
15124
15125 $name_parts = preg_split( '/\s+/', $name, 2 );
15126
15127 return array(
15128 isset( $name_parts[0] ) ? $name_parts[0] : '',
15129 isset( $name_parts[1] ) ? $name_parts[1] : '',
15130 );
15131 }
15132
15133 function ebook_store_extract_stripe_billing_details( $session ) {
15134 $details = array(
15135 'name' => '',
15136 'email' => '',
15137 'address' => array(),
15138 'charge' => null,
15139 );
15140
15141 if ( ! empty( $session->customer_details ) ) {
15142 $details['name'] = isset( $session->customer_details->name ) ? (string) $session->customer_details->name : '';
15143 $details['email'] = isset( $session->customer_details->email ) ? (string) $session->customer_details->email : '';
15144 $session_address = ! empty( $session->customer_details->address ) ? $session->customer_details->address : null;
15145 // (array) on a StripeObject exposes its private props with mangled keys; toArray() gives line1/city/... .
15146 $details['address'] = is_object( $session_address ) && method_exists( $session_address, 'toArray' ) ? $session_address->toArray() : ( $session_address ? (array) $session_address : array() );
15147 }
15148
15149 $payment_intent_id = '';
15150 if ( ! empty( $session->payment_intent ) ) {
15151 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
15152 ? (string) $session->payment_intent->id
15153 : (string) $session->payment_intent;
15154 }
15155
15156 if ( $payment_intent_id !== '' ) {
15157 try {
15158 $payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_id );
15159 $charge = null;
15160
15161 if ( ! empty( $payment_intent->latest_charge ) ) {
15162 $charge = is_object( $payment_intent->latest_charge ) && isset( $payment_intent->latest_charge->id )
15163 ? $payment_intent->latest_charge
15164 : \Stripe\Charge::retrieve( (string) $payment_intent->latest_charge );
15165 } elseif ( ! empty( $payment_intent->charges ) && ! empty( $payment_intent->charges->data[0] ) ) {
15166 $charge = $payment_intent->charges->data[0];
15167 }
15168
15169 if ( $charge && ! empty( $charge->billing_details ) ) {
15170 $details['charge'] = $charge;
15171
15172 if ( $details['name'] === '' && ! empty( $charge->billing_details->name ) ) {
15173 $details['name'] = (string) $charge->billing_details->name;
15174 }
15175
15176 if ( $details['email'] === '' && ! empty( $charge->billing_details->email ) ) {
15177 $details['email'] = (string) $charge->billing_details->email;
15178 }
15179
15180 if ( empty( $details['address'] ) && ! empty( $charge->billing_details->address ) ) {
15181 $charge_address = $charge->billing_details->address;
15182 $details['address'] = is_object( $charge_address ) && method_exists( $charge_address, 'toArray' ) ? $charge_address->toArray() : (array) $charge_address;
15183 }
15184 }
15185 } catch ( Exception $e ) {
15186 ebook_store_log_stripe_event(
15187 'Stripe billing details lookup failed',
15188 array(
15189 'payment_intent_id' => $payment_intent_id,
15190 'error' => $e->getMessage(),
15191 )
15192 );
15193 }
15194 }
15195
15196 return $details;
15197 }
15198
15199 function ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id ) {
15200 if ( $payment_intent_id === '' ) {
15201 return null;
15202 }
15203
15204 try {
15205 $sessions = \Stripe\Checkout\Session::all(
15206 array(
15207 'payment_intent' => $payment_intent_id,
15208 'limit' => 1,
15209 )
15210 );
15211 } catch ( Exception $e ) {
15212 ebook_store_log_stripe_event(
15213 'Stripe session lookup by payment intent failed',
15214 array(
15215 'payment_intent_id' => $payment_intent_id,
15216 'error' => $e->getMessage(),
15217 )
15218 );
15219
15220 return null;
15221 }
15222
15223 return ! empty( $sessions->data[0] ) ? $sessions->data[0] : null;
15224 }
15225
15226 function ebook_store_find_stripe_session_by_ebook_key( $ebook_key ) {
15227 $ebook_key = (string) $ebook_key;
15228 if ( $ebook_key === '' ) {
15229 return null;
15230 }
15231
15232 try {
15233 $sessions = \Stripe\Checkout\Session::all(
15234 array(
15235 'limit' => 25,
15236 )
15237 );
15238 } catch ( Exception $e ) {
15239 ebook_store_log_stripe_event(
15240 'Stripe session lookup by ebook key failed',
15241 array(
15242 'ebook_key' => $ebook_key,
15243 'error' => $e->getMessage(),
15244 )
15245 );
15246
15247 return null;
15248 }
15249
15250 foreach ( $sessions->data as $session ) {
15251 if (
15252 ! empty( $session->metadata )
15253 && isset( $session->metadata->ebook_key )
15254 && (string) $session->metadata->ebook_key === $ebook_key
15255 ) {
15256 return $session;
15257 }
15258 }
15259
15260 return null;
15261 }
15262
15263 /**
15264 * Load the script that turns a Stripe button into a create-on-click button.
15265 *
15266 * Enqueued only where a Stripe button is actually rendered.
15267 */
15268 function ebook_store_enqueue_stripe_checkout_script() {
15269 if ( wp_script_is( 'ebook-store-stripe', 'enqueued' ) ) {
15270 return;
15271 }
15272
15273 wp_enqueue_script(
15274 'ebook-store-stripe',
15275 plugins_url( 'js/ebook-store-stripe.js', __FILE__ ),
15276 array(),
15277 ebook_store_get_plugin_version(),
15278 true
15279 );
15280
15281 wp_localize_script(
15282 'ebook-store-stripe',
15283 'ebookStoreStripe',
15284 array(
15285 'endpoint' => home_url( 'index.php' ),
15286 'strings' => array(
15287 'working' => __( 'One moment…', 'ebook-store' ),
15288 'failed' => __( 'Sorry, the checkout could not be opened. Please try again.', 'ebook-store' ),
15289 ),
15290 )
15291 );
15292 }
15293
15294 function ebook_store_get_stripe_order_creation_lock_key( $session_id ) {
15295 return 'ebook_store_stripe_lock_' . md5( (string) $session_id );
15296 }
15297
15298 /**
15299 * Take the creation lock for one Stripe session.
15300 *
15301 * The webhook and the thank-you page's recovery both create the order for a
15302 * session, and both used a check-then-act with several network calls in the
15303 * middle — long enough for the other one to pass the same checks, so a buyer
15304 * ended up with two orders and two delivery emails.
15305 *
15306 * @param string $session_id Stripe checkout session ID.
15307 * @return bool True when this caller owns the lock.
15308 */
15309 function ebook_store_acquire_stripe_order_creation_lock( $session_id ) {
15310 $session_id = (string) $session_id;
15311 if ( $session_id === '' ) {
15312 return false;
15313 }
15314
15315 $lock_key = ebook_store_get_stripe_order_creation_lock_key( $session_id );
15316
15317 if ( add_option( $lock_key, time(), '', 'no' ) ) {
15318 return true;
15319 }
15320
15321 // A request that died mid-creation would otherwise hold the lock forever.
15322 $held_since = (int) get_option( $lock_key, 0 );
15323 if ( $held_since > 0 && ( time() - $held_since ) > 60 ) {
15324 delete_option( $lock_key );
15325
15326 return add_option( $lock_key, time(), '', 'no' );
15327 }
15328
15329 return false;
15330 }
15331
15332 function ebook_store_release_stripe_order_creation_lock( $session_id ) {
15333 $session_id = (string) $session_id;
15334 if ( $session_id === '' ) {
15335 return;
15336 }
15337
15338 delete_option( ebook_store_get_stripe_order_creation_lock_key( $session_id ) );
15339 }
15340
15341 /**
15342 * Wait briefly for the other request to finish creating this session's order.
15343 *
15344 * @param string $session_id Stripe checkout session ID.
15345 * @param string $ebook_key Order key, when known.
15346 * @return int Order ID, or 0.
15347 */
15348 function ebook_store_wait_for_stripe_order( $session_id, $ebook_key = '' ) {
15349 $session_id = (string) $session_id;
15350 $ebook_key = (string) $ebook_key;
15351
15352 for ( $attempt = 0; $attempt < 10; $attempt++ ) {
15353 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15354 if ( $existing_order_id ) {
15355 return $existing_order_id;
15356 }
15357
15358 if ( $ebook_key !== '' ) {
15359 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15360 if ( $existing_order_id ) {
15361 return $existing_order_id;
15362 }
15363 }
15364
15365 usleep( 200000 );
15366 }
15367
15368 return 0;
15369 }
15370
15371 function ebook_store_create_order_from_stripe_session( $session, $source = 'webhook' ) {
15372 if ( empty( $session ) || empty( $session->id ) ) {
15373 return new WP_Error( 'stripe_missing_session', 'Stripe session data is missing' );
15374 }
15375
15376 $session_id = (string) $session->id;
15377 $payment_status = isset( $session->payment_status ) ? (string) $session->payment_status : '';
15378 $payment_intent_id = '';
15379 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15380
15381 if ( $existing_order_id ) {
15382 return $existing_order_id;
15383 }
15384
15385 if ( ! empty( $session->payment_intent ) ) {
15386 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
15387 ? (string) $session->payment_intent->id
15388 : (string) $session->payment_intent;
15389
15390 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15391 if ( $existing_order_id ) {
15392 return $existing_order_id;
15393 }
15394 }
15395
15396 if ( $payment_status !== 'paid' ) {
15397 return new WP_Error(
15398 'stripe_payment_not_paid',
15399 sprintf( 'Stripe session %s is not paid (%s)', $session_id, $payment_status )
15400 );
15401 }
15402
15403 $metadata = ! empty( $session->metadata ) ? $session->metadata : (object) array();
15404 $ebook_id = isset( $metadata->ebook_id ) ? absint( $metadata->ebook_id ) : 0;
15405 $ebook_key = isset( $metadata->ebook_key ) ? (string) $metadata->ebook_key : '';
15406
15407 if ( ! $ebook_id && ! empty( $session->client_reference_id ) ) {
15408 $ebook_id = absint( $session->client_reference_id );
15409 }
15410
15411 if ( $ebook_key !== '' ) {
15412 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15413 if ( $existing_order_id ) {
15414 return $existing_order_id;
15415 }
15416 }
15417
15418 if ( ! $ebook_id ) {
15419 return new WP_Error( 'stripe_missing_ebook', sprintf( 'Stripe session %s has no ebook_id metadata', $session_id ) );
15420 }
15421
15422 $ebook_post = get_post( $ebook_id );
15423 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
15424 return new WP_Error( 'stripe_invalid_ebook', sprintf( 'Stripe session %s points to invalid ebook %s', $session_id, $ebook_id ) );
15425 }
15426
15427 // Everything above is cheap and read-only. From here the function fetches
15428 // billing details from Stripe and inserts the order, which is long enough for
15429 // the webhook and the thank-you page to do it twice for the same session.
15430 if ( ! ebook_store_acquire_stripe_order_creation_lock( $session_id ) ) {
15431 $existing_order_id = ebook_store_wait_for_stripe_order( $session_id, $ebook_key );
15432 if ( $existing_order_id ) {
15433 return $existing_order_id;
15434 }
15435
15436 return new WP_Error( 'stripe_order_creation_in_progress', sprintf( 'Stripe order creation is already in progress for session %s', $session_id ) );
15437 }
15438
15439 try {
15440 // Re-check inside the lock: the other request may have finished between our
15441 // first look and taking it.
15442 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15443 if ( ! $existing_order_id && $ebook_key !== '' ) {
15444 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15445 }
15446 if ( $existing_order_id ) {
15447 return $existing_order_id;
15448 }
15449
15450 $billing_details = ebook_store_extract_stripe_billing_details( $session );
15451 $name_parts = ebook_store_parse_customer_name( $billing_details['name'] );
15452 $address = is_array( $billing_details['address'] ) ? $billing_details['address'] : array();
15453 $session_currency = isset( $session->currency ) && $session->currency ? strtoupper( (string) $session->currency ) : ebook_store_get_store_currency();
15454 $amount_total = isset( $session->amount_total ) ? ((float) $session->amount_total / ebook_store_currency_minor_unit_factor( $session_currency )) : 0;
15455 $amount_value = isset( $metadata->price ) && $metadata->price !== '' ? (float) $metadata->price : $amount_total;
15456 $txn_id = $payment_intent_id !== '' ? $payment_intent_id : $session_id;
15457
15458 $order_data = array(
15459 'first_name' => $name_parts[0],
15460 'last_name' => $name_parts[1],
15461 'payer_email' => (string) $billing_details['email'],
15462 'payment_status' => 'Completed',
15463 'txn_id' => $txn_id,
15464 'payment_type' => 'stripe',
15465 'mc_gross' => $amount_value,
15466 'ebook_key' => $ebook_key,
15467 'ebook' => $ebook_id,
15468 'post_id' => $ebook_id,
15469 'mc_currency' => isset( $session->currency ) && $session->currency ? strtoupper( (string) $session->currency ) : ebook_store_get_store_currency(),
15470 'residence_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
15471 'address_name' => (string) $billing_details['name'],
15472 'address_street' => isset( $address['line1'] ) ? (string) $address['line1'] : '',
15473 'address_city' => isset( $address['city'] ) ? (string) $address['city'] : '',
15474 'address_state' => isset( $address['state'] ) ? (string) $address['state'] : '',
15475 'address_zip' => isset( $address['postal_code'] ) ? (string) $address['postal_code'] : '',
15476 'address_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
15477 'address_status' => 'confirmed',
15478 'downloads' => 0,
15479 'stripe_session_id' => $session_id,
15480 'stripe_payment_intent_id' => $payment_intent_id,
15481 'stripe_order_source' => $source,
15482 );
15483
15484 $order_id = ebook_store_add_order( $order_data, true, false, $ebook_key );
15485
15486 if ( ! $order_id ) {
15487 return new WP_Error( 'stripe_order_create_failed', sprintf( 'Failed to create order from Stripe session %s', $session_id ) );
15488 }
15489
15490 ebook_store_ga4_track_purchase_for_order( $order_id );
15491
15492 return (int) $order_id;
15493 } finally {
15494 ebook_store_release_stripe_order_creation_lock( $session_id );
15495 }
15496 }
15497
15498 function ebook_store_recover_stripe_order_by_ebook_key( $ebook_key ) {
15499 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15500 if ( is_wp_error( $bootstrap ) ) {
15501 return $bootstrap;
15502 }
15503
15504 $session = ebook_store_find_stripe_session_by_ebook_key( $ebook_key );
15505 if ( ! $session ) {
15506 return new WP_Error( 'stripe_session_not_found', sprintf( 'No Stripe session found for ebook key %s', $ebook_key ) );
15507 }
15508
15509 return ebook_store_create_order_from_stripe_session( $session, 'thank_you' );
15510 }
15511
15512 function ebook_store_recover_stripe_order_by_session_id( $session_id ) {
15513 $session_id = (string) $session_id;
15514 if ( $session_id === '' ) {
15515 return new WP_Error( 'stripe_missing_session_id', 'Stripe session ID is missing' );
15516 }
15517
15518 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15519 if ( is_wp_error( $bootstrap ) ) {
15520 return $bootstrap;
15521 }
15522
15523 try {
15524 $session = \Stripe\Checkout\Session::retrieve( $session_id );
15525 } catch ( Exception $e ) {
15526 return new WP_Error( 'stripe_session_lookup_failed', $e->getMessage() );
15527 }
15528
15529 return ebook_store_create_order_from_stripe_session( $session, 'thank_you_session' );
15530 }
15531
15532 function ebook_store_create_stripe_checkout($ebook_id, $price, $title, $ebook_key) {
15533 $ebook_id = absint($ebook_id);
15534 if (!$ebook_id) {
15535 $ebook_id = get_the_ID();
15536 }
15537
15538 try {
15539 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15540 if (is_wp_error($bootstrap)) {
15541 throw new Exception($bootstrap->get_error_message());
15542 }
15543
15544 $checkout_page = get_option('ebook_store_checkout_page');
15545 $minor_unit_factor = ebook_store_currency_minor_unit_factor( ebook_store_get_store_currency() );
15546 // Stripe only accepts an integer amount in minor units; record the amount really charged.
15547 $unit_amount = (int) round( (float) $price * $minor_unit_factor );
15548 $metadata = [
15549 'ebook_id' => $ebook_id,
15550 'ebook_key' => $ebook_key,
15551 'price' => $unit_amount / $minor_unit_factor,
15552 'title' => $title
15553 ];
15554
15555 $success_url = add_query_arg(
15556 array(
15557 'ebook_key' => $metadata['ebook_key'],
15558 'action' => 'thank_you',
15559 'stripe_session_id' => '{CHECKOUT_SESSION_ID}'
15560 ),
15561 get_permalink($checkout_page)
15562 );
15563
15564 $session_data = [
15565 'line_items' => [[
15566 'price_data' => [
15567 'currency' => ebook_store_get_store_currency(),
15568 'product_data' => [
15569 'name' => $title,
15570 ],
15571 'unit_amount' => $unit_amount,
15572 ],
15573 'quantity' => 1,
15574 ]],
15575 'mode' => 'payment',
15576 'success_url' => $success_url,
15577 'cancel_url' => get_option('stripe_cancel_url', home_url()),
15578 'metadata' => $metadata,
15579 'client_reference_id' => (string) $ebook_id,
15580 'payment_intent_data' => [
15581 'metadata' => $metadata,
15582 ],
15583 'customer_email' => isset($_POST['email']) ? sanitize_email($_POST['email']) : null
15584 ];
15585
15586 $checkout_session = \Stripe\Checkout\Session::create($session_data);
15587
15588 return $checkout_session->url;
15589 } catch(Exception $e) {
15590 ebook_store_log_stripe_event(
15591 'Stripe checkout creation failed',
15592 [
15593 'ebook_id' => $ebook_id,
15594 'error' => $e->getMessage(),
15595 ]
15596 );
15597 return false;
15598 }
15599 }
15600
15601 // Add this near the other add_action calls
15602 add_action('rest_api_init', 'ebook_store_register_stripe_webhook');
15603
15604 function ebook_store_register_stripe_webhook() {
15605 register_rest_route('ebook-store/v1', '/stripe-webhook', array(
15606 'methods' => 'POST',
15607 'callback' => 'ebook_store_handle_stripe_webhook',
15608 'permission_callback' => '__return_true' // Public endpoint
15609 ));
15610 }
15611
15612 /**
15613 * Whether a failed Stripe order creation should make Stripe redeliver the event.
15614 *
15615 * Answering 200 tells Stripe the event was handled, so a failure that is only
15616 * temporary — the thank-you page holds the creation lock, or the insert
15617 * failed — would otherwise never be retried and the paid order would be lost.
15618 *
15619 * @param mixed $error Return value of ebook_store_create_order_from_stripe_session().
15620 * @return bool
15621 */
15622 function ebook_store_is_retryable_stripe_order_error( $error ) {
15623 return is_wp_error( $error ) && in_array( $error->get_error_code(), array( 'stripe_order_creation_in_progress', 'stripe_order_create_failed' ), true );
15624 }
15625
15626 function ebook_store_handle_stripe_webhook(WP_REST_Request $request) {
15627 $payload = $request->get_body();
15628 $sig_header = $request->get_header( 'stripe-signature' );
15629
15630 try {
15631 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15632 if ( is_wp_error( $bootstrap ) ) {
15633 throw new Exception( $bootstrap->get_error_message() );
15634 }
15635
15636 $webhook_secret = (string) get_option( 'stripe_webhook_secret' );
15637 if ( $webhook_secret === '' ) {
15638 throw new Exception( 'Stripe webhook secret not configured' );
15639 }
15640
15641 $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $webhook_secret );
15642
15643 ebook_store_log_stripe_event(
15644 'Stripe webhook verified',
15645 array(
15646 'type' => $event->type,
15647 'id' => isset( $event->id ) ? $event->id : '',
15648 )
15649 );
15650
15651 switch ( $event->type ) {
15652 case 'checkout.session.completed':
15653 case 'checkout.session.async_payment_succeeded':
15654 $order_id = ebook_store_create_order_from_stripe_session( $event->data->object, 'webhook' );
15655 if ( is_wp_error( $order_id ) ) {
15656 ebook_store_log_stripe_event(
15657 'Stripe checkout session order creation skipped',
15658 array(
15659 'type' => $event->type,
15660 'code' => $order_id->get_error_code(),
15661 'error' => $order_id->get_error_message(),
15662 )
15663 );
15664 if ( ebook_store_is_retryable_stripe_order_error( $order_id ) ) {
15665 return new WP_REST_Response( 'retry', 500 );
15666 }
15667 } else {
15668 ebook_store_log_stripe_event(
15669 'Stripe order created from checkout session',
15670 array(
15671 'type' => $event->type,
15672 'order_id' => $order_id,
15673 'session' => isset( $event->data->object->id ) ? $event->data->object->id : '',
15674 )
15675 );
15676 }
15677 break;
15678
15679 case 'charge.updated':
15680 case 'charge.succeeded':
15681 $charge = $event->data->object;
15682
15683 if ( empty( $charge->paid ) || $charge->status !== 'succeeded' ) {
15684 break;
15685 }
15686
15687 $payment_intent_id = ! empty( $charge->payment_intent ) ? (string) $charge->payment_intent : '';
15688 $session = ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id );
15689
15690 if ( ! $session ) {
15691 ebook_store_log_stripe_event(
15692 'Stripe charge event had no checkout session match',
15693 array(
15694 'type' => $event->type,
15695 'payment_intent_id' => $payment_intent_id,
15696 'charge_id' => isset( $charge->id ) ? $charge->id : '',
15697 )
15698 );
15699 break;
15700 }
15701
15702 $order_id = ebook_store_create_order_from_stripe_session( $session, 'charge_event' );
15703 if ( is_wp_error( $order_id ) ) {
15704 ebook_store_log_stripe_event(
15705 'Stripe charge event order creation skipped',
15706 array(
15707 'type' => $event->type,
15708 'payment_intent_id' => $payment_intent_id,
15709 'code' => $order_id->get_error_code(),
15710 'error' => $order_id->get_error_message(),
15711 )
15712 );
15713 if ( ebook_store_is_retryable_stripe_order_error( $order_id ) ) {
15714 return new WP_REST_Response( 'retry', 500 );
15715 }
15716 } else {
15717 ebook_store_log_stripe_event(
15718 'Stripe order created from charge event',
15719 array(
15720 'type' => $event->type,
15721 'order_id' => $order_id,
15722 'payment_intent_id' => $payment_intent_id,
15723 )
15724 );
15725 }
15726 break;
15727
15728 case 'charge.refunded':
15729 $charge = $event->data->object;
15730 $payment_intent_id = ! empty( $charge->payment_intent ) ? (string) $charge->payment_intent : '';
15731 $order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15732
15733 if ( $order_id && ! empty( $charge->refunded ) ) {
15734 // charge.refunded also fires for partial refunds; `refunded` is true only when the full amount is back.
15735 $refund_currency = ! empty( $charge->currency ) ? strtoupper( (string) $charge->currency ) : (string) get_post_meta( $order_id, 'mc_currency', true );
15736 $refund_amount = isset( $charge->amount_refunded ) ? ( (float) $charge->amount_refunded / ebook_store_currency_minor_unit_factor( $refund_currency ) ) : '';
15737 $refund_id = (string) get_post_meta( $order_id, 'refund_gateway_id', true );
15738 if ( ! empty( $charge->refunds->data[0]->id ) ) {
15739 $refund_id = (string) $charge->refunds->data[0]->id;
15740 }
15741 ebook_store_mark_order_refunded( $order_id, 'stripe', $refund_id, 'succeeded', $refund_amount, $refund_currency );
15742 }
15743
15744 ebook_store_log_stripe_event(
15745 'Stripe charge refund event processed',
15746 array(
15747 'type' => $event->type,
15748 'order_id' => $order_id,
15749 'payment_intent_id' => $payment_intent_id,
15750 'fully_refunded' => ! empty( $charge->refunded ),
15751 )
15752 );
15753 break;
15754
15755 case 'refund.failed':
15756 case 'refund.updated':
15757 case 'charge.refund.updated':
15758 $refund = $event->data->object;
15759 $refund_status = isset( $refund->status ) ? (string) $refund->status : '';
15760 $payment_intent_id = ! empty( $refund->payment_intent ) ? (string) $refund->payment_intent : '';
15761 $order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15762
15763 if ( $order_id && ( $refund_status === 'failed' || $refund_status === 'canceled' ) ) {
15764 // The money never went back: the buyer keeps a paid, downloadable order.
15765 update_post_meta( $order_id, 'payment_status', 'Completed' );
15766 update_post_meta( $order_id, 'refund_gateway_status', $refund_status );
15767 delete_post_meta( $order_id, 'refund_gateway_id' );
15768 } elseif ( $order_id && $refund_status === 'succeeded' && isset( $refund->id ) && (string) $refund->id === (string) get_post_meta( $order_id, 'refund_gateway_id', true ) ) {
15769 update_post_meta( $order_id, 'refund_gateway_status', 'succeeded' );
15770 }
15771
15772 ebook_store_log_stripe_event(
15773 'Stripe refund status event processed',
15774 array(
15775 'type' => $event->type,
15776 'order_id' => $order_id,
15777 'payment_intent_id' => $payment_intent_id,
15778 'status' => $refund_status,
15779 )
15780 );
15781 break;
15782
15783 default:
15784 ebook_store_log_stripe_event(
15785 'Stripe webhook ignored event',
15786 array(
15787 'type' => $event->type,
15788 )
15789 );
15790 break;
15791 }
15792
15793 return new WP_REST_Response( array( 'received' => true ), 200 );
15794 } catch ( Exception $e ) {
15795 ebook_store_log_stripe_event(
15796 'Stripe webhook error',
15797 array(
15798 'error_class' => get_class( $e ),
15799 'message' => $e->getMessage(),
15800 )
15801 );
15802
15803 return new WP_REST_Response(
15804 array( 'error' => $e->getMessage() ),
15805 $e instanceof \Stripe\Exception\SignatureVerificationException ? 400 : 500
15806 );
15807 }
15808 }
15809