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

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

15,740 lines 612.2 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 if (
4074 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
4075 && get_option( 'stripe_integration_enabled', 0 )
4076 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
4077 ) {
4078 $recovered_order_id = 0;
4079 $stripe_session_id = isset( $_REQUEST['stripe_session_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['stripe_session_id'] ) ) : '';
4080 if ( $stripe_session_id !== '' ) {
4081 $recovered_order_id = ebook_store_recover_stripe_order_by_session_id( $stripe_session_id );
4082 } else {
4083 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key(
4084 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
4085 );
4086 }
4087 if ( is_wp_error( $recovered_order_id ) ) {
4088 ebook_store_log_stripe_event(
4089 'Stripe thank-you recovery skipped',
4090 array(
4091 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
4092 'stripe_session_id' => $stripe_session_id,
4093 'error' => $recovered_order_id->get_error_message(),
4094 )
4095 );
4096 } elseif ( $recovered_order_id ) {
4097 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
4098 }
4099 }
4100 if (
4101 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
4102 && get_option( 'adyen_integration_enabled', 0 )
4103 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
4104 ) {
4105 $recovered_order_id = ebook_store_recover_adyen_order_by_ebook_key(
4106 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
4107 );
4108 if ( is_wp_error( $recovered_order_id ) ) {
4109 ebook_store_log_adyen_event(
4110 'Adyen thank-you recovery skipped',
4111 array(
4112 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
4113 'error' => $recovered_order_id->get_error_message(),
4114 )
4115 );
4116 } elseif ( $recovered_order_id ) {
4117 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
4118 }
4119 }
4120 //wp_die(var_dump($ebook_order));
4121 $time = strtotime(@$ebook_order['payment_date'][0]);
4122 //wp_die("$time-".time().'='.$time - time() );
4123 //print_r($ebook_order);
4124 if (@$ebook_order['item_name'][0] == '') {
4125 $ebook_order['item_name'][0] = get_the_title(@$ebook_order['ebook'][0]);
4126 }
4127
4128 //wp_die('ebook order var ' . var_export($ebook_order,true));
4129 if (!$ebook_order || @strtotime($ebook_order['payment_date'][0]) == 0 || isset($ebook_order['ebook']) == false) { // || time() < @strtotime($ebook_order['payment_date'][0]) + 25 ||
4130 return '<h4>' . $locale['confirmation'] . '</h4>' . '<script> window.setTimeout(\'location.reload()\', 5000);
4131 </script>';
4132 }
4133 $ebook_order_ebook_id = isset($ebook_order['ebook'][0]) ? absint($ebook_order['ebook'][0]) : 0;
4134 $content = ebook_store_get_effective_confirmation_page_content($ebook_order_ebook_id);
4135 $file = get_post_meta($ebook_order['ebook'][0],'ebook_wp_custom_attachment',true);
4136 //wp_die(print_r($file,true));
4137 if (@!$file) {
4138 $file = array();
4139 }
4140 $ebook_order['downloadlink'][0] = ebook_download_link($ebook_order);
4141 $ebook_order['downloadlink'][0] = str_replace('#038;','&',$ebook_order['downloadlink'][0]);
4142 $ebook_order['downloadlink'][0] = str_replace('&&','&',$ebook_order['downloadlink'][0]);
4143 //<a href="%%downloadlink%%" target="_blank" rel="noopener">%%item_name%%</a> (%%filesize%%)
4144 @$ebook_order['pdf_reader'][0] = ebook_pdf_reader($ebook_order);
4145 @$ebook_order['download_links'][0] = implode("<br />",ebook_download_links($ebook_order));
4146 @$ebook_order['ebook_bonus'][0] = (implode("<br />",ebook_download_links_bonus($ebook_order)) != '' ? implode("<br />",ebook_download_links_bonus($ebook_order)) : __('None','ebook-store'));
4147 @$ebook_order['filesize'][0] = humanFileSize(filesize($file['file']));
4148 // $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].')';
4149 //print_r($ebook_order);
4150 //file_put_contents(__DIR__ . '/debug.log', 'downloadlink: ' . $ebook_order['downloadlink'][0] . "\n", FILE_APPEND);
4151 $ebookObj = new EbookStoreEbook(@$ebook_order['ebook'][0]);
4152 // This is a plugin order, not a WooCommerce one: hand link() the ebook_key
4153 // credentials so the non-PDF format links resolve to the download path
4154 // that this order is actually authorised for.
4155 $ebookObj->ebook_key = (strlen(@$ebook_order['ebook_key'][0]) == 32 ? @$ebook_order['ebook_key'][0] : @$ebook_order['ebook_key_array'][0]);
4156 $ebookObj->md5_nonce = @$ebook_order['md5_nonce'][0];
4157 $ebookObj->order_id = isset($ebook_order['order_id'][0]) ? absint($ebook_order['order_id'][0]) : 0;
4158 $ebookObj->setLink['pdf'] = $ebook_order['downloadlink'][0];
4159 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
4160 //$ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
4161
4162 if (@$ebook_order['password'] == '') {
4163 $ebook_order['password'] = $ebook_order['payer_email'];
4164 }
4165 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_1', true) != '') {
4166 $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>
4167 </a>');
4168
4169 }
4170 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_2', true) != '') {
4171 $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">
4172 <h1 style="color:red;">View Book Online Now</h1>
4173 </a>');
4174 }
4175
4176 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_3', true) != '') {
4177 $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">
4178 <h1 style="color:red;">Download PDF</h1>
4179 </a>');
4180 }
4181 foreach ($ebook_order as $k => $arr) {
4182 $content = str_replace('%%' . $k . '%%', $arr[0], $content);
4183 }
4184 return apply_filters('the_content',$content);
4185 //return 'test 123' . $content;
4186 }
4187
4188 $template_items = array();
4189 $args = array( 'post_type' => 'ebook', 'posts_per_page' => -1, 'p' => @$atts['ebook_id'] );
4190 $loop = new WP_Query( $args );
4191
4192 while ( $loop->have_posts() ) : $loop->the_post();
4193 /*the_title();
4194 echo '<div class="entry-content">';
4195 the_content();
4196 echo '</div>';*/
4197 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
4198 if ( $template === '' && is_array( $ebook ) && ! empty( $ebook['form_template'] ) ) {
4199 $template = ebook_store_resolve_form_template( $ebook['form_template'] );
4200 }
4201 $ebook['ebook_price'] = floatval($ebook['ebook_price']);
4202 //error_log(var_export($ebook,true));
4203 $ebook_key = md5(NONCE_KEY . get_the_ID() . $ebook['ebook_price'] . mt_rand(1,100000));
4204 //$ebook['ebook_price'] = is_float($ebook['ebook_price']) ? $ebook['ebook_price'] : 0.0;
4205 $md5_nonce = md5(mt_rand(1,9999) . NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','));
4206 //$custom = get_the_ID() . '|' . $md5_nonce;
4207 // The currency the PayPal form charges in: the ebook's own when it has a
4208 // valid one, otherwise the store's.
4209 $paypal_currency = ( ! empty( $ebook['paypal_currency'] ) ? strtoupper( trim( (string) $ebook['paypal_currency'] ) ) : ebook_store_get_store_currency() );
4210 if ( ! preg_match( '/^[A-Z]{3}$/', $paypal_currency ) ) {
4211 $paypal_currency = ebook_store_get_store_currency();
4212 }
4213 // Segment 3 binds the net price and the currency the form charges in, so
4214 // the posted form cannot be replayed with a different currency_code; the
4215 // IPN checks the tax separately, so dropping the tax line fails too. The
4216 // net price rather than the gross is signed because the IPN recovers it
4217 // exactly (mc_gross - tax), while the gross depends on how PayPal rounds a
4218 // half cent of tax. Segment 2 stays exactly as it was for orders already
4219 // in flight when the site upgrades.
4220 $custom = get_the_ID() . '|' . md5(NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','))
4221 . '|' . ebook_store_paypal_amount_signature( get_the_ID(), $ebook['ebook_price'], $paypal_currency );
4222
4223 if (get_option('ebook_store_wp_affiliate_integration') == 1) { /* && file_exists('../affiliates-manager/config.php')*/
4224 $parameters = apply_filters('ebook_store_payment_gateway_parameters', array('custom' => $custom));
4225 }
4226
4227 $c = new Currencies();
4228 $img = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment', true);
4229 $preview = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_preview', true);
4230 if (is_array($preview) == false) { $preview = array(); }
4231 if (@$preview['url'] != '') {
4232 if (get_option( 'ebook_store_no_viewerjs_previews', false ) == false) {
4233 @$preview['url'] = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html#' . $preview['url'];
4234 }
4235 }
4236
4237 $cover = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_cover',true);
4238 $side = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_side_photo',true);
4239 if (!$side) {
4240 $side = $cover;
4241 }
4242 //style="background-url:(' . $side['url'] . ');"
4243 if (is_array(@$ebook['ebook_publisher'])) {
4244 $publishers = array();
4245 foreach ($ebook['ebook_publisher'] as $p => $pp) {
4246 $publishers[] = get_the_title($pp);
4247 }
4248 }
4249 if (is_array(@$ebook['ebook_author'])) {
4250 $authors = array();
4251 foreach ($ebook['ebook_author'] as $p => $pp) {
4252 $authors[] = get_the_title($pp);
4253 }
4254 }
4255 @$publishers = is_array($publishers) ? $publishers : array();
4256 @$authors = is_array($authors) ? $authors : array();
4257 $publishers = @implode(", ", $publishers);
4258 $authors = @implode(", ", $authors);
4259 /* div class front, where is it?*/
4260
4261 if ( $img == '' && ! ebook_store_has_pro_license() && current_user_can( 'edit_post', get_the_ID() ) ) {
4262 printf(
4263 '<div class="ebook-store-notice"><h3>%1$s</h3><p><a href="%2$s">%3$s</a></p></div>',
4264 esc_html__( 'This ebook has no file attached yet, so there is nothing for buyers to download.', 'ebook-store' ),
4265 esc_url( get_edit_post_link( get_the_ID() ) ),
4266 esc_html__( 'Open the ebook and upload a file', 'ebook-store' )
4267 );
4268 }
4269 $md5rand = md5(rand(1,10000) . microtime());
4270 $payment_methods = array();
4271 $paypal_ready = ebook_store_is_paypal_enabled() && trim( (string) get_option( 'paypal_account', '' ) ) !== '';
4272
4273 $formatted_paypal_price = $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2);
4274 $buyNowLinkText = str_replace( '%%price%%', $formatted_paypal_price, ebook_store_get_gateway_button_text( 'paypal' ) );
4275 $buyNowLinkOnClick = 'document.getElementById(\'' . $md5rand . '\').submit(); return false;';
4276 $buyNowLinkHref = '';
4277 $form = '';
4278 $show_primary_buy_link = false;
4279 $primary_payment_method = '';
4280 // The price comes out of post meta as a string ("0", "0.00"), so a strict
4281 // comparison against the integer 0 was never true and a zero-priced book
4282 // only behaved as free when its delivery mode said so explicitly. A book
4283 // explicitly saved as paid is never treated as free, whatever its price
4284 // field holds (see ebook_store_is_free_ebook()).
4285 if ( ebook_store_is_free_ebook( $ebook ) ) {
4286 $buyNowLinkText = $locale['download'];
4287 $buyNowLinkOnClickOriginal = ebook_download_link(array('ebook' => array(0 => get_the_ID()), 'ebook_key' => array(0 => $ebook_key), 'md5_nonce' => array(0 => $md5_nonce)),1);
4288 $buyNowLinkOnClick = "window.location = '" . $buyNowLinkOnClickOriginal . "'; return false;";
4289 $buyNowLinkHref = $buyNowLinkOnClickOriginal;
4290 $show_primary_buy_link = true;
4291 } else if ( isset( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] == 'donate' ) {
4292 $buyNowLinkText = $locale['download'];
4293 $ebook['ebook_price'] = 0;
4294 $show_primary_buy_link = true;
4295 } else if ( $paypal_ready ) {
4296 $payment_methods['paypal'] = array(
4297 'key' => 'paypal',
4298 'label' => 'PayPal',
4299 'href' => '#',
4300 'onclick' => $buyNowLinkOnClick,
4301 'price' => $formatted_paypal_price,
4302 );
4303 $show_primary_buy_link = true;
4304 $primary_payment_method = 'paypal';
4305 }
4306
4307 if ($img == '' && get_option( 'ebook_store_license_key') == '') {
4308 $buyNowLinkOnClick = 'alert(\'There is no ebook file uploaded. Please upload a file first in the Ebook Store plugin.\');';
4309 }
4310 if (get_option('formEnabled') == 1) {
4311 $buyNowLinkOnClickOriginal = $buyNowLinkOnClick;
4312 $buyNowLinkOnClick = "ebook_store_popup(function () {" . $buyNowLinkOnClick . "}, this);";
4313 wp_register_style( 'ebook_store_tinglecss', plugins_url('/includes/tingle-master/dist/tingle.min.css', __FILE__) );
4314 wp_enqueue_style( 'ebook_store_tinglecss' );
4315 wp_enqueue_script( 'ebook_store_site_modal', plugins_url( '/includes/tingle-master/dist/tingle.min.js' , __FILE__ ), array(), '1.0.0', true );
4316 wp_enqueue_script( 'ebook_store_site', plugins_url( '/js/ebook_store_site.js' , __FILE__ ), array(), '1.0.0', true );
4317
4318 }
4319
4320 wp_enqueue_style( 'ebookstorestylesheet' );
4321 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4322 if (!$checkoutPage) {
4323 $checkoutPage = $post_id;
4324 }
4325
4326 $bookpost = get_post(get_the_ID());
4327 $add_to_cart_label = ! empty( $locale['addtocart'] ) ? $locale['addtocart'] : __( 'Add to cart', 'ebook-store' );
4328 $woocommerce_product_id = ebook_store_get_wc_product_reference_for_ebook( get_the_ID() );
4329 if ($woocommerce_product_id > 0 && class_exists('woocommerce') && get_option('ebook_store_woocommerce_integration')) {
4330 $woocommerce_checkout_url = add_query_arg(array('woocommerce_product_id' => $woocommerce_product_id), get_permalink(get_the_ID()));
4331 $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>';
4332 }
4333 if (!is_array($cover)) {
4334 $cover = array();
4335 }
4336 //$extraButtons[] = array();
4337
4338 // if (!is_array(@$extraButtons)) {
4339 // @$extraButtons = array();
4340 // }
4341 if (!is_array($side)) {
4342 $side = array();
4343 }
4344 @$extraButtons = apply_filters('ebook_store_extra_buttons',$extraButtons, $ebook['ebook_price'], $bookpost->post_title, $cover['url'], $md5_nonce, $post_id);
4345
4346 // Add Stripe button if enabled
4347 if (get_option('stripe_integration_enabled', 0)) {
4348 $button_text = ebook_store_get_gateway_button_text( 'stripe' );
4349
4350 // Only show Stripe button if price is greater than 0
4351
4352 if ($ebook['ebook_price'] > 0) {
4353 try {
4354 // The Checkout Session is created when the buyer clicks, not on
4355 // every page render: a session was one Stripe API call per view,
4356 // and a page cache could serve the same (24h-expiring) session URL
4357 // to everyone who opened the page.
4358 $c = new Currencies;
4359 // Stripe charges in the store currency. Kept apart from
4360 // $paypal_currency, which the PayPal form and its signature use.
4361 $stripe_currency = ebook_store_get_store_currency();
4362 $formatted_price = $c->getSymbol($stripe_currency) . number_format( ebook_store_price_plus_vat($ebook['ebook_price']), 2 );
4363 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4364
4365 ebook_store_enqueue_stripe_checkout_script();
4366
4367 // The click is intercepted on data-ebook-store-payment-method,
4368 // which both this markup and the bundled templates already emit,
4369 // and the ebook id comes from data-ebook-store-item-id.
4370 $extraButtons[] = sprintf(
4371 '<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>',
4372 esc_url( get_permalink( get_the_ID() ) ),
4373 esc_attr( get_the_ID() ),
4374 esc_attr( $bookpost->post_title ),
4375 esc_attr( $stripe_currency ),
4376 esc_attr( (string) ebook_store_price_plus_vat($ebook['ebook_price']) ),
4377 esc_html($display_text)
4378 );
4379 $payment_methods['stripe'] = array(
4380 'key' => 'stripe',
4381 'label' => 'Stripe',
4382 'href' => get_permalink( get_the_ID() ),
4383 'onclick' => '',
4384 'price' => $formatted_price,
4385 );
4386 } catch (Exception $e) {
4387 error_log('Stripe button creation failed: ' . $e->getMessage());
4388 }
4389 }
4390 }
4391
4392 // Add Adyen button if enabled
4393 if (get_option('adyen_integration_enabled', 0)) {
4394 $button_text = ebook_store_get_gateway_button_text( 'adyen' );
4395
4396 if ($ebook['ebook_price'] > 0) {
4397 try {
4398 $adyen_checkout_url = ebook_store_create_adyen_payment_link(
4399 get_the_ID(),
4400 ebook_store_price_plus_vat($ebook['ebook_price']),
4401 $bookpost->post_title,
4402 $ebook_key
4403 );
4404
4405 if ( $adyen_checkout_url ) {
4406 $c = new Currencies;
4407 $adyen_currency = ebook_store_get_store_currency();
4408 $adyen_amount = (float) ebook_store_price_plus_vat($ebook['ebook_price']);
4409 $formatted_price = $c->getSymbol($adyen_currency) . number_format( $adyen_amount, 2 );
4410 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4411
4412 $extraButtons[] = sprintf(
4413 '<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>',
4414 esc_url($adyen_checkout_url),
4415 esc_attr( get_the_ID() ),
4416 esc_attr( $bookpost->post_title ),
4417 esc_attr( $adyen_currency ),
4418 esc_attr( (string) $adyen_amount ),
4419 esc_html($display_text)
4420 );
4421 $payment_methods['adyen'] = array(
4422 'key' => 'adyen',
4423 'label' => 'Adyen',
4424 'href' => $adyen_checkout_url,
4425 'onclick' => '',
4426 'price' => $formatted_price,
4427 );
4428 }
4429 } catch (Exception $e) {
4430 error_log('Adyen button creation failed: ' . $e->getMessage());
4431 }
4432 }
4433 }
4434
4435 $extraButtons = is_array($extraButtons) ? $extraButtons : array();
4436 if ( $paypal_ready ) {
4437 $form = '<form method="post" id="' . $md5rand . '" name="dmp_order_form" action="https://www' . (get_option('paypal_sandbox') != '' ? '.sandbox' : '') . '.paypal.com/cgi-bin/webscr">
4438 <input type="hidden" name="rm" value="0">
4439 <input type="hidden" name="discount_rate" value="0">
4440 <input type="hidden" name="cmd" value="_xclick">
4441 <input type="hidden" name="charset" value="utf-8">
4442 <input type="hidden" name="md5_nonce" value="' . $md5_nonce . '">
4443 <input type="hidden" name="lc" value="' . get_option('paypal_language') . '">
4444 <input type="hidden" name="no_shipping" value="' . get_option('ebook_store_require_shipping') . '">
4445 <input type="hidden" name="button_subtype" value="products">
4446 <input type="hidden" name="return" value="' . add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage)) . '">
4447 <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')))) . '">
4448 <input type="hidden" name="notify_url" value="' . add_query_arg(array('task' => 'ipn','ebook_key' => $ebook_key, 'md5_nonce' => $md5_nonce), home_url('/')) . '">
4449 <input type="hidden" name="item_name" value="' . $bookpost->post_title . '">
4450 <input type="hidden" name="item_number" value="1">
4451 <input type="hidden" name="tax_rate" value="' . ebook_store_get_vat_percent() . '">
4452 <input type="hidden" name="amount" value="' . $ebook['ebook_price'] . '">
4453 <input type="hidden" name="upload" value="1">
4454 <input type="hidden" name="custom" value="' . $custom .'">
4455 <input type="hidden" name="business" id="af69ae50c1be74757508c8f7fae10abd" value="' . get_option('paypal_account') . '">
4456 <input type="hidden" name="receiver_email" id="af69ae50c1be74757508c8f7fae10abd0xff" value="' . get_option('paypal_account') . '">
4457 <input type="hidden" name="currency_code" value="' . $paypal_currency . '">
4458 <input type="hidden" name="cbt" value="' . get_option('paypal_return_button_text') . '">
4459 <input type="hidden" name="no_note" value="1">
4460 </form>';
4461 $buyNowLinkHref = '#';
4462 }
4463
4464 //$form .= var_export($bookpost, true);
4465 if (@$_GET['task'] == 'direct_order') {
4466 $is_free_ebook = ebook_store_is_free_ebook( $ebook );
4467 if ($ebook['ebook_price'] > 0 && $paypal_ready) {
4468 die($form .
4469 '<script>
4470 document.getElementById("' . $md5rand . '").submit();
4471 </script>'
4472 );
4473 } else if ( $is_free_ebook && get_option( 'formEnabled' ) == 1 ) {
4474 // The download endpoint insists on the address the "Your Details"
4475 // form collects, so the direct link shows the ebook page — where
4476 // that form opens — instead of jumping to a download it would
4477 // refuse. Nothing to do here: rendering continues below.
4478 } else if ( $is_free_ebook ) {
4479 $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 ) );
4480 if ( ! headers_sent() ) {
4481 wp_safe_redirect( $direct_url );
4482 exit;
4483 }
4484 // The shortcode runs after the theme has started output, so a header
4485 // redirect is not always possible; fall back to a client-side one.
4486 die( '<meta http-equiv="refresh" content="0;url=' . esc_attr( $direct_url ) . '"><script>window.location = ' . wp_json_encode( $direct_url ) . ';</script>' );
4487 } else if ( $paypal_ready && $form !== '' ) {
4488 die($form .
4489 '<script>
4490 document.getElementById("' . $md5rand . '").submit();
4491 </script>'
4492 );
4493 }
4494 // Nothing to submit automatically (Stripe/Adyen-only store): render the normal page.
4495 }
4496 $date_text = (!empty($ebook['ebook_date']) && strtotime($ebook['ebook_date'])) ? date(get_option('date_format'), strtotime($ebook['ebook_date'])) : '';
4497 $pages_text = (!empty($ebook['ebook_pages']) && (int) $ebook['ebook_pages'] > 0) ? $ebook['ebook_pages'] . ' ' . $locale['pages'] : '';
4498 $preview_label = ! empty( $locale['preview'] ) ? $locale['preview'] : __( 'Preview', 'ebook-store' );
4499 $file_facts = ebook_store_get_ebook_file_facts( get_the_ID() );
4500 $template_items[] = array(
4501 'ebook_id' => get_the_ID(),
4502 'title' => $bookpost->post_title,
4503 'content_html' => wpautop( do_shortcode( shortcode_unautop( get_the_content() ) ) ),
4504 'content_raw' => get_the_content(),
4505 'authors' => $authors,
4506 'publishers' => $publishers,
4507 'date_text' => $date_text,
4508 'pages_text' => $pages_text,
4509 'file_facts' => $file_facts,
4510 'cover_url' => isset( $cover['url'] ) ? $cover['url'] : '',
4511 'side_url' => isset( $side['url'] ) ? $side['url'] : '',
4512 'preview_url' => isset( $preview['url'] ) ? $preview['url'] : '',
4513 'add_to_cart_html' => isset( $addToCartButton ) ? $addToCartButton : '',
4514 'extra_buttons_html' => implode( '', $extraButtons ),
4515 'extra_buttons' => $extraButtons,
4516 'payment_methods' => array_values( $payment_methods ),
4517 'buy_link_text' => $buyNowLinkText,
4518 'buy_link_onclick' => $buyNowLinkOnClick,
4519 'buy_link_href' => $buyNowLinkHref,
4520 'primary_payment_method' => $primary_payment_method,
4521 'show_primary_buy_link' => $show_primary_buy_link ? 1 : 0,
4522 'form_html' => $form,
4523 'md5_nonce' => $md5_nonce,
4524 'locale' => $locale,
4525 'preview_label' => $preview_label,
4526 'ebook' => $ebook,
4527 'paypal_currency' => $paypal_currency,
4528 'currency_symbol' => $c->getSymbol($paypal_currency),
4529 'price_display' => $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2),
4530 'disable_cover_buy_now' => (int) get_option('ebook_store_disable_cover_buy_now',0),
4531 'hide_buy_now' => (int) get_option('ebook_store_hide_buy_now',0),
4532 'buy_link_font_size' => get_option('ebook_store_buy_link_font_size', 0.65),
4533 'title_font_size' => get_option('ebook_store_title_font_size', 1.8),
4534 'form_scale' => get_option('form_scale', 1),
4535 );
4536 endwhile;
4537 wp_reset_postdata();
4538 if ( $template === '' ) {
4539 $template = ebook_store_resolve_form_template( '' );
4540 }
4541
4542 if ($buyNowOnly == true) {
4543 if ( ! $show_primary_buy_link ) {
4544 return '';
4545 }
4546 $ga_attributes = '';
4547 if ( ebook_store_ga4_is_enabled() && $primary_payment_method !== '' ) {
4548 $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']) ) . '"';
4549 }
4550 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>';
4551 }
4552 if ( $template === 'legacy' ) {
4553 wp_enqueue_style( 'ebookstore-bookblock', plugins_url('css/bookblock.css',__FILE__) );
4554 wp_enqueue_style( 'ebookstore-component', plugins_url('css/component.css',__FILE__) );
4555 wp_enqueue_script( 'ebookstore-modernizr', plugins_url('js/modernizr.custom.js',__FILE__), array(), null, false );
4556 wp_enqueue_script( 'ebookstore-bookblock', plugins_url('js/bookblock.min.js',__FILE__), array(), null, true );
4557 wp_enqueue_script( 'ebookstore-classie', plugins_url('js/classie.js',__FILE__), array(), null, true );
4558 wp_enqueue_script( 'ebookstore-bookshelf', plugins_url('js/bookshelf.js',__FILE__), array('ebookstore-bookblock','ebookstore-classie'), null, true );
4559 }
4560
4561 $context = array(
4562 'items' => $template_items,
4563 'template' => $template,
4564 'plugin_url' => plugins_url('', __FILE__),
4565 'locale' => $locale,
4566 );
4567
4568 return ebook_store_render_form_template( $template, $context ) . ebook_store_ga4_render_view_item_tracking( $template_items );
4569 } catch ( Throwable $e ) {
4570 error_log(
4571 sprintf(
4572 'Ebook Store render error for ebook %d: %s in %s:%d',
4573 (int) $atts['ebook_id'],
4574 $e->getMessage(),
4575 $e->getFile(),
4576 (int) $e->getLine()
4577 )
4578 );
4579
4580 return '<div class="ebook-store-alert ebook-store-alert-error">' . esc_html__( 'The ebook order form could not be loaded.', 'ebook-store' ) . '</div>';
4581 }
4582 }
4583 function custom_enter_title_author( $input ) {
4584 global $post_type;
4585
4586 if ( is_admin() && 'ebook_author' == $post_type )
4587 return __( 'Enter Author Name' );
4588
4589 return $input;
4590 }
4591
4592 function custom_enter_title_publisher( $input ) {
4593 global $post_type;
4594
4595 if ( is_admin() && 'ebook_publisher' == $post_type )
4596 return __( 'Enter Publisher Name' );
4597
4598 return $input;
4599 }
4600
4601 class Currencies {
4602
4603 public $currencies = array(
4604
4605 'AUD' => array('name' => "Australian Dollar", 'symbol' => "A$", 'ASCII' => "A&#36;"),
4606
4607 'CAD' => array('name' => "Canadian Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4608
4609 'CZK' => array('name' => "Czech Koruna", 'symbol' => "kr", 'ASCII' => "kr"),
4610
4611 'DKK' => array('name' => "Danish Krone", 'symbol' => "Kr", 'ASCII' => ""),
4612
4613 'EUR' => array('name' => "Euro", 'symbol' => "€", 'ASCII' => "&#128;"),
4614
4615 'HKD' => array('name' => "Hong Kong Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4616
4617 'HUF' => array('name' => "Hungarian Forint", 'symbol' => "Ft", 'ASCII' => "Ft"),
4618
4619 'ILS' => array('name' => "Israeli New Sheqel", 'symbol' => "₪", 'ASCII' => "&#x20AA;"),
4620
4621 'INR' => array('name' => "Indian Rupee", 'symbol' => "₹", 'ASCII' => "&#8377;"),
4622
4623 'JPY' => array('name' => "Japanese Yen", 'symbol' => "¥", 'ASCII' => "&#165;"),
4624
4625 'MXN' => array('name' => "Mexican Peso", 'symbol' => "$", 'ASCII' => "&#36;"),
4626
4627 'NOK' => array('name' => "Norwegian Krone", 'symbol' => "Kr", 'ASCII' => ""),
4628
4629 'NZD' => array('name' => "New Zealand Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4630
4631 'PHP' => array('name' => "Philippine Peso", 'symbol' => "₱", 'ASCII' => ""),
4632
4633 'PLN' => array('name' => "Polish Zloty", 'symbol' => "zł‚", 'ASCII' => "zł"),
4634
4635 'GBP' => array('name' => "Pound Sterling", 'symbol' => "£", 'ASCII' => "&#163;"),
4636
4637 'SGD' => array('name' => "Singapore Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4638
4639 'SEK' => array('name' => "Swedish Krona", 'symbol' => "kr", 'ASCII' => ""),
4640
4641 'CHF' => array('name' => "Swiss Franc", 'symbol' => "CHF", 'ASCII' => "Fr"),
4642
4643 'TWD' => array('name' => "Taiwan New Dollar", 'symbol' => "NT$", 'ASCII' => "NT&#36;"),
4644
4645 'THB' => array('name' => "Thai Baht", 'symbol' => "฿", 'ASCII' => "&#3647;"),
4646
4647 'USD' => array('name' => "U.S. Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4648
4649 'BRL' => array('name' => "Brazilian Real", 'symbol' => "R$", 'ASCII' => "&#x0024;"),
4650
4651 'TRY' => array('name' => "Turkish Lira", 'symbol' => "TL", 'ASCII' => "TL"),
4652
4653 'MYR' => array('name' => "Malaysian Ringgit", 'symbol' => "RM", 'ASCII' => "RM"),
4654
4655 'NGN' => array('name' => "Nigerian naira", 'symbol' => "₦", 'ASCII' => "₦"),
4656
4657 'RON' => array('name' => "Romanian Leu", 'symbol' => "Lei", 'ASCII' => "Lei"),
4658
4659 'ZAR' => array('name' => "South African Rands", 'symbol' => "R", 'ASCII' => "R"),
4660 );
4661
4662 public function getSymbol($code = 'USD') {
4663
4664 if (!empty($this->currencies[$code]['ASCII'])) {
4665
4666 return (string) $this->currencies[$code]['ASCII'];
4667
4668 }
4669
4670 return (string) @$this->currencies[$code]['symbol'];
4671
4672 }
4673
4674 }
4675
4676 function order_columns($columns)
4677 {
4678 $columns = array(
4679 'cb' => '<input type="checkbox" />',
4680 'order_id' => __('Order', 'ebook-store'),
4681 'title' => __('Buyer', 'ebook-store'),
4682 'product' => __('Product', 'ebook-store'),
4683 'amount' => __('Amount', 'ebook-store'),
4684 'paypal' => __('Payment', 'ebook-store'),
4685 'downloadlink' => __('Delivery', 'ebook-store'),
4686 'formData' => __('Form Data', 'ebook-store'),
4687 'date' => __('Date', 'ebook-store'),
4688 );
4689 return $columns;
4690 }
4691 function order_custom_columns($column)
4692 {
4693 global $post;
4694 $c = new Currencies();
4695 switch ($column) {
4696 case 'order_id':
4697 $payment_status = get_post_meta($post->ID,'payment_status',true);
4698 $downloads = intval(get_post_meta($post->ID,'downloads',true));
4699 echo '<div class="ebook-order-list">';
4700 echo '<div class="ebook-order-list__title">#' . esc_html($post->ID) . '</div>';
4701 echo '<div class="ebook-order-list__badges">';
4702 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status ? $payment_status : __('Unknown', 'ebook-store'), ebook_store_order_status_class($payment_status) );
4703 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), number_format_i18n($downloads) );
4704 echo '</div>';
4705 $txn_id = get_post_meta($post->ID,'txn_id',true);
4706 if ($txn_id) {
4707 echo '<span class="ebook-order-list__inline-code">' . esc_html($txn_id) . '</span>';
4708 }
4709 echo '</div>';
4710 break;
4711 case "title":
4712 $first_name = trim((string)get_post_meta($post->ID,'first_name',true));
4713 $last_name = trim((string)get_post_meta($post->ID,'last_name',true));
4714 $buyer_name = trim($first_name . ' ' . $last_name);
4715 $mail = trim((string)get_post_meta($post->ID,'payer_email',true));
4716 $country = trim((string)get_post_meta($post->ID,'residence_country',true));
4717 echo '<div class="ebook-order-list">';
4718 echo '<div class="ebook-order-list__title">' . esc_html($buyer_name !== '' ? $buyer_name : __('Unnamed buyer', 'ebook-store')) . '</div>';
4719 if ($mail !== '') {
4720 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4721 }
4722 echo '<div class="ebook-order-list__badges">';
4723 if ($country !== '') {
4724 echo ebook_store_render_order_badge( __('Country', 'ebook-store'), $country );
4725 }
4726 $payer_status = get_post_meta($post->ID,'payer_status',true);
4727 if ($payer_status !== '') {
4728 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status, ebook_store_order_status_class($payer_status) );
4729 }
4730 echo '</div>';
4731 echo '</div>';
4732 break;
4733 case "product":
4734 $ebook_id = intval(get_post_meta($post->ID,'ebook',true));
4735 $product_title = $ebook_id ? get_the_title($ebook_id) : '';
4736 $item_number = get_post_meta($post->ID,'item_number',true);
4737 $quantity = get_post_meta($post->ID,'quantity',true);
4738 echo '<div class="ebook-order-list">';
4739 if ($ebook_id && $product_title) {
4740 echo '<div class="ebook-order-list__title"><a href="post.php?post=' . esc_attr($ebook_id) . '&action=edit">' . esc_html($product_title) . '</a></div>';
4741 } else {
4742 echo '<div class="ebook-order-list__title">' . esc_html(get_post_meta($post->ID,'item_name',true)) . '</div>';
4743 }
4744 echo '<div class="ebook-order-list__badges">';
4745 if ($quantity !== '') {
4746 echo ebook_store_render_order_badge( __('Qty', 'ebook-store'), $quantity );
4747 }
4748 if ($item_number !== '') {
4749 echo ebook_store_render_order_badge( __('SKU', 'ebook-store'), $item_number );
4750 }
4751 echo '</div>';
4752 echo '</div>';
4753 break;
4754 case "amount":
4755 $mc_currency = get_post_meta($post->ID,'mc_currency',true);
4756 $mc_gross = get_post_meta($post->ID,'mc_gross',true);
4757 $mc_fee = get_post_meta($post->ID,'mc_fee',true);
4758 $total = floatval($mc_gross) - floatval($mc_fee);
4759 echo '<div class="ebook-order-list">';
4760 echo '<div class="ebook-order-list__title">' . esc_html( ebook_store_order_money_display( $mc_gross, $mc_currency ) ) . '</div>';
4761 echo '<div class="ebook-order-list__stack">';
4762 echo '<span>' . esc_html__( 'Fee:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $mc_fee, $mc_currency ) ) . '</span>';
4763 echo '<span>' . esc_html__( 'Net:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $total, $mc_currency ) ) . '</span>';
4764 echo '</div>';
4765 echo '</div>';
4766 break;
4767 case "paypal":
4768 $mail = esc_attr(get_post_meta($post->ID,'payer_email',true));
4769 $payment_type = get_post_meta($post->ID,'payment_type',true);
4770 $payment_status = get_post_meta($post->ID,'payment_status',true);
4771 $refund_gateway = ebook_store_get_order_refund_gateway( $post->ID );
4772 echo '<div class="ebook-order-list">';
4773 echo '<div class="ebook-order-list__title">' . esc_html( $payment_type !== '' ? $payment_type : __('PayPal', 'ebook-store') ) . '</div>';
4774 echo '<div class="ebook-order-list__badges">';
4775 if ( $payment_status !== '' ) {
4776 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status, ebook_store_order_status_class($payment_status) );
4777 }
4778 $payer_status = get_post_meta($post->ID,'payer_status',true);
4779 if ( $payer_status !== '' ) {
4780 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status );
4781 }
4782 echo '</div>';
4783 if ($mail !== '') {
4784 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4785 }
4786 if ( $refund_gateway !== '' ) {
4787 echo '<div class="ebook-order-list__actions">';
4788 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>';
4789 echo '</div>';
4790 }
4791 echo '</div>';
4792 break;
4793 case 'downloadlink':
4794 $downloadlink = ebook_download_link(
4795 array(
4796 'ebook_key' => array( get_post_meta( $post->ID, 'ebook_key', true ) ),
4797 'md5_nonce' => array( get_post_meta( $post->ID, 'md5_nonce', true ) ),
4798 'ebook' => array( get_post_meta( $post->ID, 'ebook', true ) ),
4799 'order_id' => array( $post->ID ),
4800 )
4801 );
4802 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4803 $link = add_query_arg(array('ebook_key' => get_post_meta($post->ID,'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
4804 $password = get_post_meta($post->ID,'password',true);
4805 echo '<div class="ebook-order-list">';
4806 echo '<div class="ebook-order-list__actions">';
4807 if ($downloadlink != '') {
4808 echo '<a href="' . esc_url($downloadlink) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Download', 'ebook-store') . '</a>';
4809 } else {
4810 echo '<span class="ebook-order-list__empty">' . esc_html__('No direct link saved', 'ebook-store') . '</span>';
4811 }
4812 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>';
4813 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>';
4814 echo '<a href="' . esc_url($link) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Thank you page', 'ebook-store') . '</a>';
4815 echo '</div>';
4816 echo '<div class="ebook-order-list__badges">';
4817 $downloads = get_post_meta($post->ID,'downloads',true);
4818 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), $downloads !== '' ? $downloads : '0' );
4819 if ( $password !== '' ) {
4820 echo ebook_store_render_order_badge( __('Password', 'ebook-store'), $password );
4821 }
4822 echo '</div>';
4823 echo '</div>';
4824 break;
4825 case 'formData':
4826 $md5_nonce = get_post_meta($post->ID,'md5_nonce',true);
4827 $data = get_post_meta($post->ID,'formData',true);
4828 $data = json_decode($data, true);
4829 if (is_array($data) && !empty($data)) {
4830 unset($data['md5_nonce']);
4831 echo '<div class="ebook-order-list ebook-order-list__pairs">';
4832 $count = 0;
4833 foreach ($data as $key => $value) {
4834 if ($count >= 5) {
4835 break;
4836 }
4837 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>';
4838 $count++;
4839 }
4840 if ($count === 0) {
4841 echo '<span class="ebook-order-list__empty">' . esc_html__('No form fields saved', 'ebook-store') . '</span>';
4842 }
4843 echo '</div>';
4844 } else {
4845 echo '<span class="ebook-order-list__empty">' . esc_html__('No extra form data', 'ebook-store') . '</span>';
4846 }
4847
4848 break;
4849 }
4850 }
4851 function ebook_readfile_chunked( $file, $retbytes = TRUE ) {
4852 $chunksize = 1 * (1024 * 1024);
4853 $buffer = '';
4854 $cnt = 0;
4855 $handle = fopen( $file, 'r' );
4856 header('Cache-Control: no-store');
4857
4858 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4859 return readfile($file);
4860 //if ob_clean / flush are executed on some hosts, the file is not properly downloaded.
4861
4862 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4863 //wp_die($file);
4864 ob_clean();
4865 flush();
4866 return readfile($file);
4867
4868 if ( $handle === FALSE ) return FALSE;
4869
4870 while ( ! feof( $handle ) ) :
4871 $buffer = fread( $handle, $chunksize );
4872 echo $buffer;
4873 //ob_flush();
4874 //flush();
4875
4876 if ( $retbytes ) $cnt += strlen( $buffer );
4877 endwhile;
4878
4879 $status = fclose( $handle );
4880
4881 if ( $retbytes AND $status ) return $cnt;
4882
4883 return $status;
4884 }
4885 function ebook_get_file_ctype( $extension ) {
4886 switch( $extension ):
4887 case 'ac' : $ctype = "application/pkix-attr-cert"; break;
4888 case 'adp' : $ctype = "audio/adpcm"; break;
4889 case 'ai' : $ctype = "application/postscript"; break;
4890 case 'aif' : $ctype = "audio/x-aiff"; break;
4891 case 'aifc' : $ctype = "audio/x-aiff"; break;
4892 case 'aiff' : $ctype = "audio/x-aiff"; break;
4893 case 'air' : $ctype = "application/vnd.adobe.air-application-installer-package+zip"; break;
4894 case 'apk' : $ctype = "application/vnd.android.package-archive"; break;
4895 case 'asc' : $ctype = "application/pgp-signature"; break;
4896 case 'atom' : $ctype = "application/atom+xml"; break;
4897 case 'atomcat' : $ctype = "application/atomcat+xml"; break;
4898 case 'atomsvc' : $ctype = "application/atomsvc+xml"; break;
4899 case 'au' : $ctype = "audio/basic"; break;
4900 case 'aw' : $ctype = "application/applixware"; break;
4901 case 'avi' : $ctype = "video/x-msvideo"; break;
4902 case 'bcpio' : $ctype = "application/x-bcpio"; break;
4903 case 'bin' : $ctype = "application/octet-stream"; break;
4904 case 'bmp' : $ctype = "image/bmp"; break;
4905 case 'boz' : $ctype = "application/x-bzip2"; break;
4906 case 'bpk' : $ctype = "application/octet-stream"; break;
4907 case 'bz' : $ctype = "application/x-bzip"; break;
4908 case 'bz2' : $ctype = "application/x-bzip2"; break;
4909 case 'ccxml' : $ctype = "application/ccxml+xml"; break;
4910 case 'cdmia' : $ctype = "application/cdmi-capability"; break;
4911 case 'cdmic' : $ctype = "application/cdmi-container"; break;
4912 case 'cdmid' : $ctype = "application/cdmi-domain"; break;
4913 case 'cdmio' : $ctype = "application/cdmi-object"; break;
4914 case 'cdmiq' : $ctype = "application/cdmi-queue"; break;
4915 case 'cdf' : $ctype = "application/x-netcdf"; break;
4916 case 'cer' : $ctype = "application/pkix-cert"; break;
4917 case 'cgm' : $ctype = "image/cgm"; break;
4918 case 'class' : $ctype = "application/octet-stream"; break;
4919 case 'cpio' : $ctype = "application/x-cpio"; break;
4920 case 'cpt' : $ctype = "application/mac-compactpro"; break;
4921 case 'crl' : $ctype = "application/pkix-crl"; break;
4922 case 'csh' : $ctype = "application/x-csh"; break;
4923 case 'css' : $ctype = "text/css"; break;
4924 case 'cu' : $ctype = "application/cu-seeme"; break;
4925 case 'davmount' : $ctype = "application/davmount+xml"; break;
4926 case 'dbk' : $ctype = "application/docbook+xml"; break;
4927 case 'dcr' : $ctype = "application/x-director"; break;
4928 case 'deploy' : $ctype = "application/octet-stream"; break;
4929 case 'dif' : $ctype = "video/x-dv"; break;
4930 case 'dir' : $ctype = "application/x-director"; break;
4931 case 'dist' : $ctype = "application/octet-stream"; break;
4932 case 'distz' : $ctype = "application/octet-stream"; break;
4933 case 'djv' : $ctype = "image/vnd.djvu"; break;
4934 case 'djvu' : $ctype = "image/vnd.djvu"; break;
4935 case 'dll' : $ctype = "application/octet-stream"; break;
4936 case 'dmg' : $ctype = "application/octet-stream"; break;
4937 case 'dms' : $ctype = "application/octet-stream"; break;
4938 case 'doc' : $ctype = "application/msword"; break;
4939 case 'docx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; break;
4940 case 'dotx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; break;
4941 case 'dssc' : $ctype = "application/dssc+der"; break;
4942 case 'dtd' : $ctype = "application/xml-dtd"; break;
4943 case 'dump' : $ctype = "application/octet-stream"; break;
4944 case 'dv' : $ctype = "video/x-dv"; break;
4945 case 'dvi' : $ctype = "application/x-dvi"; break;
4946 case 'dxr' : $ctype = "application/x-director"; break;
4947 case 'ecma' : $ctype = "application/ecmascript"; break;
4948 case 'elc' : $ctype = "application/octet-stream"; break;
4949 case 'emma' : $ctype = "application/emma+xml"; break;
4950 case 'eps' : $ctype = "application/postscript"; break;
4951 case 'epub' : $ctype = "application/epub+zip"; break;
4952 case 'etx' : $ctype = "text/x-setext"; break;
4953 case 'exe' : $ctype = "application/octet-stream"; break;
4954 case 'exi' : $ctype = "application/exi"; break;
4955 case 'ez' : $ctype = "application/andrew-inset"; break;
4956 case 'f4v' : $ctype = "video/x-f4v"; break;
4957 case 'fli' : $ctype = "video/x-fli"; break;
4958 case 'flv' : $ctype = "video/x-flv"; break;
4959 case 'gif' : $ctype = "image/gif"; break;
4960 case 'gml' : $ctype = "application/srgs"; break;
4961 case 'gpx' : $ctype = "application/gml+xml"; break;
4962 case 'gram' : $ctype = "application/gpx+xml"; break;
4963 case 'grxml' : $ctype = "application/srgs+xml"; break;
4964 case 'gtar' : $ctype = "application/x-gtar"; break;
4965 case 'gxf' : $ctype = "application/gxf"; break;
4966 case 'hdf' : $ctype = "application/x-hdf"; break;
4967 case 'hqx' : $ctype = "application/mac-binhex40"; break;
4968 case 'htm' : $ctype = "text/html"; break;
4969 case 'html' : $ctype = "text/html"; break;
4970 case 'ice' : $ctype = "x-conference/x-cooltalk"; break;
4971 case 'ico' : $ctype = "image/x-icon"; break;
4972 case 'ics' : $ctype = "text/calendar"; break;
4973 case 'ief' : $ctype = "image/ief"; break;
4974 case 'ifb' : $ctype = "text/calendar"; break;
4975 case 'iges' : $ctype = "model/iges"; break;
4976 case 'igs' : $ctype = "model/iges"; break;
4977 case 'ink' : $ctype = "application/inkml+xml"; break;
4978 case 'inkml' : $ctype = "application/inkml+xml"; break;
4979 case 'ipfix' : $ctype = "application/ipfix"; break;
4980 case 'jar' : $ctype = "application/java-archive"; break;
4981 case 'jnlp' : $ctype = "application/x-java-jnlp-file"; break;
4982 case 'jp2' : $ctype = "image/jp2"; break;
4983 case 'jpe' : $ctype = "image/jpeg"; break;
4984 case 'jpeg' : $ctype = "image/jpeg"; break;
4985 case 'jpg' : $ctype = "image/jpeg"; break;
4986 case 'js' : $ctype = "application/javascript"; break;
4987 case 'json' : $ctype = "application/json"; break;
4988 case 'jsonml' : $ctype = "application/jsonml+json"; break;
4989 case 'kar' : $ctype = "audio/midi"; break;
4990 case 'latex' : $ctype = "application/x-latex"; break;
4991 case 'lha' : $ctype = "application/octet-stream"; break;
4992 case 'lrf' : $ctype = "application/octet-stream"; break;
4993 case 'lzh' : $ctype = "application/octet-stream"; break;
4994 case 'lostxml' : $ctype = "application/lost+xml"; break;
4995 case 'm3u' : $ctype = "audio/x-mpegurl"; break;
4996 case 'm4a' : $ctype = "audio/mp4a-latm"; break;
4997 case 'm4b' : $ctype = "audio/mp4a-latm"; break;
4998 case 'm4p' : $ctype = "audio/mp4a-latm"; break;
4999 case 'm4u' : $ctype = "video/vnd.mpegurl"; break;
5000 case 'm4v' : $ctype = "video/x-m4v"; break;
5001 case 'm21' : $ctype = "application/mp21"; break;
5002 case 'ma' : $ctype = "application/mathematica"; break;
5003 case 'mac' : $ctype = "image/x-macpaint"; break;
5004 case 'mads' : $ctype = "application/mads+xml"; break;
5005 case 'man' : $ctype = "application/x-troff-man"; break;
5006 case 'mar' : $ctype = "application/octet-stream"; break;
5007 case 'mathml' : $ctype = "application/mathml+xml"; break;
5008 case 'mbox' : $ctype = "application/mbox"; break;
5009 case 'me' : $ctype = "application/x-troff-me"; break;
5010 case 'mesh' : $ctype = "model/mesh"; break;
5011 case 'metalink' : $ctype = "application/metalink+xml"; break;
5012 case 'meta4' : $ctype = "application/metalink4+xml"; break;
5013 case 'mets' : $ctype = "application/mets+xml"; break;
5014 case 'mid' : $ctype = "audio/midi"; break;
5015 case 'midi' : $ctype = "audio/midi"; break;
5016 case 'mif' : $ctype = "application/vnd.mif"; break;
5017 case 'mods' : $ctype = "application/mods+xml"; break;
5018 case 'mov' : $ctype = "video/quicktime"; break;
5019 case 'movie' : $ctype = "video/x-sgi-movie"; break;
5020 case 'm1v' : $ctype = "video/mpeg"; break;
5021 case 'm2v' : $ctype = "video/mpeg"; break;
5022 case 'mp2' : $ctype = "audio/mpeg"; break;
5023 case 'mp2a' : $ctype = "audio/mpeg"; break;
5024 case 'mp21' : $ctype = "application/mp21"; break;
5025 case 'mp3' : $ctype = "audio/mpeg"; break;
5026 case 'mp3a' : $ctype = "audio/mpeg"; break;
5027 case 'mp4' : $ctype = "video/mp4"; break;
5028 case 'mp4s' : $ctype = "application/mp4"; break;
5029 case 'mpe' : $ctype = "video/mpeg"; break;
5030 case 'mpeg' : $ctype = "video/mpeg"; break;
5031 case 'mpg' : $ctype = "video/mpeg"; break;
5032 case 'mpg4' : $ctype = "video/mpeg"; break;
5033 case 'mpga' : $ctype = "audio/mpeg"; break;
5034 case 'mrc' : $ctype = "application/marc"; break;
5035 case 'mrcx' : $ctype = "application/marcxml+xml"; break;
5036 case 'ms' : $ctype = "application/x-troff-ms"; break;
5037 case 'mscml' : $ctype = "application/mediaservercontrol+xml"; break;
5038 case 'msh' : $ctype = "model/mesh"; break;
5039 case 'mxf' : $ctype = "application/mxf"; break;
5040 case 'mxu' : $ctype = "video/vnd.mpegurl"; break;
5041 case 'nc' : $ctype = "application/x-netcdf"; break;
5042 case 'oda' : $ctype = "application/oda"; break;
5043 case 'oga' : $ctype = "application/ogg"; break;
5044 case 'ogg' : $ctype = "application/ogg"; break;
5045 case 'ogx' : $ctype = "application/ogg"; break;
5046 case 'omdoc' : $ctype = "application/omdoc+xml"; break;
5047 case 'onetoc' : $ctype = "application/onenote"; break;
5048 case 'onetoc2' : $ctype = "application/onenote"; break;
5049 case 'onetmp' : $ctype = "application/onenote"; break;
5050 case 'onepkg' : $ctype = "application/onenote"; break;
5051 case 'opf' : $ctype = "application/oebps-package+xml"; break;
5052 case 'oxps' : $ctype = "application/oxps"; break;
5053 case 'p7c' : $ctype = "application/pkcs7-mime"; break;
5054 case 'p7m' : $ctype = "application/pkcs7-mime"; break;
5055 case 'p7s' : $ctype = "application/pkcs7-signature"; break;
5056 case 'p8' : $ctype = "application/pkcs8"; break;
5057 case 'p10' : $ctype = "application/pkcs10"; break;
5058 case 'pbm' : $ctype = "image/x-portable-bitmap"; break;
5059 case 'pct' : $ctype = "image/pict"; break;
5060 case 'pdb' : $ctype = "chemical/x-pdb"; break;
5061 case 'pdf' : $ctype = "application/pdf"; break;
5062 case 'pki' : $ctype = "application/pkixcmp"; break;
5063 case 'pkipath' : $ctype = "application/pkix-pkipath"; break;
5064 case 'pfr' : $ctype = "application/font-tdpfr"; break;
5065 case 'pgm' : $ctype = "image/x-portable-graymap"; break;
5066 case 'pgn' : $ctype = "application/x-chess-pgn"; break;
5067 case 'pgp' : $ctype = "application/pgp-encrypted"; break;
5068 case 'pic' : $ctype = "image/pict"; break;
5069 case 'pict' : $ctype = "image/pict"; break;
5070 case 'pkg' : $ctype = "application/octet-stream"; break;
5071 case 'png' : $ctype = "image/png"; break;
5072 case 'pnm' : $ctype = "image/x-portable-anymap"; break;
5073 case 'pnt' : $ctype = "image/x-macpaint"; break;
5074 case 'pntg' : $ctype = "image/x-macpaint"; break;
5075 case 'pot' : $ctype = "application/vnd.ms-powerpoint"; break;
5076 case 'potx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.template"; break;
5077 case 'ppm' : $ctype = "image/x-portable-pixmap"; break;
5078 case 'pps' : $ctype = "application/vnd.ms-powerpoint"; break;
5079 case 'ppsx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; break;
5080 case 'ppt' : $ctype = "application/vnd.ms-powerpoint"; break;
5081 case 'pptx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; break;
5082 case 'prf' : $ctype = "application/pics-rules"; break;
5083 case 'ps' : $ctype = "application/postscript"; break;
5084 case 'psd' : $ctype = "image/photoshop"; break;
5085 case 'qt' : $ctype = "video/quicktime"; break;
5086 case 'qti' : $ctype = "image/x-quicktime"; break;
5087 case 'qtif' : $ctype = "image/x-quicktime"; break;
5088 case 'ra' : $ctype = "audio/x-pn-realaudio"; break;
5089 case 'ram' : $ctype = "audio/x-pn-realaudio"; break;
5090 case 'ras' : $ctype = "image/x-cmu-raster"; break;
5091 case 'rdf' : $ctype = "application/rdf+xml"; break;
5092 case 'rgb' : $ctype = "image/x-rgb"; break;
5093 case 'rm' : $ctype = "application/vnd.rn-realmedia"; break;
5094 case 'rmi' : $ctype = "audio/midi"; break;
5095 case 'roff' : $ctype = "application/x-troff"; break;
5096 case 'rss' : $ctype = "application/rss+xml"; break;
5097 case 'rtf' : $ctype = "text/rtf"; break;
5098 case 'rtx' : $ctype = "text/richtext"; break;
5099 case 'sgm' : $ctype = "text/sgml"; break;
5100 case 'sgml' : $ctype = "text/sgml"; break;
5101 case 'sh' : $ctype = "application/x-sh"; break;
5102 case 'shar' : $ctype = "application/x-shar"; break;
5103 case 'sig' : $ctype = "application/pgp-signature"; break;
5104 case 'silo' : $ctype = "model/mesh"; break;
5105 case 'sit' : $ctype = "application/x-stuffit"; break;
5106 case 'skd' : $ctype = "application/x-koan"; break;
5107 case 'skm' : $ctype = "application/x-koan"; break;
5108 case 'skp' : $ctype = "application/x-koan"; break;
5109 case 'skt' : $ctype = "application/x-koan"; break;
5110 case 'sldx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slide"; break;
5111 case 'smi' : $ctype = "application/smil"; break;
5112 case 'smil' : $ctype = "application/smil"; break;
5113 case 'snd' : $ctype = "audio/basic"; break;
5114 case 'so' : $ctype = "application/octet-stream"; break;
5115 case 'spl' : $ctype = "application/x-futuresplash"; break;
5116 case 'spx' : $ctype = "audio/ogg"; break;
5117 case 'src' : $ctype = "application/x-wais-source"; break;
5118 case 'stk' : $ctype = "application/hyperstudio"; break;
5119 case 'sv4cpio' : $ctype = "application/x-sv4cpio"; break;
5120 case 'sv4crc' : $ctype = "application/x-sv4crc"; break;
5121 case 'svg' : $ctype = "image/svg+xml"; break;
5122 case 'swf' : $ctype = "application/x-shockwave-flash"; break;
5123 case 't' : $ctype = "application/x-troff"; break;
5124 case 'tar' : $ctype = "application/x-tar"; break;
5125 case 'tcl' : $ctype = "application/x-tcl"; break;
5126 case 'tex' : $ctype = "application/x-tex"; break;
5127 case 'texi' : $ctype = "application/x-texinfo"; break;
5128 case 'texinfo' : $ctype = "application/x-texinfo"; break;
5129 case 'tif' : $ctype = "image/tiff"; break;
5130 case 'tiff' : $ctype = "image/tiff"; break;
5131 case 'torrent' : $ctype = "application/x-bittorrent"; break;
5132 case 'tr' : $ctype = "application/x-troff"; break;
5133 case 'tsv' : $ctype = "text/tab-separated-values"; break;
5134 case 'txt' : $ctype = "text/plain"; break;
5135 case 'ustar' : $ctype = "application/x-ustar"; break;
5136 case 'vcd' : $ctype = "application/x-cdlink"; break;
5137 case 'vrml' : $ctype = "model/vrml"; break;
5138 case 'vsd' : $ctype = "application/vnd.visio"; break;
5139 case 'vss' : $ctype = "application/vnd.visio"; break;
5140 case 'vst' : $ctype = "application/vnd.visio"; break;
5141 case 'vsw' : $ctype = "application/vnd.visio"; break;
5142 case 'vxml' : $ctype = "application/voicexml+xml"; break;
5143 case 'wav' : $ctype = "audio/x-wav"; break;
5144 case 'wbmp' : $ctype = "image/vnd.wap.wbmp"; break;
5145 case 'wbmxl' : $ctype = "application/vnd.wap.wbxml"; break;
5146 case 'wm' : $ctype = "video/x-ms-wm"; break;
5147 case 'wml' : $ctype = "text/vnd.wap.wml"; break;
5148 case 'wmlc' : $ctype = "application/vnd.wap.wmlc"; break;
5149 case 'wmls' : $ctype = "text/vnd.wap.wmlscript"; break;
5150 case 'wmlsc' : $ctype = "application/vnd.wap.wmlscriptc"; break;
5151 case 'wmv' : $ctype = "video/x-ms-wmv"; break;
5152 case 'wmx' : $ctype = "video/x-ms-wmx"; break;
5153 case 'wrl' : $ctype = "model/vrml"; break;
5154 case 'xbm' : $ctype = "image/x-xbitmap"; break;
5155 case 'xdssc' : $ctype = "application/dssc+xml"; break;
5156 case 'xer' : $ctype = "application/patch-ops-error+xml"; break;
5157 case 'xht' : $ctype = "application/xhtml+xml"; break;
5158 case 'xhtml' : $ctype = "application/xhtml+xml"; break;
5159 case 'xla' : $ctype = "application/vnd.ms-excel"; break;
5160 case 'xlam' : $ctype = "application/vnd.ms-excel.addin.macroEnabled.12"; break;
5161 case 'xlc' : $ctype = "application/vnd.ms-excel"; break;
5162 case 'xlm' : $ctype = "application/vnd.ms-excel"; break;
5163 case 'xls' : $ctype = "application/vnd.ms-excel"; break;
5164 case 'xlsx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; break;
5165 case 'xlsb' : $ctype = "application/vnd.ms-excel.sheet.binary.macroEnabled.12"; break;
5166 case 'xlt' : $ctype = "application/vnd.ms-excel"; break;
5167 case 'xltx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; break;
5168 case 'xlw' : $ctype = "application/vnd.ms-excel"; break;
5169 case 'xml' : $ctype = "application/xml"; break;
5170 case 'xpm' : $ctype = "image/x-xpixmap"; break;
5171 case 'xsl' : $ctype = "application/xml"; break;
5172 case 'xslt' : $ctype = "application/xslt+xml"; break;
5173 case 'xul' : $ctype = "application/vnd.mozilla.xul+xml"; break;
5174 case 'xwd' : $ctype = "image/x-xwindowdump"; break;
5175 case 'xyz' : $ctype = "chemical/x-xyz"; break;
5176 case 'zip' : $ctype = "application/zip"; break;
5177 default : $ctype = "application/force-download";
5178 endswitch;
5179
5180 return apply_filters( 'ebook_file_ctype', $ctype );
5181 }
5182 function ebook_process_download($data = false) {
5183 $QSWPOptions = new QSWPOptions();
5184 if ($data) {
5185 extract($data);
5186 }
5187 // Errors must not be printed into a binary file download, but the site's
5188 // error *reporting* level is the site owner's business, not this plugin's.
5189 @ini_set( 'display_errors', '0' );
5190
5191 $formats = array('mobi','txt','epub','zip', 'mp3','mp4');
5192 if (@$_GET['wc_order'] != '') {
5193 $formats[] = 'pdf';
5194 }
5195 if ((@$_REQUEST['ebook_key'] != false && $_REQUEST['action'] == 'download') || (@$_REQUEST['action'] == 'wc_order_download')) {
5196 //error_reporting(E_ALL);
5197 if( function_exists( 'apache_setenv' ) ) @apache_setenv('no-gzip', 1);
5198 @ini_set( 'zlib.output_compression', 'Off' );
5199 nocache_headers();
5200 $order_id = 0;
5201 $ebook = 0;
5202 $request_ebook_key = isset( $_REQUEST['ebook_key'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ) : '';
5203 $request_order_id = isset( $_REQUEST['order_id'] ) ? absint( $_REQUEST['order_id'] ) : 0;
5204
5205 if ( $request_order_id > 0 && $request_ebook_key !== '' ) {
5206 $stored_ebook_key = (string) get_post_meta( $request_order_id, 'ebook_key', true );
5207 if ( $stored_ebook_key !== '' && hash_equals( $stored_ebook_key, $request_ebook_key ) ) {
5208 $order_id = $request_order_id;
5209 $ebook = (int) get_post_meta( $order_id, 'ebook', true );
5210 }
5211 }
5212
5213 if ( ! $order_id && $request_ebook_key !== '' ) {
5214 $loop = new WP_Query(
5215 array(
5216 'post_type' => 'ebook_order',
5217 'meta_key' => 'ebook_key',
5218 'meta_value' => $request_ebook_key,
5219 'posts_per_page' => 1,
5220 'orderby' => 'ID',
5221 'order' => 'DESC',
5222 )
5223 );
5224 while ( $loop->have_posts() ) : $loop->the_post();
5225 $order_id = get_the_ID();
5226 $ebook = get_post_meta(get_the_ID(),'ebook',true);
5227 endwhile;
5228 wp_reset_postdata();
5229 }
5230
5231 if (@$_REQUEST['action'] == 'wc_order_download') {
5232 // A WooCommerce download is only authorised by the order key. Verify it
5233 // here, before any file is resolved: this branch used to trust order_id
5234 // and ebook_id straight from the query string, which handed every paid
5235 // ebook to anonymous visitors.
5236 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
5237 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5238
5239 if ( ! ebook_store_wc_order_key_matches( $order_id, isset( $_GET['wc_order'] ) ? wp_unslash( $_GET['wc_order'] ) : '' ) ) {
5240 wp_die( esc_html__( 'This download link is not valid for that order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5241 }
5242
5243 if ( ! ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) ) {
5244 wp_die( esc_html__( 'That ebook is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5245 }
5246
5247 if ( ! ebook_store_wc_order_is_deliverable( $order_id ) ) {
5248 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 ) );
5249 }
5250
5251 $ebook = $ebook_id;
5252 }
5253 if (@$_REQUEST['action'] == 'download' && @$_REQUEST['type'] == 'bonus') {
5254 $bonus_ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5255 // The key must have resolved to an order, and the requested book must be a
5256 // bonus of the ebook that order was placed for; otherwise any key holder
5257 // could pull every PDF in the catalogue through this branch.
5258 if ( ! $order_id || ! $bonus_ebook_id || ! in_array( $bonus_ebook_id, ebook_store_get_bonus_ebook_ids( $ebook ), true ) ) {
5259 wp_die( esc_html__( 'That bonus book is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5260 }
5261 $ebook = $bonus_ebook_id;
5262 $ebookObj = new EbookStoreEbook($ebook);
5263 $requested_file = $ebookObj->encrypted($order_id, 'pdf');
5264 }
5265 $attachment = get_post_meta($ebook,'ebook_wp_custom_attachment');
5266 //wp_die(print_r($attachment,true));
5267 if (@!$requested_file) {
5268 $requested_file = $attachment[0]['file'];
5269 }
5270
5271 // The in_progress marker only concerns the order's own protected PDF copy;
5272 // other formats and bonus books never wait on it.
5273 $requested_format = ( isset( $_GET['format'] ) && is_string( $_GET['format'] ) ) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : '';
5274 $wants_order_pdf = ( $requested_format === '' || $requested_format === 'pdf' || ! in_array( $requested_format, $formats, true ) ) && @$_REQUEST['type'] != 'bonus';
5275 if ($wants_order_pdf && get_post_meta($order_id,'encrypted_pdf',true) == 'in_progress') {
5276 if (!ebook_store_encryptable($requested_file)) {
5277 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>');
5278 }
5279 $encryption_started = (int) get_post_meta( $order_id, 'encrypted_pdf_started', true );
5280 if ( $encryption_started === 0 || ( time() - $encryption_started ) > 10 * MINUTE_IN_SECONDS ) {
5281 // The request that was building the copy died part-way (or the marker
5282 // predates the timestamp): clear it and build the copy for this buyer now.
5283 delete_post_meta( $order_id, 'encrypted_pdf' );
5284 delete_post_meta( $order_id, 'encrypted_pdf_started' );
5285 if ( get_post_type( $order_id ) === 'ebook_order' && get_option( 'encrypt_pdf' ) ) {
5286 $regenerate = array();
5287 foreach ( (array) get_post_meta( $order_id ) as $meta_key => $meta_values ) {
5288 if ( isset( $meta_values[0] ) && is_scalar( $meta_values[0] ) ) {
5289 $regenerate[ $meta_key ] = $meta_values[0];
5290 }
5291 }
5292 $regenerate['order_id'] = $order_id;
5293 $GLOBALS['attachment'] = array( array( 'file' => $requested_file ) );
5294 $requested_file = ebook_encrypt_pdf( $regenerate, $order_id );
5295 }
5296 } elseif (get_option('ebook_store_no_autorefresh', false) == false) {
5297 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">
5298 setInterval(\'window.location.reload()\', 15000);
5299 </script>');
5300 } else {
5301 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>
5302 ");
5303 }
5304
5305 }
5306 if (get_post_meta($order_id,'encrypted_pdf',true) != '' && $_REQUEST['type'] != 'bonus') {
5307 $requested_file = get_post_meta($order_id,'encrypted_pdf',true);
5308 }
5309 if (@in_array($_GET['format'], $formats)) {
5310 $format_meta_tag = 'ebook_wp_custom_attachment' . (@$_GET['format'] != 'pdf' ? '_' . $_GET['format'] : '');
5311 $attachment = get_post_meta($ebook,$format_meta_tag);
5312 //wp_die(print_r($attachment,true));
5313 $requested_file = $attachment[0]['file'];
5314 if ( @$_GET['format'] === 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
5315 // Standalone (non-WooCommerce) epub downloads were served raw; run
5316 // them through the watermarker too, using the resolved order id.
5317 $ebookObj = new EbookStoreEbook( $ebook );
5318 $ebookObj->order_id = $order_id;
5319 $requested_file = $ebookObj->encrypted( $order_id, 'epub' );
5320 }
5321 if (@$_GET['wc_order'] != '') {
5322 $wc_order = sanitize_text_field($_GET['wc_order']);
5323 $ebookObj = new EbookStoreEbook($ebook);
5324 $requested_file = $ebookObj->encrypted((int)$_GET['order_id'], $_GET['format']);
5325 }
5326 }
5327 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5328 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
5329 if ( $payment_status !== '' && ebook_store_is_negative_order_status( $payment_status ) ) {
5330 wp_die( esc_html__( 'Oops, this order is no longer eligible for downloads.', 'ebook-store' ) );
5331 }
5332 // Reading a PDF in the browser's viewer is not a download: it neither
5333 // counts against the allowance nor is served as an attachment. Only the
5334 // WooCommerce "Read online" reader asks for it, and only a PDF can be
5335 // shown that way; any other request is a download, limited and counted.
5336 $is_inline_view = ! empty( $_GET['inline'] ) && isset( $_REQUEST['action'] ) && $_REQUEST['action'] === 'wc_order_download'
5337 && strtolower( pathinfo( (string) $requested_file, PATHINFO_EXTENSION ) ) === 'pdf';
5338 $downloads = ebook_store_get_download_count( $order_id, $ebook );
5339 $downloads_limit = ebook_store_get_download_limit();
5340 //wp_die('Downloads ' . $downloads);
5341 if ( ! $is_inline_view && $downloads >= $downloads_limit ) {
5342 wp_die('Oops, you have reached the maximum amount of downloads for this order, you need to order again.');
5343 }
5344 // $gmt_timestamp = get_post_time();
5345 // if (!$gmt_timestamp) {
5346 // $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5347 // }
5348 //wp_die("GMT timestamp $gmt_timestamp, time " . time() . ' post id ' . get_the_ID());
5349
5350
5351 $gmt_timestamp = get_post_time('U', false, ($order_id > 0 ? $order_id : get_the_ID()));
5352 if (!$gmt_timestamp) {
5353 $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5354 }
5355
5356
5357 $link_expiration = trim( (string) get_option( 'link_expiration', $QSWPOptions->link_expiration ) );
5358 if ( $link_expiration === '' ) {
5359 $link_expiration = '1 year';
5360 }
5361
5362 $strtotime = strtotime("+" . $link_expiration,$gmt_timestamp);
5363
5364 if (WP_DEBUG == true) {
5365 //error_log("strtotime $strtotime vs time " . time() . ' vs GMT TIMESTAMP ' . $gmt_timestamp);
5366 }
5367 // A value strtotime() cannot parse, or one that does not land after the
5368 // order time, is "no expiry" here, never "already expired".
5369 if ($strtotime !== false && $strtotime > $gmt_timestamp && $strtotime < time()) {
5370 wp_die('Oops, the link you are using has expired, you need to order again.');
5371 }
5372
5373 if ( ! $requested_file || ! file_exists( $requested_file ) ) {
5374 wp_die( esc_html__( 'Sorry, the ebook file was not found for that order. It may have expired.', 'ebook-store' ) );
5375 }
5376
5377 // A PDF opened in the reader is shown in the browser; everything else
5378 // is a download.
5379 $disposition = $is_inline_view ? 'inline' : 'attachment';
5380
5381 header("Robots: none");
5382 header("Content-Type: " . $ctype . "");
5383 header("Content-Description: File Transfer");
5384 header("Content-Disposition: " . $disposition . "; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5385 header("Content-Transfer-Encoding: binary");
5386 ebook_readfile_chunked($requested_file);
5387 if ( $disposition !== 'inline' ) {
5388 ebook_count_download($order_id, $ebook);
5389 }
5390 die();
5391 //endwhile;
5392 } else if (@$_REQUEST['action'] == 'download_free') {
5393
5394 @$formData = ebook_store_get_form($_REQUEST['md5_nonce']);
5395 if (is_object($formData)) {
5396 $marketing_emails = array_unique(
5397 array_filter(
5398 array(
5399 isset( $formData->email ) ? $formData->email : '',
5400 isset( $formData->payer_email ) ? $formData->payer_email : '',
5401 )
5402 )
5403 );
5404 foreach ( $marketing_emails as $marketing_email ) {
5405 ebook_store_subscribe_buyer_to_marketing_lists( $marketing_email );
5406 }
5407 }
5408
5409 // "Ask for an email before the free download" was enforced only by the
5410 // pop-up in the browser, while the download URL itself sat in the
5411 // button's href — so the form could simply be skipped and the order
5412 // was recorded with no address at all. Require it here too.
5413 if ( get_option( 'formEnabled' ) == 1 ) {
5414 $free_email = '';
5415 if ( is_object( $formData ) ) {
5416 foreach ( array( 'email', 'payer_email' ) as $free_email_field ) {
5417 if ( ! empty( $formData->$free_email_field ) && is_email( $formData->$free_email_field ) ) {
5418 $free_email = (string) $formData->$free_email_field;
5419 break;
5420 }
5421 }
5422 }
5423
5424 if ( $free_email === '' ) {
5425 $free_ebook_id = isset( $_REQUEST['p'] ) ? absint( $_REQUEST['p'] ) : 0;
5426 $free_ebook_link = $free_ebook_id ? get_permalink( $free_ebook_id ) : home_url( '/' );
5427 wp_die(
5428 sprintf(
5429 /* translators: %s: link back to the ebook page. */
5430 esc_html__( 'Please request this download using the form on the ebook page so we know where to send it. %s', 'ebook-store' ),
5431 '<a href="' . esc_url( $free_ebook_link ) . '">' . esc_html__( 'Back to the ebook', 'ebook-store' ) . '</a>'
5432 ),
5433 '',
5434 array( 'response' => 403 )
5435 );
5436 }
5437 }
5438 // Only suppress *display* of errors — this runs while streaming a file.
5439 @ini_set( 'display_errors', '0' );
5440
5441 $id = $_REQUEST['p'];
5442 $loop = new WP_Query( array ( 'post_type' => 'ebook', 'p' => $id ) );
5443
5444
5445
5446
5447 while ( $loop->have_posts() ) : $loop->the_post();
5448 $attachment = get_post_meta($id,'ebook_wp_custom_attachment');
5449 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
5450
5451 if (get_option('encrypt_pdf') == 1) {
5452 if (!ebook_store_encryptable($attachment[0]['file'])) {
5453 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>');
5454 }
5455 }
5456 //die();
5457 // A zero-priced book whose mode was never set is free, and a donation
5458 // book with no price can be downloaded too. A book saved as paid is
5459 // never free here, even when its price field is blank.
5460 $ebook['donate_or_download'] = isset( $ebook['donate_or_download'] ) ? (string) $ebook['donate_or_download'] : '';
5461 $free_price = ( isset( $ebook['ebook_price'] ) && is_numeric( $ebook['ebook_price'] ) ) ? (float) $ebook['ebook_price'] : 0.0;
5462 if ( ebook_store_is_free_ebook( $ebook ) || ( $ebook['donate_or_download'] === 'donate' && $free_price == 0 ) ) {
5463 $ebook['donate_or_download'] = 'free';
5464 }
5465 if ( $ebook['donate_or_download'] != 'free' ) {
5466 wp_die('You are trying to download a file that is not free - ' . get_the_title());
5467 }
5468 $status = get_post_status();
5469 erl($status);
5470
5471 if ($status != 'publish') {
5472 wp_die('You are trying to download a file that is not available.');
5473 }
5474
5475 $requested_file = $attachment[0]['file'];
5476
5477 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5478 $gmt_timestamp = get_post_time('U');
5479 //insert order
5480 $my_post = array(
5481 'post_title' => 'Free Download',
5482 'post_type' => 'ebook_order',
5483 'post_status' => 'publish',
5484 'post_author' => 1,
5485 'post_category' => array(8,39));
5486 $post_id = wp_insert_post( $my_post, @$wp_error );
5487 //txn_id payment_date payer_email
5488 //encrypt here.
5489 global $current_user;
5490 $current_user = wp_get_current_user();
5491 $data = array();
5492
5493 $data['txn_id'] = 'free';
5494 //$data['payment_date'] = date("F j, Y, g:i a");
5495 $data['payment_date'] = current_time('mysql');
5496
5497
5498
5499 if (get_option('encrypt_files_for_logged_in_users') == 1) {
5500 $data['payer_email'] = $current_user->user_email;
5501 } else {
5502 $data['payer_email'] = '';
5503 if (@$formData->email != '') {
5504 $data['payer_email'] = $formData->email;
5505 }
5506 }
5507
5508 if ($formData->payer_email != '') {
5509 $data['payer_email'] = $formData->payer_email;
5510 }
5511
5512 //error_log(print_r($formData,true));
5513
5514
5515 $data['first_name'] = $current_user->user_firstname;
5516 $data['last_name'] = $current_user->user_lastname;
5517 $data['mc_currency'] = ebook_store_get_store_currency();
5518 $data['tax'] = 0;
5519 $data['ip'] = $_SERVER['REMOTE_ADDR'];
5520 $data['residence_country'] = 'n/a';
5521 //error_log('data ' . print_r($data,true));
5522 if ($data['first_name'] == '') {
5523 if ($formData->name != '') {
5524 $data['first_name'] = $formData->name;
5525 $data['last_name'] = '';
5526 } else if ($formData->first_name != '') {
5527 $data['first_name'] = $formData->first_name;
5528 $data['last_name'] = $formData->last_name;
5529 } else {
5530 $data['first_name'] = 'Anonymous';
5531 $data['last_name'] = 'User';
5532
5533 }
5534 }
5535
5536
5537 //wp_die($data['payer_email']);
5538
5539 $md5_nonce = strip_tags($_REQUEST['md5_nonce']);
5540 $ebook_key = $md5_nonce;
5541
5542 $data['password'] = ebook_store_get_password_for_order($post_id, array(
5543 'password' => isset($data['password']) ? $data['password'] : '',
5544 'payer_email' => $data['payer_email'],
5545 'md5_nonce' => $md5_nonce,
5546 'ebook_key' => $ebook_key,
5547 ));
5548
5549 global $attachment;
5550 @$attachment[0]['file'] = $requested_file;
5551
5552 $fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
5553 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
5554 //ebook_encrypt_pdf($data, $post_id);
5555 $requested_file = ebook_encrypt_pdf($data, $post_id);
5556 }
5557
5558 $formData = ebook_store_get_form($md5_nonce);
5559 $formData = json_encode($formData);
5560 $item_name = get_the_title((int)$_REQUEST['p']);
5561 //die($item_name);
5562 update_post_meta($post_id,'ebook_key',$md5_nonce);
5563 update_post_meta($post_id,'downloads',0);
5564 update_post_meta($post_id,'formData',wp_slash($formData));
5565 update_post_meta($post_id, 'downloadlink', 'http://' . sanitize_text_field($_SERVER['HTTP_HOST']) . esc_url_raw($_SERVER['REQUEST_URI']));
5566 update_post_meta($post_id,'ebook',(int)$_REQUEST['p']);
5567 update_post_meta($post_id,'item_name',$item_name);
5568 update_post_meta($post_id,'payer_email',(@$data['payer_email'] != '' ? $data['payer_email'] : 'N/A'));
5569 update_post_meta($post_id,'mc_gross',0);
5570 update_post_meta($post_id,'mc_fee',0);
5571 update_post_meta($post_id,'payment_date',date("F j, Y, g:i a"));
5572 foreach ($data as $dk => $dv) {
5573 update_post_meta($post_id,$dk,$dv);
5574 }
5575 //get_post_meta($post->ID,'item_name',true)
5576 //insert order end
5577 if (headers_sent()) {
5578 die('Sorry, headers sent.');
5579 }
5580 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
5581 if (!$checkoutPage) {
5582 $checkoutPage = $post_id;
5583 }
5584 update_post_meta($post_id,'encrypted_pdf',wp_slash($requested_file));
5585 header("Location: ". add_query_arg(array('ebook_key' => $md5_nonce, 'action' => 'thank_you'),get_permalink($checkoutPage)));
5586
5587 // header("Robots: none");
5588 // header("Content-Type: " . $ctype . "");
5589 // header("Content-Description: File Transfer");
5590 // header("Content-Disposition: attachment; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5591 // header("Content-Transfer-Encoding: binary");
5592 // ebook_readfile_chunked($requested_file);
5593 endwhile;
5594 die();
5595 } else if ($_REQUEST['action'] == 'ebook_bonus_download') {
5596 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5597 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', (string) wp_unslash( $_GET['ebook_key'] ) ) : '';
5598
5599 // The key must resolve to a real order before we do anything. Without this
5600 // an anonymous GET with a bogus key created a junk ebook_order (null email,
5601 // garbage details) and could trigger a marketing subscribe + PDF encrypt.
5602 $ebook_order_meta = $ebook_key !== '' ? ebook_get_order( 'ebook_key', $ebook_key ) : null;
5603 if ( empty( $ebook_order_meta['order_id'][0] ) ) {
5604 wp_die( esc_html__( 'This download link is not valid.', 'ebook-store' ), '', array( 'response' => 403 ) );
5605 }
5606
5607 // The requested bonus book must actually be a bonus of that order's ebook.
5608 $parent_ebook = isset( $ebook_order_meta['ebook'][0] ) ? absint( $ebook_order_meta['ebook'][0] ) : 0;
5609 $allowed_bonus = $parent_ebook ? (array) get_post_meta( $parent_ebook, 'ebook_bonus', true ) : array();
5610 $allowed_bonus = array_map( 'absint', $allowed_bonus );
5611 if ( ! in_array( $ebook_id, $allowed_bonus, true ) ) {
5612 wp_die( esc_html__( 'That bonus book is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5613 }
5614
5615 $data['ebook'] = get_post_meta( $ebook_id, 'ebook_bonus', true );
5616 $data['txn_id'] = 'Bonus Download (' . ( isset( $ebook_order_meta['item_name'][0] ) ? $ebook_order_meta['item_name'][0] : '' ) . ')';
5617 $data['payer_email'] = isset( $ebook_order_meta['payer_email'][0] ) ? $ebook_order_meta['payer_email'][0] : '';
5618 $data['first_name'] = 'Bonus';
5619 $data['last_name'] = 'Download (Order #' . $ebook_order_meta['order_id'][0] . ')';
5620 ebook_store_add_order($data, true);
5621 @$ebook_order['ebook_key'][0] = $ebook_key;
5622 @$ebook_order['ebook'][0] = $data['ebook'];
5623 $downloadlink = ebook_download_link($ebook_order, 0, 1);
5624 header("Location: " . $downloadlink); //add proper ebook id here and allow it.
5625 //wp_die($downloadlink);
5626 }
5627 }
5628 /**
5629 * The WooCommerce order behind an id, or false when the id is not one.
5630 *
5631 * WooCommerce orders live in wp_posts or in the High-Performance Order Storage
5632 * tables depending on the shop, so the CRUD layer is the only reliable way to
5633 * read and write their meta; get_post_meta() silently returns nothing on HPOS.
5634 *
5635 * @param int $order_id Order ID.
5636 * @return WC_Order|false
5637 */
5638 function ebook_store_get_wc_order_object( $order_id ) {
5639 $order_id = absint( $order_id );
5640
5641 if ( $order_id < 1 || ! function_exists( 'wc_get_order' ) ) {
5642 return false;
5643 }
5644
5645 // The plugin's own orders are ordinary posts and keep using post meta.
5646 if ( get_post_type( $order_id ) === 'ebook_order' ) {
5647 return false;
5648 }
5649
5650 $order = wc_get_order( $order_id );
5651
5652 return ( $order && is_a( $order, 'WC_Order' ) ) ? $order : false;
5653 }
5654
5655 /**
5656 * Meta key holding an order's download count for one ebook.
5657 *
5658 * @param int $ebook_id Ebook post ID, 0 for the order-wide legacy counter.
5659 * @return string
5660 */
5661 function ebook_store_download_count_meta_key( $ebook_id = 0 ) {
5662 $ebook_id = absint( $ebook_id );
5663
5664 return $ebook_id > 0 ? 'downloads_' . $ebook_id : 'downloads';
5665 }
5666
5667 /**
5668 * How many times this order has downloaded this ebook.
5669 *
5670 * A WooCommerce order can carry several ebooks in several formats. Counting all
5671 * of them against one number meant a three-book bundle reached the limit on its
5672 * fifth file, and on HPOS shops the post-meta counter was never stored at all,
5673 * so the limit did not apply. WooCommerce orders now count per ebook, through
5674 * the CRUD layer; the plugin's own orders keep their single counter.
5675 *
5676 * @param int $order_id Order ID.
5677 * @param int $ebook_id Ebook post ID.
5678 * @return int
5679 */
5680 function ebook_store_get_download_count( $order_id, $ebook_id = 0 ) {
5681 $wc_order = ebook_store_get_wc_order_object( $order_id );
5682 $meta_key = ebook_store_download_count_meta_key( $wc_order ? $ebook_id : 0 );
5683
5684 if ( $wc_order ) {
5685 return (int) $wc_order->get_meta( $meta_key, true );
5686 }
5687
5688 return (int) get_post_meta( absint( $order_id ), $meta_key, true );
5689 }
5690
5691 /**
5692 * Record one more download of this ebook against this order.
5693 *
5694 * @param int $order_id Order ID.
5695 * @param int $ebook_id Ebook post ID.
5696 * @return bool
5697 */
5698 function ebook_store_increment_download_count( $order_id, $ebook_id = 0 ) {
5699 $wc_order = ebook_store_get_wc_order_object( $order_id );
5700 $meta_key = ebook_store_download_count_meta_key( $wc_order ? $ebook_id : 0 );
5701 $count = ebook_store_get_download_count( $order_id, $ebook_id ) + 1;
5702
5703 if ( $wc_order ) {
5704 $wc_order->update_meta_data( $meta_key, $count );
5705 $wc_order->save();
5706
5707 return true;
5708 }
5709
5710 return (bool) update_post_meta( absint( $order_id ), $meta_key, $count );
5711 }
5712
5713 function ebook_count_download($order_id, $ebook_id = 0) {
5714 return ebook_store_increment_download_count( $order_id, $ebook_id );
5715 }
5716 function ebook_get_order($key = 'ebook_key',$val = '') {
5717 // Not served from the request's query cache: this lookup also decides
5718 // whether another request has just created the order.
5719 $loop = new WP_Query( array ( 'post_type' => 'ebook_order', 'meta_key' => $key, 'meta_value' => $val, 'cache_results' => false ) );
5720 while ( $loop->have_posts() ) : $loop->the_post();
5721 $meta = get_post_meta(get_the_ID(),null,true);
5722 $meta['order_id'][0] = get_the_ID();
5723 $gross = isset( $meta['mc_gross'][0] ) ? (float) $meta['mc_gross'][0] : 0;
5724 $tax = isset( $meta['tax'][0] ) ? (float) $meta['tax'][0] : 0;
5725 $meta['total'][0] = number_format( round( $gross + $tax, 2 ), 2 );
5726 return $meta;
5727 endwhile;
5728 }
5729
5730 function toMoney($val,$symbol='',$r=2)
5731 {
5732 if ( is_array( $val ) ) {
5733 $val = reset( $val );
5734 }
5735
5736 return number_format( (float) $val, 2 );
5737
5738 }
5739
5740 function ebook_store_normalize_email_log_headers( $headers ) {
5741 if ( empty( $headers ) ) {
5742 return array();
5743 }
5744
5745 if ( is_string( $headers ) ) {
5746 $headers = preg_split( '/\r\n|\r|\n/', $headers );
5747 }
5748
5749 if ( ! is_array( $headers ) ) {
5750 $headers = array( (string) $headers );
5751 }
5752
5753 $normalized = array();
5754
5755 foreach ( $headers as $header ) {
5756 $header = trim( wp_strip_all_tags( (string) $header ) );
5757 if ( $header === '' ) {
5758 continue;
5759 }
5760 $normalized[] = $header;
5761 }
5762
5763 return array_values( array_unique( $normalized ) );
5764 }
5765
5766 function ebook_store_normalize_email_log_attachments( $attachments ) {
5767 if ( empty( $attachments ) ) {
5768 return array();
5769 }
5770
5771 if ( is_string( $attachments ) ) {
5772 $attachments = array( $attachments );
5773 }
5774
5775 if ( ! is_array( $attachments ) ) {
5776 return array();
5777 }
5778
5779 $normalized = array();
5780
5781 foreach ( $attachments as $attachment ) {
5782 if ( is_array( $attachment ) && isset( $attachment['file'] ) ) {
5783 $attachment = $attachment['file'];
5784 }
5785
5786 $attachment = trim( (string) $attachment );
5787
5788 if ( $attachment === '' ) {
5789 continue;
5790 }
5791
5792 $normalized[] = $attachment;
5793 }
5794
5795 return array_values( array_unique( $normalized ) );
5796 }
5797
5798 function ebook_store_json_encode_for_log( $value ) {
5799 $encoded = wp_json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
5800
5801 return $encoded ? $encoded : '';
5802 }
5803
5804 function ebook_store_get_first_meta_value( $value ) {
5805 if ( is_array( $value ) ) {
5806 $value = reset( $value );
5807 }
5808
5809 return $value;
5810 }
5811
5812 function ebook_store_create_email_log_entry( $args = array() ) {
5813 $defaults = array(
5814 'to' => '',
5815 'subject' => '',
5816 'body' => '',
5817 'headers' => array(),
5818 'attachments' => array(),
5819 'result' => false,
5820 'source' => 'standalone',
5821 'related_order_type' => '',
5822 'related_order_id' => 0,
5823 'ebook_id' => 0,
5824 'ebook_key' => '',
5825 'gateway' => '',
5826 'order' => array(),
5827 'context' => array(),
5828 'parent_log_id' => 0,
5829 );
5830
5831 $args = wp_parse_args( $args, $defaults );
5832
5833 $recipient = trim( (string) $args['to'] );
5834 $subject = wp_strip_all_tags( (string) $args['subject'] );
5835 $sent_at = current_time( 'mysql' );
5836 $sent_at_gmt = current_time( 'mysql', true );
5837 $status = $args['result'] ? 'sent' : 'failed';
5838 $order = is_array( $args['order'] ) ? $args['order'] : array();
5839 $context = is_array( $args['context'] ) ? $args['context'] : array();
5840 $headers = ebook_store_normalize_email_log_headers( $args['headers'] );
5841 $attachments = ebook_store_normalize_email_log_attachments( $args['attachments'] );
5842 $ebook_id = absint( ebook_store_get_first_meta_value( $args['ebook_id'] ) );
5843 $related_order_id = absint( ebook_store_get_first_meta_value( $args['related_order_id'] ) );
5844 $parent_log_id = absint( $args['parent_log_id'] );
5845 $gateway = sanitize_key( (string) $args['gateway'] );
5846 $source = sanitize_key( (string) $args['source'] );
5847 $related_order_type = sanitize_key( (string) $args['related_order_type'] );
5848
5849 if ( $ebook_id < 1 && ! empty( $order['ebook'] ) ) {
5850 $ebook_id = absint( ebook_store_get_first_meta_value( $order['ebook'] ) );
5851 }
5852
5853 if ( $related_order_id < 1 && ! empty( $order['order_id'] ) ) {
5854 $related_order_id = absint( ebook_store_get_first_meta_value( $order['order_id'] ) );
5855 }
5856
5857 if ( $related_order_type === '' ) {
5858 $related_order_type = $source === 'woocommerce' ? 'shop_order' : 'ebook_order';
5859 }
5860
5861 $title_bits = array();
5862
5863 if ( $recipient !== '' ) {
5864 $title_bits[] = $recipient;
5865 }
5866
5867 if ( $subject !== '' ) {
5868 $title_bits[] = $subject;
5869 }
5870
5871 if ( empty( $title_bits ) ) {
5872 $title_bits[] = __( 'Order email', 'ebook-store' );
5873 }
5874
5875 $post_id = wp_insert_post(
5876 array(
5877 'post_type' => 'ebook_email_log',
5878 'post_status' => 'publish',
5879 'post_title' => implode( ' - ', array_slice( $title_bits, 0, 2 ) ),
5880 ),
5881 true
5882 );
5883
5884 if ( is_wp_error( $post_id ) || ! $post_id ) {
5885 return 0;
5886 }
5887
5888 update_post_meta( $post_id, 'status', $status );
5889 update_post_meta( $post_id, 'sent_at', $sent_at );
5890 update_post_meta( $post_id, 'sent_at_gmt', $sent_at_gmt );
5891 update_post_meta( $post_id, 'recipient', $recipient );
5892 update_post_meta( $post_id, 'subject', (string) $args['subject'] );
5893 update_post_meta( $post_id, 'body_html', (string) $args['body'] );
5894 update_post_meta( $post_id, 'headers', $headers );
5895 update_post_meta( $post_id, 'attachments', $attachments );
5896 update_post_meta( $post_id, 'source', $source );
5897 update_post_meta( $post_id, 'related_order_type', $related_order_type );
5898 update_post_meta( $post_id, 'related_order_id', $related_order_id );
5899 update_post_meta( $post_id, 'ebook_id', $ebook_id );
5900 update_post_meta( $post_id, 'ebook_key', isset( $args['ebook_key'] ) ? sanitize_text_field( (string) ebook_store_get_first_meta_value( $args['ebook_key'] ) ) : '' );
5901 update_post_meta( $post_id, 'gateway', $gateway );
5902 update_post_meta( $post_id, 'order_snapshot', ebook_store_json_encode_for_log( $order ) );
5903 update_post_meta( $post_id, 'context_snapshot', ebook_store_json_encode_for_log( $context ) );
5904
5905 if ( $parent_log_id > 0 ) {
5906 update_post_meta( $post_id, 'parent_log_id', $parent_log_id );
5907 }
5908
5909 return $post_id;
5910 }
5911
5912 function ebook_store_get_email_log_order_edit_link( $log_id ) {
5913 $order_id = (int) get_post_meta( $log_id, 'related_order_id', true );
5914 $order_type = (string) get_post_meta( $log_id, 'related_order_type', true );
5915
5916 if ( $order_id < 1 ) {
5917 return '';
5918 }
5919
5920 if ( $order_type === 'ebook_order' && get_post_type( $order_id ) === 'ebook_order' ) {
5921 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5922 }
5923
5924 if ( $order_type === 'shop_order' || get_post_type( $order_id ) === 'shop_order' ) {
5925 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5926 }
5927
5928 return '';
5929 }
5930
5931 function ebook_store_get_email_log_ebook_edit_link( $log_id ) {
5932 $ebook_id = (int) get_post_meta( $log_id, 'ebook_id', true );
5933
5934 if ( $ebook_id < 1 || get_post_type( $ebook_id ) !== 'ebook' ) {
5935 return '';
5936 }
5937
5938 return admin_url( 'post.php?post=' . $ebook_id . '&action=edit' );
5939 }
5940
5941 function ebook_store_get_email_log_resend_url( $log_id ) {
5942 return wp_nonce_url(
5943 admin_url( 'admin-post.php?action=ebook_store_resend_email_log&log_id=' . absint( $log_id ) ),
5944 'ebook_store_resend_email_log_' . absint( $log_id )
5945 );
5946 }
5947
5948 function ebook_store_get_order_reset_downloads_url( $order_id ) {
5949 return wp_nonce_url(
5950 admin_url( 'admin-post.php?action=ebook_store_reset_order_downloads&order_id=' . absint( $order_id ) ),
5951 'ebook_store_reset_order_downloads_' . absint( $order_id )
5952 );
5953 }
5954
5955 function ebook_store_get_order_resend_delivery_url( $order_id ) {
5956 return wp_nonce_url(
5957 admin_url( 'admin-post.php?action=ebook_store_resend_order_delivery&order_id=' . absint( $order_id ) ),
5958 'ebook_store_resend_order_delivery_' . absint( $order_id )
5959 );
5960 }
5961
5962 function ebook_store_get_order_refund_url( $order_id ) {
5963 return wp_nonce_url(
5964 admin_url( 'admin-post.php?action=ebook_store_refund_order&order_id=' . absint( $order_id ) ),
5965 'ebook_store_refund_order_' . absint( $order_id )
5966 );
5967 }
5968
5969 function ebook_store_get_order_refund_gateway( $order_id ) {
5970 $order_id = absint( $order_id );
5971 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
5972 return '';
5973 }
5974
5975 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
5976 if ( ebook_store_is_negative_order_status( $payment_status ) ) {
5977 return '';
5978 }
5979
5980 if ( get_post_meta( $order_id, 'refund_gateway_id', true ) !== '' ) {
5981 return '';
5982 }
5983
5984 $payment_type = strtolower( trim( (string) get_post_meta( $order_id, 'payment_type', true ) ) );
5985
5986 if (
5987 $payment_type === 'stripe'
5988 && trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) ) !== ''
5989 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== ''
5990 ) {
5991 return 'stripe';
5992 }
5993
5994 if (
5995 $payment_type === 'adyen'
5996 && trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) ) !== ''
5997 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
5998 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
5999 ) {
6000 return 'adyen';
6001 }
6002
6003 return '';
6004 }
6005
6006 function ebook_store_get_email_log_status_badge( $status ) {
6007 $status = sanitize_key( (string) $status );
6008 $label = $status === 'sent' ? __( 'Sent', 'ebook-store' ) : __( 'Failed', 'ebook-store' );
6009
6010 return '<span class="ebook-email-log-badge ebook-email-log-badge--' . esc_attr( $status === 'sent' ? 'sent' : 'failed' ) . '">' . esc_html( $label ) . '</span>';
6011 }
6012
6013 function ebook_store_render_email_log_box( $post ) {
6014 $recipient = (string) get_post_meta( $post->ID, 'recipient', true );
6015 $subject = (string) get_post_meta( $post->ID, 'subject', true );
6016 $body_html = (string) get_post_meta( $post->ID, 'body_html', true );
6017 $headers = get_post_meta( $post->ID, 'headers', true );
6018 $attachments = get_post_meta( $post->ID, 'attachments', true );
6019 $status = (string) get_post_meta( $post->ID, 'status', true );
6020 $source = (string) get_post_meta( $post->ID, 'source', true );
6021 $sent_at = (string) get_post_meta( $post->ID, 'sent_at', true );
6022 $order_snapshot = (string) get_post_meta( $post->ID, 'order_snapshot', true );
6023 $context_snapshot = (string) get_post_meta( $post->ID, 'context_snapshot', true );
6024 $ebook_key = (string) get_post_meta( $post->ID, 'ebook_key', true );
6025 $resend_count = (int) get_post_meta( $post->ID, 'resend_count', true );
6026 $last_resend_at = (string) get_post_meta( $post->ID, 'last_resend_at', true );
6027 $last_resend_status = (string) get_post_meta( $post->ID, 'last_resend_status', true );
6028 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
6029 $ebook_link = ebook_store_get_email_log_ebook_edit_link( $post->ID );
6030
6031 if ( ! is_array( $headers ) ) {
6032 $headers = array();
6033 }
6034
6035 if ( ! is_array( $attachments ) ) {
6036 $attachments = array();
6037 }
6038
6039 echo '<div class="ebook-email-log">';
6040 echo '<div class="ebook-email-log__actions">';
6041 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>';
6042 if ( $order_link !== '' ) {
6043 echo '<a class="button button-secondary" href="' . esc_url( $order_link ) . '">' . esc_html__( 'View related order', 'ebook-store' ) . '</a>';
6044 }
6045 if ( $ebook_link !== '' ) {
6046 echo '<a class="button button-secondary" href="' . esc_url( $ebook_link ) . '">' . esc_html__( 'Edit ebook', 'ebook-store' ) . '</a>';
6047 }
6048 echo '</div>';
6049
6050 echo '<div class="ebook-email-log__summary">';
6051 echo '<div><span>' . esc_html__( 'Status', 'ebook-store' ) . '</span><strong>' . wp_kses_post( ebook_store_get_email_log_status_badge( $status ) ) . '</strong></div>';
6052 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>';
6053 echo '<div><span>' . esc_html__( 'Source', 'ebook-store' ) . '</span><strong>' . esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
6054 echo '<div><span>' . esc_html__( 'Sent at', 'ebook-store' ) . '</span><strong>' . esc_html( $sent_at !== '' ? $sent_at : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
6055 echo '<div><span>' . esc_html__( 'Ebook key', 'ebook-store' ) . '</span><strong>' . esc_html( $ebook_key !== '' ? $ebook_key : __( 'Not set', 'ebook-store' ) ) . '</strong></div>';
6056 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>';
6057 echo '</div>';
6058
6059 echo '<div class="ebook-email-log__section">';
6060 echo '<h2>' . esc_html__( 'Message', 'ebook-store' ) . '</h2>';
6061 echo '<p><strong>' . esc_html__( 'Subject', 'ebook-store' ) . ':</strong> ' . esc_html( $subject ) . '</p>';
6062 echo '<div class="ebook-email-log__preview">' . wp_kses_post( $body_html ) . '</div>';
6063 echo '<textarea class="widefat ebook-email-log__code" rows="10" readonly>' . esc_textarea( $body_html ) . '</textarea>';
6064 echo '</div>';
6065
6066 echo '<div class="ebook-email-log__section">';
6067 echo '<h2>' . esc_html__( 'Headers and attachments', 'ebook-store' ) . '</h2>';
6068 echo '<p><strong>' . esc_html__( 'Headers', 'ebook-store' ) . ':</strong></p>';
6069 echo '<textarea class="widefat ebook-email-log__code" rows="4" readonly>' . esc_textarea( implode( "\n", $headers ) ) . '</textarea>';
6070 echo '<p><strong>' . esc_html__( 'Attachments', 'ebook-store' ) . ':</strong></p>';
6071 if ( empty( $attachments ) ) {
6072 echo '<p>' . esc_html__( 'No attachments were sent with this email.', 'ebook-store' ) . '</p>';
6073 } else {
6074 echo '<ul class="ebook-email-log__attachments">';
6075 foreach ( $attachments as $attachment ) {
6076 $label = basename( (string) $attachment );
6077 echo '<li><code>' . esc_html( $label ) . '</code><span>' . esc_html( (string) $attachment ) . '</span></li>';
6078 }
6079 echo '</ul>';
6080 }
6081 echo '</div>';
6082
6083 echo '<div class="ebook-email-log__section">';
6084 echo '<h2>' . esc_html__( 'Raw order snapshot', 'ebook-store' ) . '</h2>';
6085 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $order_snapshot ) . '</textarea>';
6086 echo '</div>';
6087
6088 echo '<div class="ebook-email-log__section">';
6089 echo '<h2>' . esc_html__( 'Raw email context', 'ebook-store' ) . '</h2>';
6090 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $context_snapshot ) . '</textarea>';
6091 echo '</div>';
6092 echo '</div>';
6093 }
6094
6095 /**
6096 * Build the list of files to attach to a delivery email.
6097 *
6098 * Always keeps the core file the caller already resolved (e.g. the encrypted
6099 * PDF), and additionally attaches a per-buyer watermarked ePub when the ebook
6100 * offers one and ePub watermarking is enabled. So a buyer who opts into email
6101 * attachments gets the ePub attached AND watermarked, for every gateway — not
6102 * only via the download link. encrypted() caches per order, so this is idempotent.
6103 *
6104 * @param string|null $core_file Path already chosen for attachment (may be null).
6105 * @param int $order_id Order post ID.
6106 * @param int $ebook_id Ebook post ID.
6107 * @return string|array|null A single path, an array of paths, or null.
6108 */
6109 function ebook_store_collect_delivery_attachments( $core_file, $order_id, $ebook_id ) {
6110 $order_id = (int) $order_id;
6111 $ebook_id = (int) $ebook_id;
6112 $files = array();
6113
6114 if ( is_string( $core_file ) && $core_file !== '' && file_exists( $core_file ) ) {
6115 $files[] = $core_file;
6116 }
6117
6118 if ( $ebook_id > 0 && get_option( 'ebook_store_epub_watermark' ) && function_exists( 'ebook_store_watermark_epub' ) ) {
6119 $obj = new EbookStoreEbook( $ebook_id );
6120 if ( ! empty( $obj->files['epub'] ) && file_exists( $obj->files['epub'] ) ) {
6121 $obj->order_id = $order_id;
6122 $epub = $obj->encrypted( $order_id, 'epub' );
6123 if ( is_string( $epub ) && $epub !== '' && file_exists( $epub ) && ! in_array( $epub, $files, true ) ) {
6124 $files[] = $epub;
6125 }
6126 }
6127 }
6128
6129 if ( empty( $files ) ) {
6130 return null;
6131 }
6132
6133 return count( $files ) === 1 ? $files[0] : $files;
6134 }
6135
6136 function ebook_email_delivery($post_id = null) {
6137 $QSWPOptions = new QSWPOptions();
6138 global $ebook_email_delivery, $formData;
6139 if (@$ebook_email_delivery['done'] == 1) {
6140 return;
6141 }
6142 $ebook_email_delivery['done'] = 1;
6143
6144 if ( isset( $formData ) ) {
6145 if ( is_string( $formData ) && $formData !== '' ) {
6146 $decoded_form_data = json_decode( $formData, true );
6147 $formData = is_array( $decoded_form_data ) ? $decoded_form_data : array();
6148 } elseif ( is_array( $formData ) ) {
6149 $formData = $formData;
6150 } else {
6151 $formData = array();
6152 }
6153 } else {
6154 $formData = array();
6155 }
6156
6157 // error_log(var_export(debug_backtrace(),true));
6158 $ebook_email_delivery['order']['download_links'] = implode("<br />",ebook_download_links($ebook_email_delivery['order']));
6159 // WooCommerce (and any caller) may already have set downloadlinks to the
6160 // full per-order format list; only fill it in when nothing was supplied.
6161 if ( empty( $ebook_email_delivery['order']['downloadlinks'] ) ) {
6162 $ebook_email_delivery['order']['downloadlinks'] = $ebook_email_delivery['order']['download_links'];
6163 }
6164 $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'));
6165
6166 $order_total = isset( $ebook_email_delivery['order']['total'] ) ? $ebook_email_delivery['order']['total'] : false;
6167 $order_tax = isset( $ebook_email_delivery['order']['tax'] ) ? $ebook_email_delivery['order']['tax'] : 0;
6168 $order_gross = isset( $ebook_email_delivery['order']['mc_gross'] ) ? $ebook_email_delivery['order']['mc_gross'] : 0;
6169
6170 if ( is_array( $order_total ) ) {
6171 $order_total = reset( $order_total );
6172 }
6173 if ( is_array( $order_tax ) ) {
6174 $order_tax = reset( $order_tax );
6175 }
6176 if ( is_array( $order_gross ) ) {
6177 $order_gross = reset( $order_gross );
6178 }
6179
6180 if ( $order_total === false || $order_total === '' ) {
6181 $order_total = (float) $order_gross + (float) $order_tax;
6182 }
6183
6184 $ebook_email_delivery['order']['total'] = toMoney( $order_total );
6185 $ebook_email_delivery['order']['tax'] = toMoney( $order_tax );
6186
6187 if (!isset($ebook_email_delivery['order']['password'])) {
6188 $ebook_email_delivery['order']['password'] = ebook_store_get_password_for_order(
6189 isset($ebook_email_delivery['order']['order_id']) ? (int)$ebook_email_delivery['order']['order_id'] : 0,
6190 array(
6191 'payer_email' => isset($ebook_email_delivery['order']['payer_email']) ? $ebook_email_delivery['order']['payer_email'] : '',
6192 'password' => '',
6193 'md5_nonce' => isset($ebook_email_delivery['order']['md5_nonce']) ? $ebook_email_delivery['order']['md5_nonce'] : '',
6194 'ebook_key' => isset($ebook_email_delivery['order']['ebook_key']) ? $ebook_email_delivery['order']['ebook_key'] : '',
6195 )
6196 );
6197 }
6198
6199 @$attachmentFile = $ebook_email_delivery['attachment'][0]['file'];
6200 $ebook_email_delivery['order']['filesize'] = ( $attachmentFile && file_exists( $attachmentFile ) ) ? ebook_store_human_filesize(filesize($attachmentFile)) : '';
6201 if (get_option('attach_files') != 1) {
6202 $attachmentFile = null;
6203 } else {
6204 // Attachments are on: attach the core file AND a watermarked ePub if the
6205 // ebook offers one, for every gateway (not only the download link).
6206 $ebook_attach_order = isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : $post_id;
6207 if ( is_array( $ebook_attach_order ) ) { $ebook_attach_order = reset( $ebook_attach_order ); }
6208 if ( ! $ebook_attach_order ) { $ebook_attach_order = $post_id; }
6209 $ebook_attach_id = isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0;
6210 if ( is_array( $ebook_attach_id ) ) { $ebook_attach_id = reset( $ebook_attach_id ); }
6211 $attachmentFile = ebook_store_collect_delivery_attachments( $attachmentFile, (int) $ebook_attach_order, (int) $ebook_attach_id );
6212 }
6213
6214
6215 //error_reporting(E_ALL);
6216 foreach ($ebook_email_delivery['order'] as $k => $v) {
6217 if (is_array($v)) {
6218 $v = $v[0];
6219 }
6220 $ebook_email_delivery['text'] = str_replace('%%' . $k . '%%', $v, $ebook_email_delivery['text']);
6221 }
6222
6223
6224 $ebook_email_delivery['text'] = nl2br($ebook_email_delivery['text']);
6225 //$ebook_email_delivery['text'] = apply_filters('the_content',$ebook_email_delivery['text']);
6226 //error_log('ebook_email_delivery ' . print_r($ebook_email_delivery,true));
6227 add_filter( 'wp_mail_content_type', 'ebook_set_content_type' );
6228 $ebook_email_delivery['mail_result'] = false;
6229 if (floatval(get_option('email_delivery',$QSWPOptions->email_delivery)) == 1) {
6230 //echo "Sending mail, data: " . implode("; ", $ebook_email_delivery);
6231 // Send-to-Kindle is an extra copy, never a replacement for the buyer's own email.
6232 $kindle_email = '';
6233 if (floatval(get_option('kindleDelivery',$QSWPOptions->kindleDelivery)) == 1) {
6234 if ( ! empty( $formData['kindle_email'] ) && is_string( $formData['kindle_email'] ) && strpos( $formData['kindle_email'], '@kindle.com' ) !== false ) {
6235 $kindle_email = (string) filter_var( trim( $formData['kindle_email'] ), FILTER_VALIDATE_EMAIL );
6236 }
6237 }
6238 if ( ! empty( $formData['gift_email'] ) ) {
6239 $formData['gift_email'] = filter_var($formData['gift_email'], FILTER_VALIDATE_EMAIL);
6240 $ebook_email_delivery['to'] = $formData['gift_email'];
6241 }
6242 $headers = array('Content-Type: text/html; charset=UTF-8');
6243
6244 $mail_result = wp_mail($ebook_email_delivery['to'],$ebook_email_delivery['subject'], $ebook_email_delivery['text'],$headers,$attachmentFile);
6245 $ebook_email_delivery['mail_result'] = (bool) $mail_result;
6246
6247 ebook_store_create_email_log_entry(
6248 array(
6249 'to' => isset( $ebook_email_delivery['to'] ) ? $ebook_email_delivery['to'] : '',
6250 'subject' => isset( $ebook_email_delivery['subject'] ) ? $ebook_email_delivery['subject'] : '',
6251 'body' => isset( $ebook_email_delivery['text'] ) ? $ebook_email_delivery['text'] : '',
6252 'headers' => $headers,
6253 'attachments' => $attachmentFile,
6254 'result' => $mail_result,
6255 'source' => isset( $ebook_email_delivery['source'] ) ? $ebook_email_delivery['source'] : 'standalone',
6256 'related_order_type' => isset( $ebook_email_delivery['related_order_type'] ) ? $ebook_email_delivery['related_order_type'] : '',
6257 'related_order_id' => isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : 0,
6258 'ebook_id' => isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0,
6259 'ebook_key' => isset( $ebook_email_delivery['order']['ebook_key'] ) ? $ebook_email_delivery['order']['ebook_key'] : '',
6260 'gateway' => isset( $ebook_email_delivery['order']['gateway'] ) ? $ebook_email_delivery['order']['gateway'] : '',
6261 'order' => isset( $ebook_email_delivery['order'] ) ? $ebook_email_delivery['order'] : array(),
6262 'context' => $ebook_email_delivery,
6263 )
6264 );
6265
6266 // Amazon only processes the attachment, so the Kindle copy is sent when there is one.
6267 if ( $kindle_email !== '' && $kindle_email !== $ebook_email_delivery['to'] && ! empty( $attachmentFile ) ) {
6268 $kindle_mail_result = wp_mail( $kindle_email, $ebook_email_delivery['subject'], $ebook_email_delivery['text'], $headers, $attachmentFile );
6269
6270 ebook_store_create_email_log_entry(
6271 array(
6272 'to' => $kindle_email,
6273 'subject' => isset( $ebook_email_delivery['subject'] ) ? $ebook_email_delivery['subject'] : '',
6274 'body' => isset( $ebook_email_delivery['text'] ) ? $ebook_email_delivery['text'] : '',
6275 'headers' => $headers,
6276 'attachments' => $attachmentFile,
6277 'result' => $kindle_mail_result,
6278 'source' => isset( $ebook_email_delivery['source'] ) ? $ebook_email_delivery['source'] : 'standalone',
6279 'related_order_type' => isset( $ebook_email_delivery['related_order_type'] ) ? $ebook_email_delivery['related_order_type'] : '',
6280 'related_order_id' => isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : 0,
6281 'ebook_id' => isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0,
6282 'ebook_key' => isset( $ebook_email_delivery['order']['ebook_key'] ) ? $ebook_email_delivery['order']['ebook_key'] : '',
6283 'gateway' => isset( $ebook_email_delivery['order']['gateway'] ) ? $ebook_email_delivery['order']['gateway'] : '',
6284 'order' => isset( $ebook_email_delivery['order'] ) ? $ebook_email_delivery['order'] : array(),
6285 'context' => $ebook_email_delivery,
6286 )
6287 );
6288 }
6289 }
6290 }
6291
6292 function ebook_set_content_type( $content_type ){
6293 return 'text/html';
6294 }
6295 function ebook_attachment($post_id, $ignoreSetting = false) {
6296 if (get_option('attach_files') == 1 || $ignoreSetting == true) {
6297 $attachment = get_post_meta($post_id,'ebook_wp_custom_attachment');
6298 if (WP_DEBUG == true) error_log('attachment - ' . print_r($attachment,true));
6299 return $attachment;
6300 }
6301 }
6302
6303 function ebook_store_set_email_log_columns( $columns ) {
6304 return array(
6305 'cb' => '<input type="checkbox" />',
6306 'title' => __( 'Email', 'ebook-store' ),
6307 'recipient' => __( 'Recipient', 'ebook-store' ),
6308 'source' => __( 'Source', 'ebook-store' ),
6309 'related_order' => __( 'Order', 'ebook-store' ),
6310 'ebook_reference' => __( 'Ebook', 'ebook-store' ),
6311 'status' => __( 'Status', 'ebook-store' ),
6312 'resends' => __( 'Resends', 'ebook-store' ),
6313 'date' => __( 'Logged', 'ebook-store' ),
6314 );
6315 }
6316
6317 function ebook_store_email_log_columns_output( $column, $post_id ) {
6318 switch ( $column ) {
6319 case 'recipient':
6320 $recipient = (string) get_post_meta( $post_id, 'recipient', true );
6321 echo $recipient !== '' ? '<a href="mailto:' . esc_attr( $recipient ) . '">' . esc_html( $recipient ) . '</a>' : '&mdash;';
6322 break;
6323 case 'source':
6324 $source = (string) get_post_meta( $post_id, 'source', true );
6325 echo esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) );
6326 break;
6327 case 'related_order':
6328 $order_id = (int) get_post_meta( $post_id, 'related_order_id', true );
6329 $link = ebook_store_get_email_log_order_edit_link( $post_id );
6330 if ( $order_id < 1 ) {
6331 echo '&mdash;';
6332 break;
6333 }
6334 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">#' . esc_html( $order_id ) . '</a>' : '#' . esc_html( $order_id );
6335 break;
6336 case 'ebook_reference':
6337 $ebook_id = (int) get_post_meta( $post_id, 'ebook_id', true );
6338 $link = ebook_store_get_email_log_ebook_edit_link( $post_id );
6339 if ( $ebook_id < 1 ) {
6340 echo '&mdash;';
6341 break;
6342 }
6343 $title = get_the_title( $ebook_id );
6344 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title !== '' ? $title : '#' . $ebook_id ) . '</a>' : esc_html( $title !== '' ? $title : '#' . $ebook_id );
6345 break;
6346 case 'status':
6347 echo wp_kses_post( ebook_store_get_email_log_status_badge( get_post_meta( $post_id, 'status', true ) ) );
6348 break;
6349 case 'resends':
6350 $resend_count = (int) get_post_meta( $post_id, 'resend_count', true );
6351 $last_resend_at = (string) get_post_meta( $post_id, 'last_resend_at', true );
6352 echo '<strong>' . esc_html( number_format_i18n( $resend_count ) ) . '</strong>';
6353 if ( $last_resend_at !== '' ) {
6354 echo '<div class="ebook-email-log-list__meta">' . esc_html( $last_resend_at ) . '</div>';
6355 }
6356 break;
6357 }
6358 }
6359
6360 function ebook_store_email_log_row_actions( $actions, $post ) {
6361 if ( ! $post || $post->post_type !== 'ebook_email_log' ) {
6362 return $actions;
6363 }
6364
6365 $actions['resend_email'] = '<a href="' . esc_url( ebook_store_get_email_log_resend_url( $post->ID ) ) . '">' . esc_html__( 'Resend', 'ebook-store' ) . '</a>';
6366
6367 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
6368 if ( $order_link !== '' ) {
6369 $actions['view_related_order'] = '<a href="' . esc_url( $order_link ) . '">' . esc_html__( 'View order', 'ebook-store' ) . '</a>';
6370 }
6371
6372 return $actions;
6373 }
6374
6375 function ebook_store_remove_email_log_submit_box() {
6376 remove_meta_box( 'submitdiv', 'ebook_email_log', 'side' );
6377 }
6378
6379 function ebook_store_cleanup_email_log_submenu() {
6380 remove_submenu_page( 'edit.php?post_type=ebook', 'post-new.php?post_type=ebook_email_log' );
6381 }
6382
6383 function ebook_store_redirect_new_email_log_entry() {
6384 global $pagenow;
6385
6386 if ( ! is_admin() || $pagenow !== 'post-new.php' ) {
6387 return;
6388 }
6389
6390 if ( empty( $_GET['post_type'] ) || wp_unslash( $_GET['post_type'] ) !== 'ebook_email_log' ) {
6391 return;
6392 }
6393
6394 wp_safe_redirect( admin_url( 'edit.php?post_type=ebook_email_log' ) );
6395 exit;
6396 }
6397
6398 function ebook_store_render_email_log_admin_styles() {
6399 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6400 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6401 return;
6402 }
6403
6404 echo '<style>
6405 .post-type-ebook_email_log #titlediv,
6406 .post-type-ebook_email_log #minor-publishing,
6407 .post-type-ebook_email_log #misc-publishing-actions,
6408 .post-type-ebook_email_log #major-publishing-actions .open-if-no-js {
6409 display: none;
6410 }
6411 .post-type-ebook_email_log .page-title-action {
6412 display: none;
6413 }
6414 .post-type-ebook_email_log #major-publishing-actions {
6415 padding: 0;
6416 border-top: 0;
6417 }
6418 .ebook-email-log {
6419 display: grid;
6420 gap: 16px;
6421 }
6422 .ebook-email-log__actions {
6423 display: flex;
6424 flex-wrap: wrap;
6425 gap: 10px;
6426 }
6427 .ebook-email-log__summary {
6428 display: grid;
6429 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
6430 gap: 12px;
6431 }
6432 .ebook-email-log__summary > div,
6433 .ebook-email-log__section {
6434 background: #fff;
6435 border: 1px solid #dcdcde;
6436 border-radius: 14px;
6437 padding: 16px;
6438 }
6439 .ebook-email-log__summary span {
6440 display: block;
6441 margin-bottom: 6px;
6442 color: #646970;
6443 font-size: 12px;
6444 text-transform: uppercase;
6445 letter-spacing: .04em;
6446 }
6447 .ebook-email-log__summary strong {
6448 display: block;
6449 font-size: 14px;
6450 line-height: 1.5;
6451 }
6452 .ebook-email-log__summary small {
6453 display: block;
6454 margin-top: 6px;
6455 color: #646970;
6456 font-weight: 400;
6457 }
6458 .ebook-email-log__section h2 {
6459 margin: 0 0 12px;
6460 font-size: 15px;
6461 }
6462 .ebook-email-log__preview {
6463 max-height: 360px;
6464 overflow: auto;
6465 padding: 16px;
6466 background: #f6f7f7;
6467 border: 1px solid #dcdcde;
6468 border-radius: 12px;
6469 margin-bottom: 12px;
6470 }
6471 .ebook-email-log__code {
6472 font-family: Consolas, Monaco, monospace;
6473 font-size: 12px;
6474 }
6475 .ebook-email-log__attachments {
6476 margin: 0;
6477 padding: 0;
6478 list-style: none;
6479 display: grid;
6480 gap: 10px;
6481 }
6482 .ebook-email-log__attachments li {
6483 display: grid;
6484 gap: 4px;
6485 padding: 10px 12px;
6486 background: #f6f7f7;
6487 border-radius: 10px;
6488 }
6489 .ebook-email-log-badge {
6490 display: inline-flex;
6491 align-items: center;
6492 padding: 4px 10px;
6493 border-radius: 999px;
6494 font-size: 12px;
6495 font-weight: 600;
6496 }
6497 .ebook-email-log-badge--sent {
6498 background: #e7f7ed;
6499 color: #116b38;
6500 }
6501 .ebook-email-log-badge--failed {
6502 background: #fdeaea;
6503 color: #b42318;
6504 }
6505 .ebook-email-log-list__meta {
6506 color: #646970;
6507 font-size: 12px;
6508 margin-top: 4px;
6509 }
6510 </style>';
6511 }
6512
6513 function ebook_store_email_log_admin_notice() {
6514 if ( ! is_admin() || empty( $_GET['ebook_store_email_log_notice'] ) ) {
6515 return;
6516 }
6517
6518 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6519 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6520 return;
6521 }
6522
6523 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_email_log_notice'] ) );
6524 if ( ! in_array( $notice, array( 'resent', 'resend_failed' ), true ) ) {
6525 return;
6526 }
6527
6528 $class = $notice === 'resent' ? 'notice-success' : 'notice-error';
6529 $message = $notice === 'resent'
6530 ? __( 'The email was resent and logged as a new entry.', 'ebook-store' )
6531 : __( 'The resend attempt failed. Check your mail configuration and the attachment paths.', 'ebook-store' );
6532
6533 echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6534 }
6535
6536 function ebook_store_order_admin_notice() {
6537 if ( ! is_admin() || empty( $_GET['ebook_store_order_notice'] ) ) {
6538 return;
6539 }
6540
6541 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6542 if ( ! $screen || $screen->post_type !== 'ebook_order' ) {
6543 return;
6544 }
6545
6546 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_order_notice'] ) );
6547 $messages = array(
6548 'downloads_reset' => array(
6549 'class' => 'notice-success',
6550 'text' => __( 'The order download counter was reset to 0.', 'ebook-store' ),
6551 ),
6552 'delivery_resent' => array(
6553 'class' => 'notice-success',
6554 'text' => __( 'The delivery email was sent again and logged as a new email entry.', 'ebook-store' ),
6555 ),
6556 'delivery_failed' => array(
6557 'class' => 'notice-error',
6558 'text' => __( 'The delivery email resend failed. Check the order email address, attachment file, and mail configuration.', 'ebook-store' ),
6559 ),
6560 'refund_succeeded' => array(
6561 'class' => 'notice-success',
6562 'text' => __( 'The payment was refunded successfully.', 'ebook-store' ),
6563 ),
6564 'refund_failed' => array(
6565 'class' => 'notice-error',
6566 'text' => __( 'The payment refund failed.', 'ebook-store' ),
6567 ),
6568 );
6569
6570 if ( ! isset( $messages[ $notice ] ) ) {
6571 return;
6572 }
6573
6574 $message = $messages[ $notice ]['text'];
6575 if ( ! empty( $_GET['ebook_store_order_message'] ) ) {
6576 $message = sanitize_text_field( wp_unslash( $_GET['ebook_store_order_message'] ) );
6577 }
6578
6579 echo '<div class="notice ' . esc_attr( $messages[ $notice ]['class'] ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6580 }
6581
6582 function ebook_store_get_order_admin_list_url( $notice = '', $args = array() ) {
6583 $args = array(
6584 'post_type' => 'ebook_order',
6585 ) + (array) $args;
6586
6587 if ( $notice !== '' ) {
6588 $args['ebook_store_order_notice'] = $notice;
6589 }
6590
6591 return add_query_arg( $args, admin_url( 'edit.php' ) );
6592 }
6593
6594 function ebook_store_mark_order_refunded( $order_id, $gateway, $refund_id, $refund_status, $amount = '', $currency = '' ) {
6595 $order_id = absint( $order_id );
6596 if ( $order_id < 1 ) {
6597 return;
6598 }
6599
6600 update_post_meta( $order_id, 'payment_status', 'Refunded' );
6601 update_post_meta( $order_id, 'refund_gateway', sanitize_key( (string) $gateway ) );
6602 update_post_meta( $order_id, 'refund_gateway_id', sanitize_text_field( (string) $refund_id ) );
6603 update_post_meta( $order_id, 'refund_gateway_status', sanitize_text_field( (string) $refund_status ) );
6604 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
6605
6606 if ( $amount !== '' && is_numeric( $amount ) ) {
6607 update_post_meta( $order_id, 'refund_amount', (string) $amount );
6608 }
6609
6610 if ( $currency !== '' ) {
6611 update_post_meta( $order_id, 'refund_currency', strtoupper( sanitize_text_field( (string) $currency ) ) );
6612 }
6613 }
6614
6615 function ebook_store_refund_stripe_order( $order_id ) {
6616 $payment_intent_id = trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) );
6617 if ( $payment_intent_id === '' ) {
6618 return new WP_Error( 'ebook_store_missing_stripe_payment_intent', __( 'This Stripe order is missing its payment intent ID.', 'ebook-store' ) );
6619 }
6620
6621 $bootstrap = ebook_store_bootstrap_stripe_sdk();
6622 if ( is_wp_error( $bootstrap ) ) {
6623 return $bootstrap;
6624 }
6625
6626 try {
6627 $refund = \Stripe\Refund::create(
6628 array(
6629 'payment_intent' => $payment_intent_id,
6630 'reason' => 'requested_by_customer',
6631 'metadata' => array(
6632 'ebook_order_id' => (string) $order_id,
6633 ),
6634 )
6635 );
6636 } catch ( Exception $e ) {
6637 return new WP_Error( 'ebook_store_stripe_refund_failed', $e->getMessage() );
6638 }
6639
6640 $refund_amount = isset( $refund->amount ) ? ((float) $refund->amount / 100) : (float) get_post_meta( $order_id, 'mc_gross', true );
6641 $refund_currency = isset( $refund->currency ) ? strtoupper( (string) $refund->currency ) : (string) get_post_meta( $order_id, 'mc_currency', true );
6642 $refund_status = isset( $refund->status ) ? (string) $refund->status : 'succeeded';
6643 $refund_id = isset( $refund->id ) ? (string) $refund->id : '';
6644
6645 ebook_store_mark_order_refunded( $order_id, 'stripe', $refund_id, $refund_status, $refund_amount, $refund_currency );
6646
6647 return array(
6648 'gateway' => 'stripe',
6649 'refund_id' => $refund_id,
6650 'status' => $refund_status,
6651 'amount' => $refund_amount,
6652 'currency' => $refund_currency,
6653 );
6654 }
6655
6656 function ebook_store_refund_adyen_order( $order_id ) {
6657 $psp_reference = trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) );
6658 if ( $psp_reference === '' ) {
6659 return new WP_Error( 'ebook_store_missing_adyen_psp_reference', __( 'This Adyen order is missing its PSP reference.', 'ebook-store' ) );
6660 }
6661
6662 $amount = (float) get_post_meta( $order_id, 'mc_gross', true );
6663 $currency = strtoupper( trim( (string) get_post_meta( $order_id, 'mc_currency', true ) ) );
6664 if ( $amount <= 0 || $currency === '' ) {
6665 return new WP_Error( 'ebook_store_invalid_adyen_refund_amount', __( 'This Adyen order is missing a valid refund amount or currency.', 'ebook-store' ) );
6666 }
6667
6668 $response = ebook_store_adyen_request(
6669 'POST',
6670 'payments/' . rawurlencode( $psp_reference ) . '/refunds',
6671 array(
6672 'merchantAccount' => trim( (string) get_option( 'adyen_merchant_account', '' ) ),
6673 'amount' => array(
6674 'currency' => $currency,
6675 'value' => (int) round( $amount * ebook_store_currency_minor_unit_factor( $currency ) ),
6676 ),
6677 'reference' => 'refund-' . $order_id . '-' . wp_generate_password( 8, false, false ),
6678 ),
6679 array(
6680 'Idempotency-Key' => wp_generate_uuid4(),
6681 )
6682 );
6683
6684 if ( is_wp_error( $response ) ) {
6685 return $response;
6686 }
6687
6688 $refund_id = ! empty( $response['pspReference'] ) ? (string) $response['pspReference'] : '';
6689 ebook_store_mark_order_refunded( $order_id, 'adyen', $refund_id, 'received', $amount, $currency );
6690
6691 return array(
6692 'gateway' => 'adyen',
6693 'refund_id' => $refund_id,
6694 'status' => 'received',
6695 'amount' => $amount,
6696 'currency' => $currency,
6697 );
6698 }
6699
6700 function ebook_store_process_order_refund( $order_id ) {
6701 $order_id = absint( $order_id );
6702 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6703 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6704 }
6705
6706 $gateway = ebook_store_get_order_refund_gateway( $order_id );
6707 if ( $gateway === '' ) {
6708 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this order or payment method.', 'ebook-store' ) );
6709 }
6710
6711 if ( $gateway === 'stripe' ) {
6712 return ebook_store_refund_stripe_order( $order_id );
6713 }
6714
6715 if ( $gateway === 'adyen' ) {
6716 return ebook_store_refund_adyen_order( $order_id );
6717 }
6718
6719 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this payment method.', 'ebook-store' ) );
6720 }
6721
6722 function ebook_store_prepare_order_delivery_email( $order_id ) {
6723 $order_id = absint( $order_id );
6724 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6725 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6726 }
6727
6728 $order_meta = get_post_meta( $order_id, null, true );
6729 if ( ! is_array( $order_meta ) || empty( $order_meta['ebook'][0] ) ) {
6730 return new WP_Error( 'ebook_store_invalid_order_meta', __( 'This order is missing ebook delivery data.', 'ebook-store' ) );
6731 }
6732
6733 $ebook_id = absint( $order_meta['ebook'][0] );
6734 if ( $ebook_id < 1 ) {
6735 return new WP_Error( 'ebook_store_invalid_order_ebook', __( 'This order is not linked to a valid ebook.', 'ebook-store' ) );
6736 }
6737
6738 $QSWPOptions = new QSWPOptions();
6739 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_pdf', true );
6740 if ( $attachment_file === '' ) {
6741 // Fall back to a watermarked EPUB copy if one was generated for this order.
6742 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_epub', true );
6743 }
6744 if ( $attachment_file === '' || ! file_exists( $attachment_file ) ) {
6745 $master_attachment = ebook_attachment( $ebook_id, true );
6746 $attachment_file = isset( $master_attachment[0]['file'] ) ? (string) $master_attachment[0]['file'] : '';
6747 if ( $attachment_file !== '' && get_option( 'encrypt_pdf' ) && strtolower( pathinfo( $attachment_file, PATHINFO_EXTENSION ) ) === 'pdf' ) {
6748 // The per-order protected copy is missing (or stuck at 'in_progress'):
6749 // rebuild it instead of mailing the unprotected master file.
6750 global $attachment;
6751 $attachment = $master_attachment;
6752 $encrypt_data = array();
6753 foreach ( $order_meta as $meta_key => $meta_values ) {
6754 if ( isset( $meta_values[0] ) && is_scalar( $meta_values[0] ) ) {
6755 $encrypt_data[ $meta_key ] = $meta_values[0];
6756 }
6757 }
6758 $encrypt_data['order_id'] = $order_id;
6759 $encrypted_file = ebook_encrypt_pdf( $encrypt_data, $order_id );
6760 $attachment_file = ( is_string( $encrypted_file ) && $encrypted_file !== '' && file_exists( $encrypted_file ) ) ? $encrypted_file : '';
6761 }
6762 }
6763
6764 $order_meta['order_id'][0] = $order_id;
6765 $order_meta['downloadlink'][0] = ebook_download_link( $order_meta );
6766
6767 // The delivery-email template substitutes %%downloadlink_html%%. The checkout
6768 // path builds it, but the admin resend path (which rebuilds the order array
6769 // from post meta) did not — so a resent email showed the raw placeholder and
6770 // the buyer had no way to download. Build it here too.
6771 $delivery_ebook = new EbookStoreEbook( $ebook_id );
6772 // Plugin order: give link() the ebook_key credentials, otherwise the non-PDF
6773 // format links come out as wc_order_download URLs with no order key, which
6774 // the download endpoint (rightly) refuses.
6775 $delivery_ebook->ebook_key = isset( $order_meta['ebook_key'][0] ) ? (string) $order_meta['ebook_key'][0] : '';
6776 $delivery_ebook->md5_nonce = isset( $order_meta['md5_nonce'][0] ) ? (string) $order_meta['md5_nonce'][0] : '';
6777 $delivery_ebook->order_id = $order_id;
6778 $delivery_ebook->setLink['pdf'] = $order_meta['downloadlink'][0];
6779 $order_meta['downloadlink_html'][0] = $delivery_ebook->format_links();
6780
6781 $recipient = isset( $order_meta['payer_email'][0] ) ? sanitize_email( (string) $order_meta['payer_email'][0] ) : '';
6782 if ( $recipient === '' ) {
6783 return new WP_Error( 'ebook_store_missing_order_email', __( 'This order does not have a valid delivery email address.', 'ebook-store' ) );
6784 }
6785 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_id );
6786
6787 return array(
6788 'to' => $recipient,
6789 'subject' => $email_template['subject'],
6790 'text' => $email_template['text'],
6791 'attachment' => array(
6792 array(
6793 'file' => $attachment_file,
6794 ),
6795 ),
6796 'order' => $order_meta,
6797 'source' => 'admin_resend',
6798 'related_order_type' => 'ebook_order',
6799 );
6800 }
6801
6802 /**
6803 * Clear every download counter a WooCommerce order carries.
6804 *
6805 * Counts are per ebook ('downloads_<id>'), plus the order-wide 'downloads' key
6806 * older versions wrote, so all of them are removed.
6807 *
6808 * @param WC_Order $order Order object.
6809 * @return int Number of counters cleared.
6810 */
6811 function ebook_store_reset_wc_order_downloads( $order ) {
6812 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
6813 return 0;
6814 }
6815
6816 $cleared = 0;
6817 foreach ( $order->get_meta_data() as $meta ) {
6818 $meta_key = isset( $meta->key ) ? (string) $meta->key : '';
6819 if ( $meta_key === 'downloads' || strpos( $meta_key, 'downloads_' ) === 0 ) {
6820 $order->delete_meta_data( $meta_key );
6821 $cleared++;
6822 }
6823 }
6824
6825 if ( $cleared > 0 ) {
6826 $order->save();
6827 }
6828
6829 return $cleared;
6830 }
6831
6832 function ebook_store_reset_order_downloads() {
6833 if ( ! current_user_can( 'manage_options' ) ) {
6834 wp_die( esc_html__( 'You are not allowed to reset order downloads.', 'ebook-store' ) );
6835 }
6836
6837 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6838 if ( $order_id < 1 ) {
6839 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6840 }
6841
6842 // A WooCommerce order is not an 'ebook_order' post (and on HPOS is not a post
6843 // at all), so this action used to refuse every WooCommerce buyer.
6844 $wc_order = ( get_post_type( $order_id ) !== 'ebook_order' ) ? ebook_store_get_wc_order_object( $order_id ) : false;
6845
6846 if ( ! $wc_order && get_post_type( $order_id ) !== 'ebook_order' ) {
6847 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6848 }
6849
6850 check_admin_referer( 'ebook_store_reset_order_downloads_' . $order_id );
6851
6852 if ( $wc_order ) {
6853 ebook_store_reset_wc_order_downloads( $wc_order );
6854 } else {
6855 update_post_meta( $order_id, 'downloads', 0 );
6856 }
6857
6858 wp_safe_redirect( ebook_store_get_order_admin_list_url( 'downloads_reset' ) );
6859 exit;
6860 }
6861
6862 /**
6863 * Offer the reset on the WooCommerce order screen, where the buyer's ebook
6864 * downloads actually live.
6865 *
6866 * @param array $actions Existing order actions.
6867 * @return array
6868 */
6869 function ebook_store_wc_order_actions( $actions ) {
6870 $actions['ebook_store_reset_downloads'] = __( 'Reset Ebook Store downloads', 'ebook-store' );
6871
6872 return $actions;
6873 }
6874 add_filter( 'woocommerce_order_actions', 'ebook_store_wc_order_actions' );
6875
6876 /**
6877 * Run the reset chosen from the WooCommerce order screen. WooCommerce has
6878 * already checked the nonce and the capability by the time this fires.
6879 *
6880 * @param WC_Order $order Order object.
6881 */
6882 function ebook_store_wc_order_action_reset_downloads( $order ) {
6883 $cleared = ebook_store_reset_wc_order_downloads( $order );
6884
6885 if ( is_a( $order, 'WC_Order' ) ) {
6886 $order->add_order_note( __( 'Ebook Store download counters were reset.', 'ebook-store' ) );
6887 }
6888
6889 return $cleared;
6890 }
6891 add_action( 'woocommerce_order_action_ebook_store_reset_downloads', 'ebook_store_wc_order_action_reset_downloads' );
6892
6893 function ebook_store_resend_order_delivery() {
6894 if ( ! current_user_can( 'manage_options' ) ) {
6895 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
6896 }
6897
6898 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6899 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6900 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6901 }
6902
6903 check_admin_referer( 'ebook_store_resend_order_delivery_' . $order_id );
6904
6905 $email_context = ebook_store_prepare_order_delivery_email( $order_id );
6906 if ( is_wp_error( $email_context ) ) {
6907 wp_die( esc_html( $email_context->get_error_message() ) );
6908 }
6909
6910 global $ebook_email_delivery, $formData;
6911 $previous_email_delivery = isset( $ebook_email_delivery ) ? $ebook_email_delivery : null;
6912 $previous_form_data = isset( $formData ) ? $formData : null;
6913 $previous_email_delivery_setting = get_option( 'email_delivery', null );
6914
6915 $ebook_email_delivery = $email_context;
6916 unset( $ebook_email_delivery['done'] );
6917 // Restore the buyer's checkout form data (gift / Kindle addresses) for this order.
6918 $formData = (string) get_post_meta( $order_id, 'formData', true );
6919 update_option( 'email_delivery', 1 );
6920
6921 ob_start();
6922 ebook_email_delivery( $order_id );
6923 ob_end_clean();
6924
6925 $resend_succeeded = ! empty( $ebook_email_delivery['mail_result'] );
6926
6927 if ( null === $previous_email_delivery_setting ) {
6928 delete_option( 'email_delivery' );
6929 } else {
6930 update_option( 'email_delivery', $previous_email_delivery_setting );
6931 }
6932 $ebook_email_delivery = $previous_email_delivery;
6933 $formData = $previous_form_data;
6934
6935 wp_safe_redirect( ebook_store_get_order_admin_list_url( $resend_succeeded ? 'delivery_resent' : 'delivery_failed' ) );
6936 exit;
6937 }
6938
6939 function ebook_store_refund_order() {
6940 if ( ! current_user_can( 'manage_options' ) ) {
6941 wp_die( esc_html__( 'You are not allowed to refund ebook orders.', 'ebook-store' ) );
6942 }
6943
6944 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6945 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6946 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6947 }
6948
6949 check_admin_referer( 'ebook_store_refund_order_' . $order_id );
6950
6951 $result = ebook_store_process_order_refund( $order_id );
6952 if ( is_wp_error( $result ) ) {
6953 wp_safe_redirect(
6954 ebook_store_get_order_admin_list_url(
6955 'refund_failed',
6956 array(
6957 'ebook_store_order_message' => $result->get_error_message(),
6958 )
6959 )
6960 );
6961 exit;
6962 }
6963
6964 $gateway_label = ucfirst( (string) $result['gateway'] );
6965 $amount_label = ebook_store_order_money_display(
6966 isset( $result['amount'] ) ? $result['amount'] : '',
6967 isset( $result['currency'] ) ? $result['currency'] : ''
6968 );
6969 $message = sprintf(
6970 /* translators: 1: payment gateway, 2: refunded amount */
6971 __( '%1$s refund created successfully for %2$s.', 'ebook-store' ),
6972 $gateway_label,
6973 $amount_label
6974 );
6975
6976 wp_safe_redirect(
6977 ebook_store_get_order_admin_list_url(
6978 'refund_succeeded',
6979 array(
6980 'ebook_store_order_message' => $message,
6981 )
6982 )
6983 );
6984 exit;
6985 }
6986
6987 function ebook_store_resend_email_log() {
6988 if ( ! current_user_can( 'manage_options' ) ) {
6989 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
6990 }
6991
6992 $log_id = isset( $_GET['log_id'] ) ? absint( wp_unslash( $_GET['log_id'] ) ) : 0;
6993 if ( $log_id < 1 || get_post_type( $log_id ) !== 'ebook_email_log' ) {
6994 wp_die( esc_html__( 'Invalid email log entry.', 'ebook-store' ) );
6995 }
6996
6997 check_admin_referer( 'ebook_store_resend_email_log_' . $log_id );
6998
6999 $recipient = (string) get_post_meta( $log_id, 'recipient', true );
7000 $subject = (string) get_post_meta( $log_id, 'subject', true );
7001 $body_html = (string) get_post_meta( $log_id, 'body_html', true );
7002 $headers = ebook_store_normalize_email_log_headers( get_post_meta( $log_id, 'headers', true ) );
7003 $attachments = ebook_store_normalize_email_log_attachments( get_post_meta( $log_id, 'attachments', true ) );
7004 $order_json = (string) get_post_meta( $log_id, 'order_snapshot', true );
7005 $context_json = (string) get_post_meta( $log_id, 'context_snapshot', true );
7006 $source = (string) get_post_meta( $log_id, 'source', true );
7007 $order = json_decode( $order_json, true );
7008 $context = json_decode( $context_json, true );
7009
7010 if ( ! is_array( $order ) ) {
7011 $order = array();
7012 }
7013
7014 if ( ! is_array( $context ) ) {
7015 $context = array();
7016 }
7017
7018 $attachments = array_values(
7019 array_filter(
7020 $attachments,
7021 static function ( $path ) {
7022 return is_string( $path ) && $path !== '' && file_exists( $path );
7023 }
7024 )
7025 );
7026
7027 $mail_result = wp_mail( $recipient, $subject, $body_html, $headers, $attachments );
7028
7029 $resend_count = (int) get_post_meta( $log_id, 'resend_count', true );
7030 update_post_meta( $log_id, 'resend_count', $resend_count + 1 );
7031 update_post_meta( $log_id, 'last_resend_at', current_time( 'mysql' ) );
7032 update_post_meta( $log_id, 'last_resend_status', $mail_result ? 'sent' : 'failed' );
7033
7034 $new_log_id = ebook_store_create_email_log_entry(
7035 array(
7036 'to' => $recipient,
7037 'subject' => $subject,
7038 'body' => $body_html,
7039 'headers' => $headers,
7040 'attachments' => $attachments,
7041 'result' => $mail_result,
7042 'source' => 'resend',
7043 'related_order_type' => get_post_meta( $log_id, 'related_order_type', true ),
7044 'related_order_id' => get_post_meta( $log_id, 'related_order_id', true ),
7045 'ebook_id' => get_post_meta( $log_id, 'ebook_id', true ),
7046 'ebook_key' => get_post_meta( $log_id, 'ebook_key', true ),
7047 'gateway' => get_post_meta( $log_id, 'gateway', true ),
7048 'order' => $order,
7049 'context' => array_merge(
7050 $context,
7051 array(
7052 'resend_of' => $log_id,
7053 'original_source' => $source,
7054 )
7055 ),
7056 'parent_log_id' => $log_id,
7057 )
7058 );
7059
7060 if ( $new_log_id > 0 ) {
7061 update_post_meta( $log_id, 'last_resend_log_id', $new_log_id );
7062 }
7063
7064 wp_safe_redirect(
7065 add_query_arg(
7066 array(
7067 'post' => $log_id,
7068 'action' => 'edit',
7069 'ebook_store_email_log_notice' => $mail_result ? 'resent' : 'resend_failed',
7070 ),
7071 admin_url( 'post.php' )
7072 )
7073 );
7074 exit;
7075 }
7076
7077 add_filter( 'manage_edit-ebook_email_log_columns', 'ebook_store_set_email_log_columns' );
7078 add_action( 'manage_ebook_email_log_posts_custom_column', 'ebook_store_email_log_columns_output', 10, 2 );
7079 add_filter( 'post_row_actions', 'ebook_store_email_log_row_actions', 10, 2 );
7080 add_action( 'add_meta_boxes', 'ebook_store_remove_email_log_submit_box', 20 );
7081 add_action( 'admin_menu', 'ebook_store_cleanup_email_log_submenu', 999 );
7082 add_action( 'admin_init', 'ebook_store_redirect_new_email_log_entry' );
7083 add_action( 'admin_head', 'ebook_store_render_email_log_admin_styles' );
7084 add_action( 'admin_notices', 'ebook_store_email_log_admin_notice' );
7085 add_action( 'admin_notices', 'ebook_store_order_admin_notice' );
7086 add_action( 'admin_post_ebook_store_resend_email_log', 'ebook_store_resend_email_log' );
7087 add_action( 'admin_post_ebook_store_reset_order_downloads', 'ebook_store_reset_order_downloads' );
7088 add_action( 'admin_post_ebook_store_resend_order_delivery', 'ebook_store_resend_order_delivery' );
7089 add_action( 'admin_post_ebook_store_refund_order', 'ebook_store_refund_order' );
7090
7091 function ebook_store_get_order_verification_url( $ebook_key ) {
7092 $ebook_key = preg_replace( '/[^a-zA-Z0-9]+/', '', (string) $ebook_key );
7093
7094 return add_query_arg(
7095 array(
7096 'ebook_action' => 'verify_order',
7097 'ebook_key' => $ebook_key,
7098 ),
7099 home_url( '/' )
7100 );
7101 }
7102
7103 /**
7104 * The QR-verification key used for a Test-button copy, so the verification page
7105 * can recognise it as a test (there is no real order behind it) instead of
7106 * showing "record not found". Kept uppercase so it can never collide with a real
7107 * order's md5 (lowercase-hex) key.
7108 *
7109 * @param int $ebook_id Ebook post ID.
7110 * @return string
7111 */
7112 function ebook_store_test_verification_key( $ebook_id ) {
7113 return 'EBOOKSTORETEST' . (int) $ebook_id;
7114 }
7115
7116 /**
7117 * If a verification key is a Test-button key, return the ebook ID it was made
7118 * from; otherwise false.
7119 *
7120 * @param string $key Sanitised (alphanumeric) verification key.
7121 * @return int|false
7122 */
7123 function ebook_store_parse_test_verification_key( $key ) {
7124 $prefix = 'EBOOKSTORETEST';
7125 if ( strpos( (string) $key, $prefix ) !== 0 ) {
7126 return false;
7127 }
7128 return (int) substr( (string) $key, strlen( $prefix ) );
7129 }
7130
7131 function ebook_store_render_public_order_field( $label, $value ) {
7132 if ( $value === '' || $value === null ) {
7133 return '';
7134 }
7135
7136 return '<div class="ebook-store-verify__field"><span>' . esc_html( $label ) . '</span><strong>' . esc_html( $value ) . '</strong></div>';
7137 }
7138
7139 function ebook_store_render_public_order_verification_page() {
7140 if ( ! isset( $_GET['ebook_action'] ) || sanitize_text_field( wp_unslash( $_GET['ebook_action'] ) ) !== 'verify_order' ) {
7141 return;
7142 }
7143
7144 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]+/', '', wp_unslash( $_GET['ebook_key'] ) ) : '';
7145
7146 // A QR scanned from a Test-button copy carries no real order, so recognise it
7147 // and show a "test ebook" confirmation rather than a scary "not found".
7148 $test_ebook_id = ebook_store_parse_test_verification_key( $ebook_key );
7149 $is_test = ( $test_ebook_id !== false );
7150
7151 $wc_order = false;
7152 if ( $is_test ) {
7153 $order = false;
7154 } elseif ( $ebook_key === '' ) {
7155 $order = false;
7156 } else {
7157 $order = ebook_get_order( 'ebook_key', $ebook_key );
7158 if ( ! $order && get_option( 'stripe_integration_enabled', 0 ) ) {
7159 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key( $ebook_key );
7160 if ( ! is_wp_error( $recovered_order_id ) && $recovered_order_id ) {
7161 $order = ebook_get_order( 'ebook_key', $ebook_key );
7162 }
7163 }
7164 if ( ! $order && function_exists( 'wc_get_orders' ) ) {
7165 // WooCommerce copies carry the key on the order itself; wc_get_orders()
7166 // finds it whether orders live in wp_posts or the HPOS tables.
7167 $wc_orders = wc_get_orders( array( 'meta_key' => 'ebook_key', 'meta_value' => $ebook_key, 'limit' => 1 ) );
7168 if ( ! empty( $wc_orders ) && is_a( $wc_orders[0], 'WC_Order' ) ) {
7169 $wc_order = $wc_orders[0];
7170 }
7171 }
7172 }
7173
7174 $found_order = is_array( $order ) && ! empty( $order['ebook'][0] );
7175 $found_wc_order = ( $wc_order && is_a( $wc_order, 'WC_Order' ) );
7176 status_header( ( $is_test || $found_order || $found_wc_order ) ? 200 : 404 );
7177 nocache_headers();
7178
7179 $site_name = wp_strip_all_tags( get_bloginfo( 'name' ) );
7180 if ( $is_test ) {
7181 $page_title = __( 'Test Copy', 'ebook-store' );
7182 $status_label = __( 'Test copy', 'ebook-store' );
7183 $status_class = 'is-test';
7184 } else {
7185 $page_title = ( $found_order || $found_wc_order ) ? __( 'Ebook Copy Verification', 'ebook-store' ) : __( 'Verification Record Not Found', 'ebook-store' );
7186 if ( $found_wc_order ) {
7187 $status_label = wc_get_order_status_name( $wc_order->get_status() );
7188 } else {
7189 $status_label = $found_order
7190 ? ( ! empty( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : __( 'Completed', 'ebook-store' ) )
7191 : __( 'Missing', 'ebook-store' );
7192 }
7193 $status_class = ( $found_order || $found_wc_order ) ? 'is-valid' : 'is-missing';
7194 }
7195
7196 $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>
7197 :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)}
7198 *{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)}
7199 a{color:inherit} .ebook-store-verify{max-width:980px;margin:0 auto;padding:32px 20px 56px}
7200 .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}
7201 .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))}
7202 .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}
7203 .ebook-store-verify__hero h1{margin:0 0 10px;font-size:clamp(2rem,4vw,3.5rem);line-height:.98}
7204 .ebook-store-verify__hero p{margin:0;max-width:62ch;font:400 16px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
7205 .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}
7206 .ebook-store-verify__status.is-valid{background:var(--ebook-verify-green-soft);color:var(--ebook-verify-green)}
7207 .ebook-store-verify__status.is-missing{background:var(--ebook-verify-red-soft);color:var(--ebook-verify-red)}
7208 .ebook-store-verify__status.is-test{background:#fbf1de;color:#7a4d00}
7209 .ebook-store-verify__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-top:20px}
7210 .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)}
7211 .ebook-store-verify__card h2{margin:0 0 14px;font-size:1.1rem}
7212 .ebook-store-verify__field{display:flex;justify-content:space-between;gap:18px;padding:12px 0;border-top:1px solid #edf1eb}
7213 .ebook-store-verify__field:first-child{border-top:0;padding-top:0}
7214 .ebook-store-verify__field span{font:600 12px/1.3 Arial,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--ebook-verify-muted)}
7215 .ebook-store-verify__field strong{font:600 15px/1.4 Arial,sans-serif;text-align:right;max-width:65%}
7216 .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)}
7217 .ebook-store-verify__footer{margin-top:16px;font:400 13px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
7218 @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}}
7219 </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>';
7220
7221 if ( $is_test ) {
7222 $test_title = $test_ebook_id ? get_the_title( $test_ebook_id ) : '';
7223 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'This is a test ebook.', 'ebook-store' ) . '</strong> ';
7224 $content .= esc_html__( 'You are verifying a copy generated with the Test button in the store admin, not a real purchase.', 'ebook-store' );
7225 if ( $test_title !== '' ) {
7226 $content .= ' ' . esc_html(
7227 sprintf(
7228 /* translators: 1: ebook title, 2: ebook ID. */
7229 __( 'It was made from "%1$s" (ebook #%2$d).', 'ebook-store' ),
7230 $test_title,
7231 $test_ebook_id
7232 )
7233 );
7234 }
7235 $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>';
7236 } elseif ( $found_order ) {
7237 $ebook_id = isset( $order['ebook'][0] ) ? (int) $order['ebook'][0] : 0;
7238 $ebook_title = $ebook_id ? get_the_title( $ebook_id ) : '';
7239 $order_id = isset( $order['order_id'][0] ) ? (int) $order['order_id'][0] : 0;
7240 $buyer_name = trim( ( isset( $order['first_name'][0] ) ? $order['first_name'][0] : '' ) . ' ' . ( isset( $order['last_name'][0] ) ? $order['last_name'][0] : '' ) );
7241 $payer_email = isset( $order['payer_email'][0] ) ? (string) $order['payer_email'][0] : '';
7242 $payment_date = isset( $order['payment_date'][0] ) ? (string) $order['payment_date'][0] : '';
7243 $payment_type = isset( $order['payment_type'][0] ) ? (string) $order['payment_type'][0] : '';
7244 $payment_status = isset( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : '';
7245 $txn_id = isset( $order['txn_id'][0] ) ? (string) $order['txn_id'][0] : '';
7246 $gross = isset( $order['mc_gross'][0] ) ? (string) $order['mc_gross'][0] : '';
7247 $currency = isset( $order['mc_currency'][0] ) && $order['mc_currency'][0] !== '' ? (string) $order['mc_currency'][0] : ebook_store_get_store_currency();
7248 $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] : '' );
7249 $city = isset( $order['address_city'][0] ) ? (string) $order['address_city'][0] : '';
7250 $state = isset( $order['address_state'][0] ) ? (string) $order['address_state'][0] : '';
7251 $zip = isset( $order['address_zip'][0] ) ? (string) $order['address_zip'][0] : '';
7252 $address_street = isset( $order['address_street'][0] ) ? (string) $order['address_street'][0] : '';
7253
7254 $content .= '<section class="ebook-store-verify__grid">';
7255 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Order Record', 'ebook-store' ) . '</h2>';
7256 $content .= ebook_store_render_public_order_field( __( 'Order ID', 'ebook-store' ), $order_id ? '#' . $order_id : '' );
7257 $content .= ebook_store_render_public_order_field( __( 'Ebook', 'ebook-store' ), $ebook_title );
7258 $content .= ebook_store_render_public_order_field( __( 'Ebook ID', 'ebook-store' ), $ebook_id ? (string) $ebook_id : '' );
7259 $content .= ebook_store_render_public_order_field( __( 'Payment date', 'ebook-store' ), $payment_date );
7260 $content .= ebook_store_render_public_order_field( __( 'Gateway', 'ebook-store' ), $payment_type !== '' ? $payment_type : __( 'PayPal', 'ebook-store' ) );
7261 $content .= ebook_store_render_public_order_field( __( 'Status', 'ebook-store' ), $payment_status );
7262 $content .= ebook_store_render_public_order_field( __( 'Transaction', 'ebook-store' ), $txn_id );
7263 $content .= ebook_store_render_public_order_field( __( 'Amount', 'ebook-store' ), trim( $gross . ' ' . $currency ) );
7264 $content .= '</article>';
7265 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Buyer Details', 'ebook-store' ) . '</h2>';
7266 $content .= ebook_store_render_public_order_field( __( 'Name', 'ebook-store' ), $buyer_name );
7267 $content .= ebook_store_render_public_order_field( __( 'Email', 'ebook-store' ), $payer_email );
7268 $content .= ebook_store_render_public_order_field( __( 'Street', 'ebook-store' ), $address_street );
7269 $content .= ebook_store_render_public_order_field( __( 'City', 'ebook-store' ), $city );
7270 $content .= ebook_store_render_public_order_field( __( 'State', 'ebook-store' ), $state );
7271 $content .= ebook_store_render_public_order_field( __( 'Postal code', 'ebook-store' ), $zip );
7272 $content .= ebook_store_render_public_order_field( __( 'Country', 'ebook-store' ), $country );
7273 $content .= ebook_store_render_public_order_field( __( 'Order key', 'ebook-store' ), $ebook_key );
7274 $content .= '</article>';
7275 $content .= '</section>';
7276 $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>';
7277 } elseif ( $found_wc_order ) {
7278 $wc_titles = array();
7279 foreach ( $wc_order->get_items() as $wc_item ) {
7280 $wc_mapping = ebook_store_get_wc_item_ebook_mapping( $wc_item );
7281 $wc_ebook_id = isset( $wc_mapping['ebook_id'] ) ? (int) $wc_mapping['ebook_id'] : 0;
7282 if ( $wc_ebook_id > 0 ) {
7283 $wc_titles[] = get_the_title( $wc_ebook_id ) . ' (#' . $wc_ebook_id . ')';
7284 }
7285 }
7286 $wc_paid_date = $wc_order->get_date_paid();
7287 if ( ! $wc_paid_date ) {
7288 $wc_paid_date = $wc_order->get_date_created();
7289 }
7290 $wc_gateway = (string) $wc_order->get_payment_method_title();
7291
7292 $content .= '<section class="ebook-store-verify__grid">';
7293 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Order Record', 'ebook-store' ) . '</h2>';
7294 $content .= ebook_store_render_public_order_field( __( 'Order ID', 'ebook-store' ), '#' . $wc_order->get_id() );
7295 $content .= ebook_store_render_public_order_field( __( 'Ebook', 'ebook-store' ), implode( ', ', $wc_titles ) );
7296 $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' ) ) : '' );
7297 $content .= ebook_store_render_public_order_field( __( 'Gateway', 'ebook-store' ), $wc_gateway !== '' ? $wc_gateway : __( 'WooCommerce', 'ebook-store' ) );
7298 $content .= ebook_store_render_public_order_field( __( 'Status', 'ebook-store' ), wc_get_order_status_name( $wc_order->get_status() ) );
7299 $content .= ebook_store_render_public_order_field( __( 'Transaction', 'ebook-store' ), (string) $wc_order->get_transaction_id() );
7300 $content .= ebook_store_render_public_order_field( __( 'Amount', 'ebook-store' ), trim( $wc_order->get_total() . ' ' . $wc_order->get_currency() ) );
7301 $content .= '</article>';
7302 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Buyer Details', 'ebook-store' ) . '</h2>';
7303 $content .= ebook_store_render_public_order_field( __( 'Name', 'ebook-store' ), trim( (string) $wc_order->get_formatted_billing_full_name() ) );
7304 $content .= ebook_store_render_public_order_field( __( 'Email', 'ebook-store' ), (string) $wc_order->get_billing_email() );
7305 $content .= ebook_store_render_public_order_field( __( 'Street', 'ebook-store' ), (string) $wc_order->get_billing_address_1() );
7306 $content .= ebook_store_render_public_order_field( __( 'City', 'ebook-store' ), (string) $wc_order->get_billing_city() );
7307 $content .= ebook_store_render_public_order_field( __( 'State', 'ebook-store' ), (string) $wc_order->get_billing_state() );
7308 $content .= ebook_store_render_public_order_field( __( 'Postal code', 'ebook-store' ), (string) $wc_order->get_billing_postcode() );
7309 $content .= ebook_store_render_public_order_field( __( 'Country', 'ebook-store' ), (string) $wc_order->get_billing_country() );
7310 $content .= ebook_store_render_public_order_field( __( 'Order key', 'ebook-store' ), $ebook_key );
7311 $content .= '</article>';
7312 $content .= '</section>';
7313 $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>';
7314 } else {
7315 $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>';
7316 }
7317
7318 $content .= '<div class="ebook-store-verify__footer">' . esc_html( $site_name ) . '</div></main></body></html>';
7319
7320 echo $content;
7321 exit;
7322 }
7323
7324 /**
7325 * Clean a string that is about to be drawn into a PDF watermark.
7326 *
7327 * PDF text is NOT HTML, so esc_attr()/esc_html() are wrong here — they turn
7328 * "Tom & Jerry" into "Tom &amp; Jerry" inside the document. Just strip control
7329 * characters and collapse whitespace.
7330 *
7331 * @param string $text Raw text (may contain %%merge%% codes before substitution).
7332 * @return string
7333 */
7334 function ebook_store_sanitize_pdf_text( $text ) {
7335 $text = (string) $text;
7336 // Drop ASCII control characters except tab/newline, then trim.
7337 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text );
7338
7339 return trim( (string) $text );
7340 }
7341
7342 /**
7343 * Resolve the buyer watermark text for an order, substituting the %%merge%%
7344 * codes. Shared by the PDF and EPUB watermark paths so the tokens resolve the
7345 * same way: WooCommerce-style meta first, then the built-in order meta.
7346 *
7347 * @param int $order_id Order post ID.
7348 * @param string $template Raw template, e.g. "Licensed to %%first_name%% %%last_name%%".
7349 * @return string Resolved, control-char-stripped text (NOT HTML-escaped).
7350 */
7351 function ebook_store_resolve_buyer_watermark( $order_id, $template ) {
7352 $order_id = (int) $order_id;
7353 $meta = $order_id > 0 ? get_post_meta( $order_id ) : array();
7354 $meta = is_array( $meta ) ? $meta : array();
7355 $mv = function ( $key ) use ( $meta ) {
7356 return isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
7357 };
7358
7359 $tokens = array(
7360 'payer_email' => $mv( '_billing_email' ),
7361 'payment_date' => $mv( '_paid_date' ),
7362 'txn_id' => $mv( '_transaction_id' ),
7363 'first_name' => $mv( '_billing_first_name' ),
7364 'last_name' => $mv( '_billing_last_name' ),
7365 'residence_country' => $mv( '_billing_country' ),
7366 );
7367
7368 // Built-in (non-Woo) orders store the buyer under their own keys. Read each
7369 // field from its own key (an earlier copy of this logic took first_name from
7370 // the last_name meta — fixed here).
7371 if ( $tokens['payer_email'] === '' ) {
7372 $tokens['payer_email'] = $mv( 'payer_email' );
7373 $tokens['payment_date'] = $mv( 'payment_date' );
7374 $tokens['txn_id'] = $mv( 'txn_id' );
7375 $tokens['first_name'] = $mv( 'first_name' );
7376 $tokens['last_name'] = $mv( 'last_name' );
7377 $tokens['residence_country'] = $mv( 'residence_country' );
7378 }
7379
7380 // Under HPOS a WooCommerce order has no post meta at all, so read the buyer
7381 // through the order object when both meta lookups came back empty.
7382 if ( $tokens['payer_email'] === '' && $order_id > 0 && function_exists( 'wc_get_order' ) ) {
7383 $wc_order = wc_get_order( $order_id );
7384 if ( $wc_order && is_a( $wc_order, 'WC_Order' ) ) {
7385 $wc_paid_date = $wc_order->get_date_paid();
7386 if ( ! $wc_paid_date ) {
7387 $wc_paid_date = $wc_order->get_date_created();
7388 }
7389 $tokens['payer_email'] = (string) $wc_order->get_billing_email();
7390 $tokens['payment_date'] = $wc_paid_date ? $wc_paid_date->date_i18n( 'Y-m-d H:i:s' ) : '';
7391 $tokens['txn_id'] = (string) $wc_order->get_transaction_id();
7392 $tokens['first_name'] = (string) $wc_order->get_billing_first_name();
7393 $tokens['last_name'] = (string) $wc_order->get_billing_last_name();
7394 $tokens['residence_country'] = (string) $wc_order->get_billing_country();
7395 }
7396 }
7397
7398 $text = ebook_store_sanitize_pdf_text( (string) $template );
7399 foreach ( $tokens as $key => $value ) {
7400 if ( is_scalar( $value ) ) {
7401 $text = str_replace( '%%' . $key . '%%', ebook_store_sanitize_pdf_text( (string) $value ), $text );
7402 }
7403 }
7404
7405 return $text;
7406 }
7407
7408 /**
7409 * Build the visible watermark HTML fragment injected into EPUB content.
7410 *
7411 * Inline CSS only, so no stylesheet or manifest edit is needed. dir="auto" lets
7412 * the reader lay the buyer line out right-to-left for Hebrew/Arabic names.
7413 *
7414 * @param string $text Buyer text (raw, not yet escaped).
7415 * @param string $style 'subtle' | 'box'.
7416 * @return string XML-well-formed XHTML fragment.
7417 */
7418 function ebook_store_epub_watermark_fragment( $text, $style ) {
7419 $wm = htmlspecialchars( (string) $text, ENT_QUOTES, 'UTF-8' );
7420 if ( $style === 'box' ) {
7421 $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';
7422 } else {
7423 $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';
7424 }
7425
7426 return '<div class="ebook-store-wm" dir="auto" style="' . $css . '">' . $wm . '</div>';
7427 }
7428
7429 /**
7430 * Insert an XHTML fragment into a content document via string ops, NOT DOM.
7431 *
7432 * Real content XHTML routinely contains named entities like &nbsp; that a strict
7433 * DOM parser rejects, so we never DOM-parse content documents — we splice the
7434 * fragment next to the <body> tag and leave everything else byte-for-byte intact.
7435 *
7436 * @param string $html The XHTML document.
7437 * @param string $fragment The fragment to insert.
7438 * @param string $position 'top' | 'bottom'.
7439 * @return string|null Modified document, or null if no <body> boundary was found.
7440 */
7441 function ebook_store_epub_inject_fragment( $html, $fragment, $position ) {
7442 $html = (string) $html;
7443
7444 if ( $position === 'top' ) {
7445 if ( preg_match( '/<body\b[^>]*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
7446 $at = $m[0][1] + strlen( $m[0][0] );
7447 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
7448 }
7449 return null;
7450 }
7451
7452 if ( preg_match( '/<\/body\s*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
7453 $at = $m[0][1];
7454 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
7455 }
7456
7457 return null;
7458 }
7459
7460 /**
7461 * Relative path from a directory inside the EPUB to a file inside it, so an
7462 * <img> in a content document (or a manifest href) can point at the QR image
7463 * wherever the two happen to sit in the archive.
7464 *
7465 * @param string $from_dir Directory of the referencing file (archive path).
7466 * @param string $to_path Target file (archive path).
7467 * @return string Relative reference.
7468 */
7469 function ebook_store_epub_relative_path( $from_dir, $to_path ) {
7470 $from_dir = ( $from_dir === '.' || $from_dir === '/' ) ? '' : trim( (string) $from_dir, '/' );
7471 $to_path = ltrim( (string) $to_path, '/' );
7472 $from = $from_dir === '' ? array() : explode( '/', $from_dir );
7473 $to = explode( '/', $to_path );
7474 while ( ! empty( $from ) && ! empty( $to ) && $from[0] === $to[0] ) {
7475 array_shift( $from );
7476 array_shift( $to );
7477 }
7478 $rel = str_repeat( '../', count( $from ) ) . implode( '/', $to );
7479 return $rel === '' ? basename( $to_path ) : $rel;
7480 }
7481
7482 /**
7483 * Render a QR code to PNG bytes on a solid white background with a quiet zone,
7484 * so it stays scannable on any reader background (including night mode).
7485 *
7486 * Uses the bundled TCPDF 2D-barcode generator, loaded narrowly so the EPUB path
7487 * never pulls in the full PDF engine. Returns '' on any problem (fail-open: the
7488 * book is still delivered, just without a QR).
7489 *
7490 * @param string $text The URL/text to encode.
7491 * @param int $module_px Pixels per QR module.
7492 * @param int $quiet Quiet-zone width in modules.
7493 * @return string PNG bytes, or '' if a QR could not be produced.
7494 */
7495 function ebook_store_generate_qr_png( $text, $module_px = 8, $quiet = 4 ) {
7496 $text = (string) $text;
7497 if ( $text === '' || ! function_exists( 'imagecreatetruecolor' ) || ! function_exists( 'imagepng' ) ) {
7498 return '';
7499 }
7500
7501 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
7502 $base = __DIR__ . '/fpdi/vendor/tecnickcom/tcpdf';
7503 $qr = $base . '/include/barcodes/qrcode.php';
7504 $bc = $base . '/tcpdf_barcodes_2d.php';
7505 if ( is_readable( $qr ) && is_readable( $bc ) ) {
7506 require_once $qr;
7507 require_once $bc;
7508 }
7509 }
7510 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
7511 return '';
7512 }
7513
7514 try {
7515 $obj = new TCPDF2DBarcode( $text, 'QRCODE,M' );
7516 $arr = $obj->getBarcodeArray();
7517 if ( empty( $arr['num_rows'] ) || empty( $arr['num_cols'] ) || empty( $arr['bcode'] ) ) {
7518 return '';
7519 }
7520 $rows = (int) $arr['num_rows'];
7521 $cols = (int) $arr['num_cols'];
7522 $w = ( $cols + 2 * $quiet ) * $module_px;
7523 $h = ( $rows + 2 * $quiet ) * $module_px;
7524
7525 $img = imagecreatetruecolor( $w, $h );
7526 $white = imagecolorallocate( $img, 255, 255, 255 );
7527 $black = imagecolorallocate( $img, 0, 0, 0 );
7528 imagefilledrectangle( $img, 0, 0, $w - 1, $h - 1, $white );
7529 for ( $r = 0; $r < $rows; $r++ ) {
7530 for ( $c = 0; $c < $cols; $c++ ) {
7531 if ( ! empty( $arr['bcode'][ $r ][ $c ] ) ) {
7532 $x0 = ( $c + $quiet ) * $module_px;
7533 $y0 = ( $r + $quiet ) * $module_px;
7534 imagefilledrectangle( $img, $x0, $y0, $x0 + $module_px - 1, $y0 + $module_px - 1, $black );
7535 }
7536 }
7537 }
7538 ob_start();
7539 imagepng( $img );
7540 $data = ob_get_clean();
7541 imagedestroy( $img );
7542
7543 return is_string( $data ) ? $data : '';
7544 } catch ( \Throwable $e ) {
7545 return '';
7546 }
7547 }
7548
7549 /**
7550 * The XHTML fragment that shows the QR image, pointing at a relative src.
7551 *
7552 * @param string $src Relative image reference from the referencing document.
7553 * @return string Well-formed XHTML fragment.
7554 */
7555 function ebook_store_epub_qr_fragment( $src ) {
7556 $src = htmlspecialchars( (string) $src, ENT_QUOTES, 'UTF-8' );
7557 $alt = htmlspecialchars( __( 'Scan to verify this copy', 'ebook-store' ), ENT_QUOTES, 'UTF-8' );
7558 return '<div class="ebook-store-qr" style="text-align:center;margin:1.2em 0">'
7559 . '<img src="' . $src . '" alt="' . $alt . '" style="width:120px;height:120px;max-width:40%"/></div>';
7560 }
7561
7562 /**
7563 * Watermark an EPUB with the buyer's identity (social DRM) while keeping it a
7564 * valid, reader-compatible book.
7565 *
7566 * EPUB cannot be password-encrypted and still open in e-readers, so instead the
7567 * buyer line is injected into the content and (optionally) the OPF metadata, and
7568 * the archive is rewritten with "mimetype" kept FIRST and STORED. This is a plain
7569 * function that never loads the PDF stack, and it FAILS OPEN: on any problem it
7570 * returns the untouched $src_path so a paid download is never blocked. It never
7571 * throws.
7572 *
7573 * @param string $src_path Absolute path to the source .epub.
7574 * @param string $dest_path Absolute path to write the watermarked copy to.
7575 * @param string $watermark_text Buyer text, already merge-resolved (raw, not escaped).
7576 * @param array $opts scope|position|style|stamp_metadata|order_id.
7577 * @return string $dest_path on success, else $src_path.
7578 */
7579 function ebook_store_watermark_epub( $src_path, $dest_path, $watermark_text, $opts = array() ) {
7580 if ( ! class_exists( 'ZipArchive' ) || ! class_exists( 'DOMDocument' ) || ! is_readable( $src_path ) ) {
7581 return $src_path;
7582 }
7583
7584 $log = function ( $message, $context = array() ) {
7585 if ( function_exists( 'ebook_store_log' ) ) {
7586 ebook_store_log( 'epub', $message, $context );
7587 }
7588 };
7589
7590 // The archive is held in memory ~twice while repacking, and an OOM is
7591 // uncatchable, so refuse very large books rather than 500 the download.
7592 $max = (int) apply_filters( 'ebook_store_epub_watermark_max_bytes', 64 * 1024 * 1024 );
7593 $size = @filesize( $src_path );
7594 if ( $size !== false && $size > $max ) {
7595 $log( 'too large, serving source', array( 'bytes' => $size ) );
7596 return $src_path;
7597 }
7598 if ( function_exists( 'ebook_store_raise_pdf_memory' ) ) {
7599 ebook_store_raise_pdf_memory();
7600 }
7601
7602 try {
7603 $scope = in_array( isset( $opts['scope'] ) ? $opts['scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['scope'] : 'first';
7604 $position = in_array( isset( $opts['position'] ) ? $opts['position'] : '', array( 'top', 'bottom' ), true ) ? $opts['position'] : 'bottom';
7605 $style = in_array( isset( $opts['style'] ) ? $opts['style'] : '', array( 'subtle', 'box' ), true ) ? $opts['style'] : 'subtle';
7606 $stamp = ! empty( $opts['stamp_metadata'] );
7607 $order_id = isset( $opts['order_id'] ) ? (int) $opts['order_id'] : 0;
7608
7609 // QR code carries its own placement so the buyer can put it where they like.
7610 $qr_scope = in_array( isset( $opts['qr_scope'] ) ? $opts['qr_scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['qr_scope'] : 'first';
7611 $qr_position = in_array( isset( $opts['qr_position'] ) ? $opts['qr_position'] : '', array( 'top', 'bottom' ), true ) ? $opts['qr_position'] : 'bottom';
7612 $qr_text = isset( $opts['qr_text'] ) ? trim( (string) $opts['qr_text'] ) : '';
7613 $qr_enabled = ! empty( $opts['qr'] ) && $qr_text !== '';
7614
7615 $text = ebook_store_sanitize_pdf_text( (string) $watermark_text );
7616 if ( $text === '' && ! $qr_enabled && ! $stamp ) {
7617 return $src_path; // nothing to add
7618 }
7619
7620 // --- STEP 1: read the whole archive into memory, then close it. ---
7621 $in = new ZipArchive();
7622 if ( $in->open( $src_path ) !== true ) {
7623 return $src_path;
7624 }
7625 $entries = array();
7626 $order = array();
7627 for ( $i = 0; $i < $in->numFiles; $i++ ) {
7628 $stat = $in->statIndex( $i );
7629 if ( $stat === false ) {
7630 $in->close();
7631 return $src_path;
7632 }
7633 $name = $stat['name'];
7634 if ( $name === '' || substr( $name, -1 ) === '/' ) {
7635 continue; // directory marker
7636 }
7637 $content = $in->getFromIndex( $i );
7638 if ( $content === false ) {
7639 $in->close();
7640 return $src_path; // corrupt entry: never ship a hollow book
7641 }
7642 if ( ! isset( $entries[ $name ] ) ) {
7643 $order[] = $name;
7644 }
7645 $entries[ $name ] = $content;
7646 }
7647 $in->close();
7648
7649 if ( ! isset( $entries['mimetype'] ) || trim( $entries['mimetype'] ) !== 'application/epub+zip' ) {
7650 return $src_path; // not a real EPUB, don't touch it
7651 }
7652
7653 // --- STEP 2: never rewrite signed or DRM-encrypted containers. ---
7654 if ( isset( $entries['META-INF/signatures.xml'] ) ) {
7655 $log( 'signed container, serving source' );
7656 return $src_path;
7657 }
7658 if ( isset( $entries['META-INF/encryption.xml'] ) ) {
7659 $enc = $entries['META-INF/encryption.xml'];
7660 $font_obfuscation_only = ( strpos( $enc, 'http://www.idpf.org/2008/embedding' ) !== false || strpos( $enc, 'http://ns.adobe.com/pdf/enc#RC' ) !== false )
7661 && stripos( $enc, 'xmlenc#aes' ) === false
7662 && stripos( $enc, 'adept' ) === false;
7663 if ( ! $font_obfuscation_only ) {
7664 $log( 'encrypted/DRM container, serving source' );
7665 return $src_path;
7666 }
7667 // Font obfuscation: every entry name/bytes are preserved verbatim and
7668 // dc:identifier is never touched, so the deobfuscation key still derives.
7669 }
7670
7671 // --- STEP 3: container.xml -> OPF path (clean XML, DOM-safe). ---
7672 $opf_path = '';
7673 if ( isset( $entries['META-INF/container.xml'] ) ) {
7674 $cx = new DOMDocument();
7675 $prev = libxml_use_internal_errors( true );
7676 $loaded = $cx->loadXML( $entries['META-INF/container.xml'], LIBXML_NONET );
7677 libxml_clear_errors();
7678 libxml_use_internal_errors( $prev );
7679 if ( $loaded ) {
7680 foreach ( $cx->getElementsByTagName( 'rootfile' ) as $rf ) {
7681 $fp = $rf->getAttribute( 'full-path' );
7682 if ( $fp !== '' ) {
7683 $opf_path = $fp;
7684 break;
7685 }
7686 }
7687 }
7688 }
7689 if ( $opf_path === '' || ! isset( $entries[ $opf_path ] ) ) {
7690 return $src_path;
7691 }
7692 $opf_dir = trim( dirname( $opf_path ), '/.' );
7693 $resolve = function ( $href ) use ( $opf_dir ) {
7694 $href = preg_replace( '/[?#].*$/', '', (string) $href );
7695 $path = ( $opf_dir === '' ? '' : $opf_dir . '/' ) . $href;
7696 $out = array();
7697 foreach ( explode( '/', $path ) as $seg ) {
7698 if ( $seg === '' || $seg === '.' ) {
7699 continue;
7700 }
7701 if ( $seg === '..' ) {
7702 array_pop( $out );
7703 continue;
7704 }
7705 $out[] = $seg;
7706 }
7707 return implode( '/', $out );
7708 };
7709
7710 // --- STEP 4: parse the OPF (clean XML), read version, manifest, spine. ---
7711 $opf = new DOMDocument();
7712 $prev = libxml_use_internal_errors( true );
7713 $opf_loaded = $opf->loadXML( $entries[ $opf_path ], LIBXML_NONET );
7714 libxml_clear_errors();
7715 libxml_use_internal_errors( $prev );
7716 if ( ! $opf_loaded ) {
7717 return $src_path;
7718 }
7719
7720 $manifest_href = array();
7721 $manifest_media = array();
7722 foreach ( $opf->getElementsByTagName( 'item' ) as $item ) {
7723 $id = $item->getAttribute( 'id' );
7724 if ( $id === '' ) {
7725 continue;
7726 }
7727 $manifest_href[ $id ] = $resolve( $item->getAttribute( 'href' ) );
7728 $manifest_media[ $id ] = $item->getAttribute( 'media-type' );
7729 }
7730 $spine_docs = array();
7731 foreach ( $opf->getElementsByTagName( 'itemref' ) as $iref ) {
7732 $idref = $iref->getAttribute( 'idref' );
7733 if ( $idref === '' || ! isset( $manifest_href[ $idref ] ) ) {
7734 continue;
7735 }
7736 $href = $manifest_href[ $idref ];
7737 $media = isset( $manifest_media[ $idref ] ) ? $manifest_media[ $idref ] : '';
7738 if ( $media === 'application/xhtml+xml' && isset( $entries[ $href ] ) ) {
7739 $spine_docs[] = $href;
7740 }
7741 }
7742
7743 $applied = false;
7744 $opf_dirty = false;
7745
7746 $manifest_node = $opf->getElementsByTagName( 'manifest' )->item( 0 );
7747 $spine_node = $opf->getElementsByTagName( 'spine' )->item( 0 );
7748
7749 // Which documents a scope targets: a list of content docs, or the string
7750 // 'colophon' (a dedicated page we append).
7751 $targets_for = function ( $target_scope ) use ( $spine_docs ) {
7752 if ( $target_scope === 'colophon' ) {
7753 return 'colophon';
7754 }
7755 if ( empty( $spine_docs ) ) {
7756 return array();
7757 }
7758 return $target_scope === 'every' ? $spine_docs : array( $spine_docs[0] );
7759 };
7760
7761 // Prepare the QR image up front (fail-open: no QR if it can't be made).
7762 $qr_png = '';
7763 $qr_abs = '';
7764 if ( $qr_enabled ) {
7765 $qr_png = ebook_store_generate_qr_png( $qr_text );
7766 if ( $qr_png === '' ) {
7767 $qr_enabled = false;
7768 $log( 'QR unavailable (no GD/barcode support), delivering without QR' );
7769 } else {
7770 $n = 0;
7771 do {
7772 $qr_rel = 'ebook-store-qr' . ( $n === 0 ? '' : '-' . $n ) . '.png';
7773 $qr_abs = $resolve( $qr_rel );
7774 $n++;
7775 } while ( isset( $entries[ $qr_abs ] ) && $n < 50 );
7776 }
7777 }
7778
7779 $text_targets = $text !== '' ? $targets_for( $scope ) : array();
7780 $qr_targets = $qr_enabled ? $targets_for( $qr_scope ) : array();
7781 $qr_referenced = false;
7782
7783 // --- STEP 5: inject text and/or QR into existing content documents. ---
7784 $content_targets = array();
7785 if ( is_array( $text_targets ) ) {
7786 $content_targets = array_merge( $content_targets, $text_targets );
7787 }
7788 if ( is_array( $qr_targets ) ) {
7789 $content_targets = array_merge( $content_targets, $qr_targets );
7790 }
7791 $content_targets = array_values( array_unique( $content_targets ) );
7792
7793 foreach ( $content_targets as $doc ) {
7794 $html = $entries[ $doc ];
7795 $changed = false;
7796
7797 if ( is_array( $text_targets ) && in_array( $doc, $text_targets, true ) ) {
7798 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_watermark_fragment( $text, $style ), $position );
7799 if ( $new !== null ) {
7800 $html = $new;
7801 $changed = true;
7802 } else {
7803 $log( 'no <body> boundary, doc left unmarked', array( 'doc' => $doc ) );
7804 }
7805 }
7806
7807 if ( is_array( $qr_targets ) && in_array( $doc, $qr_targets, true ) ) {
7808 $qr_src = ebook_store_epub_relative_path( dirname( $doc ), $qr_abs );
7809 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_qr_fragment( $qr_src ), $qr_position );
7810 if ( $new !== null ) {
7811 $html = $new;
7812 $changed = true;
7813 $qr_referenced = true;
7814 } else {
7815 $log( 'no <body> boundary, QR not placed', array( 'doc' => $doc ) );
7816 }
7817 }
7818
7819 if ( $changed ) {
7820 $entries[ $doc ] = $html;
7821 $applied = true;
7822 }
7823 }
7824
7825 // --- STEP 6: colophon page carrying whatever targets 'colophon'. ---
7826 if ( ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) && $manifest_node && $spine_node ) {
7827 $n = 0;
7828 do {
7829 $rel = 'ebook-store-licence' . ( $n === 0 ? '' : '-' . $n ) . '.xhtml';
7830 $abs = $resolve( $rel );
7831 $n++;
7832 } while ( isset( $entries[ $abs ] ) && $n < 50 );
7833
7834 $body = '';
7835 if ( $text_targets === 'colophon' && $text !== '' ) {
7836 $body .= ebook_store_epub_watermark_fragment( $text, 'box' );
7837 }
7838 if ( $qr_targets === 'colophon' ) {
7839 $body .= ebook_store_epub_qr_fragment( ebook_store_epub_relative_path( dirname( $abs ), $qr_abs ) );
7840 $qr_referenced = true;
7841 }
7842
7843 $page = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
7844 . '<!DOCTYPE html>' . "\n"
7845 . '<html xmlns="http://www.w3.org/1999/xhtml"><head><meta charset="utf-8"/><title>'
7846 . htmlspecialchars( __( 'Licence', 'ebook-store' ), ENT_QUOTES, 'UTF-8' )
7847 . '</title></head><body>' . $body . '</body></html>';
7848
7849 $mid = 'ebook-store-licence';
7850 $existing_ids = array_keys( $manifest_href );
7851 $k = 0;
7852 while ( in_array( $mid, $existing_ids, true ) ) {
7853 $k++;
7854 $mid = 'ebook-store-licence-' . $k;
7855 }
7856
7857 $entries[ $abs ] = $page;
7858 $order[] = $abs;
7859
7860 $item_el = $opf->createElement( 'item' );
7861 $item_el->setAttribute( 'id', $mid );
7862 $item_el->setAttribute( 'href', $rel );
7863 $item_el->setAttribute( 'media-type', 'application/xhtml+xml' );
7864 $manifest_node->appendChild( $item_el );
7865
7866 $iref_el = $opf->createElement( 'itemref' );
7867 $iref_el->setAttribute( 'idref', $mid );
7868 $spine_node->appendChild( $iref_el );
7869
7870 $applied = true;
7871 $opf_dirty = true;
7872 } elseif ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) {
7873 $log( 'no manifest/spine, cannot add colophon' );
7874 }
7875
7876 // --- Register the QR image in the archive + manifest (once, if used). ---
7877 if ( $qr_referenced && $manifest_node ) {
7878 $entries[ $qr_abs ] = $qr_png;
7879 $order[] = $qr_abs;
7880
7881 $qr_id = 'ebook-store-qr';
7882 $existing_ids = array();
7883 foreach ( $opf->getElementsByTagName( 'item' ) as $it ) {
7884 $existing_ids[] = $it->getAttribute( 'id' );
7885 }
7886 $k = 0;
7887 while ( in_array( $qr_id, $existing_ids, true ) ) {
7888 $k++;
7889 $qr_id = 'ebook-store-qr-' . $k;
7890 }
7891 $qr_item = $opf->createElement( 'item' );
7892 $qr_item->setAttribute( 'id', $qr_id );
7893 $qr_item->setAttribute( 'href', ebook_store_epub_relative_path( $opf_dir, $qr_abs ) );
7894 $qr_item->setAttribute( 'media-type', 'image/png' );
7895 $manifest_node->appendChild( $qr_item );
7896
7897 $opf_dirty = true;
7898 }
7899
7900 // --- STEP 7: metadata stamp (DOM auto-escapes; pass raw text). ---
7901 if ( $stamp && $text !== '' ) {
7902 $metadata = $opf->getElementsByTagName( 'metadata' )->item( 0 );
7903 if ( $metadata ) {
7904 $dc_ns = 'http://purl.org/dc/elements/1.1/';
7905
7906 // Stamp the resolved buyer line verbatim; it already reads as a
7907 // licence statement, so we don't prepend our own prefix (which
7908 // would double up on the default "Licensed to ..." template).
7909 $contributor = $opf->createElementNS( $dc_ns, 'dc:contributor' );
7910 $contributor->appendChild( $opf->createTextNode( $text ) );
7911 $metadata->appendChild( $contributor );
7912
7913 $has_rights = false;
7914 foreach ( $metadata->getElementsByTagName( 'rights' ) as $unused ) {
7915 $has_rights = true;
7916 break;
7917 }
7918 if ( ! $has_rights ) {
7919 $rights = $opf->createElementNS( $dc_ns, 'dc:rights' );
7920 $rights->appendChild( $opf->createTextNode( $text ) );
7921 $metadata->appendChild( $rights );
7922 }
7923
7924 $meta_wm = $opf->createElement( 'meta' );
7925 $meta_wm->setAttribute( 'name', 'ebook-store:watermark' );
7926 $meta_wm->setAttribute( 'content', $text );
7927 $metadata->appendChild( $meta_wm );
7928
7929 if ( $order_id > 0 ) {
7930 $meta_order = $opf->createElement( 'meta' );
7931 $meta_order->setAttribute( 'name', 'ebook-store:order' );
7932 $meta_order->setAttribute( 'content', (string) $order_id );
7933 $metadata->appendChild( $meta_order );
7934 }
7935
7936 $applied = true;
7937 $opf_dirty = true;
7938 }
7939 }
7940
7941 // Re-serialize the OPF only if we changed it.
7942 if ( $opf_dirty ) {
7943 $xml = $opf->saveXML();
7944 if ( $xml !== false && $xml !== '' ) {
7945 $entries[ $opf_path ] = $xml; // saveXML()===false must never blank the OPF
7946 }
7947 }
7948
7949 if ( ! $applied ) {
7950 return $src_path; // nothing actually changed; don't rewrite
7951 }
7952
7953 // --- STEP 8: repack, mimetype FIRST + STORED (the make-or-break rule). ---
7954 $out = new ZipArchive();
7955 if ( $out->open( $dest_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== true ) {
7956 return $src_path;
7957 }
7958 $out->addFromString( 'mimetype', 'application/epub+zip' );
7959 // Class constant only — the global ZIP_CM_STORE is undefined on this build.
7960 $out->setCompressionName( 'mimetype', ZipArchive::CM_STORE );
7961 foreach ( $order as $name ) {
7962 if ( $name === 'mimetype' || ! isset( $entries[ $name ] ) || ! is_string( $entries[ $name ] ) ) {
7963 continue;
7964 }
7965 if ( ! $out->addFromString( $name, $entries[ $name ] ) ) {
7966 $out->close();
7967 @unlink( $dest_path );
7968 return $src_path;
7969 }
7970 }
7971 if ( $out->close() !== true || ! file_exists( $dest_path ) ) {
7972 @unlink( $dest_path );
7973 return $src_path;
7974 }
7975
7976 // --- STEP 9: verify the invariant held, else discard and fail open. ---
7977 $verify = new ZipArchive();
7978 if ( $verify->open( $dest_path ) !== true ) {
7979 @unlink( $dest_path );
7980 return $src_path;
7981 }
7982 $first = $verify->statIndex( 0 );
7983 $verify->close();
7984 if ( ! $first || $first['name'] !== 'mimetype' || (int) $first['comp_method'] !== ZipArchive::CM_STORE ) {
7985 @unlink( $dest_path );
7986 return $src_path;
7987 }
7988
7989 return $dest_path;
7990 } catch ( \Throwable $e ) {
7991 if ( file_exists( $dest_path ) ) {
7992 @unlink( $dest_path );
7993 }
7994 $log( 'watermark failed', array( 'err' => $e->getMessage() ) );
7995 return $src_path;
7996 }
7997 }
7998
7999 function ebook_encrypt_pdf($r = null, $post_id = null) {
8000 //@ini_set('display_errors',1);
8001 //@error_reporting(E_ALL);
8002 if ($r == false) {
8003 $r = $_REQUEST;
8004 }
8005 global $ebook_email_delivery, $ebook_qr_text, $ebook_png_path, $ebook_pngname, $attachment, $pdfHeaderText;
8006 require_once('fpdi/qrcode.class.php');
8007
8008 // Resolve the order this encryption belongs to first, so the output can go in
8009 // that order's protected cache directory (in uploads, HTTP-denied) rather than
8010 // the web-accessible plugin folder — and so a missing folder can't fatal.
8011 $target_order_id = 0;
8012 if ( ! empty( $r['order_id'] ) ) {
8013 $target_order_id = (int) $r['order_id'];
8014 } elseif ( ! empty( $ebook_email_delivery['order']['order_id'] ) ) {
8015 $target_order_id = (int) $ebook_email_delivery['order']['order_id'];
8016 } elseif ( ! empty( $post_id ) ) {
8017 $target_order_id = (int) $post_id;
8018 }
8019
8020 $cache_dir = ebook_store_get_cache_dir( $target_order_id );
8021 if ( $cache_dir === '' ) {
8022 // Uploads unusable — fall back to the system temp dir, never the web root.
8023 $cache_dir = untrailingslashit( get_temp_dir() );
8024 }
8025
8026 $qr_ebook_key = ( isset( $r['ebook_key'] ) && is_scalar( $r['ebook_key'] ) ) ? (string) $r['ebook_key'] : '';
8027 if ( $qr_ebook_key === '' && $target_order_id ) {
8028 $qr_ebook_key = (string) get_post_meta( $target_order_id, 'ebook_key', true );
8029 }
8030 $ebook_qr_text = ebook_store_get_order_verification_url( $qr_ebook_key );
8031 // Watermark text is drawn into a PDF, not HTML — esc_attr() would leak entities
8032 // ("Tom & Jerry" -> "Tom &amp; Jerry"). Strip control chars instead.
8033 $pdfHeaderText = ebook_store_sanitize_pdf_text( (string) get_option('buyer_info_text') );
8034
8035 foreach ($r as $k => $v) {
8036 if ( is_scalar( $v ) ) {
8037 $pdfHeaderText = str_replace("%%$k%%", ebook_store_sanitize_pdf_text( (string) $v ), $pdfHeaderText);
8038 }
8039 }
8040 // The QR code is now drawn natively by TCPDF as vectors; no PNG is fetched
8041 // from an external service, and no GD / allow_url_fopen is needed.
8042 if (@is_array($attachment)) {
8043 @$file = $attachment[0]['file'];
8044 }
8045 if (@!$file) {
8046 $file = $ebook_email_delivery['attachment'][0]['file'];
8047 }
8048 // ($target_order_id was resolved at the top so the cache dir could be built.)
8049 $password = ebook_store_get_password_for_order($target_order_id, array(
8050 'password' => isset($r['password']) ? $r['password'] : '',
8051 'payer_email' => isset($r['payer_email']) ? $r['payer_email'] : '',
8052 'md5_nonce' => isset($r['md5_nonce']) ? $r['md5_nonce'] : '',
8053 'ebook_key' => isset($r['ebook_key']) ? $r['ebook_key'] : '',
8054 ));
8055
8056 if (isset($ebook_email_delivery['order']) && is_array($ebook_email_delivery['order'])) {
8057 $ebook_email_delivery['order']['password'] = $password;
8058 }
8059
8060 //free files encrypt with user email?
8061
8062
8063 //$password = $r['payer_email'];
8064 // if (get_option('ebook_store_random_password') == 1) {
8065 // if ($ebook_store_random_password != '') {
8066 // $r['password'] = $ebook_store_random_password;
8067 // }
8068 // if (@$r['password'] != '') {
8069 // $password = $r['password'];
8070 // }
8071 // }
8072
8073 // if (get_option('ebook_store_blank_password') == 1) {
8074 // $r['password'] = '';
8075 // $password = '';
8076 // }
8077
8078 //random password for non logged in users
8079 // if (!is_user_logged_in() && get_option('formEnabled') < 1) {
8080 // if (get_option('ebook_store_random_password') == 1) {
8081 // $password = substr(md5(rand()),0,8);
8082 // //$password = 'proba';
8083 // $ebook_store_random_password = $password;
8084 // if ($post_id != null) {
8085 // @update_post_meta($post_id,'password',$password);
8086 // }
8087
8088 // // Additional code to handle the generated password
8089 // }
8090 //}
8091 // if (is_user_logged_in()) {
8092 // if (get_option('ebook_store_random_password') == 1) {
8093 // $password = substr(md5(rand()),0,8);
8094 // //$password = 'proba';
8095 // $ebook_store_random_password = $password;
8096 // if ($post_id != null) {
8097 // @update_post_meta($post_id,'password',$password);
8098 // }
8099
8100 // // Additional code to handle the generated password
8101 // } else {
8102 // if (get_option('ebook_store_blank_password') < 1) {
8103 // $current_user = wp_get_current_user();
8104 // $password = $current_user->user_email;
8105 // if ($post_id != null) {
8106 // @update_post_meta($post_id,'password',$password);
8107 // }
8108 // }
8109 // }
8110 // }
8111
8112 //wp_die($password . ' post id ' . $post_id);
8113 //error_log('password ' . $password);
8114
8115 $owner_password = (string) get_option('ebook_store_owner_password');
8116
8117 // Write the finished, buyer-watermarked PDF into the protected cache dir (in
8118 // uploads, HTTP-denied), NOT the plugin folder — those files carry the buyer's
8119 // email/IP in the watermark and were previously fetchable, since the plugin
8120 // .htaccess denies .txt/.log but not .pdf.
8121 $destfile = $cache_dir . '/' . basename( $file );
8122
8123
8124 if ( function_exists( 'ebook_store_raise_pdf_memory' ) ) {
8125 ebook_store_raise_pdf_memory();
8126 }
8127 $pdf = new QRPDF();
8128 $pdf->watermarkText = $pdfHeaderText;
8129 $pdf->qrText = $ebook_qr_text;
8130 $pdf->stampCount = 0;
8131 try {
8132 $pagecount = $pdf->setSourceFile($file);
8133 } catch (Exception $e) {
8134 wp_die( esc_html( sprintf(
8135 /* translators: %s: PDF import error. */
8136 __( 'This PDF could not be processed for protection. Please re-save it as a standard PDF or turn encryption off. Details: %s', 'ebook-store' ),
8137 $e->getMessage()
8138 ) ) );
8139 }
8140
8141 // Mark the order only now that generation is actually about to run, and keep
8142 // the previous value so a failure (exception, fatal, timeout) restores it
8143 // instead of leaving 'in_progress' behind for good.
8144 if ($target_order_id) {
8145 if ( ! isset( $GLOBALS['ebook_store_pdf_in_progress'] ) || ! is_array( $GLOBALS['ebook_store_pdf_in_progress'] ) ) {
8146 $GLOBALS['ebook_store_pdf_in_progress'] = array();
8147 }
8148 $GLOBALS['ebook_store_pdf_in_progress'][$target_order_id] = (string) get_post_meta($target_order_id,'encrypted_pdf',true);
8149 update_post_meta($target_order_id,'encrypted_pdf_started',time());
8150 update_post_meta($target_order_id,'encrypted_pdf','in_progress');
8151 register_shutdown_function('ebook_store_restore_pdf_marker', $target_order_id);
8152 }
8153
8154 $generated = false;
8155 try {
8156 for ($loop = 1; $loop <= $pagecount; $loop++) {
8157 $tplidx = $pdf->importPage($loop);
8158 $size = $pdf->getTemplateSize($tplidx);
8159 $pdf->AddPage($size['orientation'], array($size['width'], $size['height']));
8160 $pdf->useTemplate($tplidx);
8161 $pdf->stampCurrentPage();
8162 }
8163
8164 $pdf->applyProtection($password, $owner_password);
8165 $pdf->Output($destfile, 'F');
8166 $generated = file_exists($destfile);
8167 } finally {
8168 if ($target_order_id) {
8169 if ($generated) {
8170 unset($GLOBALS['ebook_store_pdf_in_progress'][$target_order_id]);
8171 delete_post_meta($target_order_id,'encrypted_pdf_started');
8172 update_post_meta($target_order_id,'encrypted_pdf',wp_slash($destfile));
8173 } else {
8174 ebook_store_restore_pdf_marker($target_order_id);
8175 }
8176 }
8177 }
8178 //make sure enc file is attached
8179 //file_put_contents('ebook-store-password-debug.txt', $password . "\n" . print_r($r, true), FILE_APPEND);
8180
8181 $ebook_email_delivery['attachment'][0]['file'] = $destfile;
8182 $isPdf = true;
8183 return $destfile;
8184 }
8185 /**
8186 * Puts an order's encrypted_pdf meta back to what it was before a generation
8187 * attempt that did not finish. Also registered as a shutdown function by
8188 * ebook_encrypt_pdf(), so a fatal or timeout mid-encryption cannot leave the
8189 * 'in_progress' marker behind permanently.
8190 */
8191 function ebook_store_restore_pdf_marker( $order_id ) {
8192 $order_id = (int) $order_id;
8193 if ( ! $order_id || ! isset( $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ] ) ) {
8194 return;
8195 }
8196 $previous = $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ];
8197 unset( $GLOBALS['ebook_store_pdf_in_progress'][ $order_id ] );
8198 delete_post_meta( $order_id, 'encrypted_pdf_started' );
8199 if ( $previous !== '' && $previous !== 'in_progress' ) {
8200 update_post_meta( $order_id, 'encrypted_pdf', wp_slash( $previous ) );
8201 } else {
8202 delete_post_meta( $order_id, 'encrypted_pdf' );
8203 }
8204 }
8205 function ebook_store_missing_gd() {
8206 ?>
8207 <div class="updated">
8208 <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>
8209 </div>
8210 <?php
8211 }
8212 function ebook_store_admin_notice() {
8213 $fix_url = wp_nonce_url( admin_url( 'admin-post.php?action=ebook_store_fix_thankyou' ), 'ebook_store_fix_thankyou' );
8214 ?>
8215 <div class="updated">
8216 <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>
8217 <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>
8218 </div>
8219 <?php
8220 }
8221 function ebook_store_admin_notice_paypal() {
8222 if ( ! ebook_store_is_paypal_enabled() ) {
8223 return;
8224 }
8225 ?>
8226 <div class="updated">
8227 <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>
8228 </div>
8229 <?php
8230 }
8231 function ebook_store_admin_notice_autocompleteorders() {
8232 ?>
8233 <div class="updated">
8234 <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>
8235 </div>
8236 <?php
8237 }
8238
8239 function ebook_store_set_messages($messages) {
8240 global $post, $post_ID;
8241 $post_type = get_post_type( $post_ID );
8242
8243 $obj = get_post_type_object($post_type);
8244 $singular = $obj->labels->singular_name;
8245 if ($post_type == 'ebook') {
8246 $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 . '"]\'>';
8247 }
8248 return $messages;
8249 }
8250 function ebook_store_remove_parmelink( $return ) {
8251 if (@$post_id == false) {
8252 $post_id = get_the_ID();
8253 }
8254 if ('ebook' === get_post_type( $post_id )) {
8255 $return = '';
8256 }
8257 return $return;
8258 }
8259
8260 function ebook_admin_css() {
8261 global $post_type;
8262 $post_types = array(
8263 /* set post types */
8264 'ebook',
8265 );
8266 if(in_array($post_type, $post_types))
8267 echo '<style type="text/css">#post-preview, #view-post-btn{display: none;}</style>';
8268 }
8269 function ebook_store_set_columns( $columns ) {
8270
8271 $columns = array(
8272 'cb' => '<input type="checkbox" />',
8273 'cover' => __( 'Cover', 'ebook-store' ),
8274 'title' => __( 'Ebook', 'ebook-store' ),
8275 'date' => __( 'Date', 'ebook-store' ),
8276 'price' => __( 'Price', 'ebook-store' ),
8277 'formats' => __( 'Formats', 'ebook-store' ),
8278 'sales' => __( 'Sales', 'ebook-store' ),
8279 'embed_code' => __('Shortcode & Links', 'ebook-store')
8280 );
8281
8282 return $columns;
8283 }
8284
8285 function ebook_store_columns_output( $column, $post_id ) {
8286 global $post;
8287 $ebook = new EbookStoreEbook($post_id);
8288 switch( $column ) {
8289 case 'embed_code':
8290 $shortcode_id = 'ebook-shortcode-' . $post_id;
8291 $link_id = 'ebook-direct-link-' . $post_id;
8292 $shortcode = '[ebook_store ebook_id="' . $post_id . '"]';
8293 $order_url = add_query_arg( 'task', 'direct_order', get_permalink( $post_id ) );
8294
8295 echo '<div class="ebook-list-shortcuts">';
8296 echo '<div class="ebook-list-shortcuts__field">';
8297 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Shortcode', 'ebook-store' ) . '</span>';
8298 echo '<div class="ebook-list-shortcuts__control">';
8299 echo '<input type="text" id="' . esc_attr( $shortcode_id ) . '" readonly value="' . esc_attr( $shortcode ) . '" />';
8300 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>';
8301 echo '</div>';
8302 echo '</div>';
8303 echo '<div class="ebook-list-shortcuts__field">';
8304 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Direct order link', 'ebook-store' ) . '</span>';
8305 echo '<div class="ebook-list-shortcuts__control">';
8306 echo '<input type="text" id="' . esc_attr( $link_id ) . '" readonly value="' . esc_attr( $order_url ) . '" />';
8307 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>';
8308 echo '</div>';
8309 echo '</div>';
8310 echo '</div>';
8311 break;
8312 case 'sales':
8313 $sales_index = ebook_store_get_ebook_sales_index();
8314 $sales = isset( $sales_index[ $post_id ] ) ? $sales_index[ $post_id ] : array(
8315 'orders' => 0,
8316 'total' => 0.0,
8317 'last_7d' => 0.0,
8318 'last_30d' => 0.0,
8319 'latest_order_gmt' => '',
8320 );
8321 $currency = ebook_store_get_store_currency();
8322 $latest_order = $sales['latest_order_gmt'] !== '' ? human_time_diff( strtotime( $sales['latest_order_gmt'] . ' UTC' ), current_time( 'timestamp', true ) ) : __( 'No orders yet', 'ebook-store' );
8323
8324 echo '<div class="ebook-list-sales">';
8325 echo '<strong class="ebook-list-sales__amount">' . esc_html( ebook_store_order_money_display( $sales['total'], $currency ) ) . '</strong>';
8326 echo '<div class="ebook-list-sales__pills">';
8327 echo '<span class="ebook-list-pill is-green">' . esc_html( sprintf( __( '%s orders', 'ebook-store' ), number_format_i18n( $sales['orders'] ) ) ) . '</span>';
8328 echo '<span class="ebook-list-pill">' . esc_html( sprintf( __( '7d %s', 'ebook-store' ), ebook_store_order_money_display( $sales['last_7d'], $currency ) ) ) . '</span>';
8329 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>';
8330 echo '</div>';
8331 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>';
8332 echo '</div>';
8333 break;
8334 case 'price':
8335 $ebook = get_post_meta( get_the_ID(), 'ebook', true );
8336 $price = 0.0;
8337 if ( ! is_array( $ebook ) ) {
8338 echo '<div class="ebook-list-price"><strong class="ebook-list-price__amount">-</strong></div>';
8339 break;
8340 }
8341 if ( isset( $ebook['ebook_price'] ) ) {
8342 $price = (float) $ebook['ebook_price'];
8343 }
8344 echo '<div class="ebook-list-price">';
8345 echo '<strong class="ebook-list-price__amount">' . esc_html( ebook_store_order_money_display( $price, ebook_store_get_store_currency() ) ) . '</strong>';
8346 echo '<div class="ebook-list-price__pills">';
8347 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>';
8348 if ( ! empty( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] === 'donate' ) {
8349 echo '<span class="ebook-list-pill">' . esc_html__( 'Donation mode', 'ebook-store' ) . '</span>';
8350 }
8351 echo '</div>';
8352 echo '</div>';
8353 break;
8354 case 'cover':
8355 $cover = get_post_meta( get_the_ID(), 'ebook_wp_custom_attachment_cover', true );
8356 $cover = is_array( $cover ) ? $cover : array();
8357
8358 echo '<div class="ebook-list-cover">';
8359 echo '<div class="ebook-list-cover__frame">';
8360 if ( ! empty( $cover['url'] ) ) {
8361 echo '<img src="' . esc_url( $cover['url'] ) . '" alt="" />';
8362 } else {
8363 echo '<span class="ebook-list-cover__placeholder">' . esc_html__( 'No cover uploaded', 'ebook-store' ) . '</span>';
8364 }
8365 echo '</div>';
8366 echo '<span class="ebook-list-status">' . esc_html( get_post_status( $post_id ) ) . '</span>';
8367 echo '</div>';
8368
8369 break;
8370 case 'formats':
8371 $formats_found = array();
8372 if ( is_array( $ebook->files ) ) {
8373 foreach ( array_keys( $ebook->files ) as $key => $value ) {
8374 if ( strpos( $value, '_size' ) === false ) {
8375 $format = $value;
8376 $icon_url = plugins_url( 'img/' . $value . '.png', __FILE__ );
8377 $title = basename( $ebook->files[ $format ] ) . ' ' . $ebook->human_filesize( $ebook->files[ $format . '_size' ] );
8378 $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>';
8379 }
8380 }
8381 echo '<div class="ebook-list-formats">';
8382 echo implode( '', $formats_found );
8383 if ( empty( $formats_found ) ) {
8384 echo '<span class="ebook-list-pill">' . esc_html__( 'No files attached', 'ebook-store' ) . '</span>';
8385 }
8386 echo '</div>';
8387 $testable = array();
8388 if ( ! empty( $ebook->files['pdf'] ) && file_exists( $ebook->files['pdf'] ) ) {
8389 $testable['pdf'] = __( 'PDF', 'ebook-store' );
8390 }
8391 if ( ! empty( $ebook->files['epub'] ) && file_exists( $ebook->files['epub'] ) ) {
8392 $testable['epub'] = __( 'ePub', 'ebook-store' );
8393 }
8394 if ( ! empty( $testable ) ) {
8395 $ebook_test_link = function ( $format ) use ( $post_id ) {
8396 return wp_nonce_url(
8397 admin_url( 'admin-post.php?action=ebook_store_test_encryption&ebook_id=' . $post_id . '&format=' . $format ),
8398 'ebook_store_test_encryption_' . $post_id
8399 );
8400 };
8401 if ( count( $testable ) === 1 ) {
8402 $only = array_key_first( $testable );
8403 $label = $only === 'epub'
8404 ? __( 'Test watermark', 'ebook-store' )
8405 : __( 'Test encryption (password: test)', 'ebook-store' );
8406 echo '<div class="ebook-format-test-link"><a class="button button-small" href="' . esc_url( $ebook_test_link( $only ) ) . '">' . esc_html( $label ) . '</a></div>';
8407 } else {
8408 // Both formats present: one Test control that asks which file to use.
8409 echo '<details class="ebook-format-test">';
8410 echo '<summary class="button button-small">' . esc_html__( 'Test protection…', 'ebook-store' ) . '</summary>';
8411 echo '<div class="ebook-format-test__menu">';
8412 echo '<span class="ebook-format-test__prompt">' . esc_html__( 'Test with which file?', 'ebook-store' ) . '</span>';
8413 foreach ( $testable as $format => $flabel ) {
8414 /* translators: %s: file format, e.g. PDF or ePub. */
8415 echo '<a class="button button-small" href="' . esc_url( $ebook_test_link( $format ) ) . '">' . esc_html( sprintf( __( 'Test %s', 'ebook-store' ), $flabel ) ) . '</a>';
8416 }
8417 echo '</div></details>';
8418 }
8419 }
8420 }
8421 break;
8422
8423 /* Just break out of the switch statement for everything else. */
8424 default :
8425 break;
8426 }
8427 }
8428 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
8429 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
8430 function ebook_store_pre_get_shortlink( $false, $post_id ) {
8431 return 'ebook' === get_post_type( $post_id ) ? '' : $false;
8432 }
8433 function ebook_mime_types($mime_types){
8434 // These have to be the real types. fileinfo reports an ePub as
8435 // application/epub+zip, and when the map here said octet-stream instead,
8436 // wp_check_filetype_and_ext() saw a mismatch and returned ext=false and
8437 // type=false for a perfectly valid book — which is what made the uploader
8438 // answer "that file type is not allowed here" for every .epub.
8439 $mime_types['epub'] = 'application/epub+zip';
8440 $mime_types['mobi'] = 'application/x-mobipocket-ebook';
8441 $mime_types['txt'] = 'text/plain';
8442 $mime_types['mp3'] = 'audio/mpeg';
8443
8444 return $mime_types;
8445 }
8446 /**
8447 * The directory that holds captured checkout-form data.
8448 *
8449 * @return string Absolute path with a trailing slash, or '' on failure.
8450 */
8451 function ebook_store_get_form_data_dir() {
8452 static $dir = null;
8453
8454 if ( $dir !== null ) {
8455 return $dir;
8456 }
8457
8458 $uploads = wp_upload_dir();
8459 if ( ! empty( $uploads['error'] ) ) {
8460 $dir = '';
8461 return $dir;
8462 }
8463
8464 $dir = trailingslashit( $uploads['basedir'] ) . 'ebook_store_data/';
8465
8466 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
8467 $dir = '';
8468 return $dir;
8469 }
8470
8471 // Buyer names and email addresses live here; they must not be fetchable.
8472 if ( ! file_exists( $dir . '.htaccess' ) ) {
8473 @file_put_contents( $dir . '.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" );
8474 }
8475 if ( ! file_exists( $dir . 'index.php' ) ) {
8476 @file_put_contents( $dir . 'index.php', "<?php\n// Silence is golden.\n" );
8477 }
8478
8479 return $dir;
8480 }
8481
8482 /**
8483 * Turn a request-supplied nonce into a safe, fixed-shape filename.
8484 *
8485 * The read side used to interpolate this value straight into a path, giving an
8486 * arbitrary-file-read primitive via ?md5_nonce=../../../etc/passwd.
8487 *
8488 * @param string $md5_nonce Value from the request.
8489 * @return string Filename, or '' when the value is not a plausible token.
8490 */
8491 function ebook_store_get_form_filename( $md5_nonce ) {
8492 $md5_nonce = (string) $md5_nonce;
8493
8494 // Every token the plugin issues is an md5() hash. Anything else (including
8495 // the literal "undefined" a button without data-md5_nonce used to send)
8496 // must not get a file of its own.
8497 if ( ! preg_match( '/^[a-fA-F0-9]{32}$/', $md5_nonce ) ) {
8498 return '';
8499 }
8500
8501 return 'ebook_store_form_' . $md5_nonce . '.json';
8502 }
8503
8504 function ebook_store_formContent() {
8505 $task = isset( $_REQUEST['task'] ) ? sanitize_key( wp_unslash( $_REQUEST['task'] ) ) : '';
8506
8507 if ( $task === 'formContent' ) {
8508 // The stored markup is authored by an administrator, but it is served to
8509 // anonymous visitors, so it still goes through wp_kses(). The 'post'
8510 // allowlist has no form controls, so those are added here (no script).
8511 $allowed_html = wp_kses_allowed_html( 'post' );
8512 $form_attributes = array_fill_keys( array( 'name', 'type', 'value', 'id', 'class', 'placeholder', 'required', 'action', 'method', 'for', 'checked', 'selected', 'rows', 'cols' ), true );
8513 foreach ( array( 'form', 'input', 'select', 'option', 'textarea', 'button', 'label' ) as $form_tag ) {
8514 $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;
8515 }
8516 $QSWPOptions = new QSWPOptions();
8517 echo wp_kses( get_option( 'formContent', $QSWPOptions->formContent ), $allowed_html );
8518 die();
8519 }
8520
8521 if ( $task === 'ebook_store_stripe_session' ) {
8522 // A Checkout Session is created here, when the buyer clicks, instead of
8523 // on every page render. No nonce: the button may be served from a page
8524 // cache. Nothing is written to the database and every value below is
8525 // resolved server-side, so the only thing worth guarding is the rate of
8526 // calls into the Stripe API.
8527 nocache_headers();
8528 header( 'Content-Type: application/json; charset=utf-8' );
8529
8530 $stripe_reply = function( $status, $payload ) {
8531 status_header( $status );
8532 echo wp_json_encode( $payload );
8533 die();
8534 };
8535
8536 if ( ! get_option( 'stripe_integration_enabled', 0 ) ) {
8537 $stripe_reply( 403, array( 'error' => __( 'Card checkout is not available on this store.', 'ebook-store' ) ) );
8538 }
8539
8540 $stripe_ebook_id = isset( $_REQUEST['ebook_id'] ) ? absint( wp_unslash( $_REQUEST['ebook_id'] ) ) : 0;
8541 $stripe_post = $stripe_ebook_id ? get_post( $stripe_ebook_id ) : null;
8542
8543 if ( ! $stripe_post || $stripe_post->post_type !== 'ebook' || $stripe_post->post_status !== 'publish' ) {
8544 $stripe_reply( 404, array( 'error' => __( 'This ebook is not available.', 'ebook-store' ) ) );
8545 }
8546
8547 $stripe_meta = get_post_meta( $stripe_ebook_id, 'ebook', true );
8548 $stripe_price = ( is_array( $stripe_meta ) && isset( $stripe_meta['ebook_price'] ) ) ? (float) $stripe_meta['ebook_price'] : 0;
8549
8550 if ( $stripe_price <= 0 ) {
8551 $stripe_reply( 400, array( 'error' => __( 'This ebook is not sold through card checkout.', 'ebook-store' ) ) );
8552 }
8553
8554 $stripe_rate_key = 'ebook_store_stripe_rl_' . md5( $stripe_ebook_id . '|' . ( isset( $_SERVER['REMOTE_ADDR'] ) ? (string) $_SERVER['REMOTE_ADDR'] : '' ) );
8555 if ( get_transient( $stripe_rate_key ) ) {
8556 $stripe_reply( 429, array( 'error' => __( 'Please wait a moment and try again.', 'ebook-store' ) ) );
8557 }
8558 set_transient( $stripe_rate_key, 1, 5 );
8559
8560 // Same shape as the key the buy form generates, so the thank-you page and
8561 // the webhook recognise it exactly as before.
8562 $stripe_order_key = md5( NONCE_KEY . $stripe_ebook_id . $stripe_price . mt_rand( 1, 100000 ) );
8563
8564 $stripe_session_url = ebook_store_create_stripe_checkout(
8565 $stripe_ebook_id,
8566 ebook_store_price_plus_vat( $stripe_price ),
8567 get_the_title( $stripe_ebook_id ),
8568 $stripe_order_key
8569 );
8570
8571 if ( empty( $stripe_session_url ) ) {
8572 // The buyer is invited to try again, so the next click must not
8573 // have to wait out the limiter.
8574 delete_transient( $stripe_rate_key );
8575 $stripe_reply( 502, array( 'error' => __( 'Sorry, the checkout could not be opened. Please try again.', 'ebook-store' ) ) );
8576 }
8577
8578 $stripe_reply( 200, array( 'url' => $stripe_session_url ) );
8579 }
8580
8581 if ( $task === 'ebook_store_form_input' ) {
8582 ebook_store_save_form( isset( $_POST['md5_nonce'] ) ? wp_unslash( $_POST['md5_nonce'] ) : '', $_POST );
8583 }
8584 }
8585 /**
8586 * Persist the buyer details captured by the checkout form.
8587 *
8588 * Called from an unauthenticated request (the buyer has not logged in), so it
8589 * is deliberately narrow: a fixed filename shape, a size cap, a field cap, and
8590 * a rate limit so it cannot be used to fill the disk.
8591 *
8592 * @param string $md5_nonce Order token.
8593 * @param array $data Posted fields.
8594 */
8595 function ebook_store_save_form( $md5_nonce, $data ) {
8596 $filename = ebook_store_get_form_filename( $md5_nonce );
8597 $dir = ebook_store_get_form_data_dir();
8598
8599 if ( $filename === '' || $dir === '' ) {
8600 status_header( 400 );
8601 die( esc_html__( 'Invalid request.', 'ebook-store' ) );
8602 }
8603
8604 // One write per token per minute is plenty for a real checkout, and it stops
8605 // this endpoint being used as an unlimited disk-fill primitive.
8606 $rate_key = 'ebook_store_form_rl_' . md5( $filename );
8607 if ( get_transient( $rate_key ) ) {
8608 status_header( 429 );
8609 die( esc_html__( 'Please wait a moment and try again.', 'ebook-store' ) );
8610 }
8611 set_transient( $rate_key, 1, MINUTE_IN_SECONDS );
8612
8613 $json = array();
8614 $fields = 0;
8615 $skip = array( 'task', 'md5_nonce' );
8616
8617 foreach ( (array) $data as $k => $v ) {
8618 if ( ++$fields > 50 ) {
8619 break;
8620 }
8621 $key = sanitize_key( $k );
8622 if ( $key === '' || in_array( $key, $skip, true ) || ! is_scalar( $v ) ) {
8623 continue;
8624 }
8625 $json[ $key ] = sanitize_text_field( substr( (string) $v, 0, 500 ) );
8626 }
8627
8628 if ( file_put_contents( $dir . $filename, wp_json_encode( $json ), LOCK_EX ) === false ) {
8629 status_header( 500 );
8630 die( esc_html__( 'The form data could not be saved.', 'ebook-store' ) );
8631 }
8632
8633 status_header( 200 );
8634 die( esc_html__( 'Form data saved successfully.', 'ebook-store' ) );
8635 }
8636
8637 /**
8638 * Read back the buyer details captured at checkout.
8639 *
8640 * This used to read from get_temp_dir() with no '.json' suffix, i.e. a
8641 * different directory and a different filename than ebook_store_save_form()
8642 * writes, so it always returned false and every order fell back to
8643 * "Anonymous User".
8644 *
8645 * @param string $md5_nonce Order token.
8646 * @return object|false
8647 */
8648 function ebook_store_get_form( $md5_nonce ) {
8649 $filename = ebook_store_get_form_filename( $md5_nonce );
8650 $dir = ebook_store_get_form_data_dir();
8651
8652 if ( $filename === '' || $dir === '' ) {
8653 return false;
8654 }
8655
8656 $path = $dir . $filename;
8657 if ( ! is_readable( $path ) ) {
8658 return false;
8659 }
8660
8661 $form_data = file_get_contents( $path );
8662 if ( ! $form_data ) {
8663 return false;
8664 }
8665
8666 $decoded = json_decode( $form_data );
8667
8668 return $decoded === null ? false : $decoded;
8669 }
8670 function ebook_store_get_mailchimp_lists() {
8671 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8672 if ( $api_key === '' ) {
8673 return false;
8674 }
8675
8676 $response = ebook_store_mailchimp_request(
8677 'GET',
8678 '/lists',
8679 array(
8680 'count' => 1000,
8681 'fields' => 'lists.id,lists.name,total_items',
8682 )
8683 );
8684
8685 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8686 return false;
8687 }
8688
8689 return array_map(
8690 static function( $list ) {
8691 return (object) array(
8692 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8693 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8694 );
8695 },
8696 $response['lists']
8697 );
8698 }
8699 function ebook_store_get_brevo_lists() {
8700 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8701 if ( $api_key === '' ) {
8702 return false;
8703 }
8704
8705 $lists = array();
8706 $offset = 0;
8707 $limit = 50;
8708
8709 do {
8710 $response = ebook_store_brevo_request(
8711 'GET',
8712 '/contacts/lists',
8713 array(
8714 'limit' => $limit,
8715 'offset' => $offset,
8716 )
8717 );
8718
8719 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8720 return false;
8721 }
8722
8723 $batch = $response['lists'];
8724 $lists = array_merge( $lists, $batch );
8725 $offset += count( $batch );
8726 $total = isset( $response['count'] ) ? (int) $response['count'] : count( $lists );
8727 } while ( ! empty( $batch ) && $offset < $total );
8728
8729 return array_map(
8730 static function( $list ) {
8731 return (object) array(
8732 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8733 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8734 );
8735 },
8736 $lists
8737 );
8738 }
8739 function ebook_store_get_mailchimp_subscribe($email) {
8740 //error_log('MAILCHIMP Subscribing ' . $email);
8741 $list_id = trim( (string) get_option( 'mailchimp_lists' ) );
8742 $integration_enabled = (string) get_option( 'mailchimp_integration_enabled', $list_id !== '' ? '1' : '' );
8743 if ( $integration_enabled !== '1' ) {
8744 return false;
8745 }
8746
8747 if ( $list_id === '' ) {
8748 return false;
8749 }
8750
8751 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8752 if ( $api_key === '' ) {
8753 return false;
8754 }
8755
8756 $email = sanitize_email( (string) $email );
8757 if ( $email === '' ) {
8758 return false;
8759 }
8760
8761 $subscriber_hash = md5( strtolower( $email ) );
8762
8763 return ebook_store_mailchimp_request(
8764 'PUT',
8765 '/lists/' . rawurlencode( $list_id ) . '/members/' . $subscriber_hash,
8766 array(),
8767 array(
8768 'email_address' => $email,
8769 'status_if_new' => 'subscribed',
8770 'status' => 'subscribed',
8771 )
8772 );
8773 }
8774 function ebook_store_get_brevo_subscribe( $email ) {
8775 $list_id = trim( (string) get_option( 'brevo_lists' ) );
8776 $integration_enabled = (string) get_option( 'brevo_integration_enabled', $list_id !== '' ? '1' : '' );
8777 if ( $integration_enabled !== '1' ) {
8778 return false;
8779 }
8780
8781 if ( $list_id === '' || ! ctype_digit( $list_id ) ) {
8782 return false;
8783 }
8784
8785 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8786 if ( $api_key === '' ) {
8787 return false;
8788 }
8789
8790 $email = sanitize_email( (string) $email );
8791 if ( $email === '' ) {
8792 return false;
8793 }
8794
8795 return ebook_store_brevo_request(
8796 'POST',
8797 '/contacts',
8798 array(),
8799 array(
8800 'email' => $email,
8801 'listIds' => array( (int) $list_id ),
8802 'emailBlacklisted' => false,
8803 'smsBlacklisted' => false,
8804 'updateEnabled' => true,
8805 )
8806 );
8807 }
8808
8809 function ebook_store_subscribe_buyer_to_marketing_lists( $email ) {
8810 $email = sanitize_email( (string) $email );
8811 if ( $email === '' ) {
8812 return false;
8813 }
8814
8815 $results = array();
8816 $results['mailchimp'] = ebook_store_get_mailchimp_subscribe( $email );
8817 $results['brevo'] = ebook_store_get_brevo_subscribe( $email );
8818
8819 return $results;
8820 }
8821
8822 function ebook_store_get_mailchimp_server_prefix( $api_key ) {
8823 $api_key = trim( (string) $api_key );
8824 if ( $api_key === '' || strpos( $api_key, '-' ) === false ) {
8825 return '';
8826 }
8827
8828 $parts = explode( '-', $api_key );
8829 $server_prefix = strtolower( trim( (string) end( $parts ) ) );
8830
8831 return preg_match( '/^[a-z0-9]+$/', $server_prefix ) ? $server_prefix : '';
8832 }
8833
8834 function ebook_store_mailchimp_request( $method, $path, $query = array(), $body = null ) {
8835 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8836 if ( $api_key === '' ) {
8837 return new WP_Error( 'mailchimp_missing_api_key', 'Mailchimp API key not configured.' );
8838 }
8839
8840 $server_prefix = ebook_store_get_mailchimp_server_prefix( $api_key );
8841 if ( $server_prefix === '' ) {
8842 return new WP_Error( 'mailchimp_invalid_api_key', 'Mailchimp API key is missing a valid server prefix.' );
8843 }
8844
8845 $url = 'https://' . $server_prefix . '.api.mailchimp.com/3.0' . $path;
8846 if ( ! empty( $query ) ) {
8847 $url = add_query_arg( $query, $url );
8848 }
8849
8850 $args = array(
8851 'method' => strtoupper( (string) $method ),
8852 'timeout' => 20,
8853 'headers' => array(
8854 'Authorization' => 'Basic ' . base64_encode( 'anystring:' . $api_key ),
8855 'Accept' => 'application/json',
8856 ),
8857 );
8858
8859 if ( null !== $body ) {
8860 $args['headers']['Content-Type'] = 'application/json';
8861 $args['body'] = wp_json_encode( $body );
8862 }
8863
8864 $response = wp_remote_request( $url, $args );
8865 if ( is_wp_error( $response ) ) {
8866 return $response;
8867 }
8868
8869 $status_code = (int) wp_remote_retrieve_response_code( $response );
8870 $response_body = wp_remote_retrieve_body( $response );
8871 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8872
8873 if ( $status_code < 200 || $status_code >= 300 ) {
8874 $message = is_array( $data ) && ! empty( $data['detail'] ) ? (string) $data['detail'] : 'Mailchimp request failed.';
8875 return new WP_Error( 'mailchimp_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8876 }
8877
8878 return is_array( $data ) ? $data : array();
8879 }
8880 function ebook_store_brevo_request( $method, $path, $query = array(), $body = null ) {
8881 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8882 if ( $api_key === '' ) {
8883 return new WP_Error( 'brevo_missing_api_key', 'Brevo API key not configured.' );
8884 }
8885
8886 $url = 'https://api.brevo.com/v3' . $path;
8887 if ( ! empty( $query ) ) {
8888 $url = add_query_arg( $query, $url );
8889 }
8890
8891 $args = array(
8892 'method' => strtoupper( (string) $method ),
8893 'timeout' => 20,
8894 'headers' => array(
8895 'api-key' => $api_key,
8896 'Accept' => 'application/json',
8897 ),
8898 );
8899
8900 if ( null !== $body ) {
8901 $args['headers']['Content-Type'] = 'application/json';
8902 $args['body'] = wp_json_encode( $body );
8903 }
8904
8905 $response = wp_remote_request( $url, $args );
8906 if ( is_wp_error( $response ) ) {
8907 return $response;
8908 }
8909
8910 $status_code = (int) wp_remote_retrieve_response_code( $response );
8911 $response_body = wp_remote_retrieve_body( $response );
8912 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8913
8914 if ( $status_code < 200 || $status_code >= 300 ) {
8915 $message = 'Brevo request failed.';
8916 if ( is_array( $data ) ) {
8917 if ( ! empty( $data['message'] ) ) {
8918 $message = (string) $data['message'];
8919 } elseif ( ! empty( $data['code'] ) ) {
8920 $message = (string) $data['code'];
8921 }
8922 }
8923
8924 return new WP_Error( 'brevo_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8925 }
8926
8927 return is_array( $data ) ? $data : array();
8928 }
8929 function ebook_wp_embed_ebook() {
8930 wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
8931 ?>
8932 <div class="ebook_store_embed_all_items" style=" display:inline-block; clear:both; max-height:200px; overflow-x:auto; width:100%;">
8933 <?php
8934 $new = new WP_Query('post_type=ebook');
8935 while ($new->have_posts()) : $new->the_post();
8936 @$ebooks_found++;
8937 $img_cover = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_cover', true);
8938 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
8939 $c = new Currencies();
8940 $price = $c->getSymbol(get_option('paypal_currency','USD')) . number_format((float)$ebook['ebook_price'],2)
8941 ?>
8942 <div class="ebook_store_embed_ebook_item" style="width:100%; clear:both;">
8943 <?php
8944 if (!is_array($img_cover)) {
8945 $img_cover = array();
8946 }
8947 if (@$img_cover['url'] != '') {
8948 ?><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;" />
8949 <?php
8950 } else {
8951 ?>
8952 <div style="background:gray; width:45px; height:65px; float: left; margin:10px;" onClick="ebook_store_embed_code(<?php echo get_the_ID(); ?>);"></div>
8953 <?php
8954 }
8955
8956 ?><span class="ebook_store_embed_ebook_item_title" style="margin-top: 11px;display: inline-block;">
8957 <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>
8958 <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>
8959 <br />Form: <input onClick="this.select()" readonly type="text" size="29" value='[ebook_store ebook_id="<?php echo get_the_ID(); ?>"]' />
8960 <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>
8961
8962 </span>
8963 <p>
8964 Price: <?php echo $price; ?> Download type: <?php echo $ebook['donate_or_download']; ?>
8965
8966 </p>
8967 </div>
8968 <?php
8969 endwhile;
8970 if (@$ebooks_found == 0) {
8971 echo "<p>You have not created any ebooks yet, please click <a href='post-new.php?post_type=ebook'>here</a> to start.</p>";
8972 }
8973 ?>
8974 </div>
8975 <?php
8976 }
8977 /**
8978 * Kept as a no-op shim: third-party code and older templates call this directly.
8979 * Registration now happens once in includes/ebook-store-assets.php and the
8980 * stylesheet is enqueued only where store markup is actually rendered.
8981 */
8982 function ebookstorestylesheet() {
8983 ebook_store_enqueue_frontend_styles();
8984 }
8985 function ebook_store_post_type_view( $content ) {
8986 // is_singular() asks about the main query, not about the post whose content
8987 // is being filtered. Without the in_the_loop()/is_main_query() guard, a
8988 // related-posts widget or an excerpt on a single-ebook page also had its
8989 // content replaced by the buy form.
8990 if ( ! in_the_loop() || ! is_main_query() ) {
8991 return $content;
8992 }
8993
8994 if ( get_post_type() !== 'ebook' ) {
8995 return $content;
8996 }
8997
8998 return ebook_store( get_the_ID() );
8999 }
9000 function ebook_download_links($ebook_order, $free = 0) {
9001 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
9002 $locale = ebook_store_get_locale_strings();
9003 $formats_locale = ebook_store_get_format_labels();
9004 if ($free == 0) {
9005 $action_name = 'download';
9006 } else {
9007 $action_name = 'download_free';
9008 }
9009 $ebook_id = absint( ebook_store_order_field( $ebook_order, 'ebook', 0 ) );
9010 $ebook_key = (string) ebook_store_order_field( $ebook_order, 'ebook_key' );
9011 if ( strlen( $ebook_key ) != 32 ) {
9012 $ebook_key = (string) ebook_store_order_field( $ebook_order, 'ebook_key_array', $ebook_key );
9013 }
9014 $md5_nonce = (string) ebook_store_order_field( $ebook_order, 'md5_nonce' );
9015 $order_id = absint( ebook_store_order_field( $ebook_order, 'order_id', 0 ) );
9016 $loop = array('mobi','txt','epub','zip', 'mp3', 'mp4');
9017 $meta = get_post_meta($ebook_id);
9018 $links = array();
9019 $post = get_post($ebook_id);
9020 @$slug = $post->post_name;
9021 foreach ($loop as $l) {
9022 if (@is_array($meta['ebook_wp_custom_attachment_' . $l])) {
9023 $book = unserialize($meta['ebook_wp_custom_attachment_' . $l][0]);
9024 $file = $book['file'];
9025 $link_args = array(
9026 'format' => $l,
9027 'ebook_key' => $ebook_key,
9028 'action' => $action_name,
9029 'md5_nonce' => $md5_nonce,
9030 );
9031 if ( $order_id > 0 ) {
9032 $link_args['order_id'] = $order_id;
9033 }
9034
9035 $link = add_query_arg($link_args,get_permalink($ebook_id));
9036 $link = remove_query_arg('p',$link);
9037 $link = add_query_arg(array('ebook' => $slug),$link);
9038
9039 @$links[] = "<a href=\"$link\">" . $formats_locale[$l] . " Format</a>";
9040 }
9041 }
9042 if (count($links) == 0) {
9043 $links[] = 'No other file formats are available for this ebook.';
9044 }
9045 //error_log($link);
9046 return $links;
9047 }
9048 function ebook_download_links_bonus($ebook_order, $free = 0) {
9049 // Callers pass either raw meta arrays (value at [0]) or, for gateway-built
9050 // orders, a mix of arrays and scalars (a scalar order_id / md5_nonce next
9051 // to an ebook_key array). Normalise every field to the [0] shape.
9052 $new_order = array();
9053 foreach ( (array) $ebook_order as $k => $v ) {
9054 $new_order[ $k ][0] = ebook_store_order_field( $ebook_order, $k );
9055 }
9056 $ebook_order = $new_order;
9057 //error_log(__LINE__ . ' ' . print_r($ebook_order,true));
9058
9059 $ebook_id = absint( $ebook_order['ebook'][0] );
9060 $links = array();
9061 foreach ( ebook_store_get_bonus_ebook_ids( $ebook_id ) as $bonus_id ) {
9062 $post = get_post( $bonus_id );
9063 if ( ! $post ) {
9064 continue;
9065 }
9066 $link = ebook_download_link($ebook_order, 0, 1, $bonus_id);
9067 $links[] = "<a href=\"$link\" target=\"_blank\">" . esc_html( $post->post_title ) ."</a>";
9068 }
9069 //$links[] = '<pre>'.print_r($ebook_order,true) . '</pre>';
9070 return $links;
9071 }
9072 function ebook_store_stats($ebook_id, $days) {
9073 $seconds = 86400 * $days;
9074 if ($seconds == 0) {
9075 $seconds = 157680000;
9076 }
9077 $args = array(
9078 'post_type' => 'ebook_order',
9079 'meta_query' => array(
9080 array(
9081 'key' => 'ebook',
9082 'value' => $ebook_id,
9083 )
9084 ),
9085 'date_query' => array(
9086 array(
9087 'column' => 'post_date_gmt',
9088 'after' => $seconds . ' seconds ago',
9089 ),
9090 ));
9091 // The Query
9092 $the_query = new WP_Query( $args );
9093 $out = 0;
9094 // The Loop
9095 if ( $the_query->have_posts() ) {
9096 // echo '<ul>';
9097 while ( $the_query->have_posts() ) {
9098 $the_query->the_post();
9099 $id = get_the_ID();
9100 //echo "$id<br/>";
9101 $meta = get_post_meta($id);
9102 //print_r($meta);
9103 $out += $meta['mc_gross'][0];
9104 // echo '<li>' . get_the_title() . '</li>';
9105 }
9106 // echo '</ul>';
9107 } else {
9108 // no posts found
9109 }
9110 /* Restore original Post Data */
9111 wp_reset_postdata();
9112 return $out;
9113 }
9114
9115 /**
9116 * Sales totals bucketed by date, computed in SQL.
9117 *
9118 * Both report functions used to run WP_Query with posts_per_page => -1 and then
9119 * call get_post_meta() three times per row inside the loop. Because the PayPal
9120 * IPN handler writes one meta row per field, a store with 10k orders hydrated
9121 * ~10k WP_Post objects plus several hundred thousand meta rows in a single
9122 * request. That does not get slow, it exhausts the memory limit and white-screens
9123 * the reports page — and the cache is flushed on every incoming order, so the
9124 * expensive path is the normal path on a busy store.
9125 *
9126 * @param int $ebook_id Limit to one ebook, or 0 for the whole store.
9127 * @param string $format 'Y-m-d' for daily buckets, 'Y-m' for monthly.
9128 * @param string $since Anything strtotime() understands, e.g. '30 days ago'.
9129 * @return array bucket => total.
9130 */
9131 function ebook_store_get_sales_buckets( $ebook_id, $format, $since ) {
9132 global $wpdb;
9133
9134 $ebook_id = absint( $ebook_id );
9135 $after = gmdate( 'Y-m-d H:i:s', strtotime( $since, current_time( 'timestamp', true ) ) );
9136
9137 // MySQL's DATE_FORMAT equivalents of the PHP formats used by the callers.
9138 $sql_format = $format === 'Y-m' ? '%Y-%m' : '%Y-%m-%d';
9139
9140 // Prefer the gateway's payment_date when present, fall back to the post date,
9141 // so an order recorded late still lands in the right bucket.
9142 $join = " INNER JOIN {$wpdb->postmeta} gross ON ( gross.post_id = p.ID AND gross.meta_key = 'mc_gross' )";
9143 $join .= " LEFT JOIN {$wpdb->postmeta} pdate ON ( pdate.post_id = p.ID AND pdate.meta_key = 'payment_date' )";
9144 $join .= " LEFT JOIN {$wpdb->postmeta} pstatus ON ( pstatus.post_id = p.ID AND pstatus.meta_key = 'payment_status' )";
9145
9146 $where = $wpdb->prepare( " WHERE p.post_type = 'ebook_order' AND p.post_status = 'publish' AND p.post_date_gmt >= %s", $after );
9147 $where .= " AND ( pstatus.meta_value IS NULL OR LOWER( pstatus.meta_value ) NOT IN ( 'refunded', 'reversed', 'canceled_reversal', 'denied', 'failed', 'voided', 'expired', 'partially_refunded' ) )";
9148
9149 if ( $ebook_id > 0 ) {
9150 $join .= " INNER JOIN {$wpdb->postmeta} ebook ON ( ebook.post_id = p.ID AND ebook.meta_key = 'ebook' )";
9151 $where .= $wpdb->prepare( ' AND ebook.meta_value = %d', $ebook_id );
9152 }
9153
9154 // payment_date is stored in whatever shape the gateway sent: PayPal uses
9155 // "11:51:05 Apr 02, 2026 PDT", the built-in checkout writes "04/02/2026 11:51:05".
9156 // Try both, and fall back to post_date_gmt, which is always well formed.
9157 $paid_at = "COALESCE(
9158 STR_TO_DATE( pdate.meta_value, '%H:%i:%s %b %d, %Y' ),
9159 STR_TO_DATE( pdate.meta_value, '%m/%d/%Y %H:%i:%s' ),
9160 p.post_date_gmt
9161 )";
9162
9163 $bucket = "DATE_FORMAT( {$paid_at}, '{$sql_format}' )";
9164
9165 $rows = $wpdb->get_results(
9166 "SELECT {$bucket} AS bucket, SUM( gross.meta_value + 0 ) AS total
9167 FROM {$wpdb->posts} p
9168 {$join}
9169 {$where}
9170 GROUP BY bucket",
9171 ARRAY_A
9172 );
9173
9174 $out = array();
9175 foreach ( (array) $rows as $row ) {
9176 if ( ! empty( $row['bucket'] ) ) {
9177 $out[ $row['bucket'] ] = (float) $row['total'];
9178 }
9179 }
9180
9181 return $out;
9182 }
9183
9184 function ebook_store_stats_report_30days( $ebook_id = 0 ) {
9185 $out = array();
9186
9187 for ( $i = 29; $i >= 0; $i-- ) {
9188 $timestamp = current_time( 'timestamp', true ) - ( DAY_IN_SECONDS * $i );
9189 $out[ gmdate( 'Y-m-d', $timestamp ) ] = 0;
9190 }
9191
9192 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m-d', '30 days ago' ) as $bucket => $total ) {
9193 $out[ $bucket ] = $total;
9194 }
9195
9196 return $out;
9197 }
9198
9199 function ebook_store_stats_report_2years( $ebook_id = 0 ) {
9200 $out = array();
9201
9202 for ( $i = 23; $i >= 0; $i-- ) {
9203 $timestamp = strtotime( gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp', true ) ) . " -{$i} months" );
9204 $out[ gmdate( 'Y-m', $timestamp ) ] = 0;
9205 }
9206
9207 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m', '24 months ago' ) as $bucket => $total ) {
9208 $out[ $bucket ] = $total;
9209 }
9210
9211 return $out;
9212 }
9213
9214 function ebook_store_get_reports_data( $ebook_id = 0 ) {
9215 $ebook_id = absint( $ebook_id );
9216 $cache_key = ebook_store_get_reports_cache_key( $ebook_id );
9217 $cached = get_transient( $cache_key );
9218 if ( false !== $cached && is_array( $cached ) ) {
9219 return $cached;
9220 }
9221
9222 global $wpdb;
9223
9224 $query = "
9225 SELECT
9226 p.ID,
9227 p.post_date_gmt,
9228 status.meta_value AS payment_status,
9229 gross.meta_value AS mc_gross,
9230 fee.meta_value AS mc_fee,
9231 currency.meta_value AS mc_currency,
9232 ebook.meta_value AS ebook_id,
9233 item.meta_value AS item_name,
9234 quantity.meta_value AS quantity,
9235 payment_type.meta_value AS payment_type,
9236 payer_email.meta_value AS payer_email
9237 FROM {$wpdb->posts} p
9238 LEFT JOIN {$wpdb->postmeta} status ON (p.ID = status.post_id AND status.meta_key = 'payment_status')
9239 LEFT JOIN {$wpdb->postmeta} gross ON (p.ID = gross.post_id AND gross.meta_key = 'mc_gross')
9240 LEFT JOIN {$wpdb->postmeta} fee ON (p.ID = fee.post_id AND fee.meta_key = 'mc_fee')
9241 LEFT JOIN {$wpdb->postmeta} currency ON (p.ID = currency.post_id AND currency.meta_key = 'mc_currency')
9242 LEFT JOIN {$wpdb->postmeta} ebook ON (p.ID = ebook.post_id AND ebook.meta_key = 'ebook')
9243 LEFT JOIN {$wpdb->postmeta} item ON (p.ID = item.post_id AND item.meta_key = 'item_name')
9244 LEFT JOIN {$wpdb->postmeta} quantity ON (p.ID = quantity.post_id AND quantity.meta_key = 'quantity')
9245 LEFT JOIN {$wpdb->postmeta} payment_type ON (p.ID = payment_type.post_id AND payment_type.meta_key = 'payment_type')
9246 LEFT JOIN {$wpdb->postmeta} payer_email ON (p.ID = payer_email.post_id AND payer_email.meta_key = 'payer_email')
9247 WHERE p.post_type = 'ebook_order'
9248 AND p.post_status = 'publish'
9249 ";
9250
9251 if ( $ebook_id > 0 ) {
9252 $query .= $wpdb->prepare( " AND ebook.meta_value = %d", $ebook_id );
9253 }
9254
9255 $query .= " ORDER BY p.post_date_gmt DESC";
9256
9257 $rows = $wpdb->get_results( $query, ARRAY_A );
9258
9259 $default_currency = ebook_store_get_store_currency();
9260 $currency = $default_currency;
9261 $gateway_totals = array();
9262 $status_totals = array();
9263 $currency_totals = array();
9264 $recent_orders = array();
9265 $top_ebooks = array();
9266 $item_totals = array();
9267 $currencies_seen = array();
9268 $now_gmt = current_time( 'timestamp', true );
9269 $last_24h_cutoff = $now_gmt - DAY_IN_SECONDS;
9270 $last_7d_cutoff = $now_gmt - ( 7 * DAY_IN_SECONDS );
9271 $summary = array(
9272 'total_sales' => 0.0,
9273 'total_profit' => 0.0,
9274 'last_24h_sales' => 0.0,
9275 'last_7d_sales' => 0.0,
9276 'total_orders' => count( $rows ),
9277 'reportable_orders' => 0,
9278 'paid_orders' => 0,
9279 'free_orders' => 0,
9280 'average_order' => 0.0,
9281 'latest_order_gmt' => '',
9282 'latest_order_id' => 0,
9283 'latest_order_amount' => 0.0,
9284 'currency' => $default_currency,
9285 'mixed_currencies' => false,
9286 'currency_count' => 0,
9287 'top_item' => array(
9288 'label' => __( 'No ebook sales yet', 'ebook-store' ),
9289 'units' => 0,
9290 'orders' => 0,
9291 'revenue' => 0.0,
9292 'ebook_id' => 0,
9293 ),
9294 );
9295
9296 foreach ( $rows as $row ) {
9297 $status = trim( (string) $row['payment_status'] );
9298 $status_key = $status !== '' ? sanitize_key( $status ) : 'unknown';
9299 if ( ! isset( $status_totals[ $status_key ] ) ) {
9300 $status_totals[ $status_key ] = array(
9301 'label' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9302 'count' => 0,
9303 );
9304 }
9305 $status_totals[ $status_key ]['count']++;
9306
9307 if ( ebook_store_is_negative_order_status( $status ) ) {
9308 continue;
9309 }
9310
9311 $summary['reportable_orders']++;
9312
9313 $gross = isset( $row['mc_gross'] ) ? (float) $row['mc_gross'] : 0.0;
9314 $fee = isset( $row['mc_fee'] ) ? (float) $row['mc_fee'] : 0.0;
9315 $gateway = sanitize_key( (string) $row['payment_type'] );
9316 $gateway_label = $gateway !== '' ? ucfirst( str_replace( '_', ' ', $gateway ) ) : __( 'Manual / unknown', 'ebook-store' );
9317 $order_currency = trim( (string) $row['mc_currency'] );
9318 $order_currency = $order_currency !== '' ? $order_currency : $currency;
9319 $timestamp = ! empty( $row['post_date_gmt'] ) ? strtotime( $row['post_date_gmt'] . ' UTC' ) : false;
9320 $quantity = isset( $row['quantity'] ) && is_numeric( $row['quantity'] ) && (float) $row['quantity'] > 0 ? (float) $row['quantity'] : 1.0;
9321 $row_ebook_id = isset( $row['ebook_id'] ) ? absint( $row['ebook_id'] ) : 0;
9322 $item_name = trim( isset( $row['item_name'] ) ? (string) $row['item_name'] : '' );
9323
9324 $summary['total_sales'] += $gross;
9325 $summary['total_profit'] += ( $gross - $fee );
9326
9327 if ( $gross > 0 ) {
9328 $summary['paid_orders']++;
9329 } else {
9330 $summary['free_orders']++;
9331 }
9332
9333 if ( $order_currency !== '' ) {
9334 $currencies_seen[ $order_currency ] = true;
9335 if ( $summary['currency'] === '' || $summary['currency'] === $default_currency ) {
9336 $summary['currency'] = $order_currency;
9337 }
9338 $currency = $summary['currency'];
9339 }
9340
9341 if ( $timestamp ) {
9342 if ( $timestamp >= $last_24h_cutoff ) {
9343 $summary['last_24h_sales'] += $gross;
9344 }
9345 if ( $timestamp >= $last_7d_cutoff ) {
9346 $summary['last_7d_sales'] += $gross;
9347 }
9348 if ( $summary['latest_order_gmt'] === '' || strtotime( $summary['latest_order_gmt'] . ' UTC' ) < $timestamp ) {
9349 $summary['latest_order_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
9350 $summary['latest_order_id'] = (int) $row['ID'];
9351 $summary['latest_order_amount'] = $gross;
9352 }
9353 }
9354
9355 $item_key = $row_ebook_id > 0 ? 'ebook:' . $row_ebook_id : 'item:' . md5( $item_name !== '' ? $item_name : __( 'Unknown ebook', 'ebook-store' ) );
9356 if ( ! isset( $item_totals[ $item_key ] ) ) {
9357 $item_totals[ $item_key ] = array(
9358 'ebook_id' => $row_ebook_id,
9359 'title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : '',
9360 'orders' => 0,
9361 'total' => 0.0,
9362 'last_7d' => 0.0,
9363 'last_30d' => 0.0,
9364 'units' => 0.0,
9365 'edit_url' => $row_ebook_id > 0 ? get_edit_post_link( $row_ebook_id, 'raw' ) : '',
9366 );
9367 if ( $item_totals[ $item_key ]['title'] === '' ) {
9368 $item_totals[ $item_key ]['title'] = $item_name !== '' ? $item_name : __( 'Untitled ebook', 'ebook-store' );
9369 }
9370 }
9371
9372 $item_totals[ $item_key ]['orders']++;
9373 $item_totals[ $item_key ]['total'] += $gross;
9374 $item_totals[ $item_key ]['units'] += $quantity;
9375 if ( $timestamp && $timestamp >= $last_7d_cutoff ) {
9376 $item_totals[ $item_key ]['last_7d'] += $gross;
9377 }
9378 if ( $timestamp && $timestamp >= ( $now_gmt - ( 30 * DAY_IN_SECONDS ) ) ) {
9379 $item_totals[ $item_key ]['last_30d'] += $gross;
9380 }
9381
9382 if ( ! isset( $gateway_totals[ $gateway ?: 'manual' ] ) ) {
9383 $gateway_totals[ $gateway ?: 'manual' ] = array(
9384 'label' => $gateway_label,
9385 'count' => 0,
9386 'revenue' => 0.0,
9387 );
9388 }
9389
9390 $gateway_totals[ $gateway ?: 'manual' ]['count']++;
9391 $gateway_totals[ $gateway ?: 'manual' ]['revenue'] += $gross;
9392
9393 if ( ! isset( $currency_totals[ $order_currency ] ) ) {
9394 $currency_totals[ $order_currency ] = array(
9395 'label' => $order_currency,
9396 'count' => 0,
9397 'revenue' => 0.0,
9398 );
9399 }
9400
9401 $currency_totals[ $order_currency ]['count']++;
9402 $currency_totals[ $order_currency ]['revenue'] += $gross;
9403
9404 if ( count( $recent_orders ) < 8 ) {
9405 $recent_orders[] = array(
9406 'order_id' => (int) $row['ID'],
9407 'date_gmt' => (string) $row['post_date_gmt'],
9408 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9409 'amount' => $gross,
9410 'fee' => $fee,
9411 'currency' => $order_currency,
9412 'email' => (string) $row['payer_email'],
9413 'gateway' => $gateway_label,
9414 'ebook_id' => $row_ebook_id,
9415 'ebook_title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : ( (string) $row['item_name'] !== '' ? (string) $row['item_name'] : __( 'Unknown ebook', 'ebook-store' ) ),
9416 'edit_url' => get_edit_post_link( (int) $row['ID'], 'raw' ),
9417 );
9418 }
9419 }
9420
9421 if ( $summary['paid_orders'] > 0 ) {
9422 $summary['average_order'] = $summary['total_sales'] / $summary['paid_orders'];
9423 }
9424
9425 $summary['currency_count'] = count( $currencies_seen );
9426 $summary['mixed_currencies'] = $summary['currency_count'] > 1;
9427
9428 if ( ! empty( $item_totals ) ) {
9429 uasort(
9430 $item_totals,
9431 static function ( $left, $right ) {
9432 if ( $left['total'] === $right['total'] ) {
9433 return $right['orders'] <=> $left['orders'];
9434 }
9435 return $right['total'] <=> $left['total'];
9436 }
9437 );
9438 $top_ebooks = array_values( $item_totals );
9439 $summary['top_item'] = array(
9440 'label' => $top_ebooks[0]['title'],
9441 'units' => $top_ebooks[0]['units'],
9442 'orders' => $top_ebooks[0]['orders'],
9443 'revenue' => $top_ebooks[0]['total'],
9444 'ebook_id' => $top_ebooks[0]['ebook_id'],
9445 );
9446 if ( $ebook_id === 0 ) {
9447 $top_ebooks = array_slice( $top_ebooks, 0, 8 );
9448 }
9449 }
9450
9451 uasort(
9452 $gateway_totals,
9453 static function ( $left, $right ) {
9454 if ( $left['revenue'] === $right['revenue'] ) {
9455 return $right['count'] <=> $left['count'];
9456 }
9457 return $right['revenue'] <=> $left['revenue'];
9458 }
9459 );
9460
9461 uasort(
9462 $status_totals,
9463 static function ( $left, $right ) {
9464 return $right['count'] <=> $left['count'];
9465 }
9466 );
9467
9468 uasort(
9469 $currency_totals,
9470 static function ( $left, $right ) {
9471 return $right['revenue'] <=> $left['revenue'];
9472 }
9473 );
9474
9475 $daily = ebook_store_stats_report_30days( $ebook_id );
9476 $monthly = ebook_store_stats_report_2years( $ebook_id );
9477 if ( is_array( $daily ) ) {
9478 ksort( $daily );
9479 }
9480 if ( is_array( $monthly ) ) {
9481 ksort( $monthly );
9482 }
9483
9484 $data = array(
9485 'summary' => $summary,
9486 'currency' => $currency,
9487 'ebook_id' => $ebook_id,
9488 'ebook_title' => $ebook_id > 0 ? get_the_title( $ebook_id ) : '',
9489 'daily' => is_array( $daily ) ? $daily : array(),
9490 'monthly' => is_array( $monthly ) ? $monthly : array(),
9491 'gateways' => array_values( $gateway_totals ),
9492 'statuses' => array_values( $status_totals ),
9493 'currencies' => array_values( $currency_totals ),
9494 'top_ebooks' => $top_ebooks,
9495 'recent_orders' => $recent_orders,
9496 );
9497
9498 set_transient( $cache_key, $data, 10 * MINUTE_IN_SECONDS );
9499
9500 return $data;
9501 }
9502
9503 function ebook_store_get_setup_wizard_url( $args = array() ) {
9504 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-setup-wizard' );
9505
9506 if ( ! empty( $args ) ) {
9507 $url = add_query_arg( $args, $url );
9508 }
9509
9510 return $url;
9511 }
9512
9513 function ebook_store_get_template_editor_url( $args = array() ) {
9514 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-template-editor' );
9515
9516 if ( ! empty( $args ) ) {
9517 $url = add_query_arg( $args, $url );
9518 }
9519
9520 return $url;
9521 }
9522
9523 function ebook_store_get_add_issue_url( $args = array() ) {
9524 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-add-issue-page' );
9525
9526 if ( ! empty( $args ) ) {
9527 $url = add_query_arg( $args, $url );
9528 }
9529
9530 return $url;
9531 }
9532
9533 function ebook_store_sanitize_issue_purchase_period( $raw_period, $default = '1 year' ) {
9534 $period = strtolower( trim( preg_replace( '/[^0-9a-z\s]/i', '', (string) $raw_period ) ) );
9535
9536 if ( $period === '' ) {
9537 $period = $default;
9538 }
9539
9540 $timestamp = strtotime( $period . ' ago', current_time( 'timestamp', true ) );
9541 if ( false === $timestamp ) {
9542 $period = $default;
9543 }
9544
9545 return $period;
9546 }
9547
9548 function ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period ) {
9549 $source_ebook_id = absint( $source_ebook_id );
9550 $target_ebook_id = absint( $target_ebook_id );
9551 $purchase_period = ebook_store_sanitize_issue_purchase_period( $purchase_period );
9552 $cutoff_timestamp = strtotime( $purchase_period . ' ago', current_time( 'timestamp', true ) );
9553
9554 $result = array(
9555 'purchase_period' => $purchase_period,
9556 'cutoff_timestamp' => $cutoff_timestamp,
9557 'orders' => array(),
9558 'eligible' => array(),
9559 'skipped_existing' => array(),
9560 'skipped_invalid' => array(),
9561 'skipped_negative' => array(),
9562 'counts' => array(
9563 'total' => 0,
9564 'eligible' => 0,
9565 'skipped_existing' => 0,
9566 'skipped_invalid' => 0,
9567 'skipped_negative' => 0,
9568 ),
9569 );
9570
9571 if ( $source_ebook_id < 1 ) {
9572 return $result;
9573 }
9574
9575 $query = new WP_Query(
9576 array(
9577 'post_type' => 'ebook_order',
9578 'post_status' => 'publish',
9579 'posts_per_page' => -1,
9580 'orderby' => 'date',
9581 'order' => 'DESC',
9582 'meta_query' => array(
9583 'relation' => 'AND',
9584 array(
9585 'key' => 'ebook',
9586 'value' => $source_ebook_id,
9587 ),
9588 array(
9589 'relation' => 'OR',
9590 array(
9591 'key' => 'issue_delivery_kind',
9592 'compare' => 'NOT EXISTS',
9593 ),
9594 array(
9595 'key' => 'issue_delivery_kind',
9596 'value' => 'new_issue',
9597 'compare' => '!=',
9598 ),
9599 ),
9600 ),
9601 'date_query' => array(
9602 array(
9603 'column' => 'post_date_gmt',
9604 'after' => gmdate( 'Y-m-d H:i:s', $cutoff_timestamp ),
9605 'inclusive' => true,
9606 ),
9607 ),
9608 )
9609 );
9610
9611 $orders = array();
9612 $source_order_ids = array();
9613
9614 if ( $query->have_posts() ) {
9615 while ( $query->have_posts() ) {
9616 $query->the_post();
9617 $order_id = get_the_ID();
9618 $status = (string) get_post_meta( $order_id, 'payment_status', true );
9619 $email = sanitize_email( (string) get_post_meta( $order_id, 'payer_email', true ) );
9620 $item_name = (string) get_post_meta( $order_id, 'item_name', true );
9621 $order_date = (string) get_post_meta( $order_id, 'payment_date', true );
9622 $order_timestamp = $order_date !== '' ? strtotime( $order_date ) : false;
9623 if ( ! $order_timestamp ) {
9624 $order_timestamp = strtotime( get_post_field( 'post_date_gmt', $order_id ) . ' UTC' );
9625 }
9626
9627 $order = array(
9628 'source_order_id' => $order_id,
9629 'first_name' => (string) get_post_meta( $order_id, 'first_name', true ),
9630 'last_name' => (string) get_post_meta( $order_id, 'last_name', true ),
9631 'payer_email' => $email,
9632 'user_id' => absint( get_post_meta( $order_id, 'user_id', true ) ),
9633 'residence_country' => (string) get_post_meta( $order_id, 'residence_country', true ),
9634 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
9635 'item_name' => $item_name !== '' ? $item_name : get_the_title( $source_ebook_id ),
9636 'order_date' => $order_date,
9637 'order_timestamp' => $order_timestamp,
9638 'order_edit_url' => get_edit_post_link( $order_id, 'raw' ),
9639 'already_sent' => false,
9640 'existing_issue_order_id' => 0,
9641 );
9642
9643 $orders[ $order_id ] = $order;
9644 $source_order_ids[] = $order_id;
9645 }
9646 }
9647 wp_reset_postdata();
9648
9649 $existing_map = array();
9650 if ( $target_ebook_id > 0 && ! empty( $source_order_ids ) ) {
9651 $existing_query = new WP_Query(
9652 array(
9653 'post_type' => 'ebook_order',
9654 'post_status' => 'publish',
9655 'posts_per_page' => -1,
9656 'fields' => 'ids',
9657 'meta_query' => array(
9658 array(
9659 'key' => 'ebook',
9660 'value' => $target_ebook_id,
9661 ),
9662 array(
9663 'key' => 'issue_delivery_kind',
9664 'value' => 'new_issue',
9665 ),
9666 array(
9667 'key' => 'issue_source_order_id',
9668 'value' => $source_order_ids,
9669 'compare' => 'IN',
9670 ),
9671 ),
9672 )
9673 );
9674
9675 if ( $existing_query->have_posts() ) {
9676 foreach ( $existing_query->posts as $issue_order_id ) {
9677 $source_order_id = absint( get_post_meta( $issue_order_id, 'issue_source_order_id', true ) );
9678 if ( $source_order_id > 0 ) {
9679 $existing_map[ $source_order_id ] = (int) $issue_order_id;
9680 }
9681 }
9682 }
9683 wp_reset_postdata();
9684 }
9685
9686 foreach ( $orders as $order_id => $order ) {
9687 $result['counts']['total']++;
9688
9689 if ( ebook_store_is_negative_order_status( $order['status'] ) ) {
9690 $result['skipped_negative'][] = $order;
9691 $result['counts']['skipped_negative']++;
9692 continue;
9693 }
9694
9695 if ( $order['payer_email'] === '' ) {
9696 $result['skipped_invalid'][] = $order;
9697 $result['counts']['skipped_invalid']++;
9698 continue;
9699 }
9700
9701 if ( isset( $existing_map[ $order_id ] ) ) {
9702 $order['already_sent'] = true;
9703 $order['existing_issue_order_id'] = $existing_map[ $order_id ];
9704 $result['skipped_existing'][] = $order;
9705 $result['counts']['skipped_existing']++;
9706 continue;
9707 }
9708
9709 $result['eligible'][] = $order;
9710 $result['counts']['eligible']++;
9711 }
9712
9713 $result['orders'] = array_values( $orders );
9714
9715 return $result;
9716 }
9717
9718 function ebook_store_process_new_issue_batch( $source_ebook_id, $target_ebook_id, $purchase_period, $offset = 0, $limit = 20 ) {
9719 $source_ebook_id = absint( $source_ebook_id );
9720 $target_ebook_id = absint( $target_ebook_id );
9721 $offset = max( 0, absint( $offset ) );
9722 $limit = max( 1, absint( $limit ) );
9723 $preview = ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period );
9724 $eligible = isset( $preview['eligible'] ) ? $preview['eligible'] : array();
9725 $batch = array_slice( $eligible, $offset, $limit );
9726 $created = array();
9727 $failed = array();
9728
9729 foreach ( $batch as $order ) {
9730 $new_order_data = array(
9731 'first_name' => $order['first_name'],
9732 'last_name' => $order['last_name'],
9733 'mc_gross' => 0,
9734 'ebook' => $target_ebook_id,
9735 'payer_email' => $order['payer_email'],
9736 'mc_fee' => 0,
9737 'item_name' => get_the_title( $target_ebook_id ),
9738 'payment_date' => date( 'm/d/Y H:i:s' ),
9739 'txn_id' => 'Issue Delivery',
9740 'residence_country' => $order['residence_country'],
9741 'md5_nonce' => md5( microtime() . mt_rand( 1, 99999999 ) . NONCE_KEY ),
9742 'user_id' => isset( $order['user_id'] ) ? $order['user_id'] : 0,
9743 'payment_status' => 'Completed',
9744 'payment_type' => 'new_issue',
9745 );
9746
9747 $created_order_id = ebook_store_add_order( $new_order_data, true, false );
9748
9749 if ( $created_order_id ) {
9750 update_post_meta( $created_order_id, 'issue_delivery_kind', 'new_issue' );
9751 update_post_meta( $created_order_id, 'issue_source_order_id', $order['source_order_id'] );
9752 update_post_meta( $created_order_id, 'issue_source_ebook_id', $source_ebook_id );
9753 update_post_meta( $created_order_id, 'issue_target_ebook_id', $target_ebook_id );
9754 update_post_meta( $created_order_id, 'issue_purchase_period', $preview['purchase_period'] );
9755 $created[] = array(
9756 'new_order_id' => $created_order_id,
9757 'source_order_id' => $order['source_order_id'],
9758 'payer_email' => $order['payer_email'],
9759 );
9760 } else {
9761 $failed[] = $order;
9762 }
9763 }
9764
9765 return array(
9766 'preview' => $preview,
9767 'offset' => $offset,
9768 'limit' => $limit,
9769 'processed' => count( $batch ),
9770 'created' => $created,
9771 'failed' => $failed,
9772 'next_offset' => $offset + count( $batch ),
9773 'completed' => ( $offset + count( $batch ) ) >= count( $eligible ),
9774 );
9775 }
9776
9777 function ebook_store_is_wordfence_active() {
9778 if ( ! function_exists( 'is_plugin_active' ) ) {
9779 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9780 }
9781
9782 return function_exists( 'is_plugin_active' ) && is_plugin_active( 'wordfence/wordfence.php' );
9783 }
9784
9785 function ebook_store_get_template_directory_path() {
9786 return trailingslashit( plugin_dir_path( __FILE__ ) ) . 'templates/';
9787 }
9788
9789 function ebook_store_get_legacy_custom_template_directory_path() {
9790 return ebook_store_get_template_directory_path();
9791 }
9792
9793 function ebook_store_get_custom_template_directory_path() {
9794 $uploads = wp_upload_dir();
9795 if ( ! empty( $uploads['basedir'] ) ) {
9796 return trailingslashit( $uploads['basedir'] ) . 'ebook-store/templates/';
9797 }
9798
9799 return trailingslashit( WP_CONTENT_DIR ) . 'uploads/ebook-store/templates/';
9800 }
9801
9802 function ebook_store_ensure_custom_template_directory() {
9803 $directory = ebook_store_get_custom_template_directory_path();
9804 if ( is_dir( $directory ) ) {
9805 return $directory;
9806 }
9807
9808 if ( wp_mkdir_p( $directory ) ) {
9809 return $directory;
9810 }
9811
9812 return '';
9813 }
9814
9815 function ebook_store_sync_custom_template_name( $template_code, $template_name ) {
9816 $template_code = (string) $template_code;
9817 $template_name = trim( (string) $template_name );
9818 if ( $template_name === '' ) {
9819 return $template_code;
9820 }
9821
9822 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
9823 return (string) preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
9824 }
9825
9826 return "/*\nTemplate Name: " . $template_name . "\n*/\n" . $template_code;
9827 }
9828
9829 function ebook_store_get_unique_custom_template_identity( $preferred_name, $preferred_key = '' ) {
9830 $preferred_name = trim( (string) $preferred_name );
9831 $preferred_key = sanitize_key( (string) $preferred_key );
9832 if ( $preferred_name === '' ) {
9833 $preferred_name = __( 'Custom template', 'ebook-store' );
9834 }
9835 if ( $preferred_key === '' ) {
9836 $preferred_key = sanitize_title( $preferred_name );
9837 }
9838 if ( $preferred_key === '' ) {
9839 $preferred_key = 'custom-template';
9840 }
9841
9842 $editable = ebook_store_get_template_editor_files();
9843 $directory = ebook_store_get_custom_template_directory_path();
9844 $name = $preferred_name;
9845 $key = $preferred_key;
9846 $index = 2;
9847
9848 while ( isset( $editable[ $key ] ) || file_exists( $directory . $key . '.php' ) ) {
9849 $key = $preferred_key . '-' . $index;
9850 $name = $preferred_name . ' ' . $index;
9851 $index++;
9852 }
9853
9854 return array(
9855 'name' => $name,
9856 'key' => $key,
9857 );
9858 }
9859
9860 function ebook_store_create_custom_template_from_code( $template_name, $template_code, $preferred_key = '' ) {
9861 $directory = ebook_store_ensure_custom_template_directory();
9862 if ( $directory === '' ) {
9863 return new WP_Error( 'ebook_store_template_directory_unavailable', __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) );
9864 }
9865
9866 $identity = ebook_store_get_unique_custom_template_identity( $template_name, $preferred_key );
9867 $template_file = $directory . $identity['key'] . '.php';
9868 $template_code = ebook_store_sync_custom_template_name( $template_code, $identity['name'] );
9869 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
9870 if ( is_wp_error( $syntax_check ) ) {
9871 return $syntax_check;
9872 }
9873
9874 $result = @file_put_contents( $template_file, $template_code );
9875 if ( $result === false ) {
9876 return new WP_Error( 'ebook_store_template_create_failed', __( 'WordPress could not save the custom template file.', 'ebook-store' ) );
9877 }
9878
9879 return array(
9880 'key' => $identity['key'],
9881 'name' => $identity['name'],
9882 'file' => $template_file,
9883 'code' => $template_code,
9884 );
9885 }
9886
9887 function ebook_store_migrate_legacy_custom_templates() {
9888 static $migrated = false;
9889 if ( $migrated ) {
9890 return;
9891 }
9892 $migrated = true;
9893
9894 $legacy_dir = ebook_store_get_legacy_custom_template_directory_path();
9895 $custom_dir = ebook_store_ensure_custom_template_directory();
9896 if ( $custom_dir === '' || ! is_dir( $legacy_dir ) ) {
9897 return;
9898 }
9899
9900 $core_files = array_map( 'basename', wp_list_pluck( ebook_store_get_core_form_templates(), 'file' ) );
9901 foreach ( (array) glob( $legacy_dir . '*.php' ) as $file ) {
9902 $basename = basename( $file );
9903 if ( in_array( $basename, $core_files, true ) ) {
9904 continue;
9905 }
9906
9907 $destination = $custom_dir . $basename;
9908 if ( file_exists( $destination ) ) {
9909 continue;
9910 }
9911
9912 @copy( $file, $destination );
9913 }
9914 }
9915
9916 function ebook_store_get_core_form_templates() {
9917 return array(
9918 'modern' => array(
9919 'label' => __( 'Modern', 'ebook-store' ),
9920 'file' => ebook_store_get_template_directory_path() . 'modern.php',
9921 'core' => true,
9922 ),
9923 '2026' => array(
9924 'label' => __( '2026', 'ebook-store' ),
9925 'file' => ebook_store_get_template_directory_path() . '2026.php',
9926 'core' => true,
9927 ),
9928 'hello-elementor-2026' => array(
9929 'label' => __( 'Hello Elementor 2026', 'ebook-store' ),
9930 'file' => ebook_store_get_template_directory_path() . 'hello-elementor-2026.php',
9931 'core' => true,
9932 ),
9933 'legacy' => array(
9934 'label' => __( 'Legacy', 'ebook-store' ),
9935 'file' => ebook_store_get_template_directory_path() . 'current.php',
9936 'core' => true,
9937 ),
9938 );
9939 }
9940
9941 function ebook_store_get_core_template_keys() {
9942 return array_keys( ebook_store_get_core_form_templates() );
9943 }
9944
9945 function ebook_store_template_editor_humanize_key( $key ) {
9946 $key = trim( (string) $key );
9947 if ( $key === '' ) {
9948 return __( 'Custom template', 'ebook-store' );
9949 }
9950
9951 return ucwords( str_replace( array( '-', '_' ), ' ', $key ) );
9952 }
9953
9954 function ebook_store_can_access_template_editor() {
9955 if ( is_multisite() ) {
9956 return is_super_admin();
9957 }
9958
9959 return current_user_can( 'manage_options' );
9960 }
9961
9962 function ebook_store_get_template_header_label( $file ) {
9963 if ( ! file_exists( $file ) ) {
9964 return '';
9965 }
9966
9967 $contents = (string) @file_get_contents( $file, false, null, 0, 2048 );
9968 if ( $contents === '' ) {
9969 return '';
9970 }
9971
9972 if ( preg_match( '/Template Name:\s*(.+)$/mi', $contents, $matches ) ) {
9973 return trim( $matches[1] );
9974 }
9975
9976 return '';
9977 }
9978
9979 function ebook_store_get_custom_form_templates() {
9980 ebook_store_migrate_legacy_custom_templates();
9981 $template_dir = ebook_store_get_custom_template_directory_path();
9982 $core_templates = ebook_store_get_core_form_templates();
9983 $core_files = array_map( 'basename', wp_list_pluck( $core_templates, 'file' ) );
9984 $templates = array();
9985
9986 foreach ( (array) glob( $template_dir . '*.php' ) as $file ) {
9987 $basename = basename( $file );
9988 if ( in_array( $basename, $core_files, true ) ) {
9989 continue;
9990 }
9991
9992 $key = sanitize_key( basename( $basename, '.php' ) );
9993 if ( $key === '' ) {
9994 continue;
9995 }
9996
9997 $label = ebook_store_get_template_header_label( $file );
9998 if ( $label === '' ) {
9999 $label = ebook_store_template_editor_humanize_key( $key );
10000 }
10001
10002 $templates[ $key ] = array(
10003 'label' => $label,
10004 'file' => $file,
10005 'core' => false,
10006 );
10007 }
10008
10009 ksort( $templates );
10010
10011 return $templates;
10012 }
10013
10014 function ebook_store_get_template_usage_stats( $template_key ) {
10015 $template_key = sanitize_key( (string) $template_key );
10016 $stats = array(
10017 'ebooks' => 0,
10018 'default' => false,
10019 );
10020
10021 if ( $template_key === '' ) {
10022 return $stats;
10023 }
10024
10025 $ebook_ids = get_posts(
10026 array(
10027 'post_type' => 'ebook',
10028 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
10029 'posts_per_page' => -1,
10030 'fields' => 'ids',
10031 'no_found_rows' => true,
10032 )
10033 );
10034
10035 foreach ( (array) $ebook_ids as $ebook_id ) {
10036 $ebook_meta = get_post_meta( (int) $ebook_id, 'ebook', true );
10037 $current_template = '';
10038 if ( is_array( $ebook_meta ) && ! empty( $ebook_meta['form_template'] ) ) {
10039 $current_template = (string) $ebook_meta['form_template'];
10040 }
10041
10042 if ( function_exists( 'ebook_store_resolve_form_template' ) && ebook_store_resolve_form_template( $current_template ) === $template_key ) {
10043 $stats['ebooks']++;
10044 }
10045 }
10046
10047 $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' ) );
10048 $stats['default'] = $default_template === $template_key;
10049
10050 return $stats;
10051 }
10052
10053 function ebook_store_reassign_template_usage( $from_template, $to_template ) {
10054 $from_template = sanitize_key( (string) $from_template );
10055 $to_template = sanitize_key( (string) $to_template );
10056 $result = array(
10057 'ebook_count' => 0,
10058 'default_updated' => false,
10059 );
10060
10061 if ( $from_template === '' || $to_template === '' || $from_template === $to_template ) {
10062 return $result;
10063 }
10064
10065 $ebook_ids = get_posts(
10066 array(
10067 'post_type' => 'ebook',
10068 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
10069 'posts_per_page' => -1,
10070 'fields' => 'ids',
10071 'no_found_rows' => true,
10072 )
10073 );
10074
10075 foreach ( (array) $ebook_ids as $ebook_id ) {
10076 $ebook_id = (int) $ebook_id;
10077 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
10078 if ( ! is_array( $ebook_meta ) ) {
10079 $ebook_meta = array();
10080 }
10081
10082 $current_template = isset( $ebook_meta['form_template'] ) ? (string) $ebook_meta['form_template'] : '';
10083 $resolved_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( $current_template ) : sanitize_key( $current_template );
10084 if ( $resolved_template !== $from_template ) {
10085 continue;
10086 }
10087
10088 $ebook_meta['form_template'] = $to_template;
10089 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
10090 $result['ebook_count']++;
10091 }
10092
10093 $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' ) );
10094 if ( $default_template === $from_template ) {
10095 update_option( 'ebook_store_form_template', $to_template );
10096 $result['default_updated'] = true;
10097 }
10098
10099 return $result;
10100 }
10101
10102 function ebook_store_is_core_template( $template_key ) {
10103 return in_array( sanitize_key( (string) $template_key ), ebook_store_get_core_template_keys(), true );
10104 }
10105
10106 function ebook_store_get_template_editor_files() {
10107 $templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
10108 $editable = array();
10109
10110 foreach ( $templates as $template_key => $template ) {
10111 if ( empty( $template['file'] ) || ! file_exists( $template['file'] ) ) {
10112 continue;
10113 }
10114
10115 $editable[ $template_key ] = array(
10116 'key' => (string) $template_key,
10117 'label' => isset( $template['label'] ) ? $template['label'] : ucfirst( $template_key ),
10118 'file' => $template['file'],
10119 'filename' => basename( $template['file'] ),
10120 'core' => ! empty( $template['core'] ),
10121 );
10122 }
10123
10124 return $editable;
10125 }
10126
10127 function ebook_store_get_template_editor_current_key( $requested = '' ) {
10128 $requested = sanitize_key( (string) $requested );
10129 $editable = ebook_store_get_template_editor_files();
10130
10131 if ( $requested !== '' && isset( $editable[ $requested ] ) ) {
10132 return $requested;
10133 }
10134
10135 $keys = array_keys( $editable );
10136
10137 return ! empty( $keys ) ? $keys[0] : '';
10138 }
10139
10140 function ebook_store_get_empty_template_scaffold( $template_name ) {
10141 $template_name = trim( (string) $template_name );
10142
10143 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";
10144 }
10145
10146 function ebook_store_validate_template_php_syntax( $template_code ) {
10147 $template_code = (string) $template_code;
10148
10149 if ( trim( $template_code ) === '' ) {
10150 return new WP_Error( 'ebook_store_template_empty', __( 'Template code cannot be empty.', 'ebook-store' ) );
10151 }
10152
10153 if ( strpos( $template_code, '<?php' ) === false ) {
10154 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' ) );
10155 }
10156
10157 if ( ! function_exists( 'shell_exec' ) ) {
10158 return true;
10159 }
10160
10161 $php_binary = defined( 'PHP_BINARY' ) && PHP_BINARY ? PHP_BINARY : '';
10162 if ( ! $php_binary || ! is_executable( $php_binary ) ) {
10163 $php_binary = trim( (string) @shell_exec( 'command -v php 2>/dev/null' ) );
10164 }
10165
10166 if ( $php_binary === '' ) {
10167 return true;
10168 }
10169
10170 $temp_file = wp_tempnam( 'ebook-store-template-lint' );
10171 if ( ! $temp_file ) {
10172 return true;
10173 }
10174
10175 @file_put_contents( $temp_file, $template_code );
10176 $command = escapeshellarg( $php_binary ) . ' -l ' . escapeshellarg( $temp_file ) . ' 2>&1';
10177 $output = @shell_exec( $command );
10178 @unlink( $temp_file );
10179
10180 if ( ! is_string( $output ) ) {
10181 return true;
10182 }
10183
10184 if ( stripos( $output, 'No syntax errors detected' ) !== false ) {
10185 return true;
10186 }
10187
10188 $message = trim( preg_replace( '/\s+in\s+.+?on line\s+/i', ' on line ', $output ) );
10189 if ( $message === '' ) {
10190 $message = __( 'PHP syntax validation failed for the template code.', 'ebook-store' );
10191 }
10192
10193 return new WP_Error( 'ebook_store_template_syntax_error', $message );
10194 }
10195
10196 function ebook_store_template_editor_create_template() {
10197 $template_name = isset( $_POST['new_template_name'] ) ? sanitize_text_field( wp_unslash( $_POST['new_template_name'] ) ) : '';
10198 $template_source = isset( $_POST['new_template_source'] ) ? sanitize_key( wp_unslash( $_POST['new_template_source'] ) ) : '';
10199 $template_key = sanitize_title( $template_name );
10200 $editable = ebook_store_get_template_editor_files();
10201
10202 if ( $template_name === '' ) {
10203 return array(
10204 'errors' => array( __( 'Please enter a template name.', 'ebook-store' ) ),
10205 );
10206 }
10207
10208 if ( $template_key === '' ) {
10209 return array(
10210 'errors' => array( __( 'The template name must contain letters or numbers.', 'ebook-store' ) ),
10211 );
10212 }
10213
10214 if ( isset( $editable[ $template_key ] ) ) {
10215 return array(
10216 'errors' => array( __( 'A template with that name already exists.', 'ebook-store' ) ),
10217 );
10218 }
10219
10220 $template_dir = ebook_store_ensure_custom_template_directory();
10221 if ( $template_dir === '' ) {
10222 return array(
10223 'errors' => array( __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) ),
10224 );
10225 }
10226
10227 $template_file = $template_dir . $template_key . '.php';
10228 if ( file_exists( $template_file ) ) {
10229 return array(
10230 'errors' => array( __( 'A template file with that slug already exists.', 'ebook-store' ) ),
10231 );
10232 }
10233
10234 $template_code = '';
10235 if ( $template_source !== '' && $template_source !== 'none' && isset( $editable[ $template_source ] ) && file_exists( $editable[ $template_source ]['file'] ) ) {
10236 $template_code = (string) file_get_contents( $editable[ $template_source ]['file'] );
10237 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
10238 $template_code = preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
10239 }
10240 } else {
10241 $template_code = ebook_store_get_empty_template_scaffold( $template_name );
10242 }
10243
10244 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
10245 if ( is_wp_error( $syntax_check ) ) {
10246 return array(
10247 'errors' => array( $syntax_check->get_error_message() ),
10248 );
10249 }
10250
10251 $result = @file_put_contents( $template_file, $template_code );
10252 if ( $result === false ) {
10253 return array(
10254 'errors' => array( __( 'WordPress could not create the new template file.', 'ebook-store' ) ),
10255 );
10256 }
10257
10258 wp_safe_redirect(
10259 ebook_store_get_template_editor_url(
10260 array(
10261 'template' => $template_key,
10262 'created' => 1,
10263 )
10264 )
10265 );
10266 exit;
10267 }
10268
10269 function ebook_store_template_editor_delete_template() {
10270 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
10271 $editable = ebook_store_get_template_editor_files();
10272
10273 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
10274 return array(
10275 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
10276 );
10277 }
10278
10279 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
10280 return array(
10281 'errors' => array( __( 'Built-in templates cannot be deleted.', 'ebook-store' ) ),
10282 'template_key' => $template_key,
10283 );
10284 }
10285
10286 if ( ! file_exists( $editable[ $template_key ]['file'] ) || ! is_writable( $editable[ $template_key ]['file'] ) ) {
10287 return array(
10288 'errors' => array( __( 'That template file could not be deleted because it is missing or not writable.', 'ebook-store' ) ),
10289 'template_key' => $template_key,
10290 );
10291 }
10292
10293 $result = @unlink( $editable[ $template_key ]['file'] );
10294 if ( ! $result ) {
10295 return array(
10296 'errors' => array( __( 'WordPress could not delete that template file.', 'ebook-store' ) ),
10297 'template_key' => $template_key,
10298 );
10299 }
10300
10301 wp_safe_redirect(
10302 ebook_store_get_template_editor_url(
10303 array(
10304 'template' => 'modern',
10305 'deleted' => 1,
10306 )
10307 )
10308 );
10309 exit;
10310 }
10311
10312 function ebook_store_template_editor_save_template() {
10313 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
10314 $editable = ebook_store_get_template_editor_files();
10315
10316 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
10317 return array(
10318 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
10319 );
10320 }
10321
10322 $template_code = isset( $_POST['template_code'] ) ? wp_unslash( $_POST['template_code'] ) : '';
10323
10324 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
10325 if ( is_wp_error( $syntax_check ) ) {
10326 return array(
10327 'errors' => array( $syntax_check->get_error_message() ),
10328 'template_key' => $template_key,
10329 'template_code' => $template_code,
10330 );
10331 }
10332
10333 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
10334 $preferred_name = sprintf( __( '%s Custom', 'ebook-store' ), $editable[ $template_key ]['label'] );
10335 $preferred_key = sanitize_title( $template_key . '-custom' );
10336 $created_template = ebook_store_create_custom_template_from_code( $preferred_name, $template_code, $preferred_key );
10337 if ( is_wp_error( $created_template ) ) {
10338 return array(
10339 'errors' => array( $created_template->get_error_message() ),
10340 'template_key' => $template_key,
10341 'template_code' => $template_code,
10342 );
10343 }
10344
10345 $reassign = ! empty( $_POST['template_editor_reassign_books'] );
10346 $assignment = array(
10347 'ebook_count' => 0,
10348 'default_updated' => false,
10349 );
10350 if ( $reassign ) {
10351 $assignment = ebook_store_reassign_template_usage( $template_key, $created_template['key'] );
10352 }
10353
10354 wp_safe_redirect(
10355 ebook_store_get_template_editor_url(
10356 array(
10357 'template' => $created_template['key'],
10358 'created' => 1,
10359 'forked_from_core' => 1,
10360 'copied_from' => $template_key,
10361 'reassigned' => $reassign ? 1 : 0,
10362 'reassigned_ebooks' => isset( $assignment['ebook_count'] ) ? (int) $assignment['ebook_count'] : 0,
10363 'reassigned_default' => ! empty( $assignment['default_updated'] ) ? 1 : 0,
10364 )
10365 )
10366 );
10367 exit;
10368 }
10369
10370 $template_file = $editable[ $template_key ]['file'];
10371 if ( ! is_writable( $template_file ) ) {
10372 return array(
10373 'errors' => array( __( 'That template file is not writable on the server.', 'ebook-store' ) ),
10374 'template_key' => $template_key,
10375 'template_code' => $template_code,
10376 );
10377 }
10378
10379 $result = @file_put_contents( $template_file, $template_code );
10380
10381 if ( $result === false ) {
10382 return array(
10383 'errors' => array( __( 'WordPress could not save the updated template code.', 'ebook-store' ) ),
10384 'template_key' => $template_key,
10385 'template_code' => $template_code,
10386 );
10387 }
10388
10389 wp_safe_redirect(
10390 ebook_store_get_template_editor_url(
10391 array(
10392 'template' => $template_key,
10393 'saved' => 1,
10394 )
10395 )
10396 );
10397 exit;
10398 }
10399
10400 function ebook_store_template_editor_handle_submission() {
10401 if ( empty( $_POST['ebook_store_template_editor_action'] ) ) {
10402 return null;
10403 }
10404
10405 if ( ! ebook_store_can_access_template_editor() ) {
10406 return array(
10407 'errors' => array( __( 'You do not have permission to edit Ebook Store templates.', 'ebook-store' ) ),
10408 );
10409 }
10410
10411 check_admin_referer( 'ebook_store_template_editor' );
10412
10413 $action_type = isset( $_POST['template_editor_action_type'] ) ? sanitize_key( wp_unslash( $_POST['template_editor_action_type'] ) ) : 'save';
10414
10415 if ( $action_type === 'create' ) {
10416 return ebook_store_template_editor_create_template();
10417 }
10418
10419 if ( $action_type === 'delete' ) {
10420 return ebook_store_template_editor_delete_template();
10421 }
10422
10423 return ebook_store_template_editor_save_template();
10424 }
10425
10426 function ebook_store_template_editor_page_callback() {
10427 if ( ! ebook_store_can_access_template_editor() ) {
10428 wp_die(
10429 esc_html__( 'You do not have permission to access the Ebook Store template editor.', 'ebook-store' ),
10430 esc_html__( 'Access denied', 'ebook-store' ),
10431 array( 'response' => 403 )
10432 );
10433 }
10434
10435 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
10436 $locale = ebook_store_get_locale_strings();
10437 $formats_locale = ebook_store_get_format_labels();
10438 wp_enqueue_style( 'ebookstorestylesheet' );
10439
10440 $code_editor_settings = wp_enqueue_code_editor(
10441 array(
10442 'type' => 'application/x-httpd-php',
10443 'codemirror' => array(
10444 'indentUnit' => 4,
10445 'tabSize' => 4,
10446 'lineNumbers' => true,
10447 'lineWrapping' => false,
10448 'mode' => 'application/x-httpd-php',
10449 ),
10450 )
10451 );
10452
10453 wp_enqueue_script( 'code-editor' );
10454 wp_enqueue_script( 'wp-theme-plugin-editor' );
10455 wp_enqueue_style( 'code-editor' );
10456
10457 $submission = null;
10458 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_template_editor_action'] ) ) {
10459 $submission = ebook_store_template_editor_handle_submission();
10460 }
10461
10462 $editable = ebook_store_get_template_editor_files();
10463 $current_template = ebook_store_get_template_editor_current_key( isset( $_REQUEST['template'] ) ? wp_unslash( $_REQUEST['template'] ) : '' );
10464 if ( $submission && ! empty( $submission['template_key'] ) ) {
10465 $current_template = ebook_store_get_template_editor_current_key( $submission['template_key'] );
10466 }
10467
10468 $current = $current_template !== '' && isset( $editable[ $current_template ] ) ? $editable[ $current_template ] : null;
10469 $current_code = '';
10470
10471 if ( $submission && array_key_exists( 'template_code', (array) $submission ) ) {
10472 $current_code = (string) $submission['template_code'];
10473 } elseif ( $current && file_exists( $current['file'] ) ) {
10474 $current_code = (string) file_get_contents( $current['file'] );
10475 }
10476
10477 $saved = isset( $_GET['saved'] ) && (int) $_GET['saved'] === 1;
10478 $created = isset( $_GET['created'] ) && (int) $_GET['created'] === 1;
10479 $deleted = isset( $_GET['deleted'] ) && (int) $_GET['deleted'] === 1;
10480 $forked_from_core = isset( $_GET['forked_from_core'] ) && (int) $_GET['forked_from_core'] === 1;
10481 $copied_from = isset( $_GET['copied_from'] ) ? sanitize_key( wp_unslash( $_GET['copied_from'] ) ) : '';
10482 $reassigned = isset( $_GET['reassigned'] ) && (int) $_GET['reassigned'] === 1;
10483 $reassigned_ebooks = isset( $_GET['reassigned_ebooks'] ) ? max( 0, (int) $_GET['reassigned_ebooks'] ) : 0;
10484 $reassigned_default = isset( $_GET['reassigned_default'] ) && (int) $_GET['reassigned_default'] === 1;
10485 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
10486 $wordfence_active = ebook_store_is_wordfence_active();
10487 $current_usage = $current ? ebook_store_get_template_usage_stats( $current['key'] ) : array(
10488 'ebooks' => 0,
10489 'default' => false,
10490 );
10491
10492 ob_start();
10493 ?>
10494 <div class="wrap ebook-template-editor">
10495 <div class="ebook-template-editor__hero">
10496 <div class="ebook-template-editor__hero-copy">
10497 <span class="ebook-template-editor__eyebrow"><?php esc_html_e( 'Developer tools', 'ebook-store' ); ?></span>
10498 <h1><?php esc_html_e( 'Button templates', 'ebook-store' ); ?></h1>
10499 <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>
10500 <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>
10501 </div>
10502 <div class="ebook-template-editor__hero-meta">
10503 <span class="ebook-template-editor__pill"><?php esc_html_e( 'PHP-safe editing', 'ebook-store' ); ?></span>
10504 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Update-safe custom copies', 'ebook-store' ); ?></span>
10505 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Template-scoped access only', 'ebook-store' ); ?></span>
10506 </div>
10507 </div>
10508
10509 <?php if ( $saved && $current ) : ?>
10510 <div class="notice notice-success is-dismissible">
10511 <p>
10512 <strong><?php esc_html_e( 'Template saved.', 'ebook-store' ); ?></strong>
10513 <?php
10514 printf(
10515 esc_html__( '%s was updated successfully.', 'ebook-store' ),
10516 esc_html( $current['label'] )
10517 );
10518 ?>
10519 </p>
10520 </div>
10521 <?php endif; ?>
10522
10523 <?php if ( $created && $current && ! $forked_from_core ) : ?>
10524 <div class="notice notice-success is-dismissible">
10525 <p>
10526 <strong><?php esc_html_e( 'Template created.', 'ebook-store' ); ?></strong>
10527 <?php
10528 printf(
10529 esc_html__( '%s is now available in the template selector and stored separately from plugin updates.', 'ebook-store' ),
10530 esc_html( $current['label'] )
10531 );
10532 ?>
10533 </p>
10534 </div>
10535 <?php endif; ?>
10536
10537 <?php if ( $created && $current && $forked_from_core ) : ?>
10538 <div class="notice notice-success is-dismissible">
10539 <p>
10540 <strong><?php esc_html_e( 'Custom template created.', 'ebook-store' ); ?></strong>
10541 <?php
10542 printf(
10543 esc_html__( '%1$s was saved as the new custom template %2$s, so it will survive plugin updates.', 'ebook-store' ),
10544 esc_html( $copied_from !== '' && isset( $editable[ $copied_from ] ) ? $editable[ $copied_from ]['label'] : __( 'Built-in template', 'ebook-store' ) ),
10545 esc_html( $current['label'] )
10546 );
10547 ?>
10548 </p>
10549 <?php if ( $reassigned ) : ?>
10550 <p>
10551 <?php
10552 printf(
10553 esc_html__( '%1$d ebook(s) were reassigned to use this custom template.%2$s', 'ebook-store' ),
10554 (int) $reassigned_ebooks,
10555 $reassigned_default ? ' ' . esc_html__( 'The default template setting was updated too.', 'ebook-store' ) : ''
10556 );
10557 ?>
10558 </p>
10559 <?php endif; ?>
10560 </div>
10561 <?php endif; ?>
10562
10563 <?php if ( $deleted ) : ?>
10564 <div class="notice notice-success is-dismissible">
10565 <p><strong><?php esc_html_e( 'Template deleted.', 'ebook-store' ); ?></strong> <?php esc_html_e( 'The custom template file was removed.', 'ebook-store' ); ?></p>
10566 </div>
10567 <?php endif; ?>
10568
10569 <?php if ( ! empty( $errors ) ) : ?>
10570 <div class="notice notice-error">
10571 <p><strong><?php esc_html_e( 'The template could not be saved yet.', 'ebook-store' ); ?></strong></p>
10572 <ul class="ebook-template-editor__notice-list">
10573 <?php foreach ( $errors as $error ) : ?>
10574 <li><?php echo esc_html( $error ); ?></li>
10575 <?php endforeach; ?>
10576 </ul>
10577 </div>
10578 <?php endif; ?>
10579
10580 <?php if ( $wordfence_active ) : ?>
10581 <div class="notice notice-warning">
10582 <p>
10583 <strong><?php esc_html_e( 'Wordfence note:', 'ebook-store' ); ?></strong>
10584 <?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' ); ?>
10585 </p>
10586 <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>
10587 </div>
10588 <?php endif; ?>
10589
10590 <div class="ebook-template-editor__layout">
10591 <aside class="ebook-template-editor__sidebar">
10592 <div class="ebook-template-editor__nav-card">
10593 <div class="ebook-template-editor__nav-header">
10594 <h2><?php esc_html_e( 'Templates', 'ebook-store' ); ?></h2>
10595 <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>
10596 </div>
10597 <div class="ebook-template-editor__nav">
10598 <?php foreach ( $editable as $template ) : ?>
10599 <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'] ) ) ); ?>">
10600 <strong><?php echo esc_html( $template['label'] ); ?></strong>
10601 <small><?php echo esc_html( $template['filename'] ); ?></small>
10602 </a>
10603 <?php endforeach; ?>
10604 </div>
10605 </div>
10606 </aside>
10607
10608 <div class="ebook-template-editor__main">
10609 <?php if ( $current ) : ?>
10610 <form method="post" class="ebook-template-editor__form">
10611 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10612 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10613 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
10614
10615 <div class="ebook-template-editor__panel">
10616 <div class="ebook-template-editor__panel-header">
10617 <div>
10618 <h2><?php echo esc_html( $current['label'] ); ?></h2>
10619 <p>
10620 <?php
10621 echo ! empty( $current['core'] )
10622 ? esc_html__( 'Built-in templates are update-protected. Saving here creates a new custom template copy instead of overwriting the plugin file.', 'ebook-store' )
10623 : esc_html__( 'Editing the custom PHP template file used by Ebook Store for this layout.', 'ebook-store' );
10624 ?>
10625 </p>
10626 </div>
10627 <div class="ebook-template-editor__header-side">
10628 <div class="ebook-template-editor__file-meta">
10629 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $current['filename'] ); ?></span>
10630 <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>
10631 </div>
10632 <?php if ( empty( $current['core'] ) ) : ?>
10633 <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>
10634 <?php endif; ?>
10635 </div>
10636 </div>
10637
10638 <div class="ebook-template-editor__editor-wrap">
10639 <textarea id="ebook_store_template_code" name="template_code" class="large-text code" rows="28" spellcheck="false"><?php echo esc_textarea( $current_code ); ?></textarea>
10640 </div>
10641
10642 <?php if ( ! empty( $current['core'] ) ) : ?>
10643 <div class="ebook-template-editor__field">
10644 <label>
10645 <input type="checkbox" name="template_editor_reassign_books" value="1" />
10646 <?php
10647 printf(
10648 esc_html__( 'Also assign the new custom template automatically to ebooks currently using %s.', 'ebook-store' ),
10649 esc_html( $current['label'] )
10650 );
10651 ?>
10652 </label>
10653 <p class="description">
10654 <?php
10655 printf(
10656 esc_html__( 'Right now %1$d ebook(s)%2$s resolve to this template.', 'ebook-store' ),
10657 (int) $current_usage['ebooks'],
10658 ! empty( $current_usage['default'] ) ? ' ' . esc_html__( 'The global default template also points to it.', 'ebook-store' ) : ''
10659 );
10660 ?>
10661 </p>
10662 </div>
10663 <?php endif; ?>
10664
10665 <div class="ebook-template-editor__actions">
10666 <input type="hidden" name="template_editor_action_type" value="save" />
10667 <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>
10668 <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>
10669 </div>
10670 <?php if ( $wordfence_active ) : ?>
10671 <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>
10672 <?php endif; ?>
10673 </div>
10674 </form>
10675 <?php else : ?>
10676 <div class="ebook-template-editor__panel">
10677 <p><?php esc_html_e( 'No editable templates were found.', 'ebook-store' ); ?></p>
10678 </div>
10679 <?php endif; ?>
10680 </div>
10681 </div>
10682 </div>
10683 <div class="ebook-template-editor__modal" data-template-modal="create" hidden>
10684 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10685 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-create-title">
10686 <form method="post" class="ebook-template-editor__modal-form">
10687 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10688 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10689 <input type="hidden" name="template_editor_action_type" value="create" />
10690 <div class="ebook-template-editor__modal-header">
10691 <h2 id="ebook-template-editor-create-title"><?php esc_html_e( 'Create new template', 'ebook-store' ); ?></h2>
10692 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10693 </div>
10694 <div class="ebook-template-editor__modal-body">
10695 <div class="ebook-template-editor__field">
10696 <label for="new_template_name"><?php esc_html_e( 'Template name', 'ebook-store' ); ?></label>
10697 <input type="text" id="new_template_name" name="new_template_name" required />
10698 </div>
10699 <div class="ebook-template-editor__field">
10700 <label for="new_template_source"><?php esc_html_e( 'Copy code from', 'ebook-store' ); ?></label>
10701 <select id="new_template_source" name="new_template_source">
10702 <option value="none"><?php esc_html_e( 'None (start with an empty scaffold)', 'ebook-store' ); ?></option>
10703 <?php foreach ( $editable as $template ) : ?>
10704 <option value="<?php echo esc_attr( $template['key'] ); ?>"><?php echo esc_html( $template['label'] ); ?></option>
10705 <?php endforeach; ?>
10706 </select>
10707 <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>
10708 </div>
10709 </div>
10710 <div class="ebook-template-editor__modal-actions">
10711 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10712 <button type="submit" class="button button-primary"><?php esc_html_e( 'Create template', 'ebook-store' ); ?></button>
10713 </div>
10714 </form>
10715 </div>
10716 </div>
10717 <?php if ( $current && empty( $current['core'] ) ) : ?>
10718 <div class="ebook-template-editor__modal" data-template-modal="delete" hidden>
10719 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10720 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-delete-title">
10721 <form method="post" class="ebook-template-editor__modal-form">
10722 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10723 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10724 <input type="hidden" name="template_editor_action_type" value="delete" />
10725 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
10726 <div class="ebook-template-editor__modal-header">
10727 <h2 id="ebook-template-editor-delete-title"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></h2>
10728 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10729 </div>
10730 <div class="ebook-template-editor__modal-body">
10731 <p><?php printf( esc_html__( 'Delete the custom template “%s”? This removes the saved custom template file.', 'ebook-store' ), esc_html( $current['label'] ) ); ?></p>
10732 <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>
10733 </div>
10734 <div class="ebook-template-editor__modal-actions">
10735 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10736 <button type="submit" class="button button-primary button-link-delete"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></button>
10737 </div>
10738 </form>
10739 </div>
10740 </div>
10741 <?php endif; ?>
10742 <?php
10743
10744 if ( $code_editor_settings && $current ) {
10745 wp_add_inline_script(
10746 'code-editor',
10747 'jQuery(function(){wp.codeEditor.initialize("ebook_store_template_code",' . wp_json_encode( $code_editor_settings ) . ');});'
10748 );
10749 }
10750
10751 wp_add_inline_script(
10752 'code-editor',
10753 '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);}});});'
10754 );
10755
10756 $output = ob_get_clean();
10757 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
10758 }
10759
10760 function ebook_store_setup_wizard_menu_title() {
10761 return sprintf(
10762 '%s <span class="update-plugins count-1"><span class="plugin-count">%s</span></span>',
10763 esc_html__( 'Setup wizard', 'ebook-store' ),
10764 esc_html__( 'new', 'ebook-store' )
10765 );
10766 }
10767
10768 function ebook_store_setup_wizard_is_pdf( $file ) {
10769 if ( empty( $file['name'] ) || empty( $file['tmp_name'] ) ) {
10770 return false;
10771 }
10772
10773 $filetype = wp_check_filetype( basename( $file['name'] ) );
10774 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
10775 $mime_type = isset( $filetype['type'] ) ? strtolower( (string) $filetype['type'] ) : '';
10776
10777 return $extension === 'pdf' || $mime_type === 'application/pdf';
10778 }
10779
10780 function ebook_store_setup_wizard_human_mode_label( $mode ) {
10781 if ( $mode === 'woocommerce' ) {
10782 return __( 'WooCommerce integration', 'ebook-store' );
10783 }
10784
10785 return __( 'Standalone ebook sales', 'ebook-store' );
10786 }
10787
10788 function ebook_store_is_paypal_enabled() {
10789 return (string) get_option( 'paypal_integration_enabled', '1' ) === '1';
10790 }
10791
10792 function ebook_store_get_standalone_payment_method_state() {
10793 $paypal_ready = ebook_store_is_paypal_enabled() && trim( (string) get_option( 'paypal_account', '' ) ) !== '';
10794 $stripe_ready = (string) get_option( 'stripe_integration_enabled', '0' ) === '1'
10795 && trim( (string) get_option( 'stripe_publishable_key', '' ) ) !== ''
10796 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== '';
10797 $adyen_environment = (string) get_option( 'adyen_environment', 'test' );
10798 $adyen_ready = (string) get_option( 'adyen_integration_enabled', '0' ) === '1'
10799 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
10800 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
10801 && trim( (string) get_option( 'adyen_hmac_key', '' ) ) !== ''
10802 && ( $adyen_environment !== 'live' || trim( (string) get_option( 'adyen_live_prefix', '' ) ) !== '' );
10803
10804 return array(
10805 'paypal' => array(
10806 'enabled' => ebook_store_is_paypal_enabled(),
10807 'ready' => $paypal_ready,
10808 ),
10809 'stripe' => array(
10810 'enabled' => (string) get_option( 'stripe_integration_enabled', '0' ) === '1',
10811 'ready' => $stripe_ready,
10812 ),
10813 'adyen' => array(
10814 'enabled' => (string) get_option( 'adyen_integration_enabled', '0' ) === '1',
10815 'ready' => $adyen_ready,
10816 ),
10817 );
10818 }
10819
10820 function ebook_store_has_ready_standalone_payment_method() {
10821 $methods = ebook_store_get_standalone_payment_method_state();
10822
10823 foreach ( $methods as $method ) {
10824 if ( ! empty( $method['ready'] ) ) {
10825 return true;
10826 }
10827 }
10828
10829 return false;
10830 }
10831
10832 function ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ) {
10833 if ( ! class_exists( 'Imagick' ) || ! file_exists( $pdf_path ) ) {
10834 return 0;
10835 }
10836
10837 try {
10838 $imagick = new Imagick();
10839 $imagick->pingImage( $pdf_path );
10840 $page_count = (int) $imagick->getNumberImages();
10841 $imagick->clear();
10842 $imagick->destroy();
10843
10844 return max( 0, $page_count );
10845 } catch ( Exception $exception ) {
10846 return 0;
10847 }
10848 }
10849
10850 function ebook_store_setup_wizard_unescape_pdf_literal( $value ) {
10851 $value = preg_replace_callback(
10852 '/\\\\([0-7]{1,3})/',
10853 function ( $matches ) {
10854 return chr( octdec( $matches[1] ) );
10855 },
10856 (string) $value
10857 );
10858
10859 $replacements = array(
10860 '\\n' => "\n",
10861 '\\r' => "\r",
10862 '\\t' => "\t",
10863 '\\b' => "\b",
10864 '\\f' => "\f",
10865 '\\(' => '(',
10866 '\\)' => ')',
10867 '\\\\' => '\\',
10868 );
10869
10870 return strtr( $value, $replacements );
10871 }
10872
10873 function ebook_store_setup_wizard_clean_pdf_text( $text ) {
10874 $text = html_entity_decode( (string) $text, ENT_QUOTES, 'UTF-8' );
10875 $text = preg_replace( "/\r\n?/", "\n", $text );
10876 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', ' ', $text );
10877 $text = preg_replace( '/[ \t]+/', ' ', $text );
10878 $text = preg_replace( "/\n{3,}/", "\n\n", $text );
10879
10880 $lines = preg_split( "/\n+/", trim( $text ) );
10881 $clean_lines = array();
10882
10883 foreach ( $lines as $line ) {
10884 $line = trim( wp_strip_all_tags( $line ) );
10885 $line = preg_replace( '/\s+/', ' ', $line );
10886
10887 if ( strlen( $line ) < 3 ) {
10888 continue;
10889 }
10890
10891 if ( preg_match( '/^[^A-Za-z\\p{L}\\p{N}]+$/u', $line ) ) {
10892 continue;
10893 }
10894
10895 if ( in_array( $line, $clean_lines, true ) ) {
10896 continue;
10897 }
10898
10899 $clean_lines[] = $line;
10900 }
10901
10902 return trim( implode( "\n", $clean_lines ) );
10903 }
10904
10905 function ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path ) {
10906 if ( ! function_exists( 'shell_exec' ) || ! file_exists( $pdf_path ) ) {
10907 return '';
10908 }
10909
10910 $ghostscript_path = trim( (string) @shell_exec( 'command -v gs 2>/dev/null' ) );
10911 if ( $ghostscript_path === '' ) {
10912 return '';
10913 }
10914
10915 $command = escapeshellarg( $ghostscript_path ) . ' -q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=txtwrite -sOutputFile=- ' . escapeshellarg( $pdf_path ) . ' 2>/dev/null';
10916 $output = @shell_exec( $command );
10917
10918 return is_string( $output ) ? ebook_store_setup_wizard_clean_pdf_text( $output ) : '';
10919 }
10920
10921 function ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path ) {
10922 if ( ! file_exists( $pdf_path ) ) {
10923 return '';
10924 }
10925
10926 $pdf_contents = @file_get_contents( $pdf_path );
10927 if ( ! is_string( $pdf_contents ) || $pdf_contents === '' ) {
10928 return '';
10929 }
10930
10931 $matches = array();
10932 preg_match_all( '/stream[\r\n]+(.*?)endstream/s', $pdf_contents, $matches );
10933 if ( empty( $matches[1] ) ) {
10934 return '';
10935 }
10936
10937 $chunks = array();
10938
10939 foreach ( array_slice( $matches[1], 0, 12 ) as $stream ) {
10940 $candidates = array( $stream );
10941
10942 if ( function_exists( 'gzuncompress' ) ) {
10943 $decoded = @gzuncompress( $stream );
10944 if ( is_string( $decoded ) && $decoded !== '' ) {
10945 $candidates[] = $decoded;
10946 }
10947 }
10948
10949 if ( function_exists( 'gzinflate' ) ) {
10950 $decoded = @gzinflate( $stream );
10951 if ( is_string( $decoded ) && $decoded !== '' ) {
10952 $candidates[] = $decoded;
10953 }
10954
10955 $decoded = @gzinflate( substr( $stream, 2 ) );
10956 if ( is_string( $decoded ) && $decoded !== '' ) {
10957 $candidates[] = $decoded;
10958 }
10959 }
10960
10961 foreach ( $candidates as $candidate ) {
10962 $text_matches = array();
10963 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)\s*Tj/s', $candidate, $text_matches );
10964 foreach ( $text_matches[0] as $text_match ) {
10965 if ( preg_match( '/^\((.*)\)\s*Tj$/s', $text_match, $literal_match ) ) {
10966 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( $literal_match[1] );
10967 }
10968 }
10969
10970 $array_matches = array();
10971 preg_match_all( '/\[(.*?)\]\s*TJ/s', $candidate, $array_matches );
10972 foreach ( $array_matches[1] as $array_match ) {
10973 $literal_matches = array();
10974 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)/s', $array_match, $literal_matches );
10975 foreach ( $literal_matches[0] as $literal ) {
10976 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( trim( $literal, '()' ) );
10977 }
10978 }
10979 }
10980 }
10981
10982 return ebook_store_setup_wizard_clean_pdf_text( implode( "\n", $chunks ) );
10983 }
10984
10985 function ebook_store_setup_wizard_extract_pdf_details( $pdf_path, $fallback_title = '' ) {
10986 $details = array(
10987 'title' => '',
10988 'description' => '',
10989 'author' => '',
10990 'pages' => ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ),
10991 );
10992
10993 if ( ! file_exists( $pdf_path ) ) {
10994 return $details;
10995 }
10996
10997 $pdf_contents = @file_get_contents( $pdf_path );
10998 if ( is_string( $pdf_contents ) && $pdf_contents !== '' ) {
10999 if ( preg_match( '/\/Title\s*\((.*?)\)/s', $pdf_contents, $title_match ) ) {
11000 $details['title'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $title_match[1] ) );
11001 }
11002
11003 if ( preg_match( '/\/Subject\s*\((.*?)\)/s', $pdf_contents, $subject_match ) ) {
11004 $details['description'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $subject_match[1] ) );
11005 }
11006
11007 if ( preg_match( '/\/Author\s*\((.*?)\)/s', $pdf_contents, $author_match ) ) {
11008 $details['author'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $author_match[1] ) );
11009 }
11010 }
11011
11012 $text = ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path );
11013 if ( $text === '' ) {
11014 $text = ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path );
11015 }
11016
11017 $lines = $text !== '' ? array_values( array_filter( preg_split( "/\n+/", $text ) ) ) : array();
11018 if ( $details['title'] === '' && ! empty( $lines ) ) {
11019 foreach ( $lines as $line ) {
11020 $line = trim( $line );
11021 if ( strlen( $line ) >= 4 && strlen( $line ) <= 140 ) {
11022 $details['title'] = $line;
11023 break;
11024 }
11025 }
11026 }
11027
11028 if ( $details['description'] === '' && count( $lines ) > 1 ) {
11029 $description_lines = array();
11030 foreach ( $lines as $line ) {
11031 $line = trim( $line );
11032 if ( $line === '' ) {
11033 continue;
11034 }
11035 if ( $details['title'] !== '' && strcasecmp( $line, $details['title'] ) === 0 ) {
11036 continue;
11037 }
11038 $description_lines[] = $line;
11039 if ( strlen( implode( ' ', $description_lines ) ) >= 280 ) {
11040 break;
11041 }
11042 }
11043 $details['description'] = trim( implode( ' ', array_slice( $description_lines, 0, 4 ) ) );
11044 }
11045
11046 if ( $details['title'] === '' ) {
11047 $details['title'] = $fallback_title;
11048 }
11049
11050 if ( $details['description'] !== '' ) {
11051 $details['description'] = wp_trim_words( $details['description'], 48, '...' );
11052 }
11053
11054 return $details;
11055 }
11056
11057 function ebook_store_setup_wizard_upload_pdf( $ebook_id, $file ) {
11058 if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) {
11059 return new WP_Error( 'ebook_store_wizard_upload_missing', __( 'The uploaded PDF file could not be read.', 'ebook-store' ) );
11060 }
11061
11062 // The wizard accepted whatever it was handed. Run the same content check the
11063 // rest of the plugin uses, so a renamed file cannot be parked here.
11064 if ( function_exists( 'ebook_store_validate_uploaded_ebook' ) ) {
11065 $validated = ebook_store_validate_uploaded_ebook( $file['tmp_name'], 'pdf' );
11066
11067 if ( is_wp_error( $validated ) ) {
11068 return $validated;
11069 }
11070 }
11071
11072 // This used to be wp_upload_bits( ..., file_get_contents( $tmp ) ), which
11073 // pulled the whole book into memory — a 60 MB PDF blew memory_limit before it
11074 // ever reached disk. Moving the temp file is constant-memory.
11075 if ( function_exists( 'ebook_store_store_uploaded_ebook' ) ) {
11076 $upload = ebook_store_store_uploaded_ebook( $file['tmp_name'], $file['name'] );
11077 } else {
11078 add_filter( 'upload_dir', 'ebook_set_upload_dir' );
11079 $upload = wp_upload_bits( $file['name'], null, file_get_contents( $file['tmp_name'] ) );
11080 remove_filter( 'upload_dir', 'ebook_set_upload_dir' );
11081 }
11082
11083 if ( is_wp_error( $upload ) ) {
11084 return $upload;
11085 }
11086
11087 if ( isset( $upload['error'] ) && $upload['error'] ) {
11088 return new WP_Error( 'ebook_store_wizard_upload_failed', $upload['error'] );
11089 }
11090
11091 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment', wp_slash( $upload ) );
11092 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment_pdf', wp_slash( $upload ) );
11093 ebook_store_generate_cover_from_pdf( $ebook_id );
11094
11095 return $upload;
11096 }
11097
11098 function ebook_store_setup_wizard_create_ebook( $args ) {
11099 $title = isset( $args['title'] ) ? sanitize_text_field( $args['title'] ) : '';
11100 $description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : '';
11101 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
11102 $currency = isset( $args['currency'] ) && $args['currency'] !== '' ? sanitize_text_field( $args['currency'] ) : ebook_store_get_store_currency();
11103 $pdf_file = isset( $args['pdf_file'] ) ? $args['pdf_file'] : array();
11104 $pdf_pages = isset( $args['pages'] ) ? (int) $args['pages'] : 0;
11105 $author = isset( $args['author'] ) ? sanitize_text_field( $args['author'] ) : '';
11106 $template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( '' ) : 'modern';
11107 $delivery_mode = $price > 0 ? 'paid' : 'free';
11108
11109 if ( $title === '' ) {
11110 $title = __( 'Imported ebook', 'ebook-store' );
11111 }
11112
11113 $ebook_post = array(
11114 'post_title' => $title,
11115 'post_content' => $description,
11116 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $description ), 40, '...' ),
11117 'post_status' => 'draft',
11118 'post_type' => 'ebook',
11119 );
11120
11121 $ebook_id = wp_insert_post( $ebook_post, true );
11122 if ( is_wp_error( $ebook_id ) ) {
11123 return $ebook_id;
11124 }
11125
11126 $ebook_meta = array(
11127 'ebook_price' => number_format( $price, 2, '.', '' ),
11128 'paypal_currency' => $currency,
11129 'donate_or_download' => $delivery_mode,
11130 'form_template' => $template,
11131 );
11132
11133 if ( $pdf_pages > 0 ) {
11134 $ebook_meta['ebook_pages'] = $pdf_pages;
11135 }
11136
11137 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
11138 update_post_meta( $ebook_id, 'ebook_bonus', array() );
11139
11140 if ( $author !== '' ) {
11141 update_post_meta( $ebook_id, 'ebook_store_wizard_author_name', $author );
11142 }
11143
11144 $upload = ebook_store_setup_wizard_upload_pdf( $ebook_id, $pdf_file );
11145 if ( is_wp_error( $upload ) ) {
11146 wp_delete_post( $ebook_id, true );
11147 return $upload;
11148 }
11149
11150 return array(
11151 'ebook_id' => $ebook_id,
11152 'title' => $title,
11153 'description' => $description,
11154 'pages' => $pdf_pages,
11155 );
11156 }
11157
11158 function ebook_store_setup_wizard_create_woocommerce_bundle( $args ) {
11159 if ( ! class_exists( 'WooCommerce' ) ) {
11160 return new WP_Error( 'ebook_store_wizard_woocommerce_missing', __( 'WooCommerce is not active. Activate WooCommerce first, then run this setup path again.', 'ebook-store' ) );
11161 }
11162
11163 $ebook_result = ebook_store_setup_wizard_create_ebook( $args );
11164 if ( is_wp_error( $ebook_result ) ) {
11165 return $ebook_result;
11166 }
11167
11168 $product_name = isset( $args['product_name'] ) ? sanitize_text_field( $args['product_name'] ) : $ebook_result['title'];
11169 $product_description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : $ebook_result['description'];
11170 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
11171
11172 $product_post = array(
11173 'post_title' => $product_name,
11174 'post_content' => $product_description,
11175 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $product_description ), 40, '...' ),
11176 'post_status' => 'draft',
11177 'post_type' => 'product',
11178 );
11179
11180 $product_id = wp_insert_post( $product_post, true );
11181 if ( is_wp_error( $product_id ) ) {
11182 wp_delete_post( $ebook_result['ebook_id'], true );
11183 return $product_id;
11184 }
11185
11186 wp_set_object_terms( $product_id, 'ebookstore', 'product_type' );
11187 update_post_meta( $product_id, '_regular_price', number_format( $price, 2, '.', '' ) );
11188 update_post_meta( $product_id, '_price', number_format( $price, 2, '.', '' ) );
11189 update_post_meta( $product_id, '_virtual', 'yes' );
11190 update_post_meta( $product_id, '_downloadable', 'yes' );
11191 update_post_meta( $product_id, '_enable_ebook_store_option', 'yes' );
11192 update_post_meta( $product_id, 'ebook_store_book_id', $ebook_result['ebook_id'] );
11193 update_post_meta( $ebook_result['ebook_id'], 'woocommerce_product_id', $product_id );
11194
11195 $license_key = trim( (string) get_option( 'ebook_store_license_key', '' ) );
11196 $integration_enabled = (string) get_option( 'ebook_store_woocommerce_integration', '' ) === '1';
11197 $integration_auto_enabled = false;
11198
11199 if ( ! $integration_enabled && $license_key !== '' ) {
11200 update_option( 'ebook_store_woocommerce_integration', '1' );
11201 $integration_enabled = true;
11202 $integration_auto_enabled = true;
11203 }
11204
11205 // Virtual + downloadable orders skip "processing", so delivery has to wait
11206 // for "completed" or it never fires. Only fill the gap; never override a
11207 // status the owner chose.
11208 if ( $integration_enabled && trim( (string) get_option( 'ebook_store_woocommerce_required_order_status', '' ) ) === '' ) {
11209 update_option( 'ebook_store_woocommerce_required_order_status', 'completed' );
11210 }
11211
11212 if ( function_exists( 'wc_delete_product_transients' ) ) {
11213 wc_delete_product_transients( $product_id );
11214 }
11215
11216 return array(
11217 'ebook_id' => $ebook_result['ebook_id'],
11218 'product_id' => $product_id,
11219 'integration_enabled' => $integration_enabled,
11220 'integration_auto_enabled' => $integration_auto_enabled,
11221 );
11222 }
11223
11224 function ebook_store_setup_wizard_handle_submission() {
11225 if ( empty( $_POST['ebook_store_setup_wizard_action'] ) ) {
11226 return null;
11227 }
11228
11229 if ( ! current_user_can( 'manage_options' ) ) {
11230 return array(
11231 'errors' => array( __( 'You do not have permission to run the setup wizard.', 'ebook-store' ) ),
11232 );
11233 }
11234
11235 check_admin_referer( 'ebook_store_setup_wizard' );
11236
11237 $mode = sanitize_key( (string) wp_unslash( $_POST['wizard_mode'] ) );
11238 $price = isset( $_POST['wizard_price'] ) ? (float) wp_unslash( $_POST['wizard_price'] ) : 0.0;
11239 $product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
11240 $wizard_enable_paypal = isset( $_POST['wizard_enable_paypal'] ) && (string) wp_unslash( $_POST['wizard_enable_paypal'] ) === '1' ? '1' : '';
11241 $wizard_enable_stripe = isset( $_POST['wizard_enable_stripe'] ) && (string) wp_unslash( $_POST['wizard_enable_stripe'] ) === '1' ? '1' : '';
11242 $wizard_enable_adyen = isset( $_POST['wizard_enable_adyen'] ) && (string) wp_unslash( $_POST['wizard_enable_adyen'] ) === '1' ? '1' : '';
11243 $wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : '';
11244 $wizard_stripe_publishable_key = isset( $_POST['wizard_stripe_publishable_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_publishable_key'] ) ) : '';
11245 $wizard_stripe_secret_key = isset( $_POST['wizard_stripe_secret_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_secret_key'] ) ) : '';
11246 $wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : 'test';
11247 $wizard_adyen_api_key = isset( $_POST['wizard_adyen_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_api_key'] ) ) : '';
11248 $wizard_adyen_merchant_account = isset( $_POST['wizard_adyen_merchant_account'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_merchant_account'] ) ) : '';
11249 $wizard_adyen_hmac_key = isset( $_POST['wizard_adyen_hmac_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_hmac_key'] ) ) : '';
11250 $wizard_adyen_live_prefix = isset( $_POST['wizard_adyen_live_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_live_prefix'] ) ) : '';
11251 $pdf_file = isset( $_FILES['wizard_pdf_file'] ) ? $_FILES['wizard_pdf_file'] : array();
11252 $errors = array();
11253
11254 if ( ! in_array( $mode, array( 'standalone', 'woocommerce' ), true ) ) {
11255 $errors[] = __( 'Please choose how you want to set up Ebook Store first.', 'ebook-store' );
11256 }
11257
11258 if ( empty( $pdf_file['tmp_name'] ) ) {
11259 $errors[] = __( 'Please upload the PDF file you want to sell.', 'ebook-store' );
11260 } elseif ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
11261 $errors[] = __( 'The uploaded file must be a PDF.', 'ebook-store' );
11262 }
11263
11264 if ( $mode === 'woocommerce' && $product_name === '' ) {
11265 $errors[] = __( 'Please enter the WooCommerce product name.', 'ebook-store' );
11266 }
11267
11268 if ( $mode === 'standalone' ) {
11269 $wizard_ready_payment_methods = 0;
11270
11271 if ( $wizard_enable_paypal === '1' ) {
11272 if ( $wizard_paypal_account === '' ) {
11273 $errors[] = __( 'Enter the PayPal account email or turn off PayPal in Payment method setup.', 'ebook-store' );
11274 } else {
11275 $wizard_ready_payment_methods++;
11276 }
11277 }
11278
11279 if ( $wizard_enable_stripe === '1' ) {
11280 if ( $wizard_stripe_publishable_key === '' || $wizard_stripe_secret_key === '' ) {
11281 $errors[] = __( 'Enter both Stripe API keys or turn off Stripe in Payment method setup.', 'ebook-store' );
11282 } else {
11283 $wizard_ready_payment_methods++;
11284 }
11285 }
11286
11287 if ( $wizard_enable_adyen === '1' ) {
11288 if ( ! in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ) {
11289 $wizard_adyen_environment = 'test';
11290 }
11291 if ( $wizard_adyen_api_key === '' || $wizard_adyen_merchant_account === '' || $wizard_adyen_hmac_key === '' ) {
11292 $errors[] = __( 'Enter the Adyen API key, merchant account, and webhook HMAC key or turn off Adyen in Payment method setup.', 'ebook-store' );
11293 } elseif ( $wizard_adyen_environment === 'live' && $wizard_adyen_live_prefix === '' ) {
11294 $errors[] = __( 'Enter the Adyen live URL prefix or switch Adyen to Test mode in Payment method setup.', 'ebook-store' );
11295 } else {
11296 $wizard_ready_payment_methods++;
11297 }
11298 }
11299
11300 if ( $price > 0 && $wizard_ready_payment_methods < 1 ) {
11301 $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' );
11302 }
11303 }
11304
11305 if ( ! empty( $errors ) ) {
11306 return array(
11307 'errors' => $errors,
11308 'mode' => $mode,
11309 );
11310 }
11311
11312 if ( $mode === 'standalone' ) {
11313 update_option( 'paypal_integration_enabled', $wizard_enable_paypal );
11314 update_option( 'paypal_account', $wizard_paypal_account );
11315 update_option( 'stripe_integration_enabled', $wizard_enable_stripe );
11316 update_option( 'stripe_publishable_key', $wizard_stripe_publishable_key );
11317 update_option( 'stripe_secret_key', $wizard_stripe_secret_key );
11318 update_option( 'adyen_integration_enabled', $wizard_enable_adyen );
11319 update_option( 'adyen_environment', in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ? $wizard_adyen_environment : 'test' );
11320 update_option( 'adyen_api_key', $wizard_adyen_api_key );
11321 update_option( 'adyen_merchant_account', $wizard_adyen_merchant_account );
11322 update_option( 'adyen_hmac_key', $wizard_adyen_hmac_key );
11323 update_option( 'adyen_live_prefix', $wizard_adyen_live_prefix );
11324 }
11325
11326 $fallback_title = $mode === 'woocommerce' ? $product_name : preg_replace( '/\.[^.]+$/', '', sanitize_file_name( $pdf_file['name'] ) );
11327 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $pdf_file['tmp_name'], $fallback_title );
11328 $title = $mode === 'woocommerce' ? $product_name : $pdf_details['title'];
11329 $description = $pdf_details['description'];
11330
11331 if ( $title === '' ) {
11332 $title = $fallback_title !== '' ? $fallback_title : __( 'Imported ebook', 'ebook-store' );
11333 }
11334
11335 $shared_args = array(
11336 'title' => $title,
11337 'product_name'=> $product_name,
11338 'description' => $description,
11339 'price' => $price,
11340 'currency' => ebook_store_get_store_currency(),
11341 'pdf_file' => $pdf_file,
11342 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
11343 'author' => isset( $pdf_details['author'] ) ? $pdf_details['author'] : '',
11344 );
11345
11346 $result = $mode === 'woocommerce'
11347 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
11348 : ebook_store_setup_wizard_create_ebook( $shared_args );
11349
11350 if ( is_wp_error( $result ) ) {
11351 return array(
11352 'errors' => array( $result->get_error_message() ),
11353 'mode' => $mode,
11354 );
11355 }
11356
11357 $redirect_args = array(
11358 'wizard_mode' => $mode,
11359 'wizard_created' => 1,
11360 'ebook_id' => (int) $result['ebook_id'],
11361 );
11362
11363 if ( ! empty( $result['product_id'] ) ) {
11364 $redirect_args['product_id'] = (int) $result['product_id'];
11365 }
11366
11367 if ( isset( $result['integration_enabled'] ) ) {
11368 $redirect_args['integration_enabled'] = $result['integration_enabled'] ? 1 : 0;
11369 }
11370
11371 if ( isset( $result['integration_auto_enabled'] ) ) {
11372 $redirect_args['integration_auto_enabled'] = $result['integration_auto_enabled'] ? 1 : 0;
11373 }
11374
11375 wp_safe_redirect( ebook_store_get_setup_wizard_url( $redirect_args ) );
11376 exit;
11377 }
11378
11379 function ebook_store_setup_wizard_page_callback() {
11380 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
11381 $locale = ebook_store_get_locale_strings();
11382 $formats_locale = ebook_store_get_format_labels();
11383 wp_enqueue_style( 'ebookstorestylesheet' );
11384
11385 $submission = null;
11386 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_setup_wizard_action'] ) ) {
11387 $submission = ebook_store_setup_wizard_handle_submission();
11388 }
11389
11390 $wizard_mode = isset( $_REQUEST['wizard_mode'] ) ? sanitize_key( (string) wp_unslash( $_REQUEST['wizard_mode'] ) ) : '';
11391 if ( $submission && ! empty( $submission['mode'] ) ) {
11392 $wizard_mode = $submission['mode'];
11393 }
11394
11395 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
11396 $wizard_created = isset( $_GET['wizard_created'] ) && (int) $_GET['wizard_created'] === 1;
11397 $ebook_id = isset( $_GET['ebook_id'] ) ? (int) $_GET['ebook_id'] : 0;
11398 $product_id = isset( $_GET['product_id'] ) ? (int) $_GET['product_id'] : 0;
11399 $integration_enabled = isset( $_GET['integration_enabled'] ) ? (int) $_GET['integration_enabled'] === 1 : null;
11400 $integration_auto_enabled = isset( $_GET['integration_auto_enabled'] ) ? (int) $_GET['integration_auto_enabled'] === 1 : false;
11401 $current_currency = ebook_store_get_store_currency();
11402 $current_price = isset( $_POST['wizard_price'] ) ? (string) wp_unslash( $_POST['wizard_price'] ) : '5.00';
11403 $current_product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
11404 $payment_method_state = ebook_store_get_standalone_payment_method_state();
11405 $has_ready_standalone_payment = ebook_store_has_ready_standalone_payment_method();
11406 $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' : '' );
11407 $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' : '' );
11408 $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' : '' );
11409 $current_wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : (string) get_option( 'paypal_account', '' );
11410 $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', '' );
11411 $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', '' );
11412 $current_wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : (string) get_option( 'adyen_environment', 'test' );
11413 $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', '' );
11414 $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', '' );
11415 $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', '' );
11416 $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', '' );
11417
11418 ob_start();
11419 ?>
11420 <div class="wrap ebook-setup-wizard">
11421 <div class="ebook-setup-wizard__hero">
11422 <div class="ebook-setup-wizard__hero-copy">
11423 <span class="ebook-setup-wizard__eyebrow"><?php esc_html_e( 'New onboarding flow', 'ebook-store' ); ?></span>
11424 <h1><?php esc_html_e( 'Setup wizard', 'ebook-store' ); ?></h1>
11425 <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>
11426 </div>
11427 <div class="ebook-setup-wizard__hero-meta">
11428 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Creates draft items', 'ebook-store' ); ?></span>
11429 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Extracts title and summary', 'ebook-store' ); ?></span>
11430 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Builds first-page cover', 'ebook-store' ); ?></span>
11431 </div>
11432 </div>
11433
11434 <?php if ( ! empty( $errors ) ) : ?>
11435 <div class="notice notice-error">
11436 <p><strong><?php esc_html_e( 'The wizard could not finish yet.', 'ebook-store' ); ?></strong></p>
11437 <ul class="ebook-setup-wizard__notice-list">
11438 <?php foreach ( $errors as $error ) : ?>
11439 <li><?php echo esc_html( $error ); ?></li>
11440 <?php endforeach; ?>
11441 </ul>
11442 </div>
11443 <?php endif; ?>
11444
11445 <?php if ( $wizard_created && $ebook_id > 0 ) : ?>
11446 <div class="notice notice-success is-dismissible">
11447 <p>
11448 <strong><?php esc_html_e( 'Setup complete.', 'ebook-store' ); ?></strong>
11449 <?php
11450 printf(
11451 esc_html__( '%1$s was created as a draft so you can review it before going live.', 'ebook-store' ),
11452 esc_html( ebook_store_setup_wizard_human_mode_label( $wizard_mode ) )
11453 );
11454 ?>
11455 </p>
11456 <p class="ebook-setup-wizard__success-links">
11457 <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>
11458 <?php if ( $product_id > 0 ) : ?>
11459 <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>
11460 <?php endif; ?>
11461 <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>
11462 </p>
11463 <?php if ( $product_id > 0 && $integration_enabled === false ) : ?>
11464 <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>
11465 <?php elseif ( $product_id > 0 && $integration_auto_enabled ) : ?>
11466 <p class="description"><?php esc_html_e( 'WooCommerce integration was enabled automatically so the linked product can use Ebook Store delivery.', 'ebook-store' ); ?></p>
11467 <?php endif; ?>
11468 </div>
11469 <?php endif; ?>
11470
11471 <?php if ( $wizard_mode === 'standalone' ) : ?>
11472 <div class="ebook-setup-wizard__panel">
11473 <div class="ebook-setup-wizard__panel-header">
11474 <div>
11475 <h2><?php esc_html_e( 'Standalone ebook sales', 'ebook-store' ); ?></h2>
11476 <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>
11477 </div>
11478 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
11479 </div>
11480 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
11481 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
11482 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
11483 <input type="hidden" name="wizard_mode" value="standalone" />
11484 <div class="ebook-setup-wizard__grid">
11485 <div class="ebook-setup-wizard__field">
11486 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
11487 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
11488 <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>
11489 </div>
11490 <div class="ebook-setup-wizard__field">
11491 <label for="wizard_price"><?php esc_html_e( 'Selling price', 'ebook-store' ); ?></label>
11492 <div class="ebook-setup-wizard__input-group">
11493 <span><?php echo esc_html( $current_currency ); ?></span>
11494 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
11495 </div>
11496 <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>
11497 </div>
11498 </div>
11499 <div class="ebook-setup-wizard__gateway-panel">
11500 <div class="ebook-setup-wizard__gateway-header">
11501 <h3><?php esc_html_e( 'Payment method setup', 'ebook-store' ); ?></h3>
11502 <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>
11503 <?php if ( ! $has_ready_standalone_payment && (float) $current_price > 0 ) : ?>
11504 <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>
11505 <?php endif; ?>
11506 </div>
11507 <div class="ebook-settings-toggle-grid ebook-settings-toggle-grid--wizard">
11508 <label class="ebook-settings-toggle-card">
11509 <input type="hidden" name="wizard_enable_paypal" value="" />
11510 <input type="checkbox" name="wizard_enable_paypal" value="1" <?php checked( $current_wizard_paypal_enabled, '1' ); ?> />
11511 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11512 <span class="ebook-settings-toggle-card__content">
11513 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable PayPal', 'ebook-store' ); ?></span>
11514 <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>
11515 </span>
11516 </label>
11517 <label class="ebook-settings-toggle-card">
11518 <input type="hidden" name="wizard_enable_stripe" value="" />
11519 <input type="checkbox" name="wizard_enable_stripe" value="1" <?php checked( $current_wizard_stripe_enabled, '1' ); ?> />
11520 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11521 <span class="ebook-settings-toggle-card__content">
11522 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Stripe', 'ebook-store' ); ?></span>
11523 <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>
11524 </span>
11525 </label>
11526 <label class="ebook-settings-toggle-card">
11527 <input type="hidden" name="wizard_enable_adyen" value="" />
11528 <input type="checkbox" name="wizard_enable_adyen" value="1" <?php checked( $current_wizard_adyen_enabled, '1' ); ?> />
11529 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
11530 <span class="ebook-settings-toggle-card__content">
11531 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Adyen', 'ebook-store' ); ?></span>
11532 <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>
11533 </span>
11534 </label>
11535 </div>
11536 <div class="ebook-setup-wizard__grid">
11537 <div class="ebook-setup-wizard__field">
11538 <label for="wizard_paypal_account"><?php esc_html_e( 'PayPal account email', 'ebook-store' ); ?></label>
11539 <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' ); ?>" />
11540 <p class="description"><?php esc_html_e( 'Required when PayPal is enabled. Payments are sent to this PayPal account.', 'ebook-store' ); ?></p>
11541 </div>
11542 <div class="ebook-setup-wizard__field">
11543 <label for="wizard_stripe_publishable_key"><?php esc_html_e( 'Stripe publishable key', 'ebook-store' ); ?></label>
11544 <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" />
11545 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
11546 </div>
11547 <div class="ebook-setup-wizard__field">
11548 <label for="wizard_stripe_secret_key"><?php esc_html_e( 'Stripe secret key', 'ebook-store' ); ?></label>
11549 <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" />
11550 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
11551 </div>
11552 <div class="ebook-setup-wizard__field">
11553 <label for="wizard_adyen_environment"><?php esc_html_e( 'Adyen environment', 'ebook-store' ); ?></label>
11554 <select id="wizard_adyen_environment" name="wizard_adyen_environment">
11555 <option value="test" <?php selected( $current_wizard_adyen_environment, 'test' ); ?>><?php esc_html_e( 'Test', 'ebook-store' ); ?></option>
11556 <option value="live" <?php selected( $current_wizard_adyen_environment, 'live' ); ?>><?php esc_html_e( 'Live', 'ebook-store' ); ?></option>
11557 </select>
11558 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11559 </div>
11560 <div class="ebook-setup-wizard__field">
11561 <label for="wizard_adyen_merchant_account"><?php esc_html_e( 'Adyen merchant account', 'ebook-store' ); ?></label>
11562 <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" />
11563 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11564 </div>
11565 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11566 <label for="wizard_adyen_api_key"><?php esc_html_e( 'Adyen API key', 'ebook-store' ); ?></label>
11567 <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" />
11568 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
11569 </div>
11570 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11571 <label for="wizard_adyen_hmac_key"><?php esc_html_e( 'Adyen webhook HMAC key', 'ebook-store' ); ?></label>
11572 <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" />
11573 <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>
11574 </div>
11575 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11576 <label for="wizard_adyen_live_prefix"><?php esc_html_e( 'Adyen live URL prefix', 'ebook-store' ); ?></label>
11577 <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" />
11578 <p class="description"><?php esc_html_e( 'Required only when Adyen is enabled in Live mode.', 'ebook-store' ); ?></p>
11579 </div>
11580 </div>
11581 </div>
11582 <div class="ebook-setup-wizard__actions">
11583 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create ebook draft', 'ebook-store' ); ?></button>
11584 </div>
11585 </form>
11586 </div>
11587 <?php elseif ( $wizard_mode === 'woocommerce' ) : ?>
11588 <div class="ebook-setup-wizard__panel">
11589 <div class="ebook-setup-wizard__panel-header">
11590 <div>
11591 <h2><?php esc_html_e( 'WooCommerce integration', 'ebook-store' ); ?></h2>
11592 <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>
11593 </div>
11594 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
11595 </div>
11596 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
11597 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
11598 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
11599 <input type="hidden" name="wizard_mode" value="woocommerce" />
11600 <div class="ebook-setup-wizard__grid">
11601 <div class="ebook-setup-wizard__field">
11602 <label for="wizard_product_name"><?php esc_html_e( 'Product name', 'ebook-store' ); ?></label>
11603 <input type="text" id="wizard_product_name" name="wizard_product_name" value="<?php echo esc_attr( $current_product_name ); ?>" required />
11604 <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>
11605 </div>
11606 <div class="ebook-setup-wizard__field">
11607 <label for="wizard_price"><?php esc_html_e( 'Product price', 'ebook-store' ); ?></label>
11608 <div class="ebook-setup-wizard__input-group">
11609 <span><?php echo esc_html( $current_currency ); ?></span>
11610 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
11611 </div>
11612 <p class="description"><?php esc_html_e( 'The same amount is written to the Ebook Store item and the WooCommerce product.', 'ebook-store' ); ?></p>
11613 </div>
11614 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
11615 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
11616 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
11617 <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>
11618 </div>
11619 </div>
11620 <div class="ebook-setup-wizard__actions">
11621 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create linked draft items', 'ebook-store' ); ?></button>
11622 </div>
11623 </form>
11624 </div>
11625 <?php else : ?>
11626 <div class="ebook-setup-wizard__chooser">
11627 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'standalone' ) ) ); ?>">
11628 <span class="dashicons dashicons-book-alt"></span>
11629 <h2><?php esc_html_e( 'Sell with Ebook Store', 'ebook-store' ); ?></h2>
11630 <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>
11631 <strong><?php esc_html_e( 'Upload a PDF and generate the ebook item automatically', 'ebook-store' ); ?></strong>
11632 </a>
11633 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'woocommerce' ) ) ); ?>">
11634 <span class="dashicons dashicons-cart"></span>
11635 <h2><?php esc_html_e( 'Use WooCommerce integration', 'ebook-store' ); ?></h2>
11636 <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>
11637 <strong><?php esc_html_e( 'Create and link both items in one guided flow', 'ebook-store' ); ?></strong>
11638 </a>
11639 </div>
11640 <?php endif; ?>
11641 </div>
11642 <?php
11643 $output = ob_get_clean();
11644 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
11645 }
11646
11647 function ebook_store_get_import_books_url() {
11648 return admin_url( 'edit.php?post_type=ebook&page=ebook-store-import-books' );
11649 }
11650
11651 function ebook_store_import_books_has_pro_access() {
11652 if ( function_exists( 'ebook_store_has_pro_license' ) ) {
11653 return ebook_store_has_pro_license();
11654 }
11655
11656 return trim( (string) get_option( 'ebook_store_license_key', '' ) ) !== '';
11657 }
11658
11659 function ebook_store_import_books_admin_assets() {
11660 if ( ! is_admin() ) {
11661 return;
11662 }
11663
11664 $page = isset( $_GET['page'] ) ? sanitize_key( (string) wp_unslash( $_GET['page'] ) ) : '';
11665 if ( $page !== 'ebook-store-import-books' ) {
11666 return;
11667 }
11668
11669 wp_enqueue_style( 'ebookstorestylesheet' );
11670 wp_enqueue_script( 'plupload-all' );
11671 wp_enqueue_script(
11672 'ebook-store-import-books',
11673 plugins_url( 'js/ebook_store_import_books.js', __FILE__ ),
11674 array( 'jquery', 'plupload-all' ),
11675 filemtime( plugin_dir_path( __FILE__ ) . 'js/ebook_store_import_books.js' ),
11676 true
11677 );
11678 wp_localize_script(
11679 'ebook-store-import-books',
11680 'ebookStoreImportBooks',
11681 array(
11682 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
11683 'nonce' => wp_create_nonce( 'ebook_store_import_books' ),
11684 'sessionId' => wp_generate_uuid4(),
11685 'hasProLicense' => ebook_store_import_books_has_pro_access(),
11686 'woocommerceAvailable' => class_exists( 'WooCommerce' ),
11687 'maxFileSize' => (string) wp_max_upload_size() . 'b',
11688 'strings' => array(
11689 'dropActive' => __( 'Drop PDFs to add them to the import queue', 'ebook-store' ),
11690 'dropIdle' => __( 'Drop PDF files here or use the button below', 'ebook-store' ),
11691 'uploading' => __( 'Uploading...', 'ebook-store' ),
11692 'uploaded' => __( 'Uploaded', 'ebook-store' ),
11693 'queued' => __( 'Queued for import', 'ebook-store' ),
11694 'importing' => __( 'Creating draft items...', 'ebook-store' ),
11695 'imported' => __( 'Imported', 'ebook-store' ),
11696 'failed' => __( 'Failed', 'ebook-store' ),
11697 'invalidMode' => __( 'Choose how imported books should be created before starting the import.', 'ebook-store' ),
11698 'invalidPrice' => __( 'Enter a valid default price before starting the import.', 'ebook-store' ),
11699 'noUploadedFiles' => __( 'Upload at least one PDF before starting the import.', 'ebook-store' ),
11700 'proRequired' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ),
11701 'woocommerceMissing' => __( 'WooCommerce is not active, so the connected product option is not available.', 'ebook-store' ),
11702 'titleFallback' => __( 'Imported ebook', 'ebook-store' ),
11703 'descriptionFallback' => __( 'No description could be extracted from the PDF.', 'ebook-store' ),
11704 'ebookLink' => __( 'Edit ebook', 'ebook-store' ),
11705 'productLink' => __( 'Edit product', 'ebook-store' ),
11706 'completedSummary' => __( 'Import finished.', 'ebook-store' ),
11707 'processingSummary' => __( 'Importing uploaded books...', 'ebook-store' ),
11708 ),
11709 )
11710 );
11711 }
11712
11713 function ebook_store_import_books_get_temp_root() {
11714 return trailingslashit( get_temp_dir() ) . 'ebook-store-imports';
11715 }
11716
11717 function ebook_store_import_books_get_session_dir( $user_id, $session_id ) {
11718 $user_id = max( 0, (int) $user_id );
11719 $session_id = preg_replace( '/[^a-z0-9_-]/i', '', (string) $session_id );
11720
11721 return trailingslashit( ebook_store_import_books_get_temp_root() ) . $user_id . '/' . $session_id;
11722 }
11723
11724 function ebook_store_import_books_ensure_directory( $dir ) {
11725 if ( $dir === '' ) {
11726 return false;
11727 }
11728
11729 if ( file_exists( $dir ) ) {
11730 return is_dir( $dir );
11731 }
11732
11733 return wp_mkdir_p( $dir );
11734 }
11735
11736 function ebook_store_import_books_cleanup_empty_dirs( $dir, $stop_dir ) {
11737 $dir = untrailingslashit( (string) $dir );
11738 $stop_dir = untrailingslashit( (string) $stop_dir );
11739
11740 while ( $dir !== '' && strpos( $dir, $stop_dir ) === 0 && $dir !== $stop_dir ) {
11741 $entries = @scandir( $dir );
11742 if ( ! is_array( $entries ) || count( $entries ) > 2 ) {
11743 break;
11744 }
11745 @rmdir( $dir );
11746 $dir = dirname( $dir );
11747 }
11748 }
11749
11750 function ebook_store_import_books_store_queue_item( $token, $data ) {
11751 $user_id = get_current_user_id();
11752 set_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token, $data, DAY_IN_SECONDS );
11753 }
11754
11755 function ebook_store_import_books_get_queue_item( $token ) {
11756 $user_id = get_current_user_id();
11757 return get_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11758 }
11759
11760 function ebook_store_import_books_delete_queue_item( $token ) {
11761 $user_id = get_current_user_id();
11762 delete_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11763 }
11764
11765 function ebook_store_import_books_ajax_upload() {
11766 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11767
11768 if ( ! current_user_can( 'manage_options' ) ) {
11769 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11770 }
11771
11772 if ( ! ebook_store_import_books_has_pro_access() ) {
11773 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11774 }
11775
11776 if ( empty( $_FILES['file']['tmp_name'] ) ) {
11777 wp_send_json_error( array( 'message' => __( 'No upload chunk was received.', 'ebook-store' ) ), 400 );
11778 }
11779
11780 $session_id = isset( $_REQUEST['session'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['session'] ) ) : '';
11781 $file_id = isset( $_REQUEST['file_id'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['file_id'] ) ) : '';
11782 $original_name = isset( $_REQUEST['name'] ) ? sanitize_file_name( (string) wp_unslash( $_REQUEST['name'] ) ) : sanitize_file_name( (string) $_FILES['file']['name'] );
11783 $chunk = isset( $_REQUEST['chunk'] ) ? max( 0, (int) $_REQUEST['chunk'] ) : 0;
11784 $chunks = isset( $_REQUEST['chunks'] ) ? max( 0, (int) $_REQUEST['chunks'] ) : 0;
11785
11786 if ( $session_id === '' || $file_id === '' || $original_name === '' ) {
11787 wp_send_json_error( array( 'message' => __( 'The upload request is missing required file information.', 'ebook-store' ) ), 400 );
11788 }
11789
11790 $filetype = wp_check_filetype( $original_name );
11791 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
11792 if ( $extension !== 'pdf' ) {
11793 wp_send_json_error( array( 'message' => __( 'Only PDF files can be imported here.', 'ebook-store' ) ), 400 );
11794 }
11795
11796 $session_dir = ebook_store_import_books_get_session_dir( get_current_user_id(), $session_id );
11797 if ( ! ebook_store_import_books_ensure_directory( $session_dir ) ) {
11798 wp_send_json_error( array( 'message' => __( 'The temporary upload directory could not be created.', 'ebook-store' ) ), 500 );
11799 }
11800
11801 $stored_name = sanitize_file_name( $file_id . '-' . $original_name );
11802 $partial_path = trailingslashit( $session_dir ) . $stored_name . '.part';
11803 $final_path = trailingslashit( $session_dir ) . $stored_name;
11804 $chunk_path = $_FILES['file']['tmp_name'];
11805
11806 $input_handle = @fopen( $chunk_path, 'rb' );
11807 if ( ! $input_handle ) {
11808 wp_send_json_error( array( 'message' => __( 'The uploaded file chunk could not be read.', 'ebook-store' ) ), 500 );
11809 }
11810
11811 $output_handle = @fopen( $partial_path, $chunk === 0 ? 'wb' : 'ab' );
11812 if ( ! $output_handle ) {
11813 @fclose( $input_handle );
11814 wp_send_json_error( array( 'message' => __( 'The temporary upload file could not be written.', 'ebook-store' ) ), 500 );
11815 }
11816
11817 while ( ! feof( $input_handle ) ) {
11818 $buffer = fread( $input_handle, 1048576 );
11819 if ( $buffer === false ) {
11820 break;
11821 }
11822 fwrite( $output_handle, $buffer );
11823 }
11824
11825 @fclose( $input_handle );
11826 @fclose( $output_handle );
11827
11828 $is_last_chunk = $chunks < 2 || ( $chunk + 1 ) >= $chunks;
11829 if ( ! $is_last_chunk ) {
11830 wp_send_json_success(
11831 array(
11832 'fileId' => $file_id,
11833 'status' => 'chunked',
11834 )
11835 );
11836 }
11837
11838 @unlink( $final_path );
11839 if ( ! @rename( $partial_path, $final_path ) ) {
11840 wp_send_json_error( array( 'message' => __( 'The uploaded PDF could not be finalized.', 'ebook-store' ) ), 500 );
11841 }
11842
11843 $pdf_file = array(
11844 'name' => $original_name,
11845 'tmp_name' => $final_path,
11846 );
11847 if ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
11848 @unlink( $final_path );
11849 wp_send_json_error( array( 'message' => __( 'The uploaded file is not a valid PDF.', 'ebook-store' ) ), 400 );
11850 }
11851
11852 $fallback_title = preg_replace( '/\.[^.]+$/', '', $original_name );
11853 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $final_path, $fallback_title );
11854 $token = str_replace( '-', '', wp_generate_uuid4() );
11855 $filesize = file_exists( $final_path ) ? (int) filesize( $final_path ) : 0;
11856
11857 ebook_store_import_books_store_queue_item(
11858 $token,
11859 array(
11860 'token' => $token,
11861 'file_id' => $file_id,
11862 'session_id' => $session_id,
11863 'file_path' => $final_path,
11864 'file_name' => $original_name,
11865 'file_size' => $filesize,
11866 'details' => $pdf_details,
11867 'uploaded_at' => time(),
11868 )
11869 );
11870
11871 wp_send_json_success(
11872 array(
11873 'fileId' => $file_id,
11874 'token' => $token,
11875 'fileName' => $original_name,
11876 'fileSize' => $filesize,
11877 'title' => isset( $pdf_details['title'] ) ? (string) $pdf_details['title'] : '',
11878 'description' => isset( $pdf_details['description'] ) ? (string) $pdf_details['description'] : '',
11879 'author' => isset( $pdf_details['author'] ) ? (string) $pdf_details['author'] : '',
11880 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
11881 )
11882 );
11883 }
11884
11885 function ebook_store_import_books_ajax_process() {
11886 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11887
11888 if ( ! current_user_can( 'manage_options' ) ) {
11889 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11890 }
11891
11892 if ( ! ebook_store_import_books_has_pro_access() ) {
11893 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11894 }
11895
11896 $token = isset( $_POST['token'] ) ? sanitize_key( (string) wp_unslash( $_POST['token'] ) ) : '';
11897 $mode = isset( $_POST['import_mode'] ) ? sanitize_key( (string) wp_unslash( $_POST['import_mode'] ) ) : 'ebook';
11898 $import_price = isset( $_POST['import_price'] ) && is_scalar( $_POST['import_price'] ) ? (float) str_replace( ',', '.', (string) wp_unslash( $_POST['import_price'] ) ) : 0.0;
11899 $import_price = max( 0, $import_price );
11900
11901 if ( $token === '' ) {
11902 wp_send_json_error( array( 'message' => __( 'The uploaded file token is missing.', 'ebook-store' ) ), 400 );
11903 }
11904
11905 if ( ! in_array( $mode, array( 'ebook', 'woocommerce' ), true ) ) {
11906 wp_send_json_error( array( 'message' => __( 'The selected import mode is invalid.', 'ebook-store' ) ), 400 );
11907 }
11908
11909 $queue_item = ebook_store_import_books_get_queue_item( $token );
11910 if ( ! is_array( $queue_item ) || empty( $queue_item['file_path'] ) ) {
11911 wp_send_json_error( array( 'message' => __( 'The uploaded file could not be found. Please upload it again.', 'ebook-store' ) ), 404 );
11912 }
11913
11914 $file_path = (string) $queue_item['file_path'];
11915 if ( ! file_exists( $file_path ) ) {
11916 ebook_store_import_books_delete_queue_item( $token );
11917 wp_send_json_error( array( 'message' => __( 'The uploaded PDF is no longer available. Please upload it again.', 'ebook-store' ) ), 404 );
11918 }
11919
11920 $details = isset( $queue_item['details'] ) && is_array( $queue_item['details'] ) ? $queue_item['details'] : array();
11921 $file_name = isset( $queue_item['file_name'] ) ? (string) $queue_item['file_name'] : __( 'uploaded PDF', 'ebook-store' );
11922 $fallback_title = preg_replace( '/\.[^.]+$/', '', (string) $queue_item['file_name'] );
11923 $title = isset( $details['title'] ) && $details['title'] !== '' ? (string) $details['title'] : $fallback_title;
11924 $description = isset( $details['description'] ) ? (string) $details['description'] : '';
11925
11926 $shared_args = array(
11927 'title' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11928 'product_name' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11929 'description' => $description,
11930 'price' => $import_price,
11931 'currency' => ebook_store_get_store_currency(),
11932 'pdf_file' => array(
11933 'name' => $file_name,
11934 'tmp_name' => $file_path,
11935 ),
11936 'pages' => isset( $details['pages'] ) ? (int) $details['pages'] : 0,
11937 'author' => isset( $details['author'] ) ? (string) $details['author'] : '',
11938 );
11939
11940 try {
11941 $result = $mode === 'woocommerce'
11942 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
11943 : ebook_store_setup_wizard_create_ebook( $shared_args );
11944 } catch ( Throwable $throwable ) {
11945 wp_send_json_error(
11946 array(
11947 'message' => sprintf(
11948 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
11949 $file_name,
11950 $throwable->getMessage()
11951 ),
11952 ),
11953 500
11954 );
11955 }
11956
11957 if ( is_wp_error( $result ) ) {
11958 wp_send_json_error(
11959 array(
11960 'message' => sprintf(
11961 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
11962 $file_name,
11963 $result->get_error_message()
11964 ),
11965 ),
11966 400
11967 );
11968 }
11969
11970 ebook_store_import_books_delete_queue_item( $token );
11971 @unlink( $file_path );
11972 ebook_store_import_books_cleanup_empty_dirs( dirname( $file_path ), ebook_store_import_books_get_temp_root() );
11973
11974 wp_send_json_success(
11975 array(
11976 'title' => $shared_args['title'],
11977 'ebookId' => isset( $result['ebook_id'] ) ? (int) $result['ebook_id'] : 0,
11978 'ebookEditUrl' => ! empty( $result['ebook_id'] ) ? get_edit_post_link( (int) $result['ebook_id'], 'raw' ) : '',
11979 'productId' => ! empty( $result['product_id'] ) ? (int) $result['product_id'] : 0,
11980 'productEditUrl' => ! empty( $result['product_id'] ) ? get_edit_post_link( (int) $result['product_id'], 'raw' ) : '',
11981 )
11982 );
11983 }
11984
11985 function ebook_store_register_my_custom_submenu_page() {
11986
11987 add_submenu_page(
11988 'edit.php?post_type=ebook' //or 'options.php'
11989 , __('Orders - Add New', 'ebook-store')
11990 , __('Orders - Add New', 'ebook-store')
11991 , 'manage_options'
11992 , 'ebook-store-add-order-page'
11993 , 'ebook_store_add_order_page_callback'
11994 );
11995
11996 add_submenu_page(
11997 'edit.php?post_type=ebook' //or 'options.php'
11998 , __('Orders - Reports', 'ebook-store')
11999 , __('Orders - Reports', 'ebook-store')
12000 , 'manage_options'
12001 , 'ebook-store-order_reports'
12002 , 'ebook_store_order_reports_callback'
12003 );
12004
12005 add_submenu_page(
12006 'edit.php?post_type=ebook' //or 'options.php'
12007 , __('Orders - New Issue', 'ebook-store')
12008 , __('Orders - New Issue', 'ebook-store')
12009 , 'manage_options'
12010 , 'ebook-store-add-issue-page'
12011 , 'ebook_store_add_issue_page_callback'
12012 );
12013
12014 add_submenu_page(
12015 'edit.php?post_type=ebook',
12016 __( 'Setup wizard', 'ebook-store' ),
12017 ebook_store_setup_wizard_menu_title(),
12018 'manage_options',
12019 'ebook-store-setup-wizard',
12020 'ebook_store_setup_wizard_page_callback'
12021 );
12022
12023 add_submenu_page(
12024 'edit.php?post_type=ebook',
12025 __( 'Import books', 'ebook-store' ),
12026 __( 'Import books', 'ebook-store' ),
12027 'manage_options',
12028 'ebook-store-import-books',
12029 'ebook_store_import_books_page_callback'
12030 );
12031
12032 add_submenu_page(
12033 'edit.php?post_type=ebook',
12034 __( 'Button templates', 'ebook-store' ),
12035 __( 'Button templates', 'ebook-store' ),
12036 'manage_options',
12037 'ebook-store-template-editor',
12038 'ebook_store_template_editor_page_callback'
12039 );
12040
12041 if ( ! ebook_store_can_access_template_editor() ) {
12042 remove_submenu_page( 'edit.php?post_type=ebook', 'ebook-store-template-editor' );
12043 }
12044
12045 add_submenu_page(
12046 'edit.php?post_type=ebook',
12047 'Settings',
12048 __('Settings', 'ebook-store'),
12049 'manage_options',
12050 'settings-page',
12051 'ebook_store_settings_callback' );
12052
12053 }
12054 function ebook_store_settings_callback() {
12055 echo '<script>window.location = "options-general.php?page=ebook_options.php"; </script>';
12056 }
12057 function ebook_store_add_query_vars_filter( $vars ){
12058 $vars[] = "new_issue_purchase_period";
12059 $vars[] = "new_issue_ebook_id";
12060 $vars[] = "woocommerce_product_id";
12061 return $vars;
12062 }
12063
12064 //Add custom query vars
12065
12066 function ebook_store_add_issue_page_callback() {
12067 require_once 'ebook_store_add_issue_page_callback.php';
12068
12069 }
12070
12071 function ebook_store_import_books_page_callback() {
12072 require_once 'ebook_store_import_books_page_callback.php';
12073 }
12074
12075 function ebook_store_add_order_page_callback() {
12076 require_once 'ebook_store_add_order_page_callback.php';
12077 }
12078 function ebook_store_add_order($data, $silent = false, $thankYouRedirect = false, $ebook_key = false) {
12079 $QSWPOptions = new QSWPOptions();
12080
12081 if (@$ebook_key == '') {
12082 $ebook_key = md5(microtime() . rand(1,10000000));
12083 } else {
12084 // An explicit key was supplied — this is a payment webhook (Stripe/Adyen)
12085 // or a recovery call. If an order already exists for this key, a retried
12086 // or duplicated webhook is calling again: return the existing order rather
12087 // than creating a duplicate order and sending a second delivery email.
12088 $existing_order = ebook_get_order( 'ebook_key', $ebook_key );
12089 if ( ! empty( $existing_order['order_id'][0] ) ) {
12090 return $silent ? (int) $existing_order['order_id'][0] : null;
12091 }
12092 }
12093
12094 $data['md5_nonce'] = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
12095 if ( ! isset( $data['mc_fee'] ) ) {
12096 $data['mc_fee'] = 0;
12097 }
12098 $data['item_name'] = get_the_title($data['ebook']);
12099 $data['payment_date'] = date("m/d/Y H:i:s");
12100 @$data['txn_id'] = (@$data['txn_id'] != '' ? @$data['txn_id'] : 'Manual Payment');
12101 $data['mc_currency'] = ebook_store_get_store_currency();
12102 if ( ! isset( $data['tax'] ) || ! is_numeric( $data['tax'] ) ) {
12103 $data['tax'] = 0;
12104 }
12105 $data['residence_country'] = 'n/a';
12106
12107 ebook_store_subscribe_buyer_to_marketing_lists($data['payer_email']);
12108 if (@$data['user_id'] == 0) {
12109 $data['user_id'] = ebook_store_silent_registration($data);
12110 }
12111
12112
12113 $my_post = array(
12114 'post_title' => $data['first_name'] . ' ' . $data['last_name'],
12115 'post_type' => 'ebook_order',
12116 'post_status' => 'publish',
12117 'post_author' => 1,
12118 //'post_category' => array(8,39),
12119 );
12120 if ($data['mc_gross'] == false) {
12121 $data['mc_gross'] = 0;
12122 }
12123 // Store a machine-readable amount with NO thousands separator. It used
12124 // to be number_format()'d twice with a ',' separator: the second pass
12125 // re-cast "1,500.00" to a float, which PHP truncates at the comma to
12126 // 1.0 — so every order of 1000+ was recorded as 1.00. Thousands
12127 // separators belong at display time only.
12128 $mc_gross = number_format( (float) $data['mc_gross'], 2, '.', '' );
12129 $ebook = get_post_meta($data['ebook'], 'ebook', true);
12130 $ebookObj = new EbookStoreEbook($data['ebook']);
12131
12132 // Note: unlike the PayPal IPN path, every caller of this function has
12133 // already verified the payment (admin-entered order, or a signed
12134 // Stripe/Adyen webhook), so there is no untrusted amount to check here.
12135 $post_id = wp_insert_post( $my_post, true );
12136 if ( ! is_wp_error( $post_id ) && $post_id ) {
12137 foreach ($data as $k => $v) {
12138 update_post_meta($post_id, $k, $v);
12139 @$order[$k] = $v;
12140 }
12141
12142 $order['order_id'] = $post_id;
12143 $order['password'] = ebook_store_get_password_for_order($post_id, array(
12144 'password' => isset($data['password']) ? $data['password'] : '',
12145 'payer_email' => isset($data['payer_email']) ? $data['payer_email'] : '',
12146 'md5_nonce' => isset($data['md5_nonce']) ? $data['md5_nonce'] : '',
12147 'ebook_key' => $ebook_key,
12148 ));
12149 $data['password'] = $order['password'];
12150
12151 global $attachment, $ebook_email_delivery, $formData;
12152 $attachment = ebook_attachment($data['ebook'],true);
12153
12154 $formData = ebook_store_get_form($data['md5_nonce']);
12155 $formData = json_encode($formData);
12156 $ebook_order['ebook_key'][0] = $ebook_key;
12157 $ebook_order['ebook'][0] = $data['ebook'];
12158 $ebook_order['order_id'][0] = $post_id;
12159 $ebook_order['md5_nonce'][0] = isset( $data['md5_nonce'] ) ? $data['md5_nonce'] : '';
12160 $order['downloadlink'] = ebook_download_link($ebook_order);
12161
12162 $ebookObj->order_id = $post_id;
12163 // Plugin order: link() needs the ebook_key credentials or the
12164 // non-PDF format links point at the WooCommerce-only endpoint.
12165 $ebookObj->ebook_key = $ebook_key;
12166 $ebookObj->md5_nonce = isset( $data['md5_nonce'] ) ? (string) $data['md5_nonce'] : '';
12167 $ebookObj->setLink['pdf'] = $order['downloadlink'];
12168
12169 $order['downloadlink_html'] = $ebookObj->format_links();
12170 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
12171
12172 $order['ebook_key'] = $ebook_key;
12173 $order['order_id'] = $post_id;
12174 $order['md5_nonce'] = $data['md5_nonce'];
12175 update_post_meta($post_id,'ebook_key',$ebook_key);
12176 update_post_meta($post_id,'downloads',0);
12177 update_post_meta($post_id,'mc_gross',$mc_gross);
12178 update_post_meta($post_id,'formData',wp_slash($formData));
12179 update_post_meta($post_id,'downloadlink',$order['downloadlink']);
12180 update_post_meta($post_id,'ebook',$data['ebook']);
12181 //error_log('ebook_store_add_order - ' . print_r($order,true));
12182
12183 $email_template = ebook_store_get_effective_delivery_email_template( absint( $data['ebook'] ) );
12184 $ebook_email_delivery = array(
12185 'to' => $data['payer_email'],
12186 'subject' => $email_template['subject'],
12187 'text' => $email_template['text'],
12188 'attachment' => $attachment,
12189 'order' => $order,
12190 'source' => 'standalone',
12191 'related_order_type' => 'ebook_order',
12192 );
12193 //mail(get_option( 'admin_email' ), 'Ebook store for WordPress - Verified Order Received', print_r($ebook_email_delivery,true));
12194 @$fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
12195 //wp_mail('deian@motov.net','test','test');
12196 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
12197 $data['ebook_key'] = $ebook_key;
12198 $data['order_id'] = $post_id;
12199 ebook_encrypt_pdf($data, null, $data);
12200 }
12201 if ($silent == false) {
12202 ?>
12203 <div class="updated">
12204 <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>
12205 </div><?php
12206 } ebook_email_delivery($post_id);
12207
12208 add_action( 'init', 'ebook_email_delivery', 100);
12209 if ($thankYouRedirect) {
12210 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
12211 $link = add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage));
12212 header("Location: $link");
12213 }
12214 if ($silent) {
12215 return $post_id;
12216 }
12217 //error_log('ebook_email_delivery added to plugins_loaded');
12218 }
12219 }
12220
12221
12222
12223
12224
12225 function erl($var) {
12226 if (is_array($var)) {
12227 $varOut = print_r($varOut,true);
12228 } else {
12229 $varOut = $var;
12230 }
12231 error_log(debug_backtrace()[1]['function']);
12232 error_log($varOut);
12233 }
12234
12235
12236 function file_upload_max_size() {
12237 static $max_size = -1;
12238
12239 if ($max_size < 0) {
12240 // Start with post_max_size.
12241 $max_size = parse_size(ini_get('post_max_size'));
12242
12243 // If upload_max_size is less, then reduce. Except if upload_max_size is
12244 // zero, which indicates no limit.
12245 $upload_max = parse_size(ini_get('upload_max_filesize'));
12246 if ($upload_max > 0 && $upload_max < $max_size) {
12247 $max_size = $upload_max;
12248 }
12249 }
12250 return $max_size;
12251 }
12252
12253 function parse_size($size) {
12254 $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
12255 $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
12256 if ($unit) {
12257 // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
12258 return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
12259 }
12260 else {
12261 return round($size);
12262 }
12263 }
12264
12265
12266 function register_ebook_store_woocommerce_type() {
12267 /**
12268 * This should be in its own separate file.
12269 */
12270 // if (class_exists('WC_Product_Simple')) {
12271 // class WC_Product_Ebook_Store extends WC_Product_Simple {
12272
12273 // public function __construct( $product ) {
12274
12275 // $this->product_type = 'ebook_store';
12276
12277 // parent::__construct( $product );
12278
12279 // }
12280 // public function get_type() {
12281 // return 'ebook_store';
12282 // }
12283
12284 // }
12285 // }
12286 if (class_exists('WC_Product')) {
12287 class WC_Product_ebookstore extends WC_Product {
12288
12289 function __construct($product) {
12290 $this->supports[] = 'ajax_add_to_cart';
12291 parent::__construct($product);
12292 $this->product_type = 'ebookstore';
12293 }
12294
12295 /**
12296 * Method to read a product from the database.
12297 * @param WC_Product
12298 */
12299
12300 public function get_product_type( $product_id ) {
12301 return 'ebookstore';
12302
12303 }
12304 public function get_type() {
12305 return 'ebookstore';
12306 }
12307 }
12308
12309
12310 }
12311
12312 }
12313 function woocommerce_custom_product_tabs_for_ebook_store( $tabs) {
12314 $tabs['ebookstore'] = array(
12315 'label' => __( 'Ebook Store', 'woocommerce' ),
12316 'target' => 'ebook_store_options',
12317 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_ebookstore' ),
12318 );
12319 return $tabs;
12320 }
12321
12322
12323
12324
12325 function add_ebook_store_item( $types ){
12326
12327 // Key should be exactly the same as in the class product_type parameter
12328 //$types[ 'ebookstore' ] = __( 'Ebook Store', 'ebook-store' );
12329
12330 return $types;
12331
12332 }
12333 add_filter( 'product_type_selector', 'add_ebook_store_item' );
12334
12335 function woocommerce_ebook_store_price_field() {
12336
12337 if ( 'product' != get_post_type() ) :
12338 //return;
12339 endif;
12340
12341 ?><script type='text/javascript'>
12342 jQuery( document ).ready( function() {
12343 jQuery( '.options_group.pricing' ).addClass( 'show_if_ebook_store' ).show();
12344 jQuery( '.general_options' ).show();
12345 jQuery('label[for="_virtual"]').show().addClass('show_if_ebook_store');
12346 jQuery('label[for="_downloadable"]').show().addClass('show_if_ebook_store');
12347
12348 });
12349 jQuery('#product-type').on('change', function () {
12350 jQuery('.general_options').show();
12351 setTimeout(function () {
12352 jQuery('label[for="_virtual"], label[for="_downloadable"]').show();
12353 }, 500);
12354 });
12355
12356 </script><?php
12357 }
12358
12359
12360
12361 /*-----------------------------------*/
12362 /**
12363 * Add a custom product tab.
12364 */
12365
12366 /**
12367 * Contents of the rental options product tab.
12368 */
12369 function ebook_store_woocommerce_tab_content() {
12370 global $post;
12371 $postOrig = $post;
12372 ?><div id='ebook_store_options' class='panel woocommerce_options_panel' style=""><?php
12373 ?><div class='options_group'>
12374 <?php
12375 if (get_option('ebook_store_woocommerce_integration') == 0) {
12376 ?>
12377 <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>
12378 <?php
12379 return true;
12380 }
12381
12382 $product = function_exists( 'wc_get_product' ) ? wc_get_product( $post->ID ) : null;
12383 if ( $product && $product->is_type( 'variable' ) ) {
12384 ?>
12385 <p class="form-field">
12386 <strong><?php esc_html_e( 'Variable product note:', 'ebook-store' ); ?></strong>
12387 <?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' ); ?>
12388 </p>
12389 <?php
12390 }
12391
12392 woocommerce_wp_select(array(
12393 'id' => 'ebook_store_book_id',
12394 'label' => __( 'Select Ebook', 'woocommerce' ),
12395 'desc_tip' => 'true',
12396 'description' => __( 'The selected ebook will be delivered to your customer. Encryption and watermarking will be handled by ebook store plugin.' ),
12397 'type' => 'text',
12398 'value' => get_post_meta(get_the_ID(), 'ebook_store_book_id', true),
12399 'options' => ebook_store_get_woocommerce_options(),
12400 ) );
12401
12402 $selected_ebook_id = (int) get_post_meta( get_the_ID(), 'ebook_store_book_id', true );
12403 if ( $selected_ebook_id > 0 ) {
12404 $selected_ebook = get_post( $selected_ebook_id );
12405 if ( $selected_ebook && $selected_ebook->post_type === 'ebook' ) {
12406 $ebook_meta = get_post_meta( $selected_ebook_id, 'ebook', true );
12407 $ebook_meta = is_array( $ebook_meta ) ? $ebook_meta : array();
12408 $ebook_cover = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_cover', true );
12409 $ebook_cover = is_array( $ebook_cover ) ? $ebook_cover : array();
12410 $ebook_pdf = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_pdf', true );
12411 $ebook_pdf = is_array( $ebook_pdf ) ? $ebook_pdf : array();
12412 $ebook_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( isset( $ebook_meta['form_template'] ) ? $ebook_meta['form_template'] : '' ) : 'modern';
12413 $form_templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
12414 $template_label = isset( $form_templates[ $ebook_template ]['label'] ) ? $form_templates[ $ebook_template ]['label'] : ucfirst( $ebook_template );
12415 $delivery_mode = isset( $ebook_meta['donate_or_download'] ) ? $ebook_meta['donate_or_download'] : 'paid';
12416 $delivery_label = __( 'Paid download', 'ebook-store' );
12417 if ( $delivery_mode === 'free' ) {
12418 $delivery_label = __( 'Free download', 'ebook-store' );
12419 } elseif ( $delivery_mode === 'donate' ) {
12420 $delivery_label = __( 'Donate to download', 'ebook-store' );
12421 }
12422 $currency = isset( $ebook_meta['paypal_currency'] ) && $ebook_meta['paypal_currency'] !== '' ? $ebook_meta['paypal_currency'] : ebook_store_get_store_currency();
12423 $price = isset( $ebook_meta['ebook_price'] ) ? (float) $ebook_meta['ebook_price'] : 0.0;
12424 $pages = isset( $ebook_meta['ebook_pages'] ) ? (int) $ebook_meta['ebook_pages'] : 0;
12425 $cover_url = ! empty( $ebook_cover['url'] ) ? $ebook_cover['url'] : '';
12426 $pdf_name = ! empty( $ebook_pdf['file'] ) ? basename( $ebook_pdf['file'] ) : '';
12427 $edit_ebook_link = get_edit_post_link( $selected_ebook_id, 'raw' );
12428 ?>
12429 <div class="ebook-woocommerce-linked-ebook">
12430 <div class="ebook-woocommerce-linked-ebook__media">
12431 <div class="ebook-woocommerce-linked-ebook__cover<?php echo $cover_url ? ' has-image' : ' is-empty'; ?>">
12432 <?php if ( $cover_url ) : ?>
12433 <img src="<?php echo esc_url( $cover_url ); ?>" alt="<?php echo esc_attr( get_the_title( $selected_ebook_id ) ); ?>" />
12434 <?php else : ?>
12435 <span><?php esc_html_e( 'No cover', 'ebook-store' ); ?></span>
12436 <?php endif; ?>
12437 </div>
12438 </div>
12439 <div class="ebook-woocommerce-linked-ebook__content">
12440 <div class="ebook-woocommerce-linked-ebook__header">
12441 <div>
12442 <h4><?php echo esc_html( get_the_title( $selected_ebook_id ) ); ?></h4>
12443 <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>
12444 </div>
12445 <?php if ( $edit_ebook_link ) : ?>
12446 <a class="button button-secondary" href="<?php echo esc_url( $edit_ebook_link ); ?>"><?php esc_html_e( 'Edit ebook', 'ebook-store' ); ?></a>
12447 <?php endif; ?>
12448 </div>
12449 <div class="ebook-woocommerce-linked-ebook__meta">
12450 <span><strong><?php esc_html_e( 'Status:', 'ebook-store' ); ?></strong> <?php echo esc_html( ucfirst( get_post_status( $selected_ebook_id ) ) ); ?></span>
12451 <span><strong><?php esc_html_e( 'Price:', 'ebook-store' ); ?></strong> <?php echo esc_html( ebook_store_order_money_display( $price, $currency ) ); ?></span>
12452 <span><strong><?php esc_html_e( 'Delivery:', 'ebook-store' ); ?></strong> <?php echo esc_html( $delivery_label ); ?></span>
12453 <span><strong><?php esc_html_e( 'Template:', 'ebook-store' ); ?></strong> <?php echo esc_html( $template_label ); ?></span>
12454 <?php if ( $pages > 0 ) : ?>
12455 <span><strong><?php esc_html_e( 'Pages:', 'ebook-store' ); ?></strong> <?php echo esc_html( number_format_i18n( $pages ) ); ?></span>
12456 <?php endif; ?>
12457 <?php if ( $pdf_name !== '' ) : ?>
12458 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $pdf_name ); ?></span>
12459 <?php endif; ?>
12460 </div>
12461 <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>
12462 </div>
12463 </div>
12464 <?php
12465 }
12466 }
12467
12468 ?></div>
12469
12470 </div><?php
12471 $post = $postOrig;
12472 }
12473 /**
12474 * Save the custom fields.
12475 */
12476 function save_woocommerce_ebook_store_data( $post_id ) {
12477
12478 $ebook_store_ebook_id = isset( $_POST['_enable_ebook_store_option'] ) ? 'yes' : 'no';
12479 update_post_meta( $post_id, '_enable_ebook_store_option', $ebook_store_ebook_id );
12480
12481 if ( isset( $_POST['ebook_store_book_id'] ) ) {
12482 //first reset the old ebook store item to 0 in case id of the assigned woocommerce prduct is changed
12483 $ebook_store_book_id = get_post_meta($post_id, 'ebook_store_book_id', true);
12484 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', 0 );
12485 //then update to the currently set it.
12486 $ebook_store_book_id = sanitize_text_field( $_POST['ebook_store_book_id'] );
12487 update_post_meta( $post_id, 'ebook_store_book_id', $ebook_store_book_id );
12488 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', $post_id );
12489 //wp_die('woocommerce_product_id - ' . get_post_meta($ebook_store_book_id, 'woocommerce_product_id', true));
12490 }
12491
12492 }
12493 function ebook_store_get_woocommerce_options() {
12494 $new = new WP_Query(
12495 array(
12496 'posts_per_page' => -1,
12497 'post_type' => 'ebook',
12498 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
12499 'orderby' => 'title',
12500 'order' => 'ASC',
12501 'no_found_rows' => true,
12502 )
12503 );
12504 $ebooks = array();
12505 $ebooks[0] = '-- Please select --';
12506 while ($new->have_posts()) : $new->the_post();
12507 $ebook_title = get_the_title(get_the_ID());
12508 if ($ebook_title == '') {
12509 $ebook_title = '(no title)';
12510 }
12511 $ebook_status = get_post_status( get_the_ID() );
12512 if ( $ebook_status && $ebook_status !== 'publish' ) {
12513 $ebook_title .= ' [' . ucfirst( $ebook_status ) . ']';
12514 }
12515 $ebooks[get_the_ID()] = $ebook_title;
12516 endwhile;
12517 wp_reset_postdata();
12518
12519 return $ebooks;
12520 }
12521
12522 /**
12523 * The WooCommerce order status that releases the ebook.
12524 *
12525 * Sites set up through the wizard never had the option written, and older
12526 * installs may hold the legacy '0' ("any status"); both mean "completed".
12527 *
12528 * @return string Status slug without the 'wc-' prefix.
12529 */
12530 function ebook_store_get_wc_required_status() {
12531 $status = trim( (string) get_option( 'ebook_store_woocommerce_required_order_status', 'completed' ) );
12532 if ( $status === '' || $status === '0' ) {
12533 return 'completed';
12534 }
12535
12536 return sanitize_key( $status );
12537 }
12538
12539 /**
12540 * May the files for this WooCommerce order be handed out?
12541 *
12542 * The order key only proves the visitor saw the checkout page; this is the
12543 * payment gate shared by the download endpoint and the order-details table.
12544 *
12545 * @param int $order_id WooCommerce order ID.
12546 * @return bool
12547 */
12548 function ebook_store_wc_order_is_deliverable( $order_id ) {
12549 if ( ! function_exists( 'wc_get_order' ) ) {
12550 return false;
12551 }
12552
12553 $order = wc_get_order( absint( $order_id ) );
12554 if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
12555 return false;
12556 }
12557
12558 if ( $order->has_status( array( 'cancelled', 'refunded', 'failed', 'pending', 'on-hold', 'checkout-draft', 'trash' ) ) ) {
12559 return false;
12560 }
12561
12562 return $order->is_paid() || $order->has_status( ebook_store_get_wc_required_status() );
12563 }
12564
12565 function ebook_store_woocommerce_email_delivery_pending($order_id) {
12566 if ('pending' != ebook_store_get_wc_required_status()) return;
12567 ebook_store_woocommerce_email_delivery($order_id);
12568 }
12569
12570 function ebook_store_woocommerce_email_delivery_processing($order_id) {
12571 if ('processing' != ebook_store_get_wc_required_status()) return;
12572 ebook_store_woocommerce_email_delivery($order_id);
12573 }
12574
12575 function ebook_store_woocommerce_email_delivery_on_hold($order_id) {
12576 if ('on-hold' != ebook_store_get_wc_required_status()) return;
12577 ebook_store_woocommerce_email_delivery($order_id);
12578 }
12579
12580 function ebook_store_woocommerce_email_delivery_cancelled($order_id) {
12581 if ('cancelled' != ebook_store_get_wc_required_status()) return;
12582 ebook_store_woocommerce_email_delivery($order_id);
12583 }
12584 function ebook_store_woocommerce_email_delivery_completed($order_id) {
12585 // WooCommerce skips "processing" for all-virtual, all-downloadable orders and
12586 // goes straight to "completed", so a "processing" setting must fire here too.
12587 $required_status = ebook_store_get_wc_required_status();
12588 if ('completed' != $required_status && 'processing' != $required_status) return;
12589 ebook_store_woocommerce_email_delivery($order_id);
12590 }
12591
12592 function ebook_store_woocommerce_email_delivery($order_id = 0, $event = 'completed' ) {
12593
12594 global $ebook_email_delivery;
12595 // erl('triggered completed');
12596 //error_reporting(E_ALL);
12597 $ebook_email_delivery = array();
12598 $order = new WC_Order( $order_id );
12599 if ( (string) $order->get_meta( '_ebook_store_delivered_at', true ) !== '' ) {
12600 return true;
12601 }
12602 $delivered = false;
12603 $order_items = $order->get_items();
12604 $order->address = $order->get_address();
12605 // error_log('WC ORDER' . print_r($order, true));
12606
12607 foreach ($order_items as $id => $item) {
12608 // error_log(print_r($item,true));
12609 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
12610 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
12611 if ($ebook_store_book_id == 0) {
12612 continue;
12613 }
12614 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_store_book_id );
12615 $ebook_email_delivery = array(
12616 'to' => $order->address['email'],
12617 'subject' => $email_template['subject'],
12618 'text' => $email_template['text'],
12619 'source' => 'woocommerce',
12620 'related_order_type' => 'shop_order',
12621 'woocommerce_event' => sanitize_key( (string) $event ),
12622 );
12623 //create new order array to make email delivery work
12624 $ebook = new EbookStoreEbook($ebook_store_book_id);
12625 $ebook->order_id = $order_id;
12626 $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.
12627 $taxes = $order->get_taxes();
12628
12629 // error_log('taxes' . print_r($taxes,true));
12630 // foreach ($taxes as $tax) {
12631 // $total_taxes = $tax->get_tax_total();
12632 // error_log('total_taxes' . print_r($total_taxes,true));
12633 // //break;
12634 // }
12635 $vat = ebook_store_get_vat_percent();
12636 if ($vat > 0) {
12637 $tax = $ebook->price * ($vat / 100);
12638 } else {
12639 $tax = 0;
12640 }
12641 // Reuse the key already stored on the order so the QR code and links keep
12642 // pointing at the same record across status changes; generate it only once.
12643 $md5_nonce = (string) $order->get_meta( 'md5_nonce', true );
12644 $ebook_key = (string) $order->get_meta( 'ebook_key', true );
12645 if ( $md5_nonce === '' || $ebook_key === '' ) {
12646 $md5_nonce = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
12647 $ebook_key = md5($md5_nonce);
12648 }
12649 $payment_date = $order->get_date_created() ? $order->get_date_created()->__toString() : '';
12650 // Stored through the CRUD layer so HPOS sites can find the buyer later for
12651 // watermarks, passwords and the verification page.
12652 $order->update_meta_data( 'md5_nonce', $md5_nonce );
12653 $order->update_meta_data( 'ebook_key', $ebook_key );
12654 $order->update_meta_data( 'payer_email', (string) $order->address['email'] );
12655 $order->update_meta_data( 'first_name', (string) $order->address['first_name'] );
12656 $order->update_meta_data( 'last_name', (string) $order->address['last_name'] );
12657 $order->update_meta_data( 'txn_id', (string) $order->get_transaction_id() );
12658 $order->update_meta_data( 'payment_date', $payment_date );
12659 $order->update_meta_data( 'residence_country', (string) $order->address['country'] );
12660 $order->save();
12661 // Post-meta copies keep the older get_post_meta() lookups working when orders live in wp_posts.
12662 update_post_meta( $order_id, 'md5_nonce', $md5_nonce );
12663 update_post_meta( $order_id, 'ebook_key', $ebook_key );
12664 $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 ) ) );
12665
12666 $ebook_email_delivery['order'] = array(
12667 'first_name' => $order->address['first_name'],
12668 'last_name' => $order->address['last_name'],
12669 'mc_gross' => $item['line_subtotal'],
12670 'ebook' => $ebook_store_book_id,
12671 'payer_email' => $order->address['email'],
12672 'md5_nonce' => $md5_nonce,
12673 'ebook_key' => $ebook_key,
12674 'mc_fee' => '',
12675 'item_name' => get_the_title($ebook_store_book_id),
12676 'payment_date' => $payment_date,
12677 'mc_currency' => $order->get_currency(),
12678 'residence_country' => $order->address['country'],
12679 'order_id' => $order_id,
12680 'password' => $ebook->password(),
12681 'txn_id' => $order->get_transaction_id(),
12682 'downloadlink' => $pdf_download_link,
12683 'downloadlink_html' => $ebook->format_links(),
12684 'downloadlinks' => $ebook->format_links(),
12685 'tax' => $tax,
12686 'ip' => $_SERVER['REMOTE_ADDR'],
12687 );
12688
12689
12690 $ebook_email_delivery['attachment'][0]['file'] = $ebook->file;
12691 $ebook_email_delivery['attachment'][0]['file'] = $ebook_email_delivery['attachment'][0]['file']['file'];
12692
12693 @$fileExt = pathinfo($ebook_email_delivery['attachment'][0]['file'],PATHINFO_EXTENSION);
12694
12695 //wp_mail('deian@motov.net','test','test');
12696 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
12697 $ebook_email_delivery['attachment'][0]['file'] = ebook_encrypt_pdf($ebook_email_delivery['order']);
12698 }
12699 if ( $fileExt == 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
12700 $ebook_email_delivery['attachment'][0]['file'] = $ebook->encrypted( $order_id, 'epub' );
12701 }
12702
12703 $ebook_store_woocommerce_required_order_status = esc_attr(get_option('ebook_store_woocommerce_required_order_status', 0));
12704 // if ($order_status != $ebook_store_woocommerce_required_order_status && $ebook_store_woocommerce_required_order_status != '0') {
12705 // error_log('ORDER STATUS ' . $ebook_store_woocommerce_required_order_status . ' VS ' . $order_status);
12706 // erl('will return now');
12707 // return $order_status;
12708 // }
12709
12710 ebook_email_delivery();
12711 $delivered = true;
12712 // error_log('ebook id ' . $ebook_store_book_id);
12713 # code...
12714 }
12715 if ( $delivered ) {
12716 $order->update_meta_data( '_ebook_store_delivered_at', current_time( 'mysql', true ) );
12717 $order->save();
12718 }
12719 // if ( 'processing' == $order_status && ( 'on-hold' == $order->status || 'pending' == $order->status || 'failed' == $order->status ) ) {
12720 // return 'completed';
12721 // }
12722 return true;
12723 }
12724
12725 function ebook_store_woocommerce_order_details($order, $skipHeaders = false){
12726 $order_items = $order->get_items();
12727 $order_status = $order->get_status();
12728 $order_id = 0;
12729 if (is_object($order)) {
12730 if (method_exists($order, 'get_id')) {
12731 $order_id = $order->get_id();
12732 } elseif (isset($order->id)) {
12733 $order_id = $order->id;
12734 }
12735 }
12736 if ( ! ebook_store_wc_order_is_deliverable( $order_id ) ) {
12737 return true;
12738 }
12739 if ($skipHeaders == false) {
12740 @$out .= "<h2>". __('Downloads', 'ebook-store') . "</h2>";
12741 @$out .= '<table class="shop_table order_downloads_ebook_store">
12742 <thead>
12743 <tr>
12744 <th class="product-name">'. __('Ebook (digital copy)', 'ebook-store') . '</th>
12745 <th class="product-total">'. __('Download', 'ebook-store') . '</th>
12746 </tr>
12747 </thead>
12748 <tbody>';
12749
12750 }
12751 // echo "<pre>";
12752 // print_r($order_items);
12753 // echo "<pre>";
12754 foreach ($order_items as $item_id => $item) {
12755 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
12756 $woocommerce_product_id = $item->get_product_id();
12757 $product = $item->get_product();
12758 $title = $product ? $product->get_title() : $item->get_name();
12759
12760 //wp_die("ID is $woocommerce_product_id");
12761 //$woocommerce_product_id = 100;
12762 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
12763
12764 if ($ebook_store_book_id != 0) {
12765 @$ebooks++;
12766 }
12767
12768 $ebook = new EbookStoreEbook($ebook_store_book_id);
12769 $ebook->order_id = $order_id;
12770 $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.
12771 $cell = '<span class="amount">' . $ebook->format_links() . '</span><br /><small>' . $ebook->password_desc() . '</small>';
12772 // The reader is only offered when the main file really is a PDF: for an
12773 // ePub- or ZIP-only title the viewer cannot show anything, and its probe
12774 // of the download URL would count as a download.
12775 $main_file_is_pdf = ! empty( $ebook->files['pdf'] ) && strtolower( pathinfo( (string) $ebook->files['pdf'], PATHINFO_EXTENSION ) ) === 'pdf';
12776 if ( get_option( 'ebook_store_woocommerce_pdf_reader', false ) && $main_file_is_pdf ) {
12777 // Read the buyer's own protected copy through the download endpoint
12778 // (inline=1 shows it in the viewer without spending a download), and
12779 // keep the download links: the reader used to replace them entirely.
12780 $reader_url = add_query_arg(
12781 array(
12782 'action' => 'wc_order_download',
12783 'ebook_id' => $ebook_store_book_id,
12784 'format' => 'pdf',
12785 'wc_order' => $ebook->wc_order,
12786 'order_id' => $order_id,
12787 'inline' => 1,
12788 ),
12789 get_permalink( $ebook_store_book_id )
12790 );
12791 $cell = ebook_pdf_reader(false, $ebook_store_book_id, $reader_url) . '<div style="clear:both;"></div>' . $cell;
12792 }
12793
12794 if ($ebook_store_book_id > 0) {
12795 @$out .= '
12796 <tr class="order_item">
12797 <td class="product-name">
12798 ' . $title . '</td>
12799 <td class="product-total">
12800 ' . $cell . '
12801 </td>
12802 </tr>';
12803
12804 }
12805 }
12806 if ($skipHeaders == false) {
12807 @$out .= '
12808 </tbody>
12809 </table>';
12810 }
12811
12812 if (@$ebooks > 0) {
12813 echo $out;
12814 }
12815
12816 // echo '<pre>' . print_r($order,true) . '</pre>';
12817 // echo '<pre>' . print_r($ord,true) . '</pre>';
12818
12819 // echo '<p><strong>'.__('Pickup Location').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12820 // echo '<p><strong>'.__('Pickup Date').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12821 }
12822 /**
12823 * The order key stored against a WooCommerce order.
12824 *
12825 * Reads through the WooCommerce CRUD layer so the lookup keeps working under
12826 * High-Performance Order Storage, where orders no longer live in wp_posts and
12827 * get_post_meta() returns nothing.
12828 *
12829 * @param int $order_id WooCommerce order ID.
12830 * @return string Order key, or '' when the order does not exist.
12831 */
12832 function ebook_store_get_wc_order_key( $order_id ) {
12833 $order_id = absint( $order_id );
12834 if ( $order_id < 1 ) {
12835 return '';
12836 }
12837
12838 if ( function_exists( 'wc_get_order' ) ) {
12839 $order = wc_get_order( $order_id );
12840 if ( $order ) {
12841 return (string) $order->get_order_key();
12842 }
12843
12844 return '';
12845 }
12846
12847 return (string) get_post_meta( $order_id, '_order_key', true );
12848 }
12849
12850 /**
12851 * Does the supplied key authorise access to this WooCommerce order?
12852 *
12853 * @param int $order_id WooCommerce order ID.
12854 * @param string $order_key Key supplied in the request.
12855 * @return bool
12856 */
12857 function ebook_store_wc_order_key_matches( $order_id, $order_key ) {
12858 $order_key = sanitize_text_field( (string) $order_key );
12859 if ( $order_key === '' ) {
12860 return false;
12861 }
12862
12863 $stored = ebook_store_get_wc_order_key( $order_id );
12864
12865 return $stored !== '' && hash_equals( $stored, $order_key );
12866 }
12867
12868 /**
12869 * Is this ebook actually one of the items in that WooCommerce order?
12870 *
12871 * Without this check a valid order key for a $1 book would unlock every other
12872 * ebook in the catalogue.
12873 *
12874 * @param int $order_id WooCommerce order ID.
12875 * @param int $ebook_id Ebook post ID.
12876 * @return bool
12877 */
12878 function ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) {
12879 $ebook_id = absint( $ebook_id );
12880 if ( $ebook_id < 1 ) {
12881 return false;
12882 }
12883
12884 if ( ! function_exists( 'wc_get_order' ) ) {
12885 // Without WooCommerce there is nothing to cross-check against; the order
12886 // key match above is the only gate.
12887 return true;
12888 }
12889
12890 $order = wc_get_order( absint( $order_id ) );
12891 if ( ! $order ) {
12892 return false;
12893 }
12894
12895 foreach ( $order->get_items() as $item ) {
12896 $product_id = (int) $item->get_product_id();
12897 $variation_id = (int) $item->get_variation_id();
12898
12899 foreach ( array( $variation_id, $product_id ) as $candidate ) {
12900 if ( $candidate < 1 ) {
12901 continue;
12902 }
12903 if ( (int) get_post_meta( $candidate, 'ebook_store_book_id', true ) === $ebook_id ) {
12904 return true;
12905 }
12906 }
12907 }
12908
12909 return (bool) apply_filters( 'ebook_store_wc_order_contains_ebook', false, $order_id, $ebook_id );
12910 }
12911
12912 function ebook_process_download_woocomerce() {
12913 if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'wc_order_download' ) {
12914 return false;
12915 }
12916
12917 // ebook_process_download() now performs the order-key and order-contents
12918 // checks itself, so this wrapper only forwards the request.
12919 ebook_process_download( isset( $_GET['wc_order'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_order'] ) ) : '' );
12920 die();
12921 }
12922
12923 function ebook_store_add_to_cart() {
12924 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
12925 $locale = ebook_store_get_locale_strings();
12926 $formats_locale = ebook_store_get_format_labels();
12927
12928 if ($_GET['woocommerce_product_id'] > 0) {
12929 global $woocommerce;
12930 $woocommerce_product_id = (int)$_GET['woocommerce_product_id'];
12931 $cart_config = ebook_store_get_wc_add_to_cart_config( $woocommerce_product_id );
12932 if ( ! $cart_config ) {
12933 return;
12934 }
12935
12936 if ( ! ebook_store_cart_contains_wc_product( $cart_config ) ) {
12937 WC()->cart->add_to_cart(
12938 $cart_config['product_id'],
12939 1,
12940 $cart_config['variation_id'],
12941 $cart_config['variation']
12942 );
12943 }
12944
12945 global $ebook_store_messages;
12946
12947 if (get_option('ebook_store_woocommerce_integration_no_added_to_cart') < 1) {
12948 $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>';
12949 }
12950 }
12951 }
12952
12953
12954
12955 function ebook_store_action_links ( $links ) {
12956 $mylinks = array(
12957 '<a href="' . admin_url( 'options-general.php?page=ebook_options.php' ) . '">Settings</a>',
12958 '<a href="https://www.youtube.com/watch?v=T0mrdvrbmbc&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-" target="_blank">Video Tutorials</a>',
12959 '<a href="https://shopfiles.com/index.php/products/wordpress-ebook-store" target="_blank">Upgrade</a>',
12960 );
12961 return array_merge( $links, $mylinks );
12962 }
12963
12964 function ebook_store_payment_completed_wpam($ipn_data, $order_id)
12965 {
12966 if ( ! class_exists( 'WPAM_Logger' ) || ! class_exists( 'WPAM_Tracking_RequestTracker' ) ) {
12967 return;
12968 }
12969 $custom_data = isset( $ipn_data['custom'] ) ? (string) $ipn_data['custom'] : '';
12970 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - payment completed hook fired. Custom field value: '.$custom_data);
12971 $custom_values = array();
12972 parse_str($custom_data, $custom_values);
12973 if(isset($custom_values['wpam_tracking']) && !empty($custom_values['wpam_tracking']))
12974 {
12975 $tracking_value = $custom_values['wpam_tracking'];
12976 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Tracking data present. Need to track affiliate commission. Tracking value: '.$tracking_value);
12977 $purchaseLogId = $ipn_data['txn_id'];
12978 $purchaseAmount = $ipn_data['mc_gross']; //TODO - later calculate sub-total only
12979 $strRefKey = $tracking_value;
12980 $requestTracker = new WPAM_Tracking_RequestTracker();
12981 $requestTracker->handleCheckoutWithRefKey( $purchaseLogId, $purchaseAmount, $strRefKey);
12982 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Commission tracked for transaction ID: '.$purchaseLogId.'. Purchase amt: '.$purchaseAmount);
12983 }
12984 }
12985
12986 function ebook_store_payment_gateway_parameters_wpam($parameters)
12987 {
12988 if (class_exists('WPAM_PluginConfig') == false) {
12989 return;
12990 }
12991 if(isset($_COOKIE['wpam_id']))
12992 {
12993 $name = 'wpam_tracking';
12994 $value = $_COOKIE['wpam_id'];
12995 $new_val = $name.'='.$value;
12996 $current_val = $parameters['custom'];
12997 if(empty($current_val)){
12998 $parameters['custom'] = $new_val;
12999 }
13000 else{
13001 $parameters['custom'] = $current_val.'&'.$new_val;
13002 }
13003 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
13004 }
13005
13006 else if(isset($_COOKIE[WPAM_PluginConfig::$RefKey]))
13007 {
13008 $name = 'wpam_tracking';
13009 $value = $_COOKIE[WPAM_PluginConfig::$RefKey];
13010 $new_val = $name.'='.$value;
13011 $current_val = $parameters['custom'];
13012 if(empty($current_val)){
13013 $parameters['custom'] = $new_val;
13014 }
13015 else{
13016 $parameters['custom'] = $current_val.'&'.$new_val;
13017 }
13018 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
13019 }
13020
13021 return $parameters;
13022 }
13023
13024 function ebook_store_my_taxonomies_product() {
13025 $labels = array(
13026 'name' => _x( 'Ebook Categories', 'Ebook Categories', 'ebook-store' ),
13027 'singular_name' => _x( 'Ebook Category', 'Ebook Category', 'ebook-store' ),
13028 'search_items' => __( 'Search Ebook Categories', 'ebook-store' ),
13029 'all_items' => __( 'All Ebook Categories', 'ebook-store' ),
13030 'parent_item' => __( 'Parent Ebook Category', 'ebook-store' ),
13031 'parent_item_colon' => __( 'Parent Ebook Category:', 'ebook-store' ),
13032 'edit_item' => __( 'Edit Ebook Category', 'ebook-store' ),
13033 'update_item' => __( 'Update Ebook Category', 'ebook-store' ),
13034 'add_new_item' => __( 'Add New Ebook Category', 'ebook-store' ),
13035 'new_item_name' => __( 'New Ebook Category', 'ebook-store' ),
13036 'menu_name' => __( 'Ebook Categories', 'ebook-store' ),
13037 );
13038 $args = array(
13039 'labels' => $labels,
13040 'hierarchical' => true,
13041 );
13042 register_taxonomy( 'ebook_category', 'ebook', $args );
13043 }
13044
13045 function ebook_store_vc_before_init_actions() {
13046 class vcInfoBox extends WPBakeryShortCode {
13047
13048 // Element Init
13049 function __construct() {
13050 add_action( 'init', array( $this, 'vc_infobox_mapping' ) );
13051 add_shortcode( 'vc_infobox', array( $this, 'vc_infobox_html' ) );
13052 }
13053
13054 // Element Mapping
13055 public function vc_infobox_mapping() {
13056
13057 // Stop all if VC is not enabled
13058 if ( !defined( 'WPB_VC_VERSION' ) ) {
13059 return;
13060 }
13061
13062 // Map the block with vc_map()
13063 vc_map(
13064 array(
13065 'name' => __('Ebook Order Form', 'text-domain'),
13066 'base' => 'ebook_store',
13067 'description' => __('Inserts shortcode that will call the Ebook Order form.', 'text-domain'),
13068 'category' => __('Ebook Store', 'ebook-store'),
13069 'icon' => 'dashicons-cart', //get_template_directory_uri().'/assets/img/vc-icon.png',
13070 'params' => array(
13071
13072 array(
13073 'type' => 'dropdown',
13074 'holder' => 'h3',
13075 'class' => 'title-class',
13076 'heading' => __( 'Select Ebook', 'text-domain' ),
13077 'param_name' => 'ebook_id',
13078 'value' => vc_ebook_store_array(),
13079 'description' => __( 'You are embedding a direct order form for the Ebook via Ebook Store plugin.', 'text-domain' ),
13080 'admin_label' => false,
13081 'weight' => 0,
13082 'group' => 'Ebook Store',
13083 ),
13084
13085
13086 ),
13087 )
13088 );
13089
13090 }
13091
13092
13093 // Element HTML
13094 public function vc_infobox_html( $atts ) {
13095
13096 // Params extraction
13097 extract(
13098 shortcode_atts(
13099 array(
13100 'title' => '',
13101 'text' => '',
13102 'ebook_id' => '',
13103 ),
13104 $atts
13105 )
13106 );
13107
13108 // Fill $html var with data
13109 $html = '
13110 <div class="vc-infobox-wrap">
13111
13112 <h2 class="vc-infobox-title">' . $title . '</h2>
13113
13114 <div class="vc-infobox-text"></div>
13115
13116
13117
13118 </div>';
13119
13120 return $html;
13121
13122 }
13123
13124 } // End Element Class
13125
13126
13127 // Element Class Init
13128 new vcInfoBox();
13129 }
13130 function vc_ebook_store_array() {
13131 $args = array(
13132 'post_type' => 'ebook',
13133 );
13134 $out = array();
13135 $query = new WP_Query($args);
13136 while ($query->have_posts()) {
13137 $query->the_post();
13138 $out[get_the_title()] = get_the_ID();
13139 }
13140 return $out;
13141 }
13142 function ebook_store_order_reports_callback() {
13143 include_once('ebook_store_order_reports_callback.php');
13144 }
13145 /**
13146 * Stream the orders list as a CSV download.
13147 *
13148 * The previous version loaded every order at once, called get_post_meta() per
13149 * row, buffered the whole file in a PHP array and then die()'d with an implode.
13150 * At 10k orders that peaked several hundred megabytes and never finished. It
13151 * also took the column list from the FIRST row only, so every order with a
13152 * different meta shape emitted an undefined-index warning per missing field.
13153 *
13154 * This walks the orders in pages of 200, writes straight to the output stream,
13155 * and uses the union of all meta keys as the header row.
13156 */
13157 function ebook_store_export_orders() {
13158 if ( ! is_admin() ) {
13159 return;
13160 }
13161
13162 if ( ! isset( $_GET['export'] ) || (int) $_GET['export'] !== 1 ) {
13163 return;
13164 }
13165
13166 if ( ! current_user_can( 'manage_options' ) ) {
13167 wp_die( esc_html__( 'You are not allowed to export orders.', 'ebook-store' ) );
13168 }
13169
13170 check_admin_referer( 'ebook_store_export_orders' );
13171
13172 $selected_ebook_id = isset( $_GET['ebook_id'] ) ? absint( wp_unslash( $_GET['ebook_id'] ) ) : 0;
13173
13174 $base_args = array(
13175 'post_type' => 'ebook_order',
13176 'post_status' => 'any',
13177 'posts_per_page' => 200,
13178 'fields' => 'ids',
13179 'no_found_rows' => true,
13180 'update_post_term_cache' => false,
13181 'orderby' => 'ID',
13182 'order' => 'ASC',
13183 );
13184
13185 if ( $selected_ebook_id > 0 ) {
13186 $base_args['meta_key'] = 'ebook';
13187 $base_args['meta_value'] = $selected_ebook_id;
13188 }
13189
13190 // First pass: collect the union of meta keys so no row is missing a column.
13191 $keys = array();
13192 $page = 1;
13193 do {
13194 $args = $base_args;
13195 $args['paged'] = $page;
13196 $ids = get_posts( $args );
13197
13198 foreach ( $ids as $id ) {
13199 foreach ( array_keys( (array) get_post_meta( $id ) ) as $key ) {
13200 $keys[ $key ] = true;
13201 }
13202 }
13203
13204 $page++;
13205 } while ( count( $ids ) === $base_args['posts_per_page'] );
13206
13207 if ( empty( $keys ) ) {
13208 wp_die( esc_html__( 'There are no orders to export yet.', 'ebook-store' ) );
13209 }
13210
13211 $keys = array_keys( $keys );
13212 sort( $keys );
13213
13214 nocache_headers();
13215 header( 'Content-Type: text/csv; charset=utf-8' );
13216 header( 'Content-Disposition: attachment; filename="ebook-store-orders-' . gmdate( 'Y-m-d' ) . '.csv"' );
13217
13218 $out = fopen( 'php://output', 'w' );
13219
13220 // A UTF-8 BOM so Excel opens accented buyer names correctly.
13221 fwrite( $out, "\xEF\xBB\xBF" );
13222 fputcsv( $out, array_merge( array( 'order_id', 'order_date' ), $keys ) );
13223
13224 $page = 1;
13225 do {
13226 $args = $base_args;
13227 $args['paged'] = $page;
13228 $ids = get_posts( $args );
13229
13230 foreach ( $ids as $id ) {
13231 $meta = (array) get_post_meta( $id );
13232 $row = array( $id, get_post_field( 'post_date_gmt', $id ) );
13233
13234 foreach ( $keys as $key ) {
13235 $row[] = isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
13236 }
13237
13238 fputcsv( $out, $row );
13239 }
13240
13241 // Release the meta cache for the page we just wrote, otherwise the
13242 // pagination saves nothing.
13243 foreach ( $ids as $id ) {
13244 wp_cache_delete( $id, 'post_meta' );
13245 }
13246
13247 flush();
13248 $page++;
13249 } while ( count( $ids ) === $base_args['posts_per_page'] );
13250
13251 fclose( $out );
13252 exit;
13253 }
13254
13255 function ebook_store_session_end() {
13256 session_destroy ();
13257 }
13258 function ebook_store_encryptable($file) {
13259 require_once __DIR__ . '/fpdi/bootstrap.php';
13260 try {
13261 $pdf = new \setasign\Fpdi\Fpdi();
13262 $pdf->setSourceFile($file);
13263 return true;
13264 } catch (\Throwable $e) {
13265 return false;
13266 }
13267 }
13268
13269 function ebook_store_offer_tutorial() {
13270 if (ebook_store_endsWith(esc_url_raw($_SERVER['REQUEST_URI']), 'edit.php?post_type=ebook')) {
13271 $args = array(
13272 'post_type' => 'ebook',
13273 );
13274 $myquery = new WP_Query($args);
13275 if ($myquery->found_posts == 0) {
13276 ?>
13277 <div class="updated">
13278 <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 />
13279 <a href="https://www.youtube.com/watch?v=HxYzlaEHPU4&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-&index=1" target="_blank">Watch Tutorials on YouTube</a>', 'ebooks-store' ); ?></p>
13280 </div>
13281 <?php
13282 }
13283 }
13284 }
13285 function ebook_store_not_encryptable() {
13286 $class = 'notice notice-error';
13287 $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' );
13288
13289 printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), $message );
13290 }
13291
13292 function ebook_store_get_password_for_order($order_id = 0, $context = array()) {
13293 static $runtime_password_cache = array();
13294
13295 $random_enabled = intval(get_option('ebook_store_random_password')) === 1;
13296 $blank_enabled = intval(get_option('ebook_store_blank_password')) === 1;
13297
13298 $cache_key = '';
13299 if ($order_id > 0) {
13300 $cache_key = 'order_' . $order_id;
13301 } elseif (!empty($context['md5_nonce'])) {
13302 $cache_key = 'nonce_' . $context['md5_nonce'];
13303 } elseif (!empty($context['ebook_key'])) {
13304 $cache_key = 'ebook_key_' . $context['ebook_key'];
13305 }
13306
13307 if ($cache_key && isset($runtime_password_cache[$cache_key])) {
13308 return $runtime_password_cache[$cache_key];
13309 }
13310
13311 $stored_exists = ($order_id > 0 && function_exists('metadata_exists')) ? metadata_exists('post', $order_id, 'password') : false;
13312 if ($stored_exists) {
13313 $stored_password = get_post_meta($order_id,'password',true);
13314 if ($cache_key) {
13315 $runtime_password_cache[$cache_key] = $stored_password;
13316 }
13317 return $stored_password;
13318 }
13319
13320 $payer_email = isset($context['payer_email']) ? trim($context['payer_email']) : '';
13321 if (!$payer_email && $order_id > 0) {
13322 $payer_email = get_post_meta($order_id,'payer_email',true);
13323 }
13324 $provided_password = isset($context['password']) ? (string)$context['password'] : '';
13325 $billing_email = isset($context['billing_email']) ? trim($context['billing_email']) : '';
13326 if ($billing_email === '' && $order_id > 0) {
13327 $billing_email = get_post_meta($order_id,'_billing_email',true);
13328 $billing_email = is_string($billing_email) ? trim($billing_email) : '';
13329 }
13330
13331 if ($blank_enabled) {
13332 $password = '';
13333 } elseif ($random_enabled) {
13334 $password = $provided_password;
13335 if ($password === '') {
13336 $password = substr(md5(uniqid('ebook_pw_', true)), 0, 8);
13337 }
13338 } else {
13339 $password = $provided_password ?: $payer_email;
13340 if ($password === '') {
13341 $password = $billing_email;
13342 }
13343 if ($password === '' && !empty($context['fallback_email'])) {
13344 $password = $context['fallback_email'];
13345 }
13346 }
13347
13348 if ($order_id > 0) {
13349 update_post_meta($order_id,'password',$password);
13350 }
13351
13352 if ($cache_key) {
13353 $runtime_password_cache[$cache_key] = $password;
13354 }
13355
13356 return $password;
13357 }
13358
13359 /**
13360 * The order-detail block shared by the confirmation page and the delivery email.
13361 *
13362 * @param bool $with_vat Append the VAT figure to the total line.
13363 * @return string
13364 */
13365 function ebook_store_default_order_details_block( $with_vat = false ) {
13366 $total = $with_vat
13367 /* translators: %%mc_currency%%, %%total%% and %%tax%% are merge codes — keep them exactly as they are. */
13368 ? __( 'Total: %%mc_currency%% %%total%% (VAT %%tax%%)', 'ebook-store' )
13369 /* translators: %%mc_currency%% and %%total%% are merge codes — keep them exactly as they are. */
13370 : __( 'Total: %%mc_currency%% %%total%%', 'ebook-store' );
13371
13372 return '<h3>' . __( 'Details for your order:', 'ebook-store' ) . '</h3>' . "\n"
13373 /* translators: %%order_id%% is a merge code — keep it exactly as it is. */
13374 . __( 'Order #: %%order_id%%', 'ebook-store' ) . "\n"
13375 /* translators: %%txn_id%% is a merge code — keep it exactly as it is. */
13376 . __( 'Transaction: %%txn_id%%', 'ebook-store' ) . "\n"
13377 /* translators: %%mc_currency%% and %%mc_gross%% are merge codes — keep them exactly as they are. */
13378 . __( 'Amount: %%mc_currency%% %%mc_gross%%', 'ebook-store' ) . "\n"
13379 . $total . "\n\n"
13380 /* translators: %%password%% is a merge code — keep it exactly as it is. */
13381 . '<strong>' . __( 'Your password is: %%password%%', 'ebook-store' ) . '</strong>' . "\n\n"
13382 . __( 'Thank you!', 'ebook-store' );
13383 }
13384
13385 /**
13386 * Default content of the order confirmation ("thank you") page.
13387 *
13388 * This used to be a hardcoded English string on QSWPOptions, so a shop selling
13389 * in German or Arabic showed its buyers an English confirmation page until
13390 * somebody rewrote it by hand. Built from translated pieces instead, so a store
13391 * gets the page in its own language out of the box.
13392 *
13393 * The %%...%% merge codes are substituted later and must survive translation.
13394 *
13395 * @return string
13396 */
13397 function ebook_store_default_thankyou_page() {
13398 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
13399 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
13400 . __( 'You have successfully completed the order process! Please use the link(s) below to download your copy:', 'ebook-store' ) . "\n\n"
13401 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
13402 . '<strong>' . __( 'Use the link(s) to start the download:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
13403 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
13404 . __( 'Bonus ebook download links:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n"
13405 . ebook_store_default_order_details_block( false );
13406 }
13407
13408 /**
13409 * Default body of the delivery email.
13410 *
13411 * Same reasoning as the confirmation page: it shipped in English only.
13412 *
13413 * @return string
13414 */
13415 function ebook_store_default_email_delivery_text() {
13416 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
13417 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
13418 . __( 'You have successfully completed the order process!', 'ebook-store' ) . "\n\n"
13419 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
13420 . '<strong>' . __( 'Please use the link(s) below to download your copy:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
13421 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
13422 . __( 'Bonus ebooks:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n\n"
13423 . ebook_store_default_order_details_block( true );
13424 }
13425
13426 /**
13427 * Default subject line of the delivery email.
13428 *
13429 * @return string
13430 */
13431 function ebook_store_default_email_delivery_subject() {
13432 return __( 'Email Delivery - Order complete!', 'ebook-store' );
13433 }
13434
13435 /**
13436 * Mark the admin body when this site holds a valid Pro licence.
13437 *
13438 * The "Pro" badge is drawn by CSS on any element carrying .goPro2, and several
13439 * of those classes are printed unconditionally — the delivery options, the bonus
13440 * books picker, the importer. A paying customer was still shown a locked-looking
13441 * screen covered in upsell badges. One body class lets the stylesheet stand every
13442 * one of them down, including any added later.
13443 *
13444 * @param string $classes Existing body classes.
13445 * @return string
13446 */
13447 function ebook_store_admin_body_class( $classes ) {
13448 if ( function_exists( 'ebook_store_has_pro_license' ) && ebook_store_has_pro_license() ) {
13449 $classes .= ' ebook-store-pro-active';
13450 }
13451
13452 return $classes;
13453 }
13454 add_filter( 'admin_body_class', 'ebook_store_admin_body_class' );
13455
13456 function ebook_store_file_formats_form($ebook_id) {
13457 $formats = array('pdf','zip','mobi','txt','mp3','epub','mp4');
13458 $ebook = new EbookStoreEbook($ebook_id);
13459 $ebook->formats = is_array($ebook->formats) ? $ebook->formats : array();
13460
13461 // Everything but PDF is a Pro format, but the "Pro" badge should only appear
13462 // to someone who does not have Pro. It used to be keyed on the format index
13463 // alone, so a paying licence holder still saw every format tab stamped and
13464 // styled as locked.
13465 $pro_unlocked = function_exists('ebook_store_has_pro_license') ? ebook_store_has_pro_license() : false;
13466
13467 $entries = array();
13468
13469 foreach ($formats as $index => $format) {
13470 if ($format == 'pdf' && get_option('encrypt_pdf')) {
13471 if (!ebook_store_encryptable(@$ebook->files[$format]) && @$ebook->files[$format] != '') {
13472 ebook_store_not_encryptable();
13473 }
13474 }
13475
13476 $has_file = @in_array($format, $ebook->formats);
13477 $filename = ($has_file && !empty($ebook->files[$format])) ? basename($ebook->files[$format]) : '';
13478 $size_label = @$ebook->human_filesize(@$ebook->files[$format . '_size']);
13479 $entries[] = array(
13480 'slug' => $format,
13481 'label' => strtoupper($format),
13482 'has_file' => $has_file,
13483 'filename' => $filename,
13484 'size_label' => $size_label,
13485 'exists_class'=> $has_file ? '' : 'hiddenEbookStore',
13486 'is_pro' => ($index > 0) && ! $pro_unlocked,
13487 );
13488 }
13489 ?>
13490 <div class="ebook_store_file_formats_form">
13491 <h3><?php _e('Upload eBook Files', 'ebook-store'); ?></h3>
13492 <p class="description"><?php _e('Upload your eBook in different formats. Supported formats are grouped by format tabs below.', 'ebook-store'); ?></p>
13493
13494 <div class="ebook-format-tabs" id="ebook-format-tabs-<?php echo esc_attr($ebook_id); ?>">
13495 <div class="ebook-format-tabs__nav" role="tablist">
13496 <?php foreach ($entries as $idx => $entry) :
13497 $panel_id = 'ebook-format-panel-' . $entry['slug'];
13498 $status_icon = $entry['has_file'] ? 'dashicons-yes-alt' : 'dashicons-minus';
13499 $nav_classes = array('ebook-format-tabs__nav-item');
13500 if ($entry['is_pro']) {
13501 $nav_classes[] = 'goPro2';
13502 }
13503 if ($entry['has_file']) {
13504 $nav_classes[] = 'has-file';
13505 }
13506 if ($idx === 0) {
13507 $nav_classes[] = 'is-active';
13508 }
13509 ?>
13510 <button type="button"
13511 class="<?php echo esc_attr(implode(' ', $nav_classes)); ?>"
13512 data-format="<?php echo esc_attr($entry['slug']); ?>"
13513 data-target="#<?php echo esc_attr($panel_id); ?>"
13514 role="tab"
13515 aria-controls="<?php echo esc_attr($panel_id); ?>"
13516 aria-selected="<?php echo $idx === 0 ? 'true' : 'false'; ?>">
13517 <span class="ebook-format-tab__icon">
13518 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?>" />
13519 </span>
13520 <span class="ebook-format-tab__label"><?php echo esc_html($entry['label']); ?></span>
13521 <span class="ebook-format-tab__status dashicons <?php echo esc_attr($status_icon); ?>"></span>
13522 </button>
13523 <?php endforeach; ?>
13524 </div>
13525 <div class="ebook-format-tabs__panels">
13526 <?php foreach ($entries as $idx => $entry) :
13527 $panel_id = 'ebook-format-panel-' . $entry['slug'];
13528 $panel_classes = array('ebook-format-tab-panel');
13529 if ($entry['is_pro']) {
13530 $panel_classes[] = 'goPro2';
13531 }
13532 if ($entry['has_file']) {
13533 $panel_classes[] = 'has-file';
13534 }
13535 if ($idx === 0) {
13536 $panel_classes[] = 'is-active';
13537 }
13538 ?>
13539 <div
13540 id="<?php echo esc_attr($panel_id); ?>"
13541 class="<?php echo esc_attr(implode(' ', $panel_classes)); ?>"
13542 data-tab-panel="<?php echo esc_attr($entry['slug']); ?>"
13543 <?php echo $idx === 0 ? '' : 'hidden'; ?>>
13544 <div class="ebook_store_file_format_item<?php echo $entry['is_pro'] ? ' goPro2' : ''; ?>">
13545 <div class="ebook_store_file_format_item_left">
13546 <span class="ebook_store_icon">
13547 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?> format" width="64" />
13548 </span>
13549 <span class="ebook_store_size <?php echo esc_attr($entry['exists_class']); ?>"><?php echo esc_html($entry['size_label']); ?></span>
13550 </div>
13551 <div class="ebook_store_file_format_item_right">
13552 <div class="ebook_store_filename">
13553 <?php if ($entry['has_file']) : ?>
13554 <a href="#" title="<?php _e('Current file', 'ebook-store'); ?>"><?php echo esc_html($entry['filename']); ?></a>
13555 <?php else : ?>
13556 <span><?php _e('No file uploaded yet', 'ebook-store'); ?></span>
13557 <?php endif; ?>
13558 </div>
13559 <div class="ebook_store_upload">
13560 <input type="file"
13561 accept=".<?php echo esc_attr($entry['slug']); ?>"
13562 name="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
13563 id="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
13564 data-format="<?php echo esc_attr($entry['slug']); ?>"
13565 title="<?php echo esc_attr( sprintf(__('Choose %s file', 'ebook-store'), $entry['label']) ); ?>"
13566 />
13567 </div>
13568 <div class="ebook_store_control <?php echo esc_attr($entry['exists_class']); ?>">
13569 <label title="<?php _e('Delete file', 'ebook-store'); ?>">
13570 <input type="checkbox" name="ebook_store_delete_<?php echo esc_attr($entry['slug']); ?>" />
13571 <i class="dashicons dashicons-trash"></i>
13572 <span class="screen-reader-text"><?php _e('Delete file', 'ebook-store'); ?></span>
13573 </label>
13574 </div>
13575 </div>
13576 </div>
13577 </div>
13578 <?php endforeach; ?>
13579 </div>
13580 </div>
13581 <?php
13582 // Files are sliced in the browser and streamed past the server's upload
13583 // limits, so quoting upload_max_filesize here only scared authors off
13584 // books that upload perfectly well.
13585 if ( function_exists( 'ebook_store_max_upload_bytes' ) ) :
13586 ?>
13587 <p class="description">
13588 <?php
13589 printf(
13590 /* translators: %s: maximum file size, already formatted. */
13591 esc_html__( 'Files are uploaded in small pieces, so the server upload limit does not apply. Maximum size per file: %s.', 'ebook-store' ),
13592 esc_html( size_format( ebook_store_max_upload_bytes() ) )
13593 );
13594 ?>
13595 </p>
13596 <?php else : ?>
13597 <p class="description"><?php _e('Maximum upload file size:', 'ebook-store'); ?> <?php echo ini_get('upload_max_filesize'); ?>B</p>
13598 <?php endif; ?>
13599 </div>
13600 <?php
13601 }
13602
13603 /**
13604 * Whether an ebook is delivered for free.
13605 *
13606 * A book is free when its delivery mode says so, or when it has no price and
13607 * its mode was never set (books from before the mode existed). A book that
13608 * was explicitly saved as paid or donate is not free, whatever its price field
13609 * holds — a paid book with a blank price is a mistake, not a giveaway.
13610 *
13611 * @param mixed $ebook The 'ebook' post meta array.
13612 * @return bool
13613 */
13614 function ebook_store_is_free_ebook( $ebook ) {
13615 if ( ! is_array( $ebook ) ) {
13616 return false;
13617 }
13618
13619 $mode = isset( $ebook['donate_or_download'] ) ? (string) $ebook['donate_or_download'] : '';
13620 $price = ( isset( $ebook['ebook_price'] ) && is_numeric( $ebook['ebook_price'] ) ) ? (float) $ebook['ebook_price'] : 0.0;
13621
13622 if ( $mode === 'free' ) {
13623 return true;
13624 }
13625
13626 return $mode !== 'paid' && $mode !== 'donate' && $price == 0;
13627 }
13628
13629 function ebook_store_price_plus_vat($price) {
13630 $vat = ebook_store_get_vat_percent();
13631 $price = (float) $price;
13632
13633 if ( $vat <= 0 ) {
13634 return $price;
13635 }
13636
13637 return $price + ( $price * ( $vat / 100 ) );
13638 }
13639
13640 /**
13641 * Signature binding an ebook to the exact amount AND currency it is sold for.
13642 *
13643 * The buy button's original signature covered only the ebook id and its net
13644 * price, so a buyer could edit the posted form to pay in a cheaper currency, or
13645 * drop the tax line, and PayPal would still verify the payment. This is carried
13646 * as a third segment of the `custom` field and re-checked against the values
13647 * PayPal itself reports.
13648 *
13649 * @param int $ebook_id Ebook post ID.
13650 * @param mixed $gross Amount the buyer is expected to pay, tax included.
13651 * @param string $currency ISO currency the store sells in.
13652 * @return string
13653 */
13654 function ebook_store_paypal_amount_signature( $ebook_id, $gross, $currency ) {
13655 $ebook_id = absint( $ebook_id );
13656 $gross = number_format( (float) $gross, 2, '.', '' );
13657 $currency = strtoupper( trim( (string) $currency ) );
13658
13659 return md5( NONCE_KEY . '|es-v2|' . $ebook_id . '|' . $gross . '|' . $currency );
13660 }
13661
13662 function ebook_store_get_store_currency() {
13663 $currency = strtoupper( trim( (string) get_option( 'paypal_currency', 'USD' ) ) );
13664
13665 return preg_match( '/^[A-Z]{3}$/', $currency ) ? $currency : 'USD';
13666 }
13667
13668 function ebook_store_get_vat_percent() {
13669 $vat = trim( (string) get_option( 'vat_percent', '0' ) );
13670
13671 if ( $vat === '' || ! is_numeric( $vat ) ) {
13672 return 0.0;
13673 }
13674
13675 return max( 0, (float) $vat );
13676 }
13677
13678 function ebook_store_get_download_limit() {
13679 $limit = trim( (string) get_option( 'downloads_limit', '5' ) );
13680
13681 if ( $limit === '' || ! is_numeric( $limit ) ) {
13682 return 5;
13683 }
13684
13685 return max( 1, (int) $limit );
13686 }
13687
13688
13689
13690 function ebook_store_row( $atts, $content = "") {
13691 return "<div class=\"ebook_store_row ebook_store_row_".intval($atts['col'])."\">" . do_shortcode($content) . "</div>";
13692 }
13693
13694 function ebook_store_silent_registration($data) {
13695 if (get_option('ebook_store_silent_registration') != 1) {
13696 return get_current_user_id();
13697 }
13698
13699 $user_email = isset( $data['payer_email'] ) ? sanitize_email( $data['payer_email'] ) : '';
13700 if ( $user_email === '' || ! is_email( $user_email ) ) {
13701 return get_current_user_id();
13702 }
13703
13704 // The email address is the buyer's identity. If an account already uses it,
13705 // reuse that account. The username must NOT be the key: the old code keyed on
13706 // "first_last", so a second buyer who happened to share a name with an
13707 // existing customer was silently attached to that customer's account.
13708 $existing = email_exists( $user_email );
13709 if ( $existing ) {
13710 return $existing;
13711 }
13712
13713 // Build a username that does not collide with any existing account.
13714 $base = sanitize_user( trim( (string) $data['first_name'] . '_' . (string) $data['last_name'] ), true );
13715 if ( $base === '' || $base === '_' ) {
13716 $parts = explode( '@', $user_email );
13717 $base = sanitize_user( $parts[0], true );
13718 }
13719 if ( $base === '' ) {
13720 $base = 'customer';
13721 }
13722
13723 $user_name = $base;
13724 $suffix = 1;
13725 while ( username_exists( $user_name ) ) {
13726 $suffix++;
13727 $user_name = $base . '_' . $suffix;
13728 }
13729
13730 $user_id = wp_create_user( $user_name, wp_generate_password( 16, true ), $user_email );
13731
13732 // A WP_Error must never be stored as the order's user_id.
13733 if ( is_wp_error( $user_id ) ) {
13734 return get_current_user_id();
13735 }
13736
13737 wp_new_user_notification( $user_id, null, 'both' );
13738
13739 return $user_id;
13740 }
13741
13742
13743
13744 function ebook_store_downloads($atts) {
13745 // Webhook-created orders are stored with user_id 0; never match an
13746 // anonymous visitor (also id 0) against them.
13747 if ( ! is_user_logged_in() || get_current_user_id() < 1 ) {
13748 return '<p>' . esc_html__( 'Please log in to see your orders.', 'ebook-store' ) . '</p>';
13749 }
13750 $args = array(
13751 'post_type' => 'ebook_order',
13752 'posts_per_page' => -1,
13753 'orderby' => 'date',
13754 'order' => 'DESC',
13755 'meta_query' => array(
13756 array(
13757 'key' => 'user_id',
13758 'value' => get_current_user_id(),
13759 'compare' => '=',
13760 )
13761 ));
13762 // The Query
13763 $the_query = new WP_Query( $args );
13764 $out = '';
13765 // The Loop
13766 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
13767 if ( $the_query->have_posts() ) {
13768 while ( $the_query->have_posts() ) {
13769 $the_query->the_post();
13770 $link = add_query_arg(array('ebook_key' => get_post_meta(get_the_ID(),'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
13771 $out .= '<h4><a href="' . esc_url( $link ) . '">' . esc_html__('Order','ebook-store') . ' #' . get_the_ID() . '</a></h4>';
13772 }
13773 } else {
13774 $out = '<p>' . esc_html__('No orders found.','ebook-store') . '</p>';
13775 }
13776 wp_reset_postdata();
13777 return $out;
13778 }
13779 function ebook_pdf_reader($ebook_order = false, $ebook_post_id = false, $source_url = '') {
13780 $src = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html';
13781 // /return $src;
13782 $src = add_query_arg('title',__('Online Viewer','ebook-store'),$src);
13783 // An explicit source wins: the WooCommerce order screen passes the buyer's
13784 // own authenticated download URL, because the raw uploads path the reader
13785 // used to be pointed at is denied by the plugin's own .htaccess (the reader
13786 // showed a 403 instead of the book) and would be public where it is not.
13787 if ( $source_url !== '' ) {
13788 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>';
13789 }
13790 if ($ebook_order) {
13791 return '<iframe style="float:left;" src="'.$src.'#' . $ebook_order['downloadlink'][0] . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
13792 } else {
13793 $meta = get_post_meta($ebook_post_id, 'ebook_wp_custom_attachment', true);
13794
13795 if (is_array($meta)) {
13796 $pdf_url = $meta['url'];
13797 } else {
13798 $pdf_url = '';
13799 }
13800
13801 //wp_die('URL ' . $pdf_url);
13802 if (is_ssl()) {
13803 $pdf_url = str_replace('http://', 'https://', $pdf_url);
13804 }
13805 return '<iframe style="float:left;" src="'.$src.'#' . $pdf_url . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
13806 }
13807
13808 }
13809 function pr_die($array) {
13810 wp_die('<pre>'.print_r($array,true).'</pre>');
13811 }
13812 function ebook_store_redirect_add_order() {
13813 if (endswith(esc_url_raw($_SERVER['REQUEST_URI']), 'post-new.php?post_type=ebook_order')) {
13814 header("Location: edit.php?post_type=ebook&page=ebook-store-add-order-page");
13815 }
13816 }
13817
13818 if (!function_exists('endswith')) {
13819 function endswith($string, $test) {
13820 $strlen = strlen($string);
13821 $testlen = strlen($test);
13822 if ($testlen > $strlen) return false;
13823 return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
13824 }
13825 }
13826 add_action( 'woocommerce_after_account_downloads', 'ebook_store_woocommerce_after_account_downloads', 10, 1 );
13827 function ebook_store_woocommerce_after_account_downloads() {
13828
13829 echo '<h3>Ebook Downloads</h3>';
13830 //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>';
13831 echo '<table class="woocommerce-MyAccount-downloads shop_table shop_table_responsive">
13832 <thead>
13833 <tr>
13834 <th class="download-product"><span class="nobr">'.__('Product','ebook-store').'</span></th>
13835 <th class="download-remaining"><span class="nobr">'.__('Download Links','ebook-store').'</span></th>
13836 <!-- <th class="download-expires"><span class="nobr">'.__('Expires','ebook-store').'</span></th>
13837 <th class="download-file"><span class="nobr">'.__('File','ebook-store').'</span></th>-->
13838 </tr>
13839 </thead>
13840 <tbody>';
13841 $customer_user_id = get_current_user_id();
13842
13843 $customer_orders = $customer_user_id > 0 ? wc_get_orders( array(
13844 'customer_id' => $customer_user_id,
13845 'limit' => -1,
13846 ) ) : array();
13847 foreach ( $customer_orders as $order ) {
13848 try {
13849 ebook_store_woocommerce_order_details($order, true);
13850 } catch ( \Throwable $e ) {
13851 continue;
13852 }
13853 }
13854 echo '
13855 </tbody></table>';
13856 }
13857
13858 function ebook_store_wp_super_cache_warning() {
13859 if (!file_exists(WP_PLUGIN_DIR . '/wp-super-cache/wp-cache.php')) {
13860 return true;
13861 }
13862
13863 if ( isset( $_GET['dismiss'] ) && $_GET['dismiss'] === 'wpsc' ) {
13864 update_option( 'ebookstorewpsc', 1 );
13865 return true;
13866 }
13867 if (get_option( 'ebookstorewpsc') < 1) {
13868 ?>
13869 <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
13870 ebook
13871 </textarea></p><a id="wpsc-dismiss" href="<?php echo esc_url($_SERVER['REQUEST_URI']); ?>&dismiss=wpsc">Dismiss</a></div>
13872 <?php
13873
13874 }
13875 }
13876
13877 function ebook_store_woocommerce_variation_fields( $loop, $variation_data, $variation ) {
13878 if ( get_option( 'ebook_store_woocommerce_integration' ) == 0 ) {
13879 return;
13880 }
13881
13882 $variation_id = isset( $variation->ID ) ? (int) $variation->ID : 0;
13883 $selected_ebook_id = $variation_id > 0 ? (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) : 0;
13884 $options = ebook_store_get_woocommerce_options();
13885 ?>
13886 <div class="form-row form-row-full">
13887 <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>
13888 <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">
13889 <?php foreach ( $options as $ebook_id => $ebook_title ) : ?>
13890 <option value="<?php echo esc_attr( $ebook_id ); ?>" <?php selected( $selected_ebook_id, (int) $ebook_id ); ?>>
13891 <?php echo esc_html( $ebook_title ); ?>
13892 </option>
13893 <?php endforeach; ?>
13894 </select>
13895 <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>
13896 </div>
13897 <?php
13898 }
13899
13900 function ebook_store_save_woocommerce_variation_data( $variation_id, $loop ) {
13901 $variation_id = (int) $variation_id;
13902 $old_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
13903 if ( $old_ebook_id > 0 ) {
13904 update_post_meta( $old_ebook_id, 'woocommerce_product_id', 0 );
13905 }
13906
13907 $new_ebook_id = 0;
13908 if ( isset( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) ) {
13909 $new_ebook_id = absint( wp_unslash( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) );
13910 }
13911
13912 if ( $new_ebook_id > 0 ) {
13913 update_post_meta( $variation_id, 'ebook_store_book_id', $new_ebook_id );
13914 update_post_meta( $variation_id, '_enable_ebook_store_option', 'yes' );
13915 update_post_meta( $new_ebook_id, 'woocommerce_product_id', $variation_id );
13916 } else {
13917 delete_post_meta( $variation_id, 'ebook_store_book_id' );
13918 update_post_meta( $variation_id, '_enable_ebook_store_option', 'no' );
13919 }
13920 }
13921
13922 function ebook_store_get_wc_item_product_ids( $item ) {
13923 $product_id = 0;
13924 $variation_id = 0;
13925
13926 if ( is_object( $item ) ) {
13927 if ( method_exists( $item, 'get_product_id' ) ) {
13928 $product_id = (int) $item->get_product_id();
13929 }
13930 if ( method_exists( $item, 'get_variation_id' ) ) {
13931 $variation_id = (int) $item->get_variation_id();
13932 }
13933 } elseif ( is_array( $item ) ) {
13934 $product_id = isset( $item['product_id'] ) ? (int) $item['product_id'] : 0;
13935 $variation_id = isset( $item['variation_id'] ) ? (int) $item['variation_id'] : 0;
13936 }
13937
13938 return array(
13939 'product_id' => $product_id,
13940 'variation_id' => $variation_id,
13941 );
13942 }
13943
13944 function ebook_store_product_has_variation_ebook_mappings( $product_id ) {
13945 $product_id = (int) $product_id;
13946 if ( $product_id < 1 ) {
13947 return false;
13948 }
13949
13950 $variation_ids = get_posts(
13951 array(
13952 'post_parent' => $product_id,
13953 'post_type' => 'product_variation',
13954 'post_status' => array( 'publish', 'private' ),
13955 'posts_per_page' => -1,
13956 'fields' => 'ids',
13957 'no_found_rows' => true,
13958 )
13959 );
13960
13961 if ( ! is_array( $variation_ids ) || empty( $variation_ids ) ) {
13962 return false;
13963 }
13964
13965 foreach ( $variation_ids as $variation_id ) {
13966 if ( (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) > 0 ) {
13967 return true;
13968 }
13969 }
13970
13971 return false;
13972 }
13973
13974 function ebook_store_get_wc_product_ebook_mapping( $product_id, $variation_id = 0 ) {
13975 $product_id = (int) $product_id;
13976 $variation_id = (int) $variation_id;
13977
13978 if ( $variation_id > 0 ) {
13979 $variation_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
13980 if ( $variation_ebook_id > 0 ) {
13981 return array(
13982 'ebook_id' => $variation_ebook_id,
13983 'product_id' => $product_id,
13984 'variation_id' => $variation_id,
13985 'mapped_post_id' => $variation_id,
13986 );
13987 }
13988
13989 if ( ebook_store_product_has_variation_ebook_mappings( $product_id ) ) {
13990 return array(
13991 'ebook_id' => 0,
13992 'product_id' => $product_id,
13993 'variation_id' => $variation_id,
13994 'mapped_post_id' => $variation_id,
13995 );
13996 }
13997
13998 return array(
13999 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
14000 'product_id' => $product_id,
14001 'variation_id' => $variation_id,
14002 'mapped_post_id' => $product_id,
14003 );
14004 }
14005
14006 return array(
14007 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
14008 'product_id' => $product_id,
14009 'variation_id' => 0,
14010 'mapped_post_id' => $product_id,
14011 );
14012 }
14013
14014 function ebook_store_get_wc_item_ebook_mapping( $item ) {
14015 $ids = ebook_store_get_wc_item_product_ids( $item );
14016 return ebook_store_get_wc_product_ebook_mapping( $ids['product_id'], $ids['variation_id'] );
14017 }
14018
14019 function ebook_store_get_wc_product_reference_for_ebook( $ebook_id ) {
14020 global $wpdb;
14021
14022 $ebook_id = (int) $ebook_id;
14023 if ( $ebook_id < 1 || ! function_exists( 'wc_get_product' ) ) {
14024 return 0;
14025 }
14026
14027 $stored_reference_id = (int) get_post_meta( $ebook_id, 'woocommerce_product_id', true );
14028 $allowed_statuses = array( 'publish', 'private' );
14029 if ( $stored_reference_id > 0 && in_array( get_post_status( $stored_reference_id ), $allowed_statuses, true ) ) {
14030 $stored_mapping = (int) get_post_meta( $stored_reference_id, 'ebook_store_book_id', true );
14031 if ( $stored_mapping === $ebook_id ) {
14032 return $stored_reference_id;
14033 }
14034 }
14035
14036 $product_reference_id = 0;
14037 $status_placeholders = implode( ',', array_fill( 0, count( $allowed_statuses ), '%s' ) );
14038 $query_params = array_merge(
14039 array( 'ebook_store_book_id', (string) $ebook_id, 'product', 'product_variation' ),
14040 $allowed_statuses
14041 );
14042
14043 $product_reference_id = (int) $wpdb->get_var(
14044 $wpdb->prepare(
14045 "
14046 SELECT pm.post_id
14047 FROM {$wpdb->postmeta} pm
14048 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
14049 WHERE pm.meta_key = %s
14050 AND pm.meta_value = %s
14051 AND p.post_type IN (%s, %s)
14052 AND p.post_status IN ($status_placeholders)
14053 ORDER BY pm.post_id DESC
14054 LIMIT 1
14055 ",
14056 $query_params
14057 )
14058 );
14059
14060 if ( $product_reference_id > 0 && $product_reference_id !== $stored_reference_id && ! wp_doing_ajax() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
14061 update_post_meta( $ebook_id, 'woocommerce_product_id', $product_reference_id );
14062 }
14063
14064 return $product_reference_id;
14065 }
14066
14067 function ebook_store_get_wc_add_to_cart_config( $product_reference_id ) {
14068 $product_reference_id = (int) $product_reference_id;
14069 if ( $product_reference_id < 1 || ! function_exists( 'wc_get_product' ) ) {
14070 return false;
14071 }
14072
14073 $product = wc_get_product( $product_reference_id );
14074 if ( ! $product ) {
14075 return false;
14076 }
14077
14078 if ( $product->is_type( 'variation' ) ) {
14079 return array(
14080 'product_id' => (int) $product->get_parent_id(),
14081 'variation_id' => (int) $product->get_id(),
14082 'variation' => $product->get_variation_attributes(),
14083 'title' => $product->get_name(),
14084 );
14085 }
14086
14087 return array(
14088 'product_id' => (int) $product->get_id(),
14089 'variation_id' => 0,
14090 'variation' => array(),
14091 'title' => $product->get_name(),
14092 );
14093 }
14094
14095 function ebook_store_cart_contains_wc_product( $cart_config ) {
14096 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
14097 return false;
14098 }
14099
14100 foreach ( WC()->cart->get_cart() as $cart_item ) {
14101 $cart_product_id = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
14102 $cart_variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0;
14103 if ( $cart_product_id === (int) $cart_config['product_id'] && $cart_variation_id === (int) $cart_config['variation_id'] ) {
14104 return true;
14105 }
14106 }
14107
14108 return false;
14109 }
14110
14111 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
14112 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
14113 function ebook_store_test_encryption() {
14114 if (!isset($_GET['ebook_id'])) {
14115 wp_die(esc_html__('Missing ebook ID.', 'ebook-store'));
14116 }
14117 error_log('Ebook Store Test Encryption: request=' . print_r($_REQUEST, true));
14118 $ebook_id = absint($_GET['ebook_id']);
14119 if (!$ebook_id || get_post_type($ebook_id) !== 'ebook') {
14120 wp_die(esc_html__('Invalid ebook.', 'ebook-store'));
14121 }
14122 if (!current_user_can('edit_post', $ebook_id)) {
14123 wp_die(esc_html__('You are not allowed to perform this test.', 'ebook-store'));
14124 }
14125 check_admin_referer('ebook_store_test_encryption_' . $ebook_id);
14126
14127 $test_format = isset($_GET['format']) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : 'pdf';
14128 if ( $test_format === 'epub' ) {
14129 ebook_store_test_epub_watermark( $ebook_id );
14130 exit;
14131 }
14132
14133 $pdf_meta = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
14134 if (!is_array($pdf_meta) || empty($pdf_meta['file']) || !file_exists($pdf_meta['file'])) {
14135 error_log('Ebook Store Test Encryption: PDF missing for ebook ' . $ebook_id);
14136 wp_die(esc_html__('No PDF found for this ebook.', 'ebook-store'));
14137 }
14138 if (!ebook_store_encryptable($pdf_meta['file'])) {
14139 error_log('Ebook Store Test Encryption: PDF not encryptable');
14140 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'));
14141 }
14142
14143 global $attachment, $ebook_email_delivery;
14144 $original_attachment = isset($attachment) ? $attachment : null;
14145 $original_email_delivery = isset($ebook_email_delivery) ? $ebook_email_delivery : null;
14146
14147 $attachment = array(
14148 array('file' => $pdf_meta['file'])
14149 );
14150 $ebook_email_delivery = array(
14151 'order' => array(
14152 'order_id' => 0,
14153 'ebook' => $ebook_id,
14154 'payer_email' => 'test@example.com',
14155 )
14156 );
14157
14158 $data = array(
14159 'txn_id' => 'encryption-test',
14160 'payment_date' => current_time('mysql'),
14161 'payer_email' => 'test@example.com',
14162 'password' => 'test',
14163 'md5_nonce' => 'encryption_test_' . $ebook_id,
14164 'ebook_key' => ebook_store_test_verification_key( $ebook_id ),
14165 );
14166
14167 try {
14168 $encrypted_file = ebook_encrypt_pdf($data, null);
14169 } catch (Throwable $e) {
14170 error_log('Ebook Store Test Encryption: exception ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
14171 $attachment = $original_attachment;
14172 $ebook_email_delivery = $original_email_delivery;
14173 wp_die(sprintf(
14174 '%s %s',
14175 esc_html__('Encryption test failed with an exception.', 'ebook-store'),
14176 esc_html($e->getMessage())
14177 ));
14178 }
14179 if (!$encrypted_file || !file_exists($encrypted_file)) {
14180 error_log('Ebook Store Test Encryption: encryption failed, file missing.');
14181 $attachment = $original_attachment;
14182 $ebook_email_delivery = $original_email_delivery;
14183 wp_die(esc_html__('Encryption test failed. Please check your PHP error log for details.', 'ebook-store'));
14184 }
14185
14186 error_log('Ebook Store Test Encryption: success, streaming file ' . $encrypted_file);
14187 header('Content-Type: application/pdf');
14188 header('Content-Disposition: inline; filename="' . basename($pdf_meta['file']) . '"');
14189 header('Content-Length: ' . filesize($encrypted_file));
14190 ebook_readfile_chunked($encrypted_file);
14191 @unlink($encrypted_file);
14192
14193 $attachment = $original_attachment;
14194 $ebook_email_delivery = $original_email_delivery;
14195 exit;
14196 }
14197
14198 /**
14199 * Stream a watermarked copy of an ebook's ePub so the owner can preview exactly
14200 * what a buyer receives. Uses the configured watermark settings with sample buyer
14201 * values, and never touches the stored upload.
14202 *
14203 * @param int $ebook_id Ebook post ID (already permission- and nonce-checked).
14204 */
14205 function ebook_store_test_epub_watermark( $ebook_id ) {
14206 $epub_meta = get_post_meta( $ebook_id, 'ebook_wp_custom_attachment_epub', true );
14207 if ( ! is_array( $epub_meta ) || empty( $epub_meta['file'] ) || ! file_exists( $epub_meta['file'] ) ) {
14208 wp_die( esc_html__( 'No ePub found for this ebook.', 'ebook-store' ) );
14209 }
14210 if ( ! function_exists( 'ebook_store_watermark_epub' ) ) {
14211 wp_die( esc_html__( 'The ePub watermarking engine is not available on this server.', 'ebook-store' ) );
14212 }
14213
14214 $src = $epub_meta['file'];
14215
14216 // Build a representative watermark from the configured template, standing in
14217 // sample buyer values for a real order.
14218 $template = (string) get_option( 'ebook_store_epub_watermark_text', 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%. Please do not share.' );
14219 if ( trim( $template ) === '' ) {
14220 $template = 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%';
14221 }
14222 $sample = str_replace(
14223 array( '%%first_name%%', '%%last_name%%', '%%payer_email%%', '%%residence_country%%', '%%payment_date%%', '%%txn_id%%' ),
14224 array( 'Test', 'Buyer', 'test@example.com', 'US', date_i18n( 'Y-m-d' ), 'watermark-test' ),
14225 $template
14226 );
14227
14228 $dir = untrailingslashit( get_temp_dir() );
14229 $dest = $dir . '/ebook-store-epub-test-' . $ebook_id . '-' . wp_generate_password( 8, false ) . '.epub';
14230
14231 $qr_text = ( get_option( 'ebook_store_epub_qr' ) && function_exists( 'ebook_store_get_order_verification_url' ) )
14232 ? ebook_store_get_order_verification_url( ebook_store_test_verification_key( $ebook_id ) )
14233 : '';
14234
14235 $out = ebook_store_watermark_epub(
14236 $src,
14237 $dest,
14238 $sample,
14239 array(
14240 'scope' => get_option( 'ebook_store_epub_watermark_scope', 'first' ),
14241 'position' => get_option( 'ebook_store_epub_watermark_position', 'bottom' ),
14242 'style' => get_option( 'ebook_store_epub_watermark_style', 'subtle' ),
14243 'stamp_metadata' => get_option( 'ebook_store_epub_stamp_metadata' ),
14244 'qr' => get_option( 'ebook_store_epub_qr' ),
14245 'qr_scope' => get_option( 'ebook_store_epub_qr_scope', 'first' ),
14246 'qr_position' => get_option( 'ebook_store_epub_qr_position', 'bottom' ),
14247 'qr_text' => $qr_text,
14248 'order_id' => 0,
14249 )
14250 );
14251
14252 if ( $out !== $dest || ! file_exists( $dest ) ) {
14253 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' ) );
14254 }
14255
14256 nocache_headers();
14257 header( 'Content-Type: application/epub+zip' );
14258 header( 'Content-Disposition: attachment; filename="watermark-test-' . basename( $src ) . '"' );
14259 header( 'Content-Length: ' . filesize( $dest ) );
14260 ebook_readfile_chunked( $dest );
14261 @unlink( $dest );
14262 exit;
14263 }
14264
14265 function ebook_store_deregister_embeds(){
14266 wp_dequeue_script( 'wp-embed' );
14267 }
14268
14269 /**
14270 * Log a adyen webhook event.
14271 *
14272 * Writes through ebook_store_log(), which keeps the file outside the web root,
14273 * caps its size and stays silent unless logging is switched on. The old version
14274 * appended to an uncapped .txt inside the plugin folder that any visitor could
14275 * fetch; one of those files reached 118 MB and contained live order tokens.
14276 *
14277 * @param string $message Message.
14278 * @param array $context Context.
14279 */
14280 function ebook_store_log_adyen_event( $message, $context = array() ) {
14281 ebook_store_log( 'adyen', $message, $context );
14282 }
14283
14284 /**
14285 * Number of minor units in one unit of a currency: 1 for JPY-style zero-decimal
14286 * currencies, 1000 for the three-decimal ones, otherwise 100. Adyen and Stripe
14287 * both express amounts in minor units.
14288 *
14289 * @param string $currency ISO 4217 code.
14290 * @return int
14291 */
14292 function ebook_store_currency_minor_unit_factor( $currency ) {
14293 $currency = strtoupper( trim( (string) $currency ) );
14294 $zero_decimal = array( 'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF' );
14295 $three_decimal = array( 'BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND' );
14296
14297 $factor = 100;
14298 if ( in_array( $currency, $zero_decimal, true ) ) {
14299 $factor = 1;
14300 } elseif ( in_array( $currency, $three_decimal, true ) ) {
14301 $factor = 1000;
14302 }
14303
14304 $factor = (int) apply_filters( 'ebook_store_currency_minor_unit_factor', $factor, $currency );
14305
14306 return $factor > 0 ? $factor : 100;
14307 }
14308
14309 function ebook_store_get_adyen_api_base_url() {
14310 $environment = (string) get_option( 'adyen_environment', 'test' );
14311 $version = 'v71';
14312
14313 if ( $environment === 'live' ) {
14314 $prefix = trim( (string) get_option( 'adyen_live_prefix', '' ) );
14315 // Older versions ran the prefix through esc_url_raw() ("http://<prefix>"), and
14316 // owners sometimes paste the whole live endpoint. Keep only the prefix itself.
14317 $prefix = preg_replace( '#^https?://#i', '', $prefix );
14318 $prefix = preg_replace( '/-checkout-live\.adyenpayments\.com.*$/i', '', $prefix );
14319 $prefix = trim( $prefix, "/ \t\n\r" );
14320 if ( $prefix === '' ) {
14321 return new WP_Error( 'adyen_missing_live_prefix', 'Adyen live URL prefix not configured' );
14322 }
14323
14324 return 'https://' . rawurlencode( $prefix ) . '-checkout-live.adyenpayments.com/checkout/' . $version;
14325 }
14326
14327 return 'https://checkout-test.adyen.com/' . $version;
14328 }
14329
14330 function ebook_store_get_adyen_webhook_url() {
14331 return site_url( 'wp-json/ebook-store/v1/adyen-webhook' );
14332 }
14333
14334 function ebook_store_adyen_request( $method, $path, $body = null, $headers = array() ) {
14335 $api_key = trim( (string) get_option( 'adyen_api_key', '' ) );
14336 if ( $api_key === '' ) {
14337 return new WP_Error( 'adyen_missing_api_key', 'Adyen API key not configured' );
14338 }
14339
14340 $base_url = ebook_store_get_adyen_api_base_url();
14341 if ( is_wp_error( $base_url ) ) {
14342 return $base_url;
14343 }
14344
14345 $request_headers = array_merge(
14346 array(
14347 'Content-Type' => 'application/json',
14348 'X-API-Key' => $api_key,
14349 ),
14350 (array) $headers
14351 );
14352
14353 $args = array(
14354 'method' => strtoupper( (string) $method ),
14355 'timeout' => 20,
14356 'headers' => $request_headers,
14357 'user-agent' => 'Ebook-Store/' . ( defined( 'EBOOK_STORE_VERSION' ) ? EBOOK_STORE_VERSION : '1.0' ),
14358 );
14359
14360 if ( null !== $body ) {
14361 $args['body'] = wp_json_encode( $body );
14362 }
14363
14364 $response = wp_remote_request( rtrim( $base_url, '/' ) . '/' . ltrim( (string) $path, '/' ), $args );
14365 if ( is_wp_error( $response ) ) {
14366 return $response;
14367 }
14368
14369 $status_code = (int) wp_remote_retrieve_response_code( $response );
14370 $response_body = wp_remote_retrieve_body( $response );
14371 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
14372
14373 if ( $status_code < 200 || $status_code >= 300 ) {
14374 $message = is_array( $data ) && ! empty( $data['message'] ) ? (string) $data['message'] : 'Adyen request failed.';
14375 return new WP_Error(
14376 'adyen_request_failed',
14377 $message,
14378 array(
14379 'status_code' => $status_code,
14380 'body' => $data,
14381 )
14382 );
14383 }
14384
14385 return is_array( $data ) ? $data : array();
14386 }
14387
14388 function ebook_store_get_adyen_shopper_locale() {
14389 $locale = get_locale();
14390 if ( function_exists( 'ebook_store_get_selected_language_code' ) ) {
14391 $language_code = (string) ebook_store_get_selected_language_code();
14392 if ( $language_code !== '' && $language_code !== '0' ) {
14393 $locale = $language_code . '_' . strtoupper( $language_code );
14394 }
14395 }
14396
14397 $locale = str_replace( '-', '_', (string) $locale );
14398 if ( preg_match( '/^[a-z]{2}_[A-Z]{2}$/', $locale ) ) {
14399 return str_replace( '_', '-', $locale );
14400 }
14401
14402 $language = strtolower( substr( $locale, 0, 2 ) );
14403 if ( preg_match( '/^[a-z]{2}$/', $language ) ) {
14404 return $language . '-' . strtoupper( $language );
14405 }
14406
14407 return 'en-US';
14408 }
14409
14410 function ebook_store_get_adyen_country_code() {
14411 $locale = str_replace( '-', '_', (string) get_locale() );
14412 if ( preg_match( '/^[a-z]{2}_([A-Z]{2})$/', $locale, $matches ) ) {
14413 return strtoupper( $matches[1] );
14414 }
14415
14416 return '';
14417 }
14418
14419 function ebook_store_store_adyen_checkout_state( $state ) {
14420 if ( ! is_array( $state ) ) {
14421 return;
14422 }
14423
14424 $state['created_at'] = isset( $state['created_at'] ) ? (int) $state['created_at'] : time();
14425 // An Adyen payment link stays payable for up to 30 days, and this state is
14426 // what maps the payment back to the ebook. At 7 days a link paid in week two
14427 // arrived with nothing to match it to. 30 days is also the longest expiry
14428 // a memcached object cache accepts; anything longer is read as a date in
14429 // 1970 and the entry is dropped on write.
14430 $ttl = MONTH_IN_SECONDS;
14431
14432 if ( ! empty( $state['payment_link_id'] ) ) {
14433 set_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ), $state, $ttl );
14434 }
14435
14436 if ( ! empty( $state['ebook_key'] ) ) {
14437 set_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ), $state, $ttl );
14438 }
14439 }
14440
14441 function ebook_store_get_adyen_checkout_state( $context = array() ) {
14442 $context = is_array( $context ) ? $context : array();
14443 $keys = array();
14444
14445 if ( ! empty( $context['payment_link_id'] ) ) {
14446 $keys[] = 'ebook_store_adyen_link_' . sanitize_key( (string) $context['payment_link_id'] );
14447 }
14448
14449 if ( ! empty( $context['ebook_key'] ) ) {
14450 $keys[] = 'ebook_store_adyen_ebook_' . sanitize_key( (string) $context['ebook_key'] );
14451 }
14452
14453 foreach ( $keys as $key ) {
14454 $state = get_transient( $key );
14455 if ( is_array( $state ) ) {
14456 return $state;
14457 }
14458 }
14459
14460 return false;
14461 }
14462
14463 function ebook_store_clear_adyen_checkout_state( $state ) {
14464 if ( ! is_array( $state ) ) {
14465 return;
14466 }
14467
14468 if ( ! empty( $state['payment_link_id'] ) ) {
14469 delete_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ) );
14470 }
14471
14472 if ( ! empty( $state['ebook_key'] ) ) {
14473 delete_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ) );
14474 }
14475 }
14476
14477 function ebook_store_fetch_adyen_payment_link( $payment_link_id ) {
14478 $payment_link_id = trim( (string) $payment_link_id );
14479 if ( $payment_link_id === '' ) {
14480 return new WP_Error( 'adyen_missing_payment_link_id', 'Adyen payment link ID is missing' );
14481 }
14482
14483 return ebook_store_adyen_request( 'GET', 'paymentLinks/' . rawurlencode( $payment_link_id ) );
14484 }
14485
14486 function ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) {
14487 return 'ebook_store_adyen_lock_' . sanitize_key( (string) $psp_reference );
14488 }
14489
14490 function ebook_store_acquire_adyen_order_creation_lock( $psp_reference ) {
14491 $psp_reference = (string) $psp_reference;
14492 if ( $psp_reference === '' ) {
14493 return false;
14494 }
14495
14496 $lock_key = ebook_store_get_adyen_order_creation_lock_key( $psp_reference );
14497
14498 if ( add_option( $lock_key, time(), '', 'no' ) ) {
14499 return true;
14500 }
14501
14502 // The lock has no expiry of its own, so a request that died between taking it
14503 // and the finally that releases it used to wedge this payment forever: every
14504 // retry then answered "creation already in progress" and no order was made.
14505 $held_since = (int) get_option( $lock_key, 0 );
14506 if ( $held_since > 0 && ( time() - $held_since ) > 60 ) {
14507 delete_option( $lock_key );
14508
14509 return add_option( $lock_key, time(), '', 'no' );
14510 }
14511
14512 return false;
14513 }
14514
14515 function ebook_store_release_adyen_order_creation_lock( $psp_reference ) {
14516 $psp_reference = (string) $psp_reference;
14517 if ( $psp_reference === '' ) {
14518 return;
14519 }
14520
14521 delete_option( ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) );
14522 }
14523
14524 function ebook_store_wait_for_adyen_order( $psp_reference, $ebook_key = '' ) {
14525 $psp_reference = (string) $psp_reference;
14526 $ebook_key = (string) $ebook_key;
14527
14528 for ( $attempt = 0; $attempt < 10; $attempt++ ) {
14529 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
14530 if ( $existing_order_id ) {
14531 return $existing_order_id;
14532 }
14533
14534 if ( $ebook_key !== '' ) {
14535 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
14536 if ( $existing_order_id ) {
14537 return $existing_order_id;
14538 }
14539 }
14540
14541 usleep( 200000 );
14542 }
14543
14544 return 0;
14545 }
14546
14547 function ebook_store_create_order_from_adyen_notification( $notification, $source = 'webhook' ) {
14548 $notification = is_array( $notification ) ? $notification : array();
14549 $psp_reference = isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '';
14550 $merchant_reference = isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '';
14551 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
14552 $payment_link_id = isset( $additional_data['paymentLinkId'] ) ? (string) $additional_data['paymentLinkId'] : '';
14553 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
14554 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
14555
14556 if ( $psp_reference === '' ) {
14557 return new WP_Error( 'adyen_missing_psp_reference', 'Adyen webhook is missing the PSP reference' );
14558 }
14559
14560 if ( $event_code !== 'AUTHORISATION' ) {
14561 return new WP_Error( 'adyen_invalid_event_code', 'Adyen notification is not an AUTHORISATION event' );
14562 }
14563
14564 if ( ! $success ) {
14565 return new WP_Error( 'adyen_payment_not_authorised', 'Adyen payment was not authorised successfully' );
14566 }
14567
14568 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
14569 if ( $existing_order_id ) {
14570 return $existing_order_id;
14571 }
14572
14573 if ( $merchant_reference !== '' ) {
14574 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $merchant_reference );
14575 if ( $existing_order_id ) {
14576 return $existing_order_id;
14577 }
14578 }
14579
14580 $has_lock = ebook_store_acquire_adyen_order_creation_lock( $psp_reference );
14581 if ( ! $has_lock ) {
14582 $existing_order_id = ebook_store_wait_for_adyen_order( $psp_reference, $merchant_reference );
14583 if ( $existing_order_id ) {
14584 return $existing_order_id;
14585 }
14586
14587 return new WP_Error( 'adyen_order_creation_in_progress', 'Adyen order creation is already in progress' );
14588 }
14589
14590 try {
14591 $state = ebook_store_get_adyen_checkout_state(
14592 array(
14593 'payment_link_id' => $payment_link_id,
14594 'ebook_key' => $merchant_reference,
14595 )
14596 );
14597 $state = is_array( $state ) ? $state : array();
14598
14599 $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' );
14600 // Only a link we HAD an id for but could not read is a transient failure.
14601 // A notification carrying no id at all can never be matched, however many
14602 // times Adyen sends it.
14603 $payment_link_error = '';
14604 if ( is_wp_error( $payment_link ) ) {
14605 if ( $payment_link_id !== '' ) {
14606 $payment_link_error = $payment_link->get_error_message();
14607 }
14608 $payment_link = array();
14609 }
14610
14611 $ebook_id = ! empty( $state['ebook_id'] ) ? absint( $state['ebook_id'] ) : 0;
14612 if ( ! $ebook_id && ! empty( $payment_link['metadata']['ebook_id'] ) ) {
14613 $ebook_id = absint( $payment_link['metadata']['ebook_id'] );
14614 }
14615
14616 if ( ! $ebook_id ) {
14617 // Two very different failures used to share one error code. If the
14618 // lookup itself failed (Adyen unreachable, credentials rejected) the
14619 // payment is fine and the webhook must be retried; only a payment we
14620 // genuinely cannot match to an ebook is permanent.
14621 if ( $payment_link_error !== '' ) {
14622 return new WP_Error(
14623 'adyen_payment_link_unavailable',
14624 sprintf( 'Adyen payment link could not be read, so the ebook is unknown: %s', $payment_link_error )
14625 );
14626 }
14627
14628 return new WP_Error( 'adyen_missing_ebook', 'Adyen payment could not be matched to an ebook' );
14629 }
14630
14631 $ebook_post = get_post( $ebook_id );
14632 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
14633 return new WP_Error( 'adyen_invalid_ebook', 'Adyen payment points to an invalid ebook' );
14634 }
14635
14636 $shopper_email = '';
14637 if ( ! empty( $payment_link['shopperEmail'] ) ) {
14638 $shopper_email = sanitize_email( (string) $payment_link['shopperEmail'] );
14639 }
14640 if ( $shopper_email === '' && ! empty( $additional_data['shopperEmail'] ) ) {
14641 $shopper_email = sanitize_email( (string) $additional_data['shopperEmail'] );
14642 }
14643 if ( $shopper_email === '' && ! empty( $state['buyer_email'] ) ) {
14644 $shopper_email = sanitize_email( (string) $state['buyer_email'] );
14645 }
14646 if ( $shopper_email === '' ) {
14647 $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 ) );
14648 }
14649
14650 $shopper_name = '';
14651 if ( ! empty( $payment_link['shopperName'] ) && is_array( $payment_link['shopperName'] ) ) {
14652 $shopper_name = trim(
14653 ( isset( $payment_link['shopperName']['firstName'] ) ? (string) $payment_link['shopperName']['firstName'] : '' ) . ' ' .
14654 ( isset( $payment_link['shopperName']['lastName'] ) ? (string) $payment_link['shopperName']['lastName'] : '' )
14655 );
14656 }
14657 $name_parts = ebook_store_parse_customer_name( $shopper_name );
14658 if ( $name_parts[0] === '' ) {
14659 $name_parts[0] = __( 'Adyen', 'ebook-store' );
14660 $name_parts[1] = __( 'Buyer', 'ebook-store' );
14661 }
14662
14663 $currency = isset( $notification['amount']['currency'] ) ? strtoupper( (string) $notification['amount']['currency'] ) : ebook_store_get_store_currency();
14664 $amount_value = isset( $notification['amount']['value'] ) ? ((float) $notification['amount']['value'] / ebook_store_currency_minor_unit_factor( $currency )) : 0.0;
14665 $country_code = ! empty( $payment_link['countryCode'] ) ? strtoupper( (string) $payment_link['countryCode'] ) : '';
14666
14667 $order_data = array(
14668 'first_name' => $name_parts[0],
14669 'last_name' => $name_parts[1],
14670 'payer_email' => $shopper_email,
14671 'payment_status' => 'Completed',
14672 'txn_id' => $psp_reference,
14673 'payment_type' => 'adyen',
14674 'mc_gross' => $amount_value,
14675 'mc_fee' => 0,
14676 'ebook_key' => $merchant_reference,
14677 'ebook' => $ebook_id,
14678 'post_id' => $ebook_id,
14679 'mc_currency' => $currency,
14680 'residence_country' => $country_code,
14681 'downloads' => 0,
14682 'adyen_psp_reference' => $psp_reference,
14683 'adyen_payment_link_id' => $payment_link_id,
14684 'adyen_event_code' => $event_code,
14685 'adyen_order_source' => $source,
14686 );
14687
14688 $order_id = ebook_store_add_order( $order_data, true, false, $merchant_reference );
14689 if ( ! $order_id ) {
14690 return new WP_Error( 'adyen_order_create_failed', 'Failed to create Ebook Store order from Adyen payment' );
14691 }
14692
14693 ebook_store_clear_adyen_checkout_state( $state );
14694 ebook_store_ga4_track_purchase_for_order( $order_id );
14695
14696 return (int) $order_id;
14697 } finally {
14698 ebook_store_release_adyen_order_creation_lock( $psp_reference );
14699 }
14700 }
14701
14702 function ebook_store_recover_adyen_order_by_ebook_key( $ebook_key ) {
14703 $ebook_key = (string) $ebook_key;
14704 if ( $ebook_key === '' ) {
14705 return new WP_Error( 'adyen_missing_ebook_key', 'Ebook key is missing for Adyen recovery' );
14706 }
14707
14708 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
14709 if ( $existing_order_id ) {
14710 return $existing_order_id;
14711 }
14712
14713 $state = ebook_store_get_adyen_checkout_state(
14714 array(
14715 'ebook_key' => $ebook_key,
14716 )
14717 );
14718 if ( ! is_array( $state ) || empty( $state['payment_link_id'] ) ) {
14719 return new WP_Error( 'adyen_checkout_state_missing', 'No Adyen checkout state found for this ebook key' );
14720 }
14721
14722 $payment_link = ebook_store_fetch_adyen_payment_link( (string) $state['payment_link_id'] );
14723 if ( is_wp_error( $payment_link ) ) {
14724 return $payment_link;
14725 }
14726
14727 if ( ! empty( $payment_link['status'] ) && strtolower( (string) $payment_link['status'] ) === 'completed' ) {
14728 return new WP_Error( 'adyen_waiting_for_webhook', 'Adyen payment is completed but Ebook Store is still waiting for the webhook confirmation.' );
14729 }
14730
14731 return 0;
14732 }
14733
14734 function ebook_store_create_adyen_payment_link( $ebook_id, $price, $title, $ebook_key ) {
14735 $ebook_id = absint( $ebook_id );
14736 if ( ! $ebook_id ) {
14737 $ebook_id = get_the_ID();
14738 }
14739
14740 $checkout_page = get_option( 'ebook_store_checkout_page' );
14741 if ( ! $checkout_page ) {
14742 $checkout_page = $ebook_id;
14743 }
14744
14745 $return_url = add_query_arg(
14746 array(
14747 'ebook_key' => $ebook_key,
14748 'action' => 'thank_you',
14749 'gateway' => 'adyen',
14750 ),
14751 get_permalink( $checkout_page )
14752 );
14753
14754 $country_code = ebook_store_get_adyen_country_code();
14755 $shopper_locale = ebook_store_get_adyen_shopper_locale();
14756 $merchant_account = trim( (string) get_option( 'adyen_merchant_account', '' ) );
14757
14758 if ( $merchant_account === '' ) {
14759 ebook_store_log_adyen_event(
14760 'Adyen payment link creation failed',
14761 array(
14762 'ebook_id' => $ebook_id,
14763 'error' => 'Adyen merchant account not configured',
14764 )
14765 );
14766 return false;
14767 }
14768
14769 $payload = array(
14770 'merchantAccount' => $merchant_account,
14771 'reference' => (string) $ebook_key,
14772 'amount' => array(
14773 'currency' => ebook_store_get_store_currency(),
14774 'value' => (int) round( (float) $price * ebook_store_currency_minor_unit_factor( ebook_store_get_store_currency() ) ),
14775 ),
14776 'description' => sprintf( 'Ebook Store order for %s', $title ),
14777 'shopperLocale' => $shopper_locale,
14778 'returnUrl' => $return_url,
14779 'requiredShopperFields'=> array( 'shopperEmail', 'shopperName' ),
14780 'expiresAt' => gmdate( 'Y-m-d\TH:i:s+00:00', time() + ( 30 * DAY_IN_SECONDS ) ),
14781 'metadata' => array(
14782 'ebook_id' => (string) $ebook_id,
14783 'ebook_key' => (string) $ebook_key,
14784 ),
14785 );
14786
14787 if ( $country_code !== '' ) {
14788 $payload['countryCode'] = $country_code;
14789 }
14790
14791 $response = ebook_store_adyen_request(
14792 'POST',
14793 'paymentLinks',
14794 $payload,
14795 array(
14796 'Idempotency-Key' => wp_generate_uuid4(),
14797 )
14798 );
14799
14800 if ( is_wp_error( $response ) ) {
14801 ebook_store_log_adyen_event(
14802 'Adyen payment link creation failed',
14803 array(
14804 'ebook_id' => $ebook_id,
14805 'error' => $response->get_error_message(),
14806 'data' => $response->get_error_data(),
14807 )
14808 );
14809 return false;
14810 }
14811
14812 if ( empty( $response['url'] ) || empty( $response['id'] ) ) {
14813 ebook_store_log_adyen_event(
14814 'Adyen payment link response missing data',
14815 array(
14816 'ebook_id' => $ebook_id,
14817 'response' => $response,
14818 )
14819 );
14820 return false;
14821 }
14822
14823 $state = array(
14824 'ebook_id' => $ebook_id,
14825 'ebook_key' => (string) $ebook_key,
14826 'title' => (string) $title,
14827 'payment_link_id' => (string) $response['id'],
14828 'store_amount' => (float) $price,
14829 'store_currency' => ebook_store_get_store_currency(),
14830 'country_code' => $country_code,
14831 );
14832 ebook_store_store_adyen_checkout_state( $state );
14833
14834 return (string) $response['url'];
14835 }
14836
14837 function ebook_store_verify_adyen_hmac( $notification ) {
14838 $notification = is_array( $notification ) ? $notification : array();
14839 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
14840 $provided_signature = isset( $additional_data['hmacSignature'] ) ? trim( (string) $additional_data['hmacSignature'] ) : '';
14841 $hmac_key = trim( (string) get_option( 'adyen_hmac_key', '' ) );
14842
14843 if ( $provided_signature === '' || $hmac_key === '' ) {
14844 return false;
14845 }
14846
14847 $signing_string = implode(
14848 ':',
14849 array(
14850 isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '',
14851 isset( $notification['originalReference'] ) ? (string) $notification['originalReference'] : '',
14852 isset( $notification['merchantAccountCode'] ) ? (string) $notification['merchantAccountCode'] : '',
14853 isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '',
14854 isset( $notification['amount']['value'] ) ? (string) $notification['amount']['value'] : '',
14855 isset( $notification['amount']['currency'] ) ? (string) $notification['amount']['currency'] : '',
14856 isset( $notification['eventCode'] ) ? (string) $notification['eventCode'] : '',
14857 isset( $notification['success'] ) ? (string) $notification['success'] : '',
14858 )
14859 );
14860
14861 // Adyen issues webhook HMAC keys as hex; keep base64 for keys pasted in that form.
14862 $decoded_key = ( strlen( $hmac_key ) % 2 === 0 && preg_match( '/^[0-9a-fA-F]+$/', $hmac_key ) ) ? hex2bin( $hmac_key ) : base64_decode( $hmac_key, true );
14863 if ( $decoded_key === false ) {
14864 return false;
14865 }
14866
14867 $calculated_signature = base64_encode( hash_hmac( 'sha256', $signing_string, $decoded_key, true ) );
14868
14869 return hash_equals( $calculated_signature, $provided_signature );
14870 }
14871
14872 function ebook_store_register_adyen_webhook() {
14873 register_rest_route(
14874 'ebook-store/v1',
14875 '/adyen-webhook',
14876 array(
14877 'methods' => 'POST',
14878 'callback' => 'ebook_store_handle_adyen_webhook',
14879 'permission_callback' => '__return_true',
14880 )
14881 );
14882 }
14883 add_action( 'rest_api_init', 'ebook_store_register_adyen_webhook' );
14884
14885 function ebook_store_handle_adyen_webhook( WP_REST_Request $request ) {
14886 $payload = $request->get_body();
14887 $data = json_decode( $payload, true );
14888 $items = isset( $data['notificationItems'] ) && is_array( $data['notificationItems'] ) ? $data['notificationItems'] : array();
14889
14890 if ( empty( $items ) ) {
14891 ebook_store_log_adyen_event(
14892 'Adyen webhook ignored empty payload',
14893 array(
14894 'payload' => $payload,
14895 )
14896 );
14897 return new WP_REST_Response( '[accepted]', 200 );
14898 }
14899
14900 foreach ( $items as $entry ) {
14901 $notification = isset( $entry['NotificationRequestItem'] ) && is_array( $entry['NotificationRequestItem'] )
14902 ? $entry['NotificationRequestItem']
14903 : array();
14904 if ( empty( $notification ) ) {
14905 continue;
14906 }
14907
14908 if ( ! ebook_store_verify_adyen_hmac( $notification ) ) {
14909 ebook_store_log_adyen_event(
14910 'Adyen webhook HMAC verification failed',
14911 array(
14912 'notification' => $notification,
14913 )
14914 );
14915 return new WP_REST_Response( 'invalid hmac', 400 );
14916 }
14917
14918 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
14919 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
14920
14921 switch ( $event_code ) {
14922 case 'AUTHORISATION':
14923 $result = ebook_store_create_order_from_adyen_notification( $notification, 'webhook' );
14924 if ( is_wp_error( $result ) ) {
14925 // A failure we could recover from must NOT be acknowledged:
14926 // answering 200 told Adyen the payment was handled, so it
14927 // never redelivered and the buyer's paid order was lost.
14928 $retryable = array(
14929 'adyen_payment_link_unavailable',
14930 'adyen_order_creation_in_progress',
14931 'adyen_order_create_failed',
14932 );
14933
14934 if ( in_array( $result->get_error_code(), $retryable, true ) ) {
14935 ebook_store_log_adyen_event(
14936 'Adyen AUTHORISATION failed, asking Adyen to retry',
14937 array(
14938 'error' => $result->get_error_message(),
14939 'code' => $result->get_error_code(),
14940 'notification' => $notification,
14941 )
14942 );
14943
14944 return new WP_REST_Response( 'retry', 500 );
14945 }
14946
14947 ebook_store_log_adyen_event(
14948 'Adyen AUTHORISATION processing skipped (not retrying)',
14949 array(
14950 'error' => $result->get_error_message(),
14951 'code' => $result->get_error_code(),
14952 'notification' => $notification,
14953 )
14954 );
14955 } else {
14956 ebook_store_log_adyen_event(
14957 'Adyen order created from AUTHORISATION',
14958 array(
14959 'order_id' => $result,
14960 'pspReference' => isset( $notification['pspReference'] ) ? $notification['pspReference'] : '',
14961 )
14962 );
14963 }
14964 break;
14965
14966 case 'REFUND':
14967 if ( $success && ! empty( $notification['originalReference'] ) ) {
14968 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
14969 if ( $order_id ) {
14970 update_post_meta( $order_id, 'payment_status', 'Refunded' );
14971 update_post_meta( $order_id, 'refund_gateway', 'adyen' );
14972 update_post_meta( $order_id, 'refund_gateway_id', isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '' );
14973 update_post_meta( $order_id, 'refund_gateway_status', 'received' );
14974 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
14975 }
14976 }
14977 break;
14978
14979 case 'REFUND_FAILED':
14980 case 'REFUNDED_REVERSED':
14981 if ( ! empty( $notification['originalReference'] ) ) {
14982 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
14983 if ( $order_id ) {
14984 update_post_meta( $order_id, 'payment_status', 'Completed' );
14985 update_post_meta( $order_id, 'refund_gateway_status', strtolower( $event_code ) );
14986 delete_post_meta( $order_id, 'refund_gateway_id' );
14987 }
14988 }
14989 break;
14990 }
14991 }
14992
14993 return new WP_REST_Response( '[accepted]', 200 );
14994 }
14995
14996 /**
14997 * Log a stripe webhook event.
14998 *
14999 * Writes through ebook_store_log(), which keeps the file outside the web root,
15000 * caps its size and stays silent unless logging is switched on. The old version
15001 * appended to an uncapped .txt inside the plugin folder that any visitor could
15002 * fetch; one of those files reached 118 MB and contained live order tokens.
15003 *
15004 * @param string $message Message.
15005 * @param array $context Context.
15006 */
15007 function ebook_store_log_stripe_event( $message, $context = array() ) {
15008 ebook_store_log( 'stripe', $message, $context );
15009 }
15010
15011 function ebook_store_bootstrap_stripe_sdk() {
15012 require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
15013
15014 $stripe_secret_key = (string) get_option( 'stripe_secret_key' );
15015 if ( $stripe_secret_key === '' ) {
15016 return new WP_Error( 'stripe_missing_secret_key', 'Stripe secret key not configured' );
15017 }
15018
15019 \Stripe\Stripe::setApiKey( $stripe_secret_key );
15020
15021 return true;
15022 }
15023
15024 function ebook_store_get_order_id_by_meta( $meta_key, $meta_value ) {
15025 if ( $meta_value === '' || $meta_value === null ) {
15026 return 0;
15027 }
15028
15029 $orders = get_posts(
15030 array(
15031 'post_type' => 'ebook_order',
15032 'post_status' => 'any',
15033 'posts_per_page' => 1,
15034 'fields' => 'ids',
15035 'meta_key' => $meta_key,
15036 'meta_value' => (string) $meta_value,
15037 // Never served from the request's query cache: this lookup is used to
15038 // spot an order that another request created a moment ago, and the
15039 // same query may already have run (and cached "nothing") earlier in
15040 // this request.
15041 'cache_results' => false,
15042 'update_post_meta_cache' => false,
15043 'update_post_term_cache' => false,
15044 )
15045 );
15046
15047 return empty( $orders ) ? 0 : (int) $orders[0];
15048 }
15049
15050 function ebook_store_parse_customer_name( $name ) {
15051 $name = trim( (string) $name );
15052 if ( $name === '' ) {
15053 return array( '', '' );
15054 }
15055
15056 $name_parts = preg_split( '/\s+/', $name, 2 );
15057
15058 return array(
15059 isset( $name_parts[0] ) ? $name_parts[0] : '',
15060 isset( $name_parts[1] ) ? $name_parts[1] : '',
15061 );
15062 }
15063
15064 function ebook_store_extract_stripe_billing_details( $session ) {
15065 $details = array(
15066 'name' => '',
15067 'email' => '',
15068 'address' => array(),
15069 'charge' => null,
15070 );
15071
15072 if ( ! empty( $session->customer_details ) ) {
15073 $details['name'] = isset( $session->customer_details->name ) ? (string) $session->customer_details->name : '';
15074 $details['email'] = isset( $session->customer_details->email ) ? (string) $session->customer_details->email : '';
15075 $session_address = ! empty( $session->customer_details->address ) ? $session->customer_details->address : null;
15076 // (array) on a StripeObject exposes its private props with mangled keys; toArray() gives line1/city/... .
15077 $details['address'] = is_object( $session_address ) && method_exists( $session_address, 'toArray' ) ? $session_address->toArray() : ( $session_address ? (array) $session_address : array() );
15078 }
15079
15080 $payment_intent_id = '';
15081 if ( ! empty( $session->payment_intent ) ) {
15082 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
15083 ? (string) $session->payment_intent->id
15084 : (string) $session->payment_intent;
15085 }
15086
15087 if ( $payment_intent_id !== '' ) {
15088 try {
15089 $payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_id );
15090 $charge = null;
15091
15092 if ( ! empty( $payment_intent->latest_charge ) ) {
15093 $charge = is_object( $payment_intent->latest_charge ) && isset( $payment_intent->latest_charge->id )
15094 ? $payment_intent->latest_charge
15095 : \Stripe\Charge::retrieve( (string) $payment_intent->latest_charge );
15096 } elseif ( ! empty( $payment_intent->charges ) && ! empty( $payment_intent->charges->data[0] ) ) {
15097 $charge = $payment_intent->charges->data[0];
15098 }
15099
15100 if ( $charge && ! empty( $charge->billing_details ) ) {
15101 $details['charge'] = $charge;
15102
15103 if ( $details['name'] === '' && ! empty( $charge->billing_details->name ) ) {
15104 $details['name'] = (string) $charge->billing_details->name;
15105 }
15106
15107 if ( $details['email'] === '' && ! empty( $charge->billing_details->email ) ) {
15108 $details['email'] = (string) $charge->billing_details->email;
15109 }
15110
15111 if ( empty( $details['address'] ) && ! empty( $charge->billing_details->address ) ) {
15112 $charge_address = $charge->billing_details->address;
15113 $details['address'] = is_object( $charge_address ) && method_exists( $charge_address, 'toArray' ) ? $charge_address->toArray() : (array) $charge_address;
15114 }
15115 }
15116 } catch ( Exception $e ) {
15117 ebook_store_log_stripe_event(
15118 'Stripe billing details lookup failed',
15119 array(
15120 'payment_intent_id' => $payment_intent_id,
15121 'error' => $e->getMessage(),
15122 )
15123 );
15124 }
15125 }
15126
15127 return $details;
15128 }
15129
15130 function ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id ) {
15131 if ( $payment_intent_id === '' ) {
15132 return null;
15133 }
15134
15135 try {
15136 $sessions = \Stripe\Checkout\Session::all(
15137 array(
15138 'payment_intent' => $payment_intent_id,
15139 'limit' => 1,
15140 )
15141 );
15142 } catch ( Exception $e ) {
15143 ebook_store_log_stripe_event(
15144 'Stripe session lookup by payment intent failed',
15145 array(
15146 'payment_intent_id' => $payment_intent_id,
15147 'error' => $e->getMessage(),
15148 )
15149 );
15150
15151 return null;
15152 }
15153
15154 return ! empty( $sessions->data[0] ) ? $sessions->data[0] : null;
15155 }
15156
15157 function ebook_store_find_stripe_session_by_ebook_key( $ebook_key ) {
15158 $ebook_key = (string) $ebook_key;
15159 if ( $ebook_key === '' ) {
15160 return null;
15161 }
15162
15163 try {
15164 $sessions = \Stripe\Checkout\Session::all(
15165 array(
15166 'limit' => 25,
15167 )
15168 );
15169 } catch ( Exception $e ) {
15170 ebook_store_log_stripe_event(
15171 'Stripe session lookup by ebook key failed',
15172 array(
15173 'ebook_key' => $ebook_key,
15174 'error' => $e->getMessage(),
15175 )
15176 );
15177
15178 return null;
15179 }
15180
15181 foreach ( $sessions->data as $session ) {
15182 if (
15183 ! empty( $session->metadata )
15184 && isset( $session->metadata->ebook_key )
15185 && (string) $session->metadata->ebook_key === $ebook_key
15186 ) {
15187 return $session;
15188 }
15189 }
15190
15191 return null;
15192 }
15193
15194 /**
15195 * Load the script that turns a Stripe button into a create-on-click button.
15196 *
15197 * Enqueued only where a Stripe button is actually rendered.
15198 */
15199 function ebook_store_enqueue_stripe_checkout_script() {
15200 if ( wp_script_is( 'ebook-store-stripe', 'enqueued' ) ) {
15201 return;
15202 }
15203
15204 wp_enqueue_script(
15205 'ebook-store-stripe',
15206 plugins_url( 'js/ebook-store-stripe.js', __FILE__ ),
15207 array(),
15208 ebook_store_get_plugin_version(),
15209 true
15210 );
15211
15212 wp_localize_script(
15213 'ebook-store-stripe',
15214 'ebookStoreStripe',
15215 array(
15216 'endpoint' => home_url( 'index.php' ),
15217 'strings' => array(
15218 'working' => __( 'One moment…', 'ebook-store' ),
15219 'failed' => __( 'Sorry, the checkout could not be opened. Please try again.', 'ebook-store' ),
15220 ),
15221 )
15222 );
15223 }
15224
15225 function ebook_store_get_stripe_order_creation_lock_key( $session_id ) {
15226 return 'ebook_store_stripe_lock_' . md5( (string) $session_id );
15227 }
15228
15229 /**
15230 * Take the creation lock for one Stripe session.
15231 *
15232 * The webhook and the thank-you page's recovery both create the order for a
15233 * session, and both used a check-then-act with several network calls in the
15234 * middle — long enough for the other one to pass the same checks, so a buyer
15235 * ended up with two orders and two delivery emails.
15236 *
15237 * @param string $session_id Stripe checkout session ID.
15238 * @return bool True when this caller owns the lock.
15239 */
15240 function ebook_store_acquire_stripe_order_creation_lock( $session_id ) {
15241 $session_id = (string) $session_id;
15242 if ( $session_id === '' ) {
15243 return false;
15244 }
15245
15246 $lock_key = ebook_store_get_stripe_order_creation_lock_key( $session_id );
15247
15248 if ( add_option( $lock_key, time(), '', 'no' ) ) {
15249 return true;
15250 }
15251
15252 // A request that died mid-creation would otherwise hold the lock forever.
15253 $held_since = (int) get_option( $lock_key, 0 );
15254 if ( $held_since > 0 && ( time() - $held_since ) > 60 ) {
15255 delete_option( $lock_key );
15256
15257 return add_option( $lock_key, time(), '', 'no' );
15258 }
15259
15260 return false;
15261 }
15262
15263 function ebook_store_release_stripe_order_creation_lock( $session_id ) {
15264 $session_id = (string) $session_id;
15265 if ( $session_id === '' ) {
15266 return;
15267 }
15268
15269 delete_option( ebook_store_get_stripe_order_creation_lock_key( $session_id ) );
15270 }
15271
15272 /**
15273 * Wait briefly for the other request to finish creating this session's order.
15274 *
15275 * @param string $session_id Stripe checkout session ID.
15276 * @param string $ebook_key Order key, when known.
15277 * @return int Order ID, or 0.
15278 */
15279 function ebook_store_wait_for_stripe_order( $session_id, $ebook_key = '' ) {
15280 $session_id = (string) $session_id;
15281 $ebook_key = (string) $ebook_key;
15282
15283 for ( $attempt = 0; $attempt < 10; $attempt++ ) {
15284 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15285 if ( $existing_order_id ) {
15286 return $existing_order_id;
15287 }
15288
15289 if ( $ebook_key !== '' ) {
15290 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15291 if ( $existing_order_id ) {
15292 return $existing_order_id;
15293 }
15294 }
15295
15296 usleep( 200000 );
15297 }
15298
15299 return 0;
15300 }
15301
15302 function ebook_store_create_order_from_stripe_session( $session, $source = 'webhook' ) {
15303 if ( empty( $session ) || empty( $session->id ) ) {
15304 return new WP_Error( 'stripe_missing_session', 'Stripe session data is missing' );
15305 }
15306
15307 $session_id = (string) $session->id;
15308 $payment_status = isset( $session->payment_status ) ? (string) $session->payment_status : '';
15309 $payment_intent_id = '';
15310 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15311
15312 if ( $existing_order_id ) {
15313 return $existing_order_id;
15314 }
15315
15316 if ( ! empty( $session->payment_intent ) ) {
15317 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
15318 ? (string) $session->payment_intent->id
15319 : (string) $session->payment_intent;
15320
15321 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15322 if ( $existing_order_id ) {
15323 return $existing_order_id;
15324 }
15325 }
15326
15327 if ( $payment_status !== 'paid' ) {
15328 return new WP_Error(
15329 'stripe_payment_not_paid',
15330 sprintf( 'Stripe session %s is not paid (%s)', $session_id, $payment_status )
15331 );
15332 }
15333
15334 $metadata = ! empty( $session->metadata ) ? $session->metadata : (object) array();
15335 $ebook_id = isset( $metadata->ebook_id ) ? absint( $metadata->ebook_id ) : 0;
15336 $ebook_key = isset( $metadata->ebook_key ) ? (string) $metadata->ebook_key : '';
15337
15338 if ( ! $ebook_id && ! empty( $session->client_reference_id ) ) {
15339 $ebook_id = absint( $session->client_reference_id );
15340 }
15341
15342 if ( $ebook_key !== '' ) {
15343 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15344 if ( $existing_order_id ) {
15345 return $existing_order_id;
15346 }
15347 }
15348
15349 if ( ! $ebook_id ) {
15350 return new WP_Error( 'stripe_missing_ebook', sprintf( 'Stripe session %s has no ebook_id metadata', $session_id ) );
15351 }
15352
15353 $ebook_post = get_post( $ebook_id );
15354 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
15355 return new WP_Error( 'stripe_invalid_ebook', sprintf( 'Stripe session %s points to invalid ebook %s', $session_id, $ebook_id ) );
15356 }
15357
15358 // Everything above is cheap and read-only. From here the function fetches
15359 // billing details from Stripe and inserts the order, which is long enough for
15360 // the webhook and the thank-you page to do it twice for the same session.
15361 if ( ! ebook_store_acquire_stripe_order_creation_lock( $session_id ) ) {
15362 $existing_order_id = ebook_store_wait_for_stripe_order( $session_id, $ebook_key );
15363 if ( $existing_order_id ) {
15364 return $existing_order_id;
15365 }
15366
15367 return new WP_Error( 'stripe_order_creation_in_progress', sprintf( 'Stripe order creation is already in progress for session %s', $session_id ) );
15368 }
15369
15370 try {
15371 // Re-check inside the lock: the other request may have finished between our
15372 // first look and taking it.
15373 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
15374 if ( ! $existing_order_id && $ebook_key !== '' ) {
15375 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
15376 }
15377 if ( $existing_order_id ) {
15378 return $existing_order_id;
15379 }
15380
15381 $billing_details = ebook_store_extract_stripe_billing_details( $session );
15382 $name_parts = ebook_store_parse_customer_name( $billing_details['name'] );
15383 $address = is_array( $billing_details['address'] ) ? $billing_details['address'] : array();
15384 $session_currency = isset( $session->currency ) && $session->currency ? strtoupper( (string) $session->currency ) : ebook_store_get_store_currency();
15385 $amount_total = isset( $session->amount_total ) ? ((float) $session->amount_total / ebook_store_currency_minor_unit_factor( $session_currency )) : 0;
15386 $amount_value = isset( $metadata->price ) && $metadata->price !== '' ? (float) $metadata->price : $amount_total;
15387 $txn_id = $payment_intent_id !== '' ? $payment_intent_id : $session_id;
15388
15389 $order_data = array(
15390 'first_name' => $name_parts[0],
15391 'last_name' => $name_parts[1],
15392 'payer_email' => (string) $billing_details['email'],
15393 'payment_status' => 'Completed',
15394 'txn_id' => $txn_id,
15395 'payment_type' => 'stripe',
15396 'mc_gross' => $amount_value,
15397 'ebook_key' => $ebook_key,
15398 'ebook' => $ebook_id,
15399 'post_id' => $ebook_id,
15400 'mc_currency' => isset( $session->currency ) && $session->currency ? strtoupper( (string) $session->currency ) : ebook_store_get_store_currency(),
15401 'residence_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
15402 'address_name' => (string) $billing_details['name'],
15403 'address_street' => isset( $address['line1'] ) ? (string) $address['line1'] : '',
15404 'address_city' => isset( $address['city'] ) ? (string) $address['city'] : '',
15405 'address_state' => isset( $address['state'] ) ? (string) $address['state'] : '',
15406 'address_zip' => isset( $address['postal_code'] ) ? (string) $address['postal_code'] : '',
15407 'address_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
15408 'address_status' => 'confirmed',
15409 'downloads' => 0,
15410 'stripe_session_id' => $session_id,
15411 'stripe_payment_intent_id' => $payment_intent_id,
15412 'stripe_order_source' => $source,
15413 );
15414
15415 $order_id = ebook_store_add_order( $order_data, true, false, $ebook_key );
15416
15417 if ( ! $order_id ) {
15418 return new WP_Error( 'stripe_order_create_failed', sprintf( 'Failed to create order from Stripe session %s', $session_id ) );
15419 }
15420
15421 ebook_store_ga4_track_purchase_for_order( $order_id );
15422
15423 return (int) $order_id;
15424 } finally {
15425 ebook_store_release_stripe_order_creation_lock( $session_id );
15426 }
15427 }
15428
15429 function ebook_store_recover_stripe_order_by_ebook_key( $ebook_key ) {
15430 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15431 if ( is_wp_error( $bootstrap ) ) {
15432 return $bootstrap;
15433 }
15434
15435 $session = ebook_store_find_stripe_session_by_ebook_key( $ebook_key );
15436 if ( ! $session ) {
15437 return new WP_Error( 'stripe_session_not_found', sprintf( 'No Stripe session found for ebook key %s', $ebook_key ) );
15438 }
15439
15440 return ebook_store_create_order_from_stripe_session( $session, 'thank_you' );
15441 }
15442
15443 function ebook_store_recover_stripe_order_by_session_id( $session_id ) {
15444 $session_id = (string) $session_id;
15445 if ( $session_id === '' ) {
15446 return new WP_Error( 'stripe_missing_session_id', 'Stripe session ID is missing' );
15447 }
15448
15449 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15450 if ( is_wp_error( $bootstrap ) ) {
15451 return $bootstrap;
15452 }
15453
15454 try {
15455 $session = \Stripe\Checkout\Session::retrieve( $session_id );
15456 } catch ( Exception $e ) {
15457 return new WP_Error( 'stripe_session_lookup_failed', $e->getMessage() );
15458 }
15459
15460 return ebook_store_create_order_from_stripe_session( $session, 'thank_you_session' );
15461 }
15462
15463 function ebook_store_create_stripe_checkout($ebook_id, $price, $title, $ebook_key) {
15464 $ebook_id = absint($ebook_id);
15465 if (!$ebook_id) {
15466 $ebook_id = get_the_ID();
15467 }
15468
15469 try {
15470 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15471 if (is_wp_error($bootstrap)) {
15472 throw new Exception($bootstrap->get_error_message());
15473 }
15474
15475 $checkout_page = get_option('ebook_store_checkout_page');
15476 $minor_unit_factor = ebook_store_currency_minor_unit_factor( ebook_store_get_store_currency() );
15477 // Stripe only accepts an integer amount in minor units; record the amount really charged.
15478 $unit_amount = (int) round( (float) $price * $minor_unit_factor );
15479 $metadata = [
15480 'ebook_id' => $ebook_id,
15481 'ebook_key' => $ebook_key,
15482 'price' => $unit_amount / $minor_unit_factor,
15483 'title' => $title
15484 ];
15485
15486 $success_url = add_query_arg(
15487 array(
15488 'ebook_key' => $metadata['ebook_key'],
15489 'action' => 'thank_you',
15490 'stripe_session_id' => '{CHECKOUT_SESSION_ID}'
15491 ),
15492 get_permalink($checkout_page)
15493 );
15494
15495 $session_data = [
15496 'line_items' => [[
15497 'price_data' => [
15498 'currency' => ebook_store_get_store_currency(),
15499 'product_data' => [
15500 'name' => $title,
15501 ],
15502 'unit_amount' => $unit_amount,
15503 ],
15504 'quantity' => 1,
15505 ]],
15506 'mode' => 'payment',
15507 'success_url' => $success_url,
15508 'cancel_url' => get_option('stripe_cancel_url', home_url()),
15509 'metadata' => $metadata,
15510 'client_reference_id' => (string) $ebook_id,
15511 'payment_intent_data' => [
15512 'metadata' => $metadata,
15513 ],
15514 'customer_email' => isset($_POST['email']) ? sanitize_email($_POST['email']) : null
15515 ];
15516
15517 $checkout_session = \Stripe\Checkout\Session::create($session_data);
15518
15519 return $checkout_session->url;
15520 } catch(Exception $e) {
15521 ebook_store_log_stripe_event(
15522 'Stripe checkout creation failed',
15523 [
15524 'ebook_id' => $ebook_id,
15525 'error' => $e->getMessage(),
15526 ]
15527 );
15528 return false;
15529 }
15530 }
15531
15532 // Add this near the other add_action calls
15533 add_action('rest_api_init', 'ebook_store_register_stripe_webhook');
15534
15535 function ebook_store_register_stripe_webhook() {
15536 register_rest_route('ebook-store/v1', '/stripe-webhook', array(
15537 'methods' => 'POST',
15538 'callback' => 'ebook_store_handle_stripe_webhook',
15539 'permission_callback' => '__return_true' // Public endpoint
15540 ));
15541 }
15542
15543 /**
15544 * Whether a failed Stripe order creation should make Stripe redeliver the event.
15545 *
15546 * Answering 200 tells Stripe the event was handled, so a failure that is only
15547 * temporary — the thank-you page holds the creation lock, or the insert
15548 * failed — would otherwise never be retried and the paid order would be lost.
15549 *
15550 * @param mixed $error Return value of ebook_store_create_order_from_stripe_session().
15551 * @return bool
15552 */
15553 function ebook_store_is_retryable_stripe_order_error( $error ) {
15554 return is_wp_error( $error ) && in_array( $error->get_error_code(), array( 'stripe_order_creation_in_progress', 'stripe_order_create_failed' ), true );
15555 }
15556
15557 function ebook_store_handle_stripe_webhook(WP_REST_Request $request) {
15558 $payload = $request->get_body();
15559 $sig_header = $request->get_header( 'stripe-signature' );
15560
15561 try {
15562 $bootstrap = ebook_store_bootstrap_stripe_sdk();
15563 if ( is_wp_error( $bootstrap ) ) {
15564 throw new Exception( $bootstrap->get_error_message() );
15565 }
15566
15567 $webhook_secret = (string) get_option( 'stripe_webhook_secret' );
15568 if ( $webhook_secret === '' ) {
15569 throw new Exception( 'Stripe webhook secret not configured' );
15570 }
15571
15572 $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $webhook_secret );
15573
15574 ebook_store_log_stripe_event(
15575 'Stripe webhook verified',
15576 array(
15577 'type' => $event->type,
15578 'id' => isset( $event->id ) ? $event->id : '',
15579 )
15580 );
15581
15582 switch ( $event->type ) {
15583 case 'checkout.session.completed':
15584 case 'checkout.session.async_payment_succeeded':
15585 $order_id = ebook_store_create_order_from_stripe_session( $event->data->object, 'webhook' );
15586 if ( is_wp_error( $order_id ) ) {
15587 ebook_store_log_stripe_event(
15588 'Stripe checkout session order creation skipped',
15589 array(
15590 'type' => $event->type,
15591 'code' => $order_id->get_error_code(),
15592 'error' => $order_id->get_error_message(),
15593 )
15594 );
15595 if ( ebook_store_is_retryable_stripe_order_error( $order_id ) ) {
15596 return new WP_REST_Response( 'retry', 500 );
15597 }
15598 } else {
15599 ebook_store_log_stripe_event(
15600 'Stripe order created from checkout session',
15601 array(
15602 'type' => $event->type,
15603 'order_id' => $order_id,
15604 'session' => isset( $event->data->object->id ) ? $event->data->object->id : '',
15605 )
15606 );
15607 }
15608 break;
15609
15610 case 'charge.updated':
15611 case 'charge.succeeded':
15612 $charge = $event->data->object;
15613
15614 if ( empty( $charge->paid ) || $charge->status !== 'succeeded' ) {
15615 break;
15616 }
15617
15618 $payment_intent_id = ! empty( $charge->payment_intent ) ? (string) $charge->payment_intent : '';
15619 $session = ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id );
15620
15621 if ( ! $session ) {
15622 ebook_store_log_stripe_event(
15623 'Stripe charge event had no checkout session match',
15624 array(
15625 'type' => $event->type,
15626 'payment_intent_id' => $payment_intent_id,
15627 'charge_id' => isset( $charge->id ) ? $charge->id : '',
15628 )
15629 );
15630 break;
15631 }
15632
15633 $order_id = ebook_store_create_order_from_stripe_session( $session, 'charge_event' );
15634 if ( is_wp_error( $order_id ) ) {
15635 ebook_store_log_stripe_event(
15636 'Stripe charge event order creation skipped',
15637 array(
15638 'type' => $event->type,
15639 'payment_intent_id' => $payment_intent_id,
15640 'code' => $order_id->get_error_code(),
15641 'error' => $order_id->get_error_message(),
15642 )
15643 );
15644 if ( ebook_store_is_retryable_stripe_order_error( $order_id ) ) {
15645 return new WP_REST_Response( 'retry', 500 );
15646 }
15647 } else {
15648 ebook_store_log_stripe_event(
15649 'Stripe order created from charge event',
15650 array(
15651 'type' => $event->type,
15652 'order_id' => $order_id,
15653 'payment_intent_id' => $payment_intent_id,
15654 )
15655 );
15656 }
15657 break;
15658
15659 case 'charge.refunded':
15660 $charge = $event->data->object;
15661 $payment_intent_id = ! empty( $charge->payment_intent ) ? (string) $charge->payment_intent : '';
15662 $order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15663
15664 if ( $order_id && ! empty( $charge->refunded ) ) {
15665 // charge.refunded also fires for partial refunds; `refunded` is true only when the full amount is back.
15666 $refund_currency = ! empty( $charge->currency ) ? strtoupper( (string) $charge->currency ) : (string) get_post_meta( $order_id, 'mc_currency', true );
15667 $refund_amount = isset( $charge->amount_refunded ) ? ( (float) $charge->amount_refunded / ebook_store_currency_minor_unit_factor( $refund_currency ) ) : '';
15668 $refund_id = (string) get_post_meta( $order_id, 'refund_gateway_id', true );
15669 if ( ! empty( $charge->refunds->data[0]->id ) ) {
15670 $refund_id = (string) $charge->refunds->data[0]->id;
15671 }
15672 ebook_store_mark_order_refunded( $order_id, 'stripe', $refund_id, 'succeeded', $refund_amount, $refund_currency );
15673 }
15674
15675 ebook_store_log_stripe_event(
15676 'Stripe charge refund event processed',
15677 array(
15678 'type' => $event->type,
15679 'order_id' => $order_id,
15680 'payment_intent_id' => $payment_intent_id,
15681 'fully_refunded' => ! empty( $charge->refunded ),
15682 )
15683 );
15684 break;
15685
15686 case 'refund.failed':
15687 case 'refund.updated':
15688 case 'charge.refund.updated':
15689 $refund = $event->data->object;
15690 $refund_status = isset( $refund->status ) ? (string) $refund->status : '';
15691 $payment_intent_id = ! empty( $refund->payment_intent ) ? (string) $refund->payment_intent : '';
15692 $order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
15693
15694 if ( $order_id && ( $refund_status === 'failed' || $refund_status === 'canceled' ) ) {
15695 // The money never went back: the buyer keeps a paid, downloadable order.
15696 update_post_meta( $order_id, 'payment_status', 'Completed' );
15697 update_post_meta( $order_id, 'refund_gateway_status', $refund_status );
15698 delete_post_meta( $order_id, 'refund_gateway_id' );
15699 } elseif ( $order_id && $refund_status === 'succeeded' && isset( $refund->id ) && (string) $refund->id === (string) get_post_meta( $order_id, 'refund_gateway_id', true ) ) {
15700 update_post_meta( $order_id, 'refund_gateway_status', 'succeeded' );
15701 }
15702
15703 ebook_store_log_stripe_event(
15704 'Stripe refund status event processed',
15705 array(
15706 'type' => $event->type,
15707 'order_id' => $order_id,
15708 'payment_intent_id' => $payment_intent_id,
15709 'status' => $refund_status,
15710 )
15711 );
15712 break;
15713
15714 default:
15715 ebook_store_log_stripe_event(
15716 'Stripe webhook ignored event',
15717 array(
15718 'type' => $event->type,
15719 )
15720 );
15721 break;
15722 }
15723
15724 return new WP_REST_Response( array( 'received' => true ), 200 );
15725 } catch ( Exception $e ) {
15726 ebook_store_log_stripe_event(
15727 'Stripe webhook error',
15728 array(
15729 'error_class' => get_class( $e ),
15730 'message' => $e->getMessage(),
15731 )
15732 );
15733
15734 return new WP_REST_Response(
15735 array( 'error' => $e->getMessage() ),
15736 $e instanceof \Stripe\Exception\SignatureVerificationException ? 400 : 500
15737 );
15738 }
15739 }
15740