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

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

14,591 lines 559.8 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 $selected = in_array( $bonus_id, $ebook_meta['ebook_bonus'], true ) ? ' selected="selected"' : '';
2177 $title = get_the_title( $bonus_id );
2178 $title = $title !== '' ? $title : __( '(no title)', 'ebook-store' );
2179 $ebook_bonus_options .= '<option value="' . esc_attr( $bonus_id ) . '"' . $selected . '>' . esc_html( $title ) . '</option>';
2180 }
2181
2182 wp_enqueue_script('jquery-ui-datepicker');
2183 // The jQuery UI datepicker styling comes from core rather than a 2010-era
2184 // stylesheet on Google's CDN, which also leaked every admin's IP to Google.
2185 wp_enqueue_style( 'wp-jquery-ui-dialog' );
2186 wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
2187
2188 wp_enqueue_style( 'ebookstorestylesheet' );
2189
2190 $img = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
2191 $img = is_array($img) ? $img : array();
2192
2193 $img_side_photo = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_side_photo', true);
2194 $img_side_photo = is_array($img_side_photo) ? $img_side_photo : array();
2195
2196 $img_cover = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_cover', true);
2197 $img_cover = is_array($img_cover) ? $img_cover : array();
2198
2199 $preview = get_post_meta($ebook_id, 'ebook_wp_custom_attachment_preview', true);
2200 $preview = is_array($preview) ? $preview : array();
2201
2202 wp_nonce_field(plugin_basename(__FILE__), 'ebook_wp_custom_attachment_nonce');
2203
2204 $selected_authors = isset($ebook_meta['ebook_author']) && is_array($ebook_meta['ebook_author']) ? $ebook_meta['ebook_author'] : array();
2205 $selected_publishers = isset($ebook_meta['ebook_publisher']) && is_array($ebook_meta['ebook_publisher']) ? $ebook_meta['ebook_publisher'] : array();
2206
2207 $ebook_authors = '';
2208 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 ) {
2209 $selected = in_array( $author_id, $selected_authors, true ) ? ' selected' : '';
2210 $ebook_authors .= '<option value="' . esc_attr( $author_id ) . '"' . $selected . '>' . esc_html( get_the_title( $author_id ) ) . '</option>';
2211 }
2212
2213 $ebook_publishers = '';
2214 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 ) {
2215 $selected = in_array( $publisher_id, $selected_publishers, true ) ? ' selected' : '';
2216 $ebook_publishers .= '<option value="' . esc_attr( $publisher_id ) . '"' . $selected . '>' . esc_html( get_the_title( $publisher_id ) ) . '</option>';
2217 }
2218
2219 $currencies = array(
2220 '' => '-- Optional --',
2221 'USD' => 'US Dollar',
2222 'EUR' => 'Euro',
2223 'ILS' => 'Israeli New Sheqel',
2224 'INR' => 'Indian Rupee',
2225 'GBP' => 'Pounds Sterling',
2226 'AUD' => 'Australian Dollar',
2227 'CAD' => 'Canadian Dollar',
2228 'JPY' => 'Japan Yen',
2229 'NZD' => 'New Zealand Dollar',
2230 'CHF' => 'Swiss Franc',
2231 'HKD' => 'Hong Kong Dollar',
2232 'SGD' => 'Singapore Dollar',
2233 'SEK' => 'Sweden Krona',
2234 'DKK' => 'Danish Krone',
2235 'PLN' => 'New Zloty',
2236 'NOK' => 'Norwegian Krone',
2237 'HUF' => 'Forint',
2238 'MXN' => 'Mexican Pesos',
2239 'CZK' => 'Czech Koruna',
2240 'BRL' => 'Brazilian Real',
2241 'TWD' => 'Taiwan New Dollar',
2242 'TRY' => 'Turkish Lira',
2243 'MYR' => 'Malaysian Ringgit',
2244 'ZAR' => 'South African Rands',
2245 'NGN' => 'Nigerian Naira',
2246 'RON' => 'Romanian leu',
2247 'THB' => 'Thai Baht'
2248 );
2249
2250 $selected_currency = isset($ebook_meta['paypal_currency']) ? $ebook_meta['paypal_currency'] : esc_attr(get_option('paypal_currency','USD'));
2251 $paypal_currency_options = '';
2252 foreach ($currencies as $currency => $name) {
2253 $is_selected = $currency === $selected_currency ? ' selected' : '';
2254 $paypal_currency_options .= '<option value="' . esc_attr($currency) . '"' . $is_selected . '>' . esc_html($name) . '</option>';
2255 }
2256 $form_templates = ebook_store_get_form_templates();
2257 $selected_form_template = isset($ebook_meta['form_template']) ? sanitize_key($ebook_meta['form_template']) : '';
2258 $selected_form_template = ebook_store_resolve_form_template($selected_form_template);
2259 if ($selected_form_template === '' || !isset($form_templates[$selected_form_template])) {
2260 $selected_form_template = ebook_store_resolve_form_template('');
2261 }
2262 $form_template_options = '';
2263 foreach ($form_templates as $template_key => $template_config) {
2264 $template_label = isset($template_config['label']) ? $template_config['label'] : $template_key;
2265 $form_template_options .= '<option value="' . esc_attr($template_key) . '"' . selected($selected_form_template, $template_key, false) . '>' . esc_html($template_label) . '</option>';
2266 }
2267
2268 $embed_shortcode = sprintf('[ebook_store ebook_id="%d"]', $ebook_id);
2269 $buy_shortcode = sprintf('[ebook_store_buy ebook_id="%d"]', $ebook_id);
2270 $max_upload_size = ini_get('upload_max_filesize') . 'B';
2271 $license_key = get_option('ebook_store_license_key');
2272 $upgrade_text = '';
2273 if ($license_key == '') {
2274 $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.';
2275 }
2276
2277 $conversion_hint = '';
2278 if (empty($img['url'])) {
2279 $conversion_hint = sprintf(
2280 __('<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'),
2281 'http://calibre-ebook.com/download'
2282 );
2283 }
2284
2285 $cover_size = '';
2286 if (!empty($img_cover['file']) && file_exists($img_cover['file'])) {
2287 $cover_size = humanFileSize(filesize($img_cover['file']));
2288 }
2289
2290 $side_size = '';
2291 if (!empty($img_side_photo['file']) && file_exists($img_side_photo['file'])) {
2292 $side_size = humanFileSize(filesize($img_side_photo['file']));
2293 }
2294
2295 $donate_or_download = isset($ebook_meta['donate_or_download']) ? $ebook_meta['donate_or_download'] : 'paid';
2296 $extendhtml = apply_filters('ebook_store_form_extend', get_the_ID());
2297 $sales_index = ebook_store_get_ebook_sales_index();
2298 $current_sales = isset($sales_index[$ebook_id]) ? $sales_index[$ebook_id] : array(
2299 'orders' => 0,
2300 'total' => 0.0,
2301 'last_7d' => 0.0,
2302 'last_30d' => 0.0,
2303 'latest_order_gmt' => '',
2304 );
2305 $current_price = isset($ebook_meta['ebook_price']) ? (float) $ebook_meta['ebook_price'] : 0.0;
2306 $current_currency = $selected_currency !== '' ? $selected_currency : esc_attr(get_option('paypal_currency','USD'));
2307 $current_status = get_post_status($ebook_id);
2308 $current_title = get_the_title($ebook_id);
2309 if ($current_title === '') {
2310 $current_title = __('Untitled ebook', 'ebook-store');
2311 }
2312 $current_permalink = $ebook_id ? get_permalink($ebook_id) : '';
2313 $current_latest_order = $current_sales['latest_order_gmt'] !== '' ? human_time_diff(strtotime($current_sales['latest_order_gmt'] . ' UTC'), current_time('timestamp', true)) : '';
2314 $delivery_mode_label = __('Paid download', 'ebook-store');
2315 if ($donate_or_download === 'free') {
2316 $delivery_mode_label = __('Free download', 'ebook-store');
2317 } elseif ($donate_or_download === 'donate') {
2318 $delivery_mode_label = __('Donate to download', 'ebook-store');
2319 }
2320 $template_label = isset($form_templates[$selected_form_template]['label']) ? $form_templates[$selected_form_template]['label'] : ucfirst($selected_form_template);
2321 $cover_preview_url = !empty($img_cover['url']) ? $img_cover['url'] : '';
2322 $cover_preview_note = $cover_preview_url ? basename($cover_preview_url) : __('No cover uploaded yet', 'ebook-store');
2323 $template_override_settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2324 $enable_custom_confirmation_page = !empty($template_override_settings['enable_custom_confirmation_page']);
2325 $enable_custom_delivery_email = !empty($template_override_settings['enable_custom_delivery_email']);
2326 $custom_confirmation_page_content = isset($template_override_settings['confirmation_page_content']) ? $template_override_settings['confirmation_page_content'] : '';
2327 $custom_delivery_email_subject = isset($template_override_settings['delivery_email_subject']) ? $template_override_settings['delivery_email_subject'] : '';
2328 $custom_delivery_email_text = isset($template_override_settings['delivery_email_text']) ? $template_override_settings['delivery_email_text'] : '';
2329
2330 ob_start();
2331 ?>
2332 <script>var ebook_store_license_key = '<?php echo esc_js($license_key); ?>'; </script>
2333 <div class="ebook-admin-panel ebook-admin-panel--workspace">
2334 <section class="ebook-editor-hero">
2335 <div class="ebook-editor-hero__cover">
2336 <div class="ebook-editor-hero__cover-frame <?php echo $cover_preview_url ? 'has-image' : 'is-empty'; ?>">
2337 <?php if ($cover_preview_url) : ?>
2338 <img src="<?php echo esc_url($cover_preview_url); ?>" alt="<?php echo esc_attr($current_title); ?>">
2339 <?php else : ?>
2340 <span class="ebook-editor-hero__cover-placeholder"><?php esc_html_e('Cover preview', 'ebook-store'); ?></span>
2341 <?php endif; ?>
2342 </div>
2343 <span class="ebook-editor-hero__cover-meta"><?php echo esc_html($cover_preview_note); ?></span>
2344 </div>
2345 <div class="ebook-editor-hero__content">
2346 <span class="ebook-editor-hero__eyebrow"><?php esc_html_e('Ebook editor', 'ebook-store'); ?></span>
2347 <h2 class="ebook-editor-hero__title"><?php echo esc_html($current_title); ?></h2>
2348 <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>
2349 <div class="ebook-editor-hero__badges">
2350 <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>
2351 <span class="ebook-editor-badge"><?php esc_html_e('Mode', 'ebook-store'); ?> <strong><?php echo esc_html($delivery_mode_label); ?></strong></span>
2352 <span class="ebook-editor-badge"><?php esc_html_e('Template', 'ebook-store'); ?> <strong><?php echo esc_html($template_label); ?></strong></span>
2353 <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>
2354 <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>
2355 </div>
2356 </div>
2357 <div class="ebook-editor-hero__side">
2358 <div class="ebook-editor-hero__stat">
2359 <span><?php esc_html_e('Revenue', 'ebook-store'); ?></span>
2360 <strong><?php echo esc_html(ebook_store_order_money_display($current_sales['total'], $current_currency)); ?></strong>
2361 <small><?php echo esc_html(sprintf(__('Last 30 days: %s', 'ebook-store'), ebook_store_order_money_display($current_sales['last_30d'], $current_currency))); ?></small>
2362 </div>
2363 <div class="ebook-editor-hero__stat">
2364 <span><?php esc_html_e('Latest order', 'ebook-store'); ?></span>
2365 <strong><?php echo esc_html($current_latest_order ? sprintf(__('%s ago', 'ebook-store'), $current_latest_order) : __('No orders yet', 'ebook-store')); ?></strong>
2366 <small><?php printf(esc_html__('Upload max: %s', 'ebook-store'), esc_html($max_upload_size)); ?></small>
2367 </div>
2368 <?php if ($current_permalink) : ?>
2369 <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>
2370 <?php endif; ?>
2371 </div>
2372 </section>
2373
2374 <div class="ebook-editor-layout">
2375 <div class="ebook-editor-layout__main">
2376 <section class="ebook-card ebook-card--summary">
2377 <div class="ebook-card__header">
2378 <h3><?php esc_html_e('Embed & launch tools', 'ebook-store'); ?></h3>
2379 <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>
2380 </div>
2381 <div class="ebook-card__body">
2382 <div class="ebook-code-grid">
2383 <div class="ebook-code-block">
2384 <label for="ebook_code"><?php esc_html_e('Order form shortcode', 'ebook-store'); ?></label>
2385 <div class="ebook-code-field">
2386 <input id="ebook_code" onClick="this.select()" readonly type="text" class="regular-text code" value="<?php echo esc_attr($embed_shortcode); ?>" />
2387 <button type="button" class="button button-secondary ebook-code__copy" data-copy-target="#ebook_code"><?php esc_html_e('Copy', 'ebook-store'); ?></button>
2388 <span class="ebook-code__status" aria-live="polite"></span>
2389 </div>
2390 </div>
2391 <div class="ebook-code-block">
2392 <label for="ebook_code_buy"><?php esc_html_e('Buy button shortcode', 'ebook-store'); ?></label>
2393 <div class="ebook-code-field">
2394 <input id="ebook_code_buy" onClick="this.select()" readonly type="text" class="regular-text code" value="<?php echo esc_attr($buy_shortcode); ?>" />
2395 <button type="button" class="button button-secondary ebook-code__copy" data-copy-target="#ebook_code_buy"><?php esc_html_e('Copy', 'ebook-store'); ?></button>
2396 <span class="ebook-code__status" aria-live="polite"></span>
2397 </div>
2398 </div>
2399 </div>
2400 <ul class="ebook-inline-meta">
2401 <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>
2402 <li><?php esc_html_e('Use the buy-button shortcode when you only need a compact call to action.', 'ebook-store'); ?></li>
2403 </ul>
2404 <?php if ($conversion_hint) : ?>
2405 <div class="ebook-inline-hint"><?php echo wp_kses_post($conversion_hint); ?></div>
2406 <?php endif; ?>
2407 <?php if ($upgrade_text) : ?>
2408 <div class="ebook-alert ebook-alert--upgrade"><?php echo wp_kses_post($upgrade_text); ?></div>
2409 <?php endif; ?>
2410 </div>
2411 </section>
2412
2413 <section class="ebook-card ebook-card--delivery">
2414 <div class="ebook-card__header">
2415 <h3><?php esc_html_e('Pricing & delivery', 'ebook-store'); ?></h3>
2416 <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>
2417 </div>
2418 <div class="ebook-card__body">
2419 <div class="ebook-field-grid ebook-field-grid--tight">
2420 <div class="ebook-field">
2421 <label for="ebook_price"><?php esc_html_e('Price', 'ebook-store'); ?> <span class="required">*</span></label>
2422 <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']) : ''; ?>">
2423 <p class="description"><?php esc_html_e('For paid mode, zero is automatically adjusted to a minimum amount on save.', 'ebook-store'); ?></p>
2424 </div>
2425 <div class="ebook-field">
2426 <label for="ebook_paypal_currency"><?php esc_html_e('Currency', 'ebook-store'); ?></label>
2427 <select id="ebook_paypal_currency" name="ebook[paypal_currency]">
2428 <?php echo $paypal_currency_options; ?>
2429 </select>
2430 <p class="description"><?php esc_html_e('Leave this aligned with your store currency unless a title needs a specific override.', 'ebook-store'); ?></p>
2431 </div>
2432 <div class="ebook-field">
2433 <label for="ebook_form_template"><?php esc_html_e('Order form template', 'ebook-store'); ?></label>
2434 <select id="ebook_form_template" name="ebook[form_template]">
2435 <?php echo $form_template_options; ?>
2436 </select>
2437 <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>
2438 </div>
2439 </div>
2440 <div class="ebook-delivery-options">
2441 <label class="ebook-delivery-option">
2442 <input name="ebook[donate_or_download]" type="radio" value="paid" <?php checked($donate_or_download === 'paid' || $donate_or_download === ''); ?>>
2443 <div class="ebook-delivery-option__content">
2444 <span class="ebook-delivery-option__title"><?php esc_html_e('Paid download', 'ebook-store'); ?></span>
2445 <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>
2446 </div>
2447 </label>
2448 <label class="ebook-delivery-option goPro2">
2449 <input class="goPro2" name="ebook[donate_or_download]" type="radio" value="free" <?php checked($donate_or_download, 'free'); ?>>
2450 <div class="ebook-delivery-option__content">
2451 <span class="ebook-delivery-option__title"><?php esc_html_e('Free download', 'ebook-store'); ?></span>
2452 <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>
2453 </div>
2454 </label>
2455 <label class="ebook-delivery-option goPro2">
2456 <input class="goPro2" name="ebook[donate_or_download]" type="radio" value="donate" <?php checked($donate_or_download, 'donate'); ?>>
2457 <div class="ebook-delivery-option__content">
2458 <span class="ebook-delivery-option__title"><?php esc_html_e('Donate to download', 'ebook-store'); ?></span>
2459 <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>
2460 </div>
2461 </label>
2462 </div>
2463 </div>
2464 </section>
2465
2466 <section class="ebook-card ebook-card--files">
2467 <div class="ebook-card__header">
2468 <h3><?php esc_html_e('Files & formats', 'ebook-store'); ?></h3>
2469 <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>
2470 </div>
2471 <div class="ebook-card__body ebook-card__body--flush">
2472 <?php do_action('ebook_store_file_formats_form', $ebook_id); ?>
2473 </div>
2474 </section>
2475 </div>
2476
2477 <aside class="ebook-editor-layout__sidebar">
2478 <section class="ebook-card ebook-card--media">
2479 <div class="ebook-card__header">
2480 <h3><?php esc_html_e('Media & preview', 'ebook-store'); ?></h3>
2481 <p><?php esc_html_e('Manage the storefront visuals buyers see first: cover, spine image, and sample preview file.', 'ebook-store'); ?></p>
2482 </div>
2483 <div class="ebook-card__body">
2484 <div class="ebook-media-grid">
2485 <div class="ebook-media-field">
2486 <span class="ebook-media-label"><?php esc_html_e('Cover image', 'ebook-store'); ?></span>
2487 <div class="ebook_store_image_wrapper">
2488 <div class="ebook_store_image_preview cover-image <?php echo !empty($img_cover['url']) ? 'has-image' : 'empty'; ?>">
2489 <?php if (!empty($img_cover['url'])) : ?>
2490 <img src="<?php echo esc_url($img_cover['url']); ?>" alt="<?php echo esc_attr(basename($img_cover['url'])); ?>">
2491 <div class="remove-image"><span class="dashicons dashicons-no-alt"></span></div>
2492 <?php if ($cover_size) : ?>
2493 <div class="image-size"><?php echo esc_html($cover_size); ?></div>
2494 <?php endif; ?>
2495 <?php endif; ?>
2496 </div>
2497 <div class="ebook-media-controls">
2498 <input name="ebook_wp_custom_attachment_cover" type="file" accept="image/*">
2499 <span class="image-upload-hint"><?php esc_html_e('Recommended size: 180 x 260px', 'ebook-store'); ?></span>
2500 <?php if (!empty($img_cover['url'])) : ?>
2501 <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>
2502 <?php endif; ?>
2503 </div>
2504 </div>
2505 </div>
2506 <div class="ebook-media-field">
2507 <span class="ebook-media-label"><?php esc_html_e('Side image', 'ebook-store'); ?></span>
2508 <div class="ebook_store_image_wrapper">
2509 <div class="ebook_store_image_preview side-image <?php echo !empty($img_side_photo['url']) ? 'has-image' : 'empty'; ?>">
2510 <?php if (!empty($img_side_photo['url'])) : ?>
2511 <img src="<?php echo esc_url($img_side_photo['url']); ?>" alt="<?php echo esc_attr(basename($img_side_photo['url'])); ?>">
2512 <div class="remove-image"><span class="dashicons dashicons-no-alt"></span></div>
2513 <?php if ($side_size) : ?>
2514 <div class="image-size"><?php echo esc_html($side_size); ?></div>
2515 <?php endif; ?>
2516 <?php endif; ?>
2517 </div>
2518 <div class="ebook-media-controls">
2519 <input name="ebook_wp_custom_attachment_side_photo" type="file" accept="image/*">
2520 <span class="image-upload-hint"><?php esc_html_e('Recommended size: 20 x 260px', 'ebook-store'); ?></span>
2521 <?php if (!empty($img_side_photo['url'])) : ?>
2522 <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>
2523 <?php endif; ?>
2524 </div>
2525 </div>
2526 </div>
2527 <div class="ebook-media-field">
2528 <label for="ebook_wp_custom_attachment_preview"><?php esc_html_e('Preview file', 'ebook-store'); ?></label>
2529 <?php if (!empty($preview['url'])) : ?>
2530 <p class="ebook-media-current">
2531 <?php
2532 printf(
2533 esc_html__('Uploaded: %s', 'ebook-store'),
2534 '<a href="' . esc_url($preview['url']) . '" target="_blank">' . esc_html(basename($preview['url'])) . '</a>'
2535 );
2536 ?>
2537 <label class="ebook-inline-delete" title="<?php esc_attr_e('Delete', 'ebook-store'); ?>">
2538 <input type="checkbox" name="ebook_store_delete_preview">
2539 <span class="dashicons dashicons-trash"></span>
2540 <span class="screen-reader-text"><?php esc_html_e('Delete preview file', 'ebook-store'); ?></span>
2541 </label>
2542 </p>
2543 <?php else : ?>
2544 <p class="ebook-media-current muted"><?php esc_html_e('No preview file uploaded yet.', 'ebook-store'); ?></p>
2545 <?php endif; ?>
2546 <input type="file" id="ebook_wp_custom_attachment_preview" name="ebook_wp_custom_attachment_preview" value="" size="25">
2547 </div>
2548 </div>
2549 </div>
2550 </section>
2551
2552 <section class="ebook-card ebook-card--additional">
2553 <div class="ebook-card__header">
2554 <h3><?php esc_html_e('Catalog metadata', 'ebook-store'); ?></h3>
2555 <p><?php esc_html_e('Add publication details and relationships so the catalog stays complete and easier to browse.', 'ebook-store'); ?></p>
2556 </div>
2557 <div class="ebook-card__body">
2558 <div class="ebook-field-grid">
2559 <div class="ebook-field">
2560 <label for="ebook_date"><?php esc_html_e('Publication date', 'ebook-store'); ?></label>
2561 <input id="ebook_date" name="ebook[ebook_date]" type="text" value="<?php echo isset($ebook_meta['ebook_date']) ? esc_attr($ebook_meta['ebook_date']) : ''; ?>">
2562 </div>
2563 <div class="ebook-field">
2564 <label for="ebook_pages"><?php esc_html_e('Pages', 'ebook-store'); ?></label>
2565 <input id="ebook_pages" name="ebook[ebook_pages]" type="number" value="<?php echo isset($ebook_meta['ebook_pages']) ? esc_attr($ebook_meta['ebook_pages']) : ''; ?>">
2566 </div>
2567 </div>
2568 <div class="ebook-field">
2569 <label for="ebook_author_select"><?php esc_html_e('Author', 'ebook-store'); ?> (<?php esc_html_e('optional', 'ebook-store'); ?>)</label>
2570 <select id="ebook_author_select" name="ebook[ebook_author][]" multiple class="ebook-multiselect" size="5">
2571 <option value="0"><?php esc_html_e('None', 'ebook-store'); ?></option>
2572 <?php echo $ebook_authors; ?>
2573 </select>
2574 </div>
2575 <div class="ebook-field">
2576 <label for="ebook_publisher_select"><?php esc_html_e('Publisher', 'ebook-store'); ?> (<?php esc_html_e('optional', 'ebook-store'); ?>)</label>
2577 <select id="ebook_publisher_select" name="ebook[ebook_publisher][]" multiple class="ebook-multiselect" size="5">
2578 <option value="0"><?php esc_html_e('None', 'ebook-store'); ?></option>
2579 <?php echo $ebook_publishers; ?>
2580 </select>
2581 </div>
2582 <div class="ebook-field goPro2">
2583 <label for="ebook_bonus_select"><?php esc_html_e('Bonus ebook', 'ebook-store'); ?></label>
2584 <select id="ebook_bonus_select" class="goPro2 ebook-multiselect" multiple size="6" name="ebook[ebook_bonus][]">
2585 <?php echo $ebook_bonus_options; ?>
2586 </select>
2587 <p class="description"><?php esc_html_e('Optional related downloads bundled with the main title.', 'ebook-store'); ?></p>
2588 </div>
2589 <?php if ($extendhtml != get_the_ID()) : ?>
2590 <div class="ebook-card__extension">
2591 <?php echo $extendhtml; ?>
2592 </div>
2593 <?php endif; ?>
2594 </div>
2595 </section>
2596 </aside>
2597 </div>
2598 <section class="ebook-card ebook-card--template-overrides">
2599 <div class="ebook-card__header">
2600 <h3><?php esc_html_e('Confirmation overrides', 'ebook-store'); ?></h3>
2601 <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>
2602 </div>
2603 <div class="ebook-card__body">
2604 <div class="ebook-template-override-grid">
2605 <div class="ebook-template-override-card">
2606 <div class="ebook-field">
2607 <span class="ebook-template-override-card__label"><?php esc_html_e('Enable custom confirmation page', 'ebook-store'); ?></span>
2608 <div class="ebook-template-override-radios" role="radiogroup" aria-label="<?php esc_attr_e('Enable custom confirmation page', 'ebook-store'); ?>">
2609 <label class="ebook-template-override-radio">
2610 <input type="radio" name="ebook_store_enable_custom_confirmation_page" value="0" <?php checked(!$enable_custom_confirmation_page); ?>>
2611 <span><?php esc_html_e('Use Settings > Templates default', 'ebook-store'); ?></span>
2612 </label>
2613 <label class="ebook-template-override-radio">
2614 <input type="radio" name="ebook_store_enable_custom_confirmation_page" value="1" <?php checked($enable_custom_confirmation_page); ?>>
2615 <span><?php esc_html_e('Enable for this ebook', 'ebook-store'); ?></span>
2616 </label>
2617 </div>
2618 <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>
2619 </div>
2620 <div class="ebook-template-override-fields" data-override-fields="confirmation" <?php echo !$enable_custom_confirmation_page ? 'hidden' : ''; ?>>
2621 <div class="ebook-field">
2622 <label for="ebook_store_custom_confirmation_page_content_editor"><?php esc_html_e('Custom confirmation page content', 'ebook-store'); ?></label>
2623 <?php
2624 wp_editor(
2625 $custom_confirmation_page_content,
2626 'ebook_store_custom_confirmation_page_content_editor',
2627 array(
2628 'textarea_name' => 'ebook_store_custom_confirmation_page_content',
2629 'textarea_rows' => 10,
2630 )
2631 );
2632 ?>
2633 </div>
2634 </div>
2635 </div>
2636 <div class="ebook-template-override-card">
2637 <div class="ebook-field">
2638 <span class="ebook-template-override-card__label"><?php esc_html_e('Enable custom delivery email', 'ebook-store'); ?></span>
2639 <div class="ebook-template-override-radios" role="radiogroup" aria-label="<?php esc_attr_e('Enable custom delivery email', 'ebook-store'); ?>">
2640 <label class="ebook-template-override-radio">
2641 <input type="radio" name="ebook_store_enable_custom_delivery_email" value="0" <?php checked(!$enable_custom_delivery_email); ?>>
2642 <span><?php esc_html_e('Use Settings > Templates default', 'ebook-store'); ?></span>
2643 </label>
2644 <label class="ebook-template-override-radio">
2645 <input type="radio" name="ebook_store_enable_custom_delivery_email" value="1" <?php checked($enable_custom_delivery_email); ?>>
2646 <span><?php esc_html_e('Enable for this ebook', 'ebook-store'); ?></span>
2647 </label>
2648 </div>
2649 <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>
2650 </div>
2651 <div class="ebook-template-override-fields" data-override-fields="email" <?php echo !$enable_custom_delivery_email ? 'hidden' : ''; ?>>
2652 <div class="ebook-field">
2653 <label for="ebook_store_custom_delivery_email_subject"><?php esc_html_e('Custom delivery email subject', 'ebook-store'); ?></label>
2654 <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); ?>">
2655 </div>
2656 <div class="ebook-field">
2657 <label for="ebook_store_custom_delivery_email_text_editor"><?php esc_html_e('Custom delivery email content', 'ebook-store'); ?></label>
2658 <?php
2659 wp_editor(
2660 $custom_delivery_email_text,
2661 'ebook_store_custom_delivery_email_text_editor',
2662 array(
2663 'textarea_name' => 'ebook_store_custom_delivery_email_text',
2664 'textarea_rows' => 10,
2665 )
2666 );
2667 ?>
2668 </div>
2669 </div>
2670 </div>
2671 </div>
2672 </div>
2673 </section>
2674 </div>
2675 <script>
2676 jQuery(document).ready(function() {
2677 jQuery('#ebook_date').datepicker({
2678 dateFormat : 'dd-mm-yy'
2679 });
2680 jQuery('#wp-admin-bar-view').hide();
2681 jQuery('input[type=file]').show();
2682
2683 function ebookStoreToggleOverrideFields(groupName, targetSelector) {
2684 var enabled = jQuery('input[name="' + groupName + '"]:checked').val() === '1';
2685 var $target = jQuery(targetSelector);
2686 if (!$target.length) {
2687 return;
2688 }
2689
2690 $target.find('input, textarea, select, button').prop('disabled', !enabled);
2691 if (enabled) {
2692 $target.prop('hidden', false).stop(true, true).slideDown(160);
2693 } else {
2694 $target.stop(true, true).slideUp(160, function() {
2695 $target.prop('hidden', true);
2696 });
2697 }
2698 }
2699
2700 function ebookStoreRefreshOverrideRadioState(groupName) {
2701 var $inputs = jQuery('input[name="' + groupName + '"]');
2702 $inputs.each(function() {
2703 var $input = jQuery(this);
2704 $input.closest('.ebook-template-override-radio').toggleClass('is-selected', $input.is(':checked'));
2705 });
2706 }
2707
2708 ebookStoreToggleOverrideFields('ebook_store_enable_custom_confirmation_page', '[data-override-fields="confirmation"]');
2709 ebookStoreToggleOverrideFields('ebook_store_enable_custom_delivery_email', '[data-override-fields="email"]');
2710 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_confirmation_page');
2711 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_delivery_email');
2712
2713 jQuery('input[name="ebook_store_enable_custom_confirmation_page"]').on('change', function() {
2714 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_confirmation_page');
2715 ebookStoreToggleOverrideFields('ebook_store_enable_custom_confirmation_page', '[data-override-fields="confirmation"]');
2716 });
2717
2718 jQuery('input[name="ebook_store_enable_custom_delivery_email"]').on('change', function() {
2719 ebookStoreRefreshOverrideRadioState('ebook_store_enable_custom_delivery_email');
2720 ebookStoreToggleOverrideFields('ebook_store_enable_custom_delivery_email', '[data-override-fields="email"]');
2721 });
2722 });
2723 </script>
2724 <?php
2725 $editor_output = ob_get_clean();
2726 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $editor_output ) : $editor_output;
2727 } // end ebook_wp_custom_attachment
2728
2729 function ebook_store_get_default_template_content_settings() {
2730 $QSWPOptions = new QSWPOptions();
2731
2732 return array(
2733 'confirmation_page_content' => get_option('thankyou_page', $QSWPOptions->thankyou_page),
2734 'delivery_email_subject' => get_option('email_delivery_subject', $QSWPOptions->email_delivery_subject),
2735 'delivery_email_text' => get_option('email_delivery_text', $QSWPOptions->email_delivery_text),
2736 );
2737 }
2738
2739 function ebook_store_get_ebook_template_override_settings($ebook_id) {
2740 $ebook_id = absint($ebook_id);
2741 $defaults = ebook_store_get_default_template_content_settings();
2742 $custom_confirmation_page_content = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_confirmation_page_content', true) : '';
2743 $custom_delivery_email_subject = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_delivery_email_subject', true) : '';
2744 $custom_delivery_email_text = $ebook_id > 0 ? (string) get_post_meta($ebook_id, 'ebook_store_custom_delivery_email_text', true) : '';
2745
2746 return array(
2747 'enable_custom_confirmation_page' => $ebook_id > 0 && (string) get_post_meta($ebook_id, 'ebook_store_enable_custom_confirmation_page', true) === '1',
2748 'enable_custom_delivery_email' => $ebook_id > 0 && (string) get_post_meta($ebook_id, 'ebook_store_enable_custom_delivery_email', true) === '1',
2749 'confirmation_page_content' => $custom_confirmation_page_content !== '' ? $custom_confirmation_page_content : $defaults['confirmation_page_content'],
2750 'delivery_email_subject' => $custom_delivery_email_subject !== '' ? $custom_delivery_email_subject : $defaults['delivery_email_subject'],
2751 'delivery_email_text' => $custom_delivery_email_text !== '' ? $custom_delivery_email_text : $defaults['delivery_email_text'],
2752 'default_confirmation_page_content' => $defaults['confirmation_page_content'],
2753 'default_delivery_email_subject' => $defaults['delivery_email_subject'],
2754 'default_delivery_email_text' => $defaults['delivery_email_text'],
2755 );
2756 }
2757
2758 function ebook_store_get_effective_confirmation_page_content($ebook_id) {
2759 $settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2760
2761 if (!empty($settings['enable_custom_confirmation_page']) && trim((string) $settings['confirmation_page_content']) !== '') {
2762 return (string) $settings['confirmation_page_content'];
2763 }
2764
2765 return (string) $settings['default_confirmation_page_content'];
2766 }
2767
2768 function ebook_store_get_effective_delivery_email_template($ebook_id) {
2769 $settings = ebook_store_get_ebook_template_override_settings($ebook_id);
2770
2771 if (!empty($settings['enable_custom_delivery_email'])) {
2772 return array(
2773 'subject' => trim((string) $settings['delivery_email_subject']) !== '' ? (string) $settings['delivery_email_subject'] : (string) $settings['default_delivery_email_subject'],
2774 'text' => trim((string) $settings['delivery_email_text']) !== '' ? (string) $settings['delivery_email_text'] : (string) $settings['default_delivery_email_text'],
2775 );
2776 }
2777
2778 return array(
2779 'subject' => (string) $settings['default_delivery_email_subject'],
2780 'text' => (string) $settings['default_delivery_email_text'],
2781 );
2782 }
2783
2784 function ebook_set_upload_dir( $upload ) {
2785 //wp_error(print_r($upload,true));
2786 // Override the year / month being based on the post publication date, if year/month organization is enabled
2787 if ( get_option( 'uploads_use_yearmonth_folders' ) ) {
2788 // Generate the yearly and monthly dirs
2789 $time = current_time( 'mysql' );
2790 $y = substr( $time, 0, 4 );
2791 $m = substr( $time, 5, 2 );
2792 $upload['subdir'] = "/$y/$m";
2793 }
2794
2795 $upload['subdir'] = '/ebooks' . $upload['subdir'];
2796 $upload['path'] = $upload['basedir'] . $upload['subdir'];
2797 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
2798 // The master files a customer paid for live here. Protection is written
2799 // unconditionally: it used to be REMOVED whenever the WooCommerce PDF reader
2800 // option was on, which published every paid ebook at a guessable URL.
2801 ebook_store_protect_uploads_dir( $upload['basedir'] . '/ebooks' );
2802
2803 return $upload;
2804 }
2805
2806 /**
2807 * Make an upload directory unreadable over HTTP.
2808 *
2809 * Writes both the modern mod_authz_core directive and the 2.2-era fallback, plus
2810 * an index.php so a server with directory indexes turned on still shows nothing.
2811 *
2812 * This is Apache/LiteSpeed only. On nginx the equivalent location block has to
2813 * be added to the server config, which is why the download handler streams files
2814 * through PHP rather than relying on the directory rules alone.
2815 *
2816 * @param string $dir Absolute path.
2817 */
2818 function ebook_store_protect_uploads_dir( $dir ) {
2819 if ( $dir === '' ) {
2820 return;
2821 }
2822
2823 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
2824 return;
2825 }
2826
2827 $htaccess = $dir . '/.htaccess';
2828
2829 if ( ! file_exists( $htaccess ) ) {
2830 @file_put_contents(
2831 $htaccess,
2832 "Options -Indexes\n"
2833 . "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n"
2834 . "<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n"
2835 );
2836 }
2837
2838 if ( ! file_exists( $dir . '/index.php' ) ) {
2839 @file_put_contents( $dir . '/index.php', "<?php\n// Silence is golden.\n" );
2840 }
2841
2842 // A previous version renamed the rules out of the way instead of deleting
2843 // them; clean that up so the directory is not left half protected.
2844 $backup = $dir . '/.htaccess-backup';
2845 if ( file_exists( $backup ) ) {
2846 @unlink( $backup );
2847 }
2848 }
2849
2850 /**
2851 * Ensure the protected upload directories exist and are locked down.
2852 *
2853 * Runs on activation and on upgrade, so protection no longer depends on somebody
2854 * happening to save an ebook.
2855 */
2856 function ebook_store_ensure_protected_dirs() {
2857 $uploads = wp_upload_dir();
2858
2859 if ( ! empty( $uploads['error'] ) ) {
2860 return;
2861 }
2862
2863 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebooks' );
2864 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook_store_data' );
2865 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook-store-cache' );
2866 ebook_store_protect_uploads_dir( trailingslashit( $uploads['basedir'] ) . 'ebook-store-logs' );
2867 }
2868 function ebook_set_upload_dir_preview( $upload ) {
2869
2870 // Override the year / month being based on the post publication date, if year/month organization is enabled
2871 if ( get_option( 'uploads_use_yearmonth_folders' ) ) {
2872 // Generate the yearly and monthly dirs
2873 $time = current_time( 'mysql' );
2874 $y = substr( $time, 0, 4 );
2875 $m = substr( $time, 5, 2 );
2876 $upload['subdir'] = "/$y/$m";
2877 }
2878
2879 $upload['subdir'] = '/ebooks_misc' . $upload['subdir'];
2880 $upload['path'] = $upload['basedir'] . $upload['subdir'];
2881 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
2882 return $upload;
2883 }
2884 function ebook_store_destroy_image_resource($image) {
2885 if (is_resource($image)) {
2886 imagedestroy($image);
2887 return;
2888 }
2889
2890 if (class_exists('GdImage') && $image instanceof GdImage) {
2891 imagedestroy($image);
2892 }
2893 }
2894 function ebook_store_prepare_cover_for_upload($tmp_name) {
2895 if (!file_exists($tmp_name)) {
2896 return false;
2897 }
2898
2899 $image = @imagecreatefromjpeg($tmp_name);
2900 if (!$image) {
2901 $image = @imagecreatefrompng($tmp_name);
2902 }
2903 if (!$image) {
2904 $image = @imagecreatefromgif($tmp_name);
2905 }
2906
2907 $size = @getimagesize($tmp_name);
2908 if (!$image || !$size) {
2909 ebook_store_destroy_image_resource($image);
2910 return false;
2911 }
2912
2913 list($width, $height) = $size;
2914
2915 if ($width != 180 || $height != 260) {
2916 $image_p = imagecreatetruecolor(180, 260);
2917 imagecopyresampled($image_p, $image, 0, 0, 0, 0, 180, 260, $width, $height);
2918 imagejpeg($image_p, $tmp_name, 100);
2919 imagedestroy($image_p);
2920 }
2921
2922 ebook_store_destroy_image_resource($image);
2923 return true;
2924 }
2925 function ebook_store_generate_cover_from_pdf($post_id) {
2926 $existing_cover = get_post_meta($post_id, 'ebook_wp_custom_attachment_cover', true);
2927 if (is_array($existing_cover) && !empty($existing_cover['url'])) {
2928 return;
2929 }
2930 if (!class_exists('Imagick')) {
2931 return;
2932 }
2933
2934 $pdf_meta = get_post_meta($post_id, 'ebook_wp_custom_attachment_pdf', true);
2935 if (!is_array($pdf_meta) || empty($pdf_meta['file'])) {
2936 $pdf_meta = get_post_meta($post_id, 'ebook_wp_custom_attachment', true);
2937 }
2938 if (!is_array($pdf_meta) || empty($pdf_meta['file'])) {
2939 return;
2940 }
2941
2942 $pdf_path = $pdf_meta['file'];
2943 if (!file_exists($pdf_path)) {
2944 return;
2945 }
2946
2947 try {
2948 $imagick = new Imagick();
2949 $imagick->setResolution(150, 150);
2950 $imagick->readImage($pdf_path . '[0]');
2951 $imagick->setImageFormat('jpg');
2952 $imagick->setImageCompression(Imagick::COMPRESSION_JPEG);
2953 $imagick->setImageCompressionQuality(90);
2954 $image_blob = $imagick->getImageBlob();
2955 $imagick->clear();
2956 $imagick->destroy();
2957 } catch (Exception $e) {
2958 error_log('Ebook Store: Unable to auto-create cover image. ' . $e->getMessage());
2959 return;
2960 }
2961
2962 $temp_file = wp_tempnam('ebook-store-cover');
2963 if (!$temp_file) {
2964 return;
2965 }
2966 file_put_contents($temp_file, $image_blob);
2967
2968 ebook_store_prepare_cover_for_upload($temp_file);
2969
2970 $base_name = basename($pdf_path);
2971 $base_name = preg_replace('/\.[^.]+$/', '', $base_name);
2972 if ($base_name === '' || $base_name === null) {
2973 $base_name = 'ebook-cover';
2974 }
2975 $cover_filename = $base_name . '-autocover.jpg';
2976
2977 $upload = wp_upload_bits($cover_filename, null, file_get_contents($temp_file));
2978 @unlink($temp_file);
2979
2980 if(isset($upload['error']) && $upload['error'] != 0) {
2981 error_log('Ebook Store: Auto cover upload error. ' . $upload['error']);
2982 return;
2983 }
2984
2985 add_post_meta($post_id, 'ebook_wp_custom_attachment_cover', $upload, true);
2986 update_post_meta($post_id, 'ebook_wp_custom_attachment_cover', $upload);
2987 }
2988 /**
2989 * Is this upload one of the types the field accepts?
2990 *
2991 * Every upload branch in save_custom_meta_data() used to compute the allowed
2992 * types and then test `if(1)`, so the check never ran and the else-branch that
2993 * rejects the file was dead code.
2994 *
2995 * @param array $file One entry from $_FILES.
2996 * @param array $supported_types Allowed MIME types.
2997 * @return bool
2998 */
2999 function ebook_store_upload_type_is_allowed( $file, $supported_types ) {
3000 if ( empty( $file['name'] ) ) {
3001 return false;
3002 }
3003
3004 $supported_types = (array) $supported_types;
3005
3006 $ext = strtolower( (string) pathinfo( $file['name'], PATHINFO_EXTENSION ) );
3007
3008 if ( $ext === '' ) {
3009 return false;
3010 }
3011
3012 // Cover images and previews come through this same function, so the
3013 // content-aware branch below is scoped to the ebook formats it understands.
3014 $ebook_formats = array( 'pdf', 'epub', 'mobi', 'txt', 'zip', 'mp3', 'mp4' );
3015
3016 if ( in_array( $ext, $ebook_formats, true ) ) {
3017 // The extension must be one this site is allowed to upload — the same
3018 // gate the chunked uploader applies, so Pro formats stay gated on both
3019 // paths.
3020 if ( function_exists( 'ebook_store_get_allowed_upload_formats' )
3021 && ! in_array( $ext, (array) ebook_store_get_allowed_upload_formats(), true ) ) {
3022 return false;
3023 }
3024
3025 // Judge the contents rather than the declared MIME type.
3026 //
3027 // This used to lean on wp_check_filetype_and_ext(), which returns
3028 // ext => false, type => false for a genuine .epub: fileinfo reports
3029 // application/epub+zip while WordPress's map said otherwise, and the
3030 // mismatch made it refuse the file. Every valid ePub was rejected with
3031 // "that file type is not allowed here". The content check is both
3032 // friendlier to real books and stricter about disguised ones.
3033 if ( ! empty( $file['tmp_name'] ) && is_readable( $file['tmp_name'] ) && function_exists( 'ebook_store_validate_uploaded_ebook' ) ) {
3034 return ! is_wp_error( ebook_store_validate_uploaded_ebook( $file['tmp_name'], $ext ) );
3035 }
3036 }
3037
3038 // Images, previews, or no readable temp file to inspect: match the declared
3039 // type against the field's list, and still reject a claimed extension that
3040 // disagrees with the real contents.
3041 $checked = function_exists( 'wp_check_filetype_and_ext' ) && ! empty( $file['tmp_name'] ) && is_uploaded_file( $file['tmp_name'] )
3042 ? wp_check_filetype_and_ext( $file['tmp_name'], $file['name'] )
3043 : wp_check_filetype( basename( $file['name'] ) );
3044
3045 $type = ! empty( $checked['type'] ) ? $checked['type'] : '';
3046
3047 if ( $type === '' || ! empty( $checked['proper_filename'] ) ) {
3048 return false;
3049 }
3050
3051 return in_array( $type, $supported_types, true );
3052 }
3053
3054 /**
3055 * Which ebook file formats may this site upload?
3056 *
3057 * PDF is the free format. The other formats are the flagship Pro feature, and
3058 * until now the only thing stopping a free install from using them was a CSS
3059 * class and a disabled attribute — the save handler accepted them regardless.
3060 *
3061 * @return array
3062 */
3063 function ebook_store_get_allowed_upload_formats() {
3064 $formats = array( 'pdf' );
3065
3066 if ( ebook_store_has_pro_license() ) {
3067 $formats = array_merge( $formats, array( 'mobi', 'txt', 'epub', 'zip', 'mp3', 'mp4' ) );
3068 }
3069
3070 return apply_filters( 'ebook_store_allowed_upload_formats', $formats );
3071 }
3072
3073 function save_custom_meta_data($id) {
3074 add_filter( 'upload_dir', 'ebook_set_upload_dir' );
3075 /* --- security verification --- */
3076 if(!wp_verify_nonce(@$_POST['ebook_wp_custom_attachment_nonce'], plugin_basename(__FILE__))) {
3077 return $id;
3078 } // end if
3079
3080 if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
3081 return $id;
3082 } // end if
3083
3084 // 'edit_post' is the correct meta capability for these custom post types.
3085 // The old code checked 'edit_page' in BOTH branches, so authorisation for
3086 // ebooks and orders was decided by page permissions.
3087 if ( ! current_user_can( 'edit_post', $id ) ) {
3088 return $id;
3089 }
3090 /* - end security verification - */
3091 if (strpos($_POST['content'], '[ebook_store') !== false) {
3092 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.");
3093 }
3094 if ($_POST['ebook']['donate_or_download'] != 'free' && $_POST['ebook']['ebook_price'] == 0) {
3095 $_POST['ebook']['ebook_price'] = 0.01;
3096 }
3097 if ( isset( $_POST['ebook']['form_template'] ) ) {
3098 $_POST['ebook']['form_template'] = ebook_store_resolve_form_template( $_POST['ebook']['form_template'] );
3099 } else {
3100 $_POST['ebook']['form_template'] = ebook_store_resolve_form_template( '' );
3101 }
3102 // "Free" and "Donate" delivery, and bonus books, are Pro features. They
3103 // were previously gated only by a CSS class, so a free install could set
3104 // them by posting the form directly.
3105 if ( ! ebook_store_has_pro_license() ) {
3106 if ( isset( $_POST['ebook']['donate_or_download'] ) && $_POST['ebook']['donate_or_download'] !== 'paid' ) {
3107 $_POST['ebook']['donate_or_download'] = 'paid';
3108 }
3109 $_POST['ebook_bonus'] = array();
3110 }
3111
3112 update_post_meta($id, 'ebook', $_POST['ebook']);
3113 update_post_meta($id, 'ebook_bonus', isset( $_POST['ebook_bonus'] ) ? (array) $_POST['ebook_bonus'] : array());
3114 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');
3115 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');
3116 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'] ) ) : '');
3117 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'] ) ) : '');
3118 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'] ) ) : '');
3119
3120 // Make sure the file array isn't empty
3121
3122 if(!empty($_FILES['ebook_wp_custom_attachment_pdf']['name'])) {
3123 $_FILES['ebook_wp_custom_attachment'] = $_FILES['ebook_wp_custom_attachment_pdf'];
3124 // Setup the array of supported file types. In this case, it's just PDF.
3125 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3126
3127 // Get the file type of the upload
3128 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment']['name']));
3129 $uploaded_type = $arr_file_type['type'];
3130
3131 // Check if the type is supported. If not, throw an error.
3132 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment'], $supported_types ) ) {
3133
3134 // Use the WordPress API to upload the file
3135 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment']['tmp_name']));
3136
3137 if(isset($upload['error']) && $upload['error'] != 0) {
3138 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3139 } else {
3140 add_post_meta($id, 'ebook_wp_custom_attachment', $upload,true);
3141 update_post_meta($id, 'ebook_wp_custom_attachment', wp_slash($upload));
3142 update_post_meta($id, 'ebook_wp_custom_attachment_pdf', wp_slash($upload));
3143 } // end if/else
3144
3145 } else {
3146 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' ) );
3147 } // end if/else
3148
3149 } // end if
3150 $loop = array( 'mobi', 'txt', 'epub', 'zip', 'mp3', 'mp4' );
3151 $allowed_formats = ebook_store_get_allowed_upload_formats();
3152 foreach ($loop as $l) {
3153 // Enforced here, on the server, rather than only in the browser.
3154 if ( ! in_array( $l, $allowed_formats, true ) ) {
3155 continue;
3156 }
3157 if(!empty($_FILES['ebook_wp_custom_attachment_' . $l]['name'])) {
3158
3159 //wp_die(print_r($_FILES['ebook_wp_custom_attachment_' . $l],true));
3160 // Setup the array of supported file types. In this case, it's just PDF.
3161 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3162
3163 // Get the file type of the upload
3164 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_' . $l]['name']));
3165 $uploaded_type = $arr_file_type['type'];
3166
3167 // Check if the type is supported. If not, throw an error.
3168 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_' . $l], $supported_types ) ) {
3169
3170 // Use the WordPress API to upload the file
3171 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_' . $l]['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_' . $l]['tmp_name']));
3172 //wp_die(print_r($upload,true));
3173 if(isset($upload['error']) && $upload['error'] != 0) {
3174 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3175 } else {
3176 add_post_meta($id, 'ebook_wp_custom_attachment_' . $l, wp_slash($upload), true);
3177 update_post_meta($id, 'ebook_wp_custom_attachment_' . $l, wp_slash($upload));
3178 } // end if/else
3179
3180 } else {
3181 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' ) );
3182 } // end if/else
3183
3184 } // end if
3185 }
3186 //separate loop for deleting ebooks
3187 $loop = array('pdf','mobi','txt','epub','zip', 'mp3', 'mp4');
3188 foreach ($loop as $l) {
3189 $li = 'ebook_store_delete_' . $l;
3190 if (isset($_REQUEST[$li])) {
3191 $file_meta = get_post_meta($id, 'ebook_wp_custom_attachment_' . $l, true);
3192 unlink($file_meta['file']);
3193 delete_post_meta($id, 'ebook_wp_custom_attachment_' . $l);
3194 if ($l == 'pdf') {
3195 delete_post_meta($id, 'ebook_wp_custom_attachment');
3196 }
3197 }
3198 }
3199 //wp_die(print_r($_REQUEST));
3200 if ($_REQUEST['ebook_store_delete_preview'] == 'on') {
3201 //wp_die('delete preview');
3202 delete_post_meta($id, 'ebook_wp_custom_attachment_preview');
3203 }
3204 //separate loop for deleting ebooks - end
3205 add_filter( 'upload_dir', 'ebook_set_upload_dir_preview' );
3206
3207 if(!empty($_FILES['ebook_wp_custom_attachment_preview']['name'])) {
3208
3209 // Setup the array of supported file types. In this case, it's just PDF.
3210 $supported_types = array('application/pdf','application/x-mobipocket-ebook','application/epub+zip','application/zip','application/octet-stream');
3211
3212 // Get the file type of the upload
3213 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_preview']['name']));
3214 $uploaded_type = $arr_file_type['type'];
3215
3216 // Check if the type is supported. If not, throw an error.
3217 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_preview'], $supported_types ) ) {
3218
3219 // Use the WordPress API to upload the file
3220 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_preview']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_preview']['tmp_name']));
3221
3222 if(isset($upload['error']) && $upload['error'] != 0) {
3223 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3224 } else {
3225 add_post_meta($id, 'ebook_wp_custom_attachment_preview', $upload);
3226 update_post_meta($id, 'ebook_wp_custom_attachment_preview', $upload);
3227 } // end if/else
3228
3229 } else {
3230 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' ) );
3231 } // end if/else
3232
3233 } // end if
3234
3235
3236 //check the side photo
3237 if(!empty($_FILES['ebook_wp_custom_attachment_side_photo']['name'])) {
3238
3239 // Setup the array of supported file types. In this case, it's just PDF.
3240 $supported_types = array('image/jpeg','image/gif','image/png','image/svg+xml');
3241
3242 // Get the file type of the upload
3243 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_side_photo']['name']));
3244 $uploaded_type = $arr_file_type['type'];
3245
3246 // Check if the type is supported. If not, throw an error.
3247 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_side_photo'], $supported_types ) ) {
3248
3249 // Use the WordPress API to upload the file
3250 $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']));
3251
3252 if(isset($upload['error']) && $upload['error'] != 0) {
3253 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3254 } else {
3255 add_post_meta($id, 'ebook_wp_custom_attachment_side_photo', $upload);
3256 update_post_meta($id, 'ebook_wp_custom_attachment_side_photo', $upload);
3257 } // end if/else
3258
3259 } else {
3260 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' ) );
3261 } // end if/else
3262
3263 } // end if
3264 //cover photo
3265 if(!empty($_FILES['ebook_wp_custom_attachment_cover']['name'])) {
3266 $filename = $_FILES['ebook_wp_custom_attachment_cover']['tmp_name'];
3267 // Setup the array of supported file types. In this case, it's just PDF.
3268 $supported_types = array('image/jpeg','image/gif','image/png','image/svg+xml');
3269
3270 // Get the file type of the upload
3271 $arr_file_type = wp_check_filetype(basename($_FILES['ebook_wp_custom_attachment_cover']['name']));
3272 $uploaded_type = $arr_file_type['type'];
3273 // Check if the type is supported. If not, throw an error.
3274 if ( ebook_store_upload_type_is_allowed( $_FILES['ebook_wp_custom_attachment_cover'], $supported_types ) ) {
3275
3276 // Use the WordPress API to upload the file
3277 $img_tmp_name = $_FILES['ebook_wp_custom_attachment_cover']['tmp_name'];
3278 ebook_store_prepare_cover_for_upload($img_tmp_name);
3279 $upload = wp_upload_bits($_FILES['ebook_wp_custom_attachment_cover']['name'], null, file_get_contents($_FILES['ebook_wp_custom_attachment_cover']['tmp_name']));
3280
3281 if(isset($upload['error']) && $upload['error'] != 0) {
3282 wp_die( esc_html( sprintf( /* translators: %s: error message from WordPress. */ __( 'The file could not be uploaded: %s', 'ebook-store' ), $upload['error'] ) ) );
3283 } else {
3284 add_post_meta($id, 'ebook_wp_custom_attachment_cover', $upload);
3285 update_post_meta($id, 'ebook_wp_custom_attachment_cover', $upload);
3286 } // end if/else
3287
3288 } else {
3289 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' ) );
3290 } // end if/else
3291
3292 } // end if
3293
3294 if(!empty($_FILES['ebook_wp_custom_attachment_pdf']['name'])) {
3295 ebook_store_generate_cover_from_pdf($id);
3296 }
3297
3298
3299
3300 } // end save_custom_meta_data
3301
3302 function ebook_update_edit_form() {
3303 echo ' enctype="multipart/form-data"';
3304 } // end ebook_update_edit_form
3305 function ebook_download_link($ebook_order, $free = false, $bonus = false, $bonus_ebook_id = null) {
3306 $action_name = ($free == 0) ? 'download' : 'download_free';
3307
3308 $ebook_key = isset($ebook_order['ebook_key'][0]) ? esc_attr($ebook_order['ebook_key'][0]) : '';
3309 $md5_nonce = isset($ebook_order['md5_nonce'][0]) ? esc_attr($ebook_order['md5_nonce'][0]) : '';
3310 $ebook_id = isset($ebook_order['ebook'][0]) ? absint($ebook_order['ebook'][0]) : 0;
3311 $order_id = isset($ebook_order['order_id'][0]) ? absint($ebook_order['order_id'][0]) : ( isset($ebook_order['order_id']) ? absint($ebook_order['order_id']) : 0 );
3312
3313 $args = array(
3314 'ebook_key' => $ebook_key,
3315 'action' => $action_name,
3316 'md5_nonce' => $md5_nonce,
3317 );
3318
3319 if ( $order_id > 0 ) {
3320 $args['order_id'] = $order_id;
3321 }
3322
3323 $link = add_query_arg($args, get_permalink($ebook_id));
3324 $link = remove_query_arg('p', $link);
3325
3326 if ($action_name === 'download_free') {
3327 $link = add_query_arg(array('p' => $ebook_id), $link);
3328 }
3329
3330 $post = get_post($ebook_id);
3331 if ($post) {
3332 $slug = sanitize_title($post->post_name);
3333 $link = add_query_arg(array('ebook' => $slug), $link);
3334 }
3335
3336 if ($bonus > 0) {
3337 $link = add_query_arg(array('type' => 'bonus'), $link);
3338 $link = add_query_arg(array('ebook_id' => $bonus_ebook_id), $link);
3339 $link = add_query_arg(array('order_id' => $ebook_order['order_id'][0]), $link);
3340 }
3341
3342 return esc_url($link);
3343 }
3344 function humanFileSize($size,$unit="") {
3345 if( (!$unit && $size >= 1<<30) || $unit == "GB")
3346 return number_format($size/(1<<30),2)."GB";
3347 if( (!$unit && $size >= 1<<20) || $unit == "MB")
3348 return number_format($size/(1<<20),2)."MB";
3349 if( (!$unit && $size >= 1<<10) || $unit == "KB")
3350 return number_format($size/(1<<10),2)."KB";
3351 return number_format($size)." bytes";
3352 }
3353
3354 function ebook_store_get_file_format_labels() {
3355 return array(
3356 'pdf' => 'PDF',
3357 'epub' => 'EPUB',
3358 'mobi' => 'MOBI',
3359 'txt' => 'TXT',
3360 'zip' => 'ZIP',
3361 'mp3' => 'MP3',
3362 'mp4' => 'MP4',
3363 );
3364 }
3365
3366 function ebook_store_get_ebook_file_assets( $ebook_id ) {
3367 $formats = array( 'pdf', 'epub', 'mobi', 'txt', 'zip', 'mp3', 'mp4' );
3368 $assets = array();
3369
3370 foreach ( $formats as $format ) {
3371 $meta_key = $format === 'pdf' ? 'ebook_wp_custom_attachment_pdf' : 'ebook_wp_custom_attachment_' . $format;
3372 $file_meta = get_post_meta( $ebook_id, $meta_key, true );
3373 if ( $format === 'pdf' && ( ! is_array( $file_meta ) || empty( $file_meta['file'] ) ) ) {
3374 $file_meta = get_post_meta( $ebook_id, 'ebook_wp_custom_attachment', true );
3375 }
3376
3377 if ( ! is_array( $file_meta ) || empty( $file_meta['file'] ) ) {
3378 continue;
3379 }
3380
3381 $assets[ $format ] = $file_meta;
3382 }
3383
3384 return $assets;
3385 }
3386
3387 function ebook_store_get_ebook_file_facts( $ebook_id ) {
3388 $assets = ebook_store_get_ebook_file_assets( $ebook_id );
3389 $labels = ebook_store_get_file_format_labels();
3390 $formats = array();
3391 $total_size = 0;
3392
3393 foreach ( $assets as $format => $asset ) {
3394 $formats[] = isset( $labels[ $format ] ) ? $labels[ $format ] : strtoupper( $format );
3395 if ( ! empty( $asset['file'] ) && file_exists( $asset['file'] ) ) {
3396 $total_size += (int) filesize( $asset['file'] );
3397 }
3398 }
3399
3400 if ( empty( $formats ) ) {
3401 return array();
3402 }
3403
3404 $facts = array();
3405 $facts[] = sprintf( __( 'Formats: %s', 'ebook-store' ), implode( ', ', $formats ) );
3406
3407 if ( $total_size > 0 ) {
3408 $facts[] = sprintf( __( 'Total download size: %s', 'ebook-store' ), humanFileSize( $total_size ) );
3409 }
3410
3411 $facts[] = sprintf(
3412 /* translators: %d is the number of included files */
3413 _n( '%d included file', '%d included files', count( $formats ), 'ebook-store' ),
3414 count( $formats )
3415 );
3416
3417 return $facts;
3418 }
3419
3420 function ebook_store_buy( $atts ){
3421 return ebook_store($atts, true);
3422 }
3423 function ebook_store_wp_super_cache_check() {
3424 // error_reporting(E_ALL);
3425 // ini_set('display_errors',true);
3426
3427 if (file_exists(WP_PLUGIN_DIR . '/wp-super-cache/wp-cache.php')) {
3428 if (strpos($_SERVER['QUERY_STRING'], 'action=thank_you') || strpos($_SERVER['QUERY_STRING'], 'ebook') || strpos($_SERVER['QUERY_STRING'], 'ipn')) {
3429 define('DONOTCACHEPAGE',true);
3430 }
3431 }
3432
3433 }
3434
3435 /**
3436 * Should the plugin attempt to recover an order from the gateway for this key?
3437 *
3438 * The thank-you page recovery path runs for any anonymous GET of
3439 * ?ebook_key=<anything>&action=thank_you. Each attempt made a live gateway API
3440 * call and wrote a log line, so it doubled as an unauthenticated way to burn
3441 * the store's API rate limit and fill its disk. One attempt per key per five
3442 * minutes is more than enough for a real buyer refreshing the page.
3443 *
3444 * @param string $ebook_key Order token from the request.
3445 * @return bool
3446 */
3447 function ebook_store_can_attempt_order_recovery( $ebook_key ) {
3448 $ebook_key = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $ebook_key );
3449
3450 if ( $ebook_key === '' ) {
3451 return false;
3452 }
3453
3454 $key = 'ebook_store_recover_' . md5( $ebook_key );
3455
3456 if ( get_transient( $key ) ) {
3457 return false;
3458 }
3459
3460 set_transient( $key, 1, 5 * MINUTE_IN_SECONDS );
3461
3462 return true;
3463 }
3464
3465 function ebook_store_get_form_templates() {
3466 $templates = ebook_store_get_core_form_templates();
3467
3468 foreach ( ebook_store_get_custom_form_templates() as $template_key => $template_config ) {
3469 $templates[ (string) $template_key ] = $template_config;
3470 }
3471
3472 return apply_filters( 'ebook_store_form_templates', $templates );
3473 }
3474
3475 function ebook_store_resolve_form_template( $requested = '' ) {
3476 $templates = ebook_store_get_form_templates();
3477 $requested = sanitize_key( (string) $requested );
3478
3479 if ( $requested === '' ) {
3480 $requested = sanitize_key( (string) get_option( 'ebook_store_form_template', '2026' ) );
3481 }
3482
3483 if ( $requested === 'current' ) {
3484 $requested = 'legacy';
3485 }
3486
3487 if ( ! isset( $templates[ $requested ] ) ) {
3488 $requested = '2026';
3489 }
3490
3491 return apply_filters( 'ebook_store_resolved_form_template', $requested, $templates );
3492 }
3493
3494 function ebook_store_get_gateway_button_default_text( $gateway ) {
3495 $defaults = array(
3496 'paypal' => __( 'Buy With PayPal (%%price%%)', 'ebook-store' ),
3497 'stripe' => __( 'Buy With Stripe (%%price%%)', 'ebook-store' ),
3498 'adyen' => __( 'Buy With Adyen (%%price%%)', 'ebook-store' ),
3499 );
3500
3501 $gateway = sanitize_key( (string) $gateway );
3502
3503 return isset( $defaults[ $gateway ] ) ? $defaults[ $gateway ] : __( 'Buy (%%price%%)', 'ebook-store' );
3504 }
3505
3506 function ebook_store_get_gateway_button_text( $gateway, $language = '' ) {
3507 $gateway = sanitize_key( (string) $gateway );
3508 $default_text = ebook_store_get_gateway_button_default_text( $gateway );
3509 $option_value = get_option( $gateway . '_button_text', array() );
3510 $option_value = is_array( $option_value ) ? $option_value : array();
3511
3512 if ( $language === '' ) {
3513 if ( function_exists( 'ebook_store_get_selected_language_code' ) ) {
3514 $language = ebook_store_get_selected_language_code();
3515 }
3516
3517 if ( $language === '' || $language === '0' ) {
3518 $language = substr( get_locale(), 0, 2 );
3519 }
3520 }
3521
3522 $language = sanitize_key( (string) $language );
3523
3524 if ( $language !== '' && $language !== '0' && ! empty( $option_value[ $language ] ) ) {
3525 return (string) $option_value[ $language ];
3526 }
3527
3528 if ( ! empty( $option_value[0] ) ) {
3529 return (string) $option_value[0];
3530 }
3531
3532 if ( ! empty( $option_value['0'] ) ) {
3533 return (string) $option_value['0'];
3534 }
3535
3536 return $default_text;
3537 }
3538
3539 function ebook_store_ga4_is_enabled() {
3540 return function_exists( 'ebook_store_has_pro_license' )
3541 && ebook_store_has_pro_license()
3542 && (string) get_option( 'ebook_store_google_analytics_enabled', '0' ) === '1'
3543 && trim( (string) get_option( 'ebook_store_google_analytics_measurement_id', '' ) ) !== '';
3544 }
3545
3546 function ebook_store_ga4_get_measurement_id() {
3547 return trim( (string) get_option( 'ebook_store_google_analytics_measurement_id', '' ) );
3548 }
3549
3550 function ebook_store_ga4_get_api_secret() {
3551 return trim( (string) get_option( 'ebook_store_google_analytics_api_secret', '' ) );
3552 }
3553
3554 function ebook_store_ga4_enqueue_client() {
3555 static $did_enqueue = false;
3556
3557 if ( $did_enqueue || ! ebook_store_ga4_is_enabled() || is_admin() ) {
3558 return;
3559 }
3560
3561 $measurement_id = ebook_store_ga4_get_measurement_id();
3562 if ( $measurement_id === '' ) {
3563 return;
3564 }
3565
3566 $did_enqueue = true;
3567
3568 wp_enqueue_script(
3569 'ebook-store-ga4',
3570 'https://www.googletagmanager.com/gtag/js?id=' . rawurlencode( $measurement_id ),
3571 array(),
3572 null,
3573 false
3574 );
3575
3576 $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);}});';
3577
3578 wp_add_inline_script( 'ebook-store-ga4', $bootstrap, 'after' );
3579 }
3580
3581 function ebook_store_ga4_get_item_payload( $ebook_id, $price = null ) {
3582 $ebook_id = absint( $ebook_id );
3583 if ( ! $ebook_id ) {
3584 return array();
3585 }
3586
3587 if ( null === $price ) {
3588 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
3589 $price = is_array( $ebook_meta ) && isset( $ebook_meta['ebook_price'] ) ? (float) ebook_store_price_plus_vat( $ebook_meta['ebook_price'] ) : 0.0;
3590 }
3591
3592 return array(
3593 'item_id' => (string) $ebook_id,
3594 'item_name' => get_the_title( $ebook_id ),
3595 'price' => (float) $price,
3596 'quantity' => 1,
3597 'item_category' => 'ebook',
3598 );
3599 }
3600
3601 function ebook_store_ga4_render_view_item_tracking( $items ) {
3602 if ( ! ebook_store_ga4_is_enabled() || empty( $items ) || ! is_array( $items ) ) {
3603 return '';
3604 }
3605
3606 $payloads = array();
3607 foreach ( $items as $item ) {
3608 $ebook_id = isset( $item['ebook_id'] ) ? absint( $item['ebook_id'] ) : 0;
3609 if ( ! $ebook_id ) {
3610 continue;
3611 }
3612
3613 $price = isset( $item['ebook']['ebook_price'] ) ? (float) ebook_store_price_plus_vat( $item['ebook']['ebook_price'] ) : 0.0;
3614 $payloads[] = array(
3615 'event_key' => 'ebook-store-view-item-' . $ebook_id,
3616 'params' => array(
3617 'currency' => isset( $item['paypal_currency'] ) && $item['paypal_currency'] !== '' ? (string) $item['paypal_currency'] : ebook_store_get_store_currency(),
3618 'value' => $price,
3619 'items' => array( ebook_store_ga4_get_item_payload( $ebook_id, $price ) ),
3620 ),
3621 );
3622 }
3623
3624 if ( empty( $payloads ) ) {
3625 return '';
3626 }
3627
3628 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>';
3629 }
3630
3631 function ebook_store_ga4_get_order_client_id( $order_id, $order = array() ) {
3632 $order_id = absint( $order_id );
3633 $stored = trim( (string) get_post_meta( $order_id, 'ebook_store_ga4_client_id', true ) );
3634 if ( $stored !== '' ) {
3635 return $stored;
3636 }
3637
3638 $email = '';
3639 if ( is_array( $order ) ) {
3640 if ( isset( $order['payer_email'][0] ) ) {
3641 $email = (string) $order['payer_email'][0];
3642 } elseif ( isset( $order['payer_email'] ) ) {
3643 $email = (string) $order['payer_email'];
3644 }
3645 }
3646
3647 $hash = md5( $order_id . '|' . strtolower( trim( $email ) ) . '|' . home_url( '/' ) );
3648 $client_id = substr( $hash, 0, 10 ) . '.' . substr( $hash, 10, 10 );
3649 update_post_meta( $order_id, 'ebook_store_ga4_client_id', $client_id );
3650
3651 return $client_id;
3652 }
3653
3654 function ebook_store_ga4_send_measurement_protocol_event( $event_name, $params, $client_id ) {
3655 if ( ! ebook_store_ga4_is_enabled() ) {
3656 return false;
3657 }
3658
3659 $measurement_id = ebook_store_ga4_get_measurement_id();
3660 $api_secret = ebook_store_ga4_get_api_secret();
3661 $client_id = trim( (string) $client_id );
3662
3663 if ( $measurement_id === '' || $api_secret === '' || $client_id === '' ) {
3664 return false;
3665 }
3666
3667 $response = wp_remote_post(
3668 'https://www.google-analytics.com/mp/collect?measurement_id=' . rawurlencode( $measurement_id ) . '&api_secret=' . rawurlencode( $api_secret ),
3669 array(
3670 'timeout' => 4,
3671 'blocking' => false,
3672 'headers' => array(
3673 'Content-Type' => 'application/json',
3674 ),
3675 'body' => wp_json_encode(
3676 array(
3677 'client_id' => $client_id,
3678 'events' => array(
3679 array(
3680 'name' => $event_name,
3681 'params' => array_merge(
3682 array(
3683 'engagement_time_msec' => 1,
3684 ),
3685 (array) $params
3686 ),
3687 ),
3688 ),
3689 )
3690 ),
3691 )
3692 );
3693
3694 return ! is_wp_error( $response );
3695 }
3696
3697 function ebook_store_ga4_get_order_payload( $order_id ) {
3698 $order_id = absint( $order_id );
3699 if ( ! $order_id || get_post_type( $order_id ) !== 'ebook_order' ) {
3700 return array();
3701 }
3702
3703 $ebook_id = absint( get_post_meta( $order_id, 'ebook', true ) );
3704 $gross = (float) get_post_meta( $order_id, 'mc_gross', true );
3705 $currency = strtoupper( (string) get_post_meta( $order_id, 'mc_currency', true ) );
3706 $txn_id = trim( (string) get_post_meta( $order_id, 'txn_id', true ) );
3707
3708 if ( ! $ebook_id || $gross <= 0 ) {
3709 return array();
3710 }
3711
3712 return array(
3713 'transaction_id' => $txn_id !== '' ? $txn_id : (string) $order_id,
3714 'currency' => $currency !== '' ? $currency : ebook_store_get_store_currency(),
3715 'value' => $gross,
3716 'items' => array( ebook_store_ga4_get_item_payload( $ebook_id, $gross ) ),
3717 );
3718 }
3719
3720 function ebook_store_ga4_track_purchase_for_order( $order_id ) {
3721 $order_id = absint( $order_id );
3722 if ( ! $order_id || ! ebook_store_ga4_is_enabled() ) {
3723 return false;
3724 }
3725
3726 if ( get_post_meta( $order_id, 'ebook_store_ga4_purchase_tracked', true ) ) {
3727 return true;
3728 }
3729
3730 $payment_type = strtolower( trim( (string) get_post_meta( $order_id, 'payment_type', true ) ) );
3731 $txn_id = strtolower( trim( (string) get_post_meta( $order_id, 'txn_id', true ) ) );
3732 $gross = (float) get_post_meta( $order_id, 'mc_gross', true );
3733
3734 if ( $gross <= 0 || in_array( $payment_type, array( 'new_issue' ), true ) || strpos( $txn_id, 'bonus download' ) === 0 ) {
3735 return false;
3736 }
3737
3738 $params = ebook_store_ga4_get_order_payload( $order_id );
3739 if ( empty( $params ) ) {
3740 return false;
3741 }
3742
3743 $sent = ebook_store_ga4_send_measurement_protocol_event(
3744 'purchase',
3745 $params,
3746 ebook_store_ga4_get_order_client_id( $order_id, ebook_get_order( 'order_id', $order_id ) )
3747 );
3748
3749 if ( $sent ) {
3750 update_post_meta( $order_id, 'ebook_store_ga4_purchase_tracked', current_time( 'mysql', true ) );
3751 }
3752
3753 return $sent;
3754 }
3755
3756 function ebook_store_ga4_track_refund_for_order( $order_id ) {
3757 $order_id = absint( $order_id );
3758 if ( ! $order_id || ! ebook_store_ga4_is_enabled() ) {
3759 return false;
3760 }
3761
3762 if ( get_post_meta( $order_id, 'ebook_store_ga4_refund_tracked', true ) ) {
3763 return true;
3764 }
3765
3766 $params = ebook_store_ga4_get_order_payload( $order_id );
3767 if ( empty( $params ) ) {
3768 return false;
3769 }
3770
3771 $sent = ebook_store_ga4_send_measurement_protocol_event(
3772 'refund',
3773 $params,
3774 ebook_store_ga4_get_order_client_id( $order_id, ebook_get_order( 'order_id', $order_id ) )
3775 );
3776
3777 if ( $sent ) {
3778 update_post_meta( $order_id, 'ebook_store_ga4_refund_tracked', current_time( 'mysql', true ) );
3779 }
3780
3781 return $sent;
3782 }
3783
3784 function ebook_store_ga4_watch_order_status_changes( $meta_id, $object_id, $meta_key, $meta_value ) {
3785 if ( 'payment_status' !== $meta_key || get_post_type( $object_id ) !== 'ebook_order' ) {
3786 return;
3787 }
3788
3789 $current_status = trim( (string) $meta_value );
3790 $previous_status = trim( (string) get_post_meta( $object_id, 'ebook_store_ga4_last_known_status', true ) );
3791 update_post_meta( $object_id, 'ebook_store_ga4_last_known_status', $current_status );
3792
3793 if ( $current_status === '' || $current_status === $previous_status ) {
3794 return;
3795 }
3796
3797 if ( ebook_store_is_negative_order_status( $current_status ) && ! ebook_store_is_negative_order_status( $previous_status ) ) {
3798 ebook_store_ga4_track_refund_for_order( $object_id );
3799 }
3800 }
3801 add_action( 'added_post_meta', 'ebook_store_ga4_watch_order_status_changes', 10, 4 );
3802 add_action( 'updated_post_meta', 'ebook_store_ga4_watch_order_status_changes', 10, 4 );
3803
3804 function ebook_store_render_form_template( $template, $context ) {
3805 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
3806 $locale = ebook_store_get_locale_strings();
3807 $formats_locale = ebook_store_get_format_labels();
3808 $templates = ebook_store_get_form_templates();
3809 if ( ! isset( $templates[ $template ] ) ) {
3810 $template = 'modern';
3811 }
3812
3813 $template_file = isset( $templates[ $template ]['file'] ) ? $templates[ $template ]['file'] : '';
3814 if ( ! $template_file || ! file_exists( $template_file ) ) {
3815 return '';
3816 }
3817
3818 $context = is_array( $context ) ? $context : array();
3819
3820 ob_start();
3821 include $template_file;
3822 $template_output = ob_get_clean();
3823 return function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $template_output ) : $template_output;
3824 }
3825
3826 function ebook_store( $atts, $buyNowOnly = false ){
3827 if (!wp_script_is('jquery', 'enqueued')) {
3828 wp_enqueue_script('jquery');
3829 }
3830 define('DONOTCACHEPAGE',true);
3831 //@error_reporting(E_ALL);
3832 //ini_set('display_errors',1);
3833 if (is_array($atts) == false) {
3834 $atts = array('ebook_id' => $atts);
3835 }
3836 $atts = shortcode_atts(
3837 array(
3838 'ebook_id' => 0,
3839 'template' => '',
3840 ),
3841 $atts,
3842 'ebook_store'
3843 );
3844 try {
3845 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
3846 $locale = ebook_store_get_locale_strings();
3847 $formats_locale = ebook_store_get_format_labels();
3848
3849 global $ebook_store_messages;
3850 global $woocommerce;
3851
3852 if (is_array($ebook_store_messages) and count($ebook_store_messages) > 0) {
3853 foreach ($ebook_store_messages as $ebook_store_messages => $message) {
3854 echo $message;
3855 }
3856 }
3857 wp_enqueue_style( 'ebookstorestylesheet' );
3858 ebook_store_ga4_enqueue_client();
3859 //wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
3860 $post_id = $atts['ebook_id'];
3861 $template = '';
3862 if ( ! empty( $atts['template'] ) ) {
3863 $template = ebook_store_resolve_form_template( $atts['template'] );
3864 }
3865
3866 if (@$_REQUEST['ebook_key'] != false && @$_REQUEST['action'] == 'thank_you') {
3867
3868 add_action( 'wp_footer', 'ebook_store_deregister_embeds' );
3869 if ($_REQUEST['ebook_key'] == -1) {
3870 $wp_query_statement = array (
3871 'post_type' => 'ebook_order',
3872 'meta_key' => 'user_id',
3873 'meta_value' => get_current_user_id(),
3874 'orderby' => 'date',
3875 'posts_per_page' => -1,
3876 'order' => 'DESC' );
3877 // print_r($wp_query_statement);
3878 // die();
3879 $loop = new WP_Query($wp_query_statement);
3880 while ( $loop->have_posts() ) {
3881 $loop->the_post();
3882 $ebook_key = get_post_meta(get_the_ID(), 'ebook_key', true);
3883 $_REQUEST['ebook_key'] = $ebook_key;
3884 $uri = esc_url_raw($_SERVER['REQUEST_URI']);
3885 $uri = remove_query_arg('ebook_key',$uri);
3886 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>");
3887 echo get_the_ID() . '<br />';
3888 @$toReset = 1;
3889 //break;
3890 }
3891 die();
3892 if (@$toReset) {
3893 wp_reset_postdata();
3894 }
3895
3896 }
3897 $content = ebook_store_get_effective_confirmation_page_content( 0 );
3898
3899 $ebook_order = ebook_get_order('ebook_key', $_REQUEST['ebook_key']);
3900 if (
3901 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
3902 && get_option( 'stripe_integration_enabled', 0 )
3903 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
3904 ) {
3905 $recovered_order_id = 0;
3906 $stripe_session_id = isset( $_REQUEST['stripe_session_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['stripe_session_id'] ) ) : '';
3907 if ( $stripe_session_id !== '' ) {
3908 $recovered_order_id = ebook_store_recover_stripe_order_by_session_id( $stripe_session_id );
3909 } else {
3910 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key(
3911 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
3912 );
3913 }
3914 if ( is_wp_error( $recovered_order_id ) ) {
3915 ebook_store_log_stripe_event(
3916 'Stripe thank-you recovery skipped',
3917 array(
3918 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
3919 'stripe_session_id' => $stripe_session_id,
3920 'error' => $recovered_order_id->get_error_message(),
3921 )
3922 );
3923 } elseif ( $recovered_order_id ) {
3924 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
3925 }
3926 }
3927 if (
3928 ( ! $ebook_order || isset( $ebook_order['ebook'] ) == false )
3929 && get_option( 'adyen_integration_enabled', 0 )
3930 && ebook_store_can_attempt_order_recovery( isset( $_REQUEST['ebook_key'] ) ? $_REQUEST['ebook_key'] : '' )
3931 ) {
3932 $recovered_order_id = ebook_store_recover_adyen_order_by_ebook_key(
3933 sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) )
3934 );
3935 if ( is_wp_error( $recovered_order_id ) ) {
3936 ebook_store_log_adyen_event(
3937 'Adyen thank-you recovery skipped',
3938 array(
3939 'ebook_key' => sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ),
3940 'error' => $recovered_order_id->get_error_message(),
3941 )
3942 );
3943 } elseif ( $recovered_order_id ) {
3944 $ebook_order = ebook_get_order( 'ebook_key', $_REQUEST['ebook_key'] );
3945 }
3946 }
3947 //wp_die(var_dump($ebook_order));
3948 $time = strtotime(@$ebook_order['payment_date'][0]);
3949 //wp_die("$time-".time().'='.$time - time() );
3950 //print_r($ebook_order);
3951 if (@$ebook_order['item_name'][0] == '') {
3952 $ebook_order['item_name'][0] = get_the_title(@$ebook_order['ebook'][0]);
3953 }
3954
3955 //wp_die('ebook order var ' . var_export($ebook_order,true));
3956 if (!$ebook_order || @strtotime($ebook_order['payment_date'][0]) == 0 || isset($ebook_order['ebook']) == false) { // || time() < @strtotime($ebook_order['payment_date'][0]) + 25 ||
3957 return '<h4>' . $locale['confirmation'] . '</h4>' . '<script> window.setTimeout(\'location.reload()\', 5000);
3958 </script>';
3959 }
3960 $ebook_order_ebook_id = isset($ebook_order['ebook'][0]) ? absint($ebook_order['ebook'][0]) : 0;
3961 $content = ebook_store_get_effective_confirmation_page_content($ebook_order_ebook_id);
3962 $file = get_post_meta($ebook_order['ebook'][0],'ebook_wp_custom_attachment',true);
3963 //wp_die(print_r($file,true));
3964 if (@!$file) {
3965 $file = array();
3966 }
3967 $ebook_order['downloadlink'][0] = ebook_download_link($ebook_order);
3968 $ebook_order['downloadlink'][0] = str_replace('#038;','&',$ebook_order['downloadlink'][0]);
3969 $ebook_order['downloadlink'][0] = str_replace('&&','&',$ebook_order['downloadlink'][0]);
3970 //<a href="%%downloadlink%%" target="_blank" rel="noopener">%%item_name%%</a> (%%filesize%%)
3971 @$ebook_order['pdf_reader'][0] = ebook_pdf_reader($ebook_order);
3972 @$ebook_order['download_links'][0] = implode("<br />",ebook_download_links($ebook_order));
3973 @$ebook_order['ebook_bonus'][0] = (implode("<br />",ebook_download_links_bonus($ebook_order)) != '' ? implode("<br />",ebook_download_links_bonus($ebook_order)) : __('None','ebook-store'));
3974 @$ebook_order['filesize'][0] = humanFileSize(filesize($file['file']));
3975 // $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].')';
3976 //print_r($ebook_order);
3977 //file_put_contents(__DIR__ . '/debug.log', 'downloadlink: ' . $ebook_order['downloadlink'][0] . "\n", FILE_APPEND);
3978 $ebookObj = new EbookStoreEbook(@$ebook_order['ebook'][0]);
3979 // This is a plugin order, not a WooCommerce one: hand link() the ebook_key
3980 // credentials so the non-PDF format links resolve to the download path
3981 // that this order is actually authorised for.
3982 $ebookObj->ebook_key = (strlen(@$ebook_order['ebook_key'][0]) == 32 ? @$ebook_order['ebook_key'][0] : @$ebook_order['ebook_key_array'][0]);
3983 $ebookObj->md5_nonce = @$ebook_order['md5_nonce'][0];
3984 $ebookObj->order_id = isset($ebook_order['order_id'][0]) ? absint($ebook_order['order_id'][0]) : 0;
3985 $ebookObj->setLink['pdf'] = $ebook_order['downloadlink'][0];
3986 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
3987 //$ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
3988
3989 if (@$ebook_order['password'] == '') {
3990 $ebook_order['password'] = $ebook_order['payer_email'];
3991 }
3992 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_1', true) != '') {
3993 $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>
3994 </a>');
3995
3996 }
3997 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_2', true) != '') {
3998 $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">
3999 <h1 style="color:red;">View Book Online Now</h1>
4000 </a>');
4001 }
4002
4003 if (get_post_meta($ebook_order['ebook'][0],'ebook_store_alternative_location_3', true) != '') {
4004 $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">
4005 <h1 style="color:red;">Download PDF</h1>
4006 </a>');
4007 }
4008 foreach ($ebook_order as $k => $arr) {
4009 $content = str_replace('%%' . $k . '%%', $arr[0], $content);
4010 }
4011 return apply_filters('the_content',$content);
4012 //return 'test 123' . $content;
4013 }
4014
4015 $template_items = array();
4016 $args = array( 'post_type' => 'ebook', 'posts_per_page' => -1, 'p' => @$atts['ebook_id'] );
4017 $loop = new WP_Query( $args );
4018
4019 while ( $loop->have_posts() ) : $loop->the_post();
4020 /*the_title();
4021 echo '<div class="entry-content">';
4022 the_content();
4023 echo '</div>';*/
4024 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
4025 if ( $template === '' && is_array( $ebook ) && ! empty( $ebook['form_template'] ) ) {
4026 $template = ebook_store_resolve_form_template( $ebook['form_template'] );
4027 }
4028 $ebook['ebook_price'] = floatval($ebook['ebook_price']);
4029 //error_log(var_export($ebook,true));
4030 $ebook_key = md5(NONCE_KEY . get_the_ID() . $ebook['ebook_price'] . mt_rand(1,100000));
4031 //$ebook['ebook_price'] = is_float($ebook['ebook_price']) ? $ebook['ebook_price'] : 0.0;
4032 $md5_nonce = md5(mt_rand(1,9999) . NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','));
4033 //$custom = get_the_ID() . '|' . $md5_nonce;
4034 $custom = get_the_ID() . '|' . md5(NONCE_KEY . get_the_ID() . @number_format($ebook['ebook_price'], 2, '.', ','));
4035
4036 if (get_option('ebook_store_wp_affiliate_integration') == 1) { /* && file_exists('../affiliates-manager/config.php')*/
4037 $parameters = apply_filters('ebook_store_payment_gateway_parameters', array('custom' => $custom));
4038 }
4039
4040 $c = new Currencies();
4041 $img = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment', true);
4042 $preview = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_preview', true);
4043 if (is_array($preview) == false) { $preview = array(); }
4044 if (@$preview['url'] != '') {
4045 if (get_option( 'ebook_store_no_viewerjs_previews', false ) == false) {
4046 @$preview['url'] = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html#' . $preview['url'];
4047 }
4048 }
4049
4050 $cover = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_cover',true);
4051 $side = get_post_meta(get_the_ID(),'ebook_wp_custom_attachment_side_photo',true);
4052 if (!$side) {
4053 $side = $cover;
4054 }
4055 //style="background-url:(' . $side['url'] . ');"
4056 if (is_array(@$ebook['ebook_publisher'])) {
4057 $publishers = array();
4058 foreach ($ebook['ebook_publisher'] as $p => $pp) {
4059 $publishers[] = get_the_title($pp);
4060 }
4061 }
4062 if (is_array(@$ebook['ebook_author'])) {
4063 $authors = array();
4064 foreach ($ebook['ebook_author'] as $p => $pp) {
4065 $authors[] = get_the_title($pp);
4066 }
4067 }
4068 @$publishers = is_array($publishers) ? $publishers : array();
4069 @$authors = is_array($authors) ? $authors : array();
4070 $publishers = @implode(", ", $publishers);
4071 $authors = @implode(", ", $authors);
4072 /* div class front, where is it?*/
4073
4074 if ( $img == '' && ! ebook_store_has_pro_license() && current_user_can( 'edit_post', get_the_ID() ) ) {
4075 printf(
4076 '<div class="ebook-store-notice"><h3>%1$s</h3><p><a href="%2$s">%3$s</a></p></div>',
4077 esc_html__( 'This ebook has no file attached yet, so there is nothing for buyers to download.', 'ebook-store' ),
4078 esc_url( get_edit_post_link( get_the_ID() ) ),
4079 esc_html__( 'Open the ebook and upload a file', 'ebook-store' )
4080 );
4081 }
4082 $paypal_currency = ( ! empty( $ebook['paypal_currency'] ) ? strtoupper( trim( (string) $ebook['paypal_currency'] ) ) : ebook_store_get_store_currency() );
4083 if ( ! preg_match( '/^[A-Z]{3}$/', $paypal_currency ) ) {
4084 $paypal_currency = ebook_store_get_store_currency();
4085 }
4086 $md5rand = md5(rand(1,10000) . microtime());
4087 $payment_methods = array();
4088 $paypal_ready = ebook_store_is_paypal_enabled() && trim( (string) get_option( 'paypal_account', '' ) ) !== '';
4089
4090 $formatted_paypal_price = $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2);
4091 $buyNowLinkText = str_replace( '%%price%%', $formatted_paypal_price, ebook_store_get_gateway_button_text( 'paypal' ) );
4092 $buyNowLinkOnClick = 'document.getElementById(\'' . $md5rand . '\').submit(); return false;';
4093 $buyNowLinkHref = '';
4094 $form = '';
4095 $show_primary_buy_link = false;
4096 $primary_payment_method = '';
4097 if ($ebook['ebook_price'] === 0 || $ebook['donate_or_download'] == 'free') {
4098 $buyNowLinkText = $locale['download'];
4099 $buyNowLinkOnClickOriginal = ebook_download_link(array('ebook' => array(0 => get_the_ID()), 'ebook_key' => array(0 => $ebook_key), 'md5_nonce' => array(0 => $md5_nonce)),1);
4100 $buyNowLinkOnClick = "window.location = '" . $buyNowLinkOnClickOriginal . "'; return false;";
4101 $buyNowLinkHref = $buyNowLinkOnClickOriginal;
4102 $show_primary_buy_link = true;
4103 } else if ($ebook['ebook_price'] === 0 || $ebook['donate_or_download'] == 'donate') {
4104 $buyNowLinkText = $locale['download'];
4105 $ebook['ebook_price'] = 0;
4106 $show_primary_buy_link = true;
4107 } else if ( $paypal_ready ) {
4108 $payment_methods['paypal'] = array(
4109 'key' => 'paypal',
4110 'label' => 'PayPal',
4111 'href' => '#',
4112 'onclick' => $buyNowLinkOnClick,
4113 'price' => $formatted_paypal_price,
4114 );
4115 $show_primary_buy_link = true;
4116 $primary_payment_method = 'paypal';
4117 }
4118
4119 if ($img == '' && get_option( 'ebook_store_license_key') == '') {
4120 $buyNowLinkOnClick = 'alert(\'There is no ebook file uploaded. Please upload a file first in the Ebook Store plugin.\');';
4121 }
4122 if (get_option('formEnabled') == 1) {
4123 $buyNowLinkOnClickOriginal = $buyNowLinkOnClick;
4124 $buyNowLinkOnClick = "ebook_store_popup(function () {" . $buyNowLinkOnClick . "}, this);";
4125 wp_register_style( 'ebook_store_tinglecss', plugins_url('/includes/tingle-master/dist/tingle.min.css', __FILE__) );
4126 wp_enqueue_style( 'ebook_store_tinglecss' );
4127 wp_enqueue_script( 'ebook_store_site_modal', plugins_url( '/includes/tingle-master/dist/tingle.min.js' , __FILE__ ), array(), '1.0.0', true );
4128 wp_enqueue_script( 'ebook_store_site', plugins_url( '/js/ebook_store_site.js' , __FILE__ ), array(), '1.0.0', true );
4129
4130 }
4131
4132 wp_enqueue_style( 'ebookstorestylesheet' );
4133 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4134 if (!$checkoutPage) {
4135 $checkoutPage = $post_id;
4136 }
4137
4138 $bookpost = get_post(get_the_ID());
4139 $add_to_cart_label = ! empty( $locale['addtocart'] ) ? $locale['addtocart'] : __( 'Add to cart', 'ebook-store' );
4140 $woocommerce_product_id = ebook_store_get_wc_product_reference_for_ebook( get_the_ID() );
4141 if ($woocommerce_product_id > 0 && class_exists('woocommerce') && get_option('ebook_store_woocommerce_integration')) {
4142 $woocommerce_checkout_url = add_query_arg(array('woocommerce_product_id' => $woocommerce_product_id), get_permalink(get_the_ID()));
4143 $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>';
4144 }
4145 if (!is_array($cover)) {
4146 $cover = array();
4147 }
4148 //$extraButtons[] = array();
4149
4150 // if (!is_array(@$extraButtons)) {
4151 // @$extraButtons = array();
4152 // }
4153 if (!is_array($side)) {
4154 $side = array();
4155 }
4156 @$extraButtons = apply_filters('ebook_store_extra_buttons',$extraButtons, $ebook['ebook_price'], $bookpost->post_title, $cover['url'], $md5_nonce, $post_id);
4157
4158 // Add Stripe button if enabled
4159 if (get_option('stripe_integration_enabled', 0)) {
4160 $button_text = ebook_store_get_gateway_button_text( 'stripe' );
4161
4162 // Only show Stripe button if price is greater than 0
4163
4164 if ($ebook['ebook_price'] > 0) {
4165 try {
4166 // Generate ebook_key here, using same method as PayPal
4167 $stripe_md5_nonce = wp_create_nonce('ebook_download_' . $post_id);
4168 $stripe_ebook_key = $ebook_key;
4169
4170 // Create Stripe checkout session with the pre-generated key
4171 $stripe_session_url = ebook_store_create_stripe_checkout(
4172 get_the_ID(),
4173 ebook_store_price_plus_vat($ebook['ebook_price']),
4174 $bookpost->post_title,
4175 $ebook_key // Pass the pre-generated key
4176 );
4177
4178 // Replace %%price%% token with the formatted price
4179 $c = new Currencies;
4180 $paypal_currency = ebook_store_get_store_currency();
4181 $formatted_price = $c->getSymbol($paypal_currency) . number_format( ebook_store_price_plus_vat($ebook['ebook_price']), 2 );
4182 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4183
4184 // Add Stripe button to extra buttons using the processed button text
4185 $extraButtons[] = sprintf(
4186 '<a href="%s" class="stripe-button" 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>',
4187 esc_url($stripe_session_url),
4188 esc_attr( get_the_ID() ),
4189 esc_attr( $bookpost->post_title ),
4190 esc_attr( $paypal_currency ),
4191 esc_attr( (string) ebook_store_price_plus_vat($ebook['ebook_price']) ),
4192 esc_html($display_text)
4193 );
4194 $payment_methods['stripe'] = array(
4195 'key' => 'stripe',
4196 'label' => 'Stripe',
4197 'href' => $stripe_session_url,
4198 'onclick' => '',
4199 'price' => $formatted_price,
4200 );
4201 } catch (Exception $e) {
4202 error_log('Stripe button creation failed: ' . $e->getMessage());
4203 }
4204 }
4205 }
4206
4207 // Add Adyen button if enabled
4208 if (get_option('adyen_integration_enabled', 0)) {
4209 $button_text = ebook_store_get_gateway_button_text( 'adyen' );
4210
4211 if ($ebook['ebook_price'] > 0) {
4212 try {
4213 $adyen_checkout_url = ebook_store_create_adyen_payment_link(
4214 get_the_ID(),
4215 ebook_store_price_plus_vat($ebook['ebook_price']),
4216 $bookpost->post_title,
4217 $ebook_key
4218 );
4219
4220 if ( $adyen_checkout_url ) {
4221 $c = new Currencies;
4222 $adyen_currency = ebook_store_get_store_currency();
4223 $adyen_amount = (float) ebook_store_price_plus_vat($ebook['ebook_price']);
4224 $formatted_price = $c->getSymbol($adyen_currency) . number_format( $adyen_amount, 2 );
4225 $display_text = str_replace('%%price%%', $formatted_price, $button_text);
4226
4227 $extraButtons[] = sprintf(
4228 '<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>',
4229 esc_url($adyen_checkout_url),
4230 esc_attr( get_the_ID() ),
4231 esc_attr( $bookpost->post_title ),
4232 esc_attr( $adyen_currency ),
4233 esc_attr( (string) $adyen_amount ),
4234 esc_html($display_text)
4235 );
4236 $payment_methods['adyen'] = array(
4237 'key' => 'adyen',
4238 'label' => 'Adyen',
4239 'href' => $adyen_checkout_url,
4240 'onclick' => '',
4241 'price' => $formatted_price,
4242 );
4243 }
4244 } catch (Exception $e) {
4245 error_log('Adyen button creation failed: ' . $e->getMessage());
4246 }
4247 }
4248 }
4249
4250 $extraButtons = is_array($extraButtons) ? $extraButtons : array();
4251 if ( $paypal_ready ) {
4252 $form = '<form method="post" id="' . $md5rand . '" name="dmp_order_form" action="https://www' . (get_option('paypal_sandbox') != '' ? '.sandbox' : '') . '.paypal.com/cgi-bin/webscr">
4253 <input type="hidden" name="rm" value="0">
4254 <input type="hidden" name="discount_rate" value="0">
4255 <input type="hidden" name="cmd" value="_xclick">
4256 <input type="hidden" name="charset" value="utf-8">
4257 <input type="hidden" name="md5_nonce" value="' . $md5_nonce . '">
4258 <input type="hidden" name="lc" value="' . get_option('paypal_language') . '">
4259 <input type="hidden" name="no_shipping" value="' . get_option('ebook_store_require_shipping') . '">
4260 <input type="hidden" name="button_subtype" value="products">
4261 <input type="hidden" name="return" value="' . add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage)) . '">
4262 <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')))) . '">
4263 <input type="hidden" name="notify_url" value="' . add_query_arg(array('task' => 'ipn','ebook_key' => $ebook_key, 'md5_nonce' => $md5_nonce), home_url('/')) . '">
4264 <input type="hidden" name="item_name" value="' . $bookpost->post_title . '">
4265 <input type="hidden" name="item_number" value="1">
4266 <input type="hidden" name="tax_rate" value="' . ebook_store_get_vat_percent() . '">
4267 <input type="hidden" name="amount" value="' . $ebook['ebook_price'] . '">
4268 <input type="hidden" name="upload" value="1">
4269 <input type="hidden" name="custom" value="' . $custom .'">
4270 <input type="hidden" name="business" id="af69ae50c1be74757508c8f7fae10abd" value="' . get_option('paypal_account') . '">
4271 <input type="hidden" name="receiver_email" id="af69ae50c1be74757508c8f7fae10abd0xff" value="' . get_option('paypal_account') . '">
4272 <input type="hidden" name="currency_code" value="' . $paypal_currency . '">
4273 <input type="hidden" name="cbt" value="' . get_option('paypal_return_button_text') . '">
4274 <input type="hidden" name="no_note" value="1">
4275 </form>';
4276 $buyNowLinkHref = '#';
4277 }
4278
4279 //$form .= var_export($bookpost, true);
4280 if (@$_GET['task'] == 'direct_order') {
4281 if ($ebook['ebook_price'] > 0 && $paypal_ready) {
4282 die($form .
4283 '<script>
4284 document.getElementById("' . $md5rand . '").submit();
4285 </script>'
4286 );
4287 } else {
4288 die($form .
4289 '<script>
4290 '.$buyNowLinkOnClick.'
4291 jQuery("body").html("");
4292 </script>'
4293 );
4294 }
4295 }
4296 $date_text = (!empty($ebook['ebook_date']) && strtotime($ebook['ebook_date'])) ? date(get_option('date_format'), strtotime($ebook['ebook_date'])) : '';
4297 $pages_text = (!empty($ebook['ebook_pages']) && (int) $ebook['ebook_pages'] > 0) ? $ebook['ebook_pages'] . ' ' . $locale['pages'] : '';
4298 $preview_label = ! empty( $locale['preview'] ) ? $locale['preview'] : __( 'Preview', 'ebook-store' );
4299 $file_facts = ebook_store_get_ebook_file_facts( get_the_ID() );
4300 $template_items[] = array(
4301 'ebook_id' => get_the_ID(),
4302 'title' => $bookpost->post_title,
4303 'content_html' => wpautop( do_shortcode( shortcode_unautop( get_the_content() ) ) ),
4304 'content_raw' => get_the_content(),
4305 'authors' => $authors,
4306 'publishers' => $publishers,
4307 'date_text' => $date_text,
4308 'pages_text' => $pages_text,
4309 'file_facts' => $file_facts,
4310 'cover_url' => isset( $cover['url'] ) ? $cover['url'] : '',
4311 'side_url' => isset( $side['url'] ) ? $side['url'] : '',
4312 'preview_url' => isset( $preview['url'] ) ? $preview['url'] : '',
4313 'add_to_cart_html' => isset( $addToCartButton ) ? $addToCartButton : '',
4314 'extra_buttons_html' => implode( '', $extraButtons ),
4315 'extra_buttons' => $extraButtons,
4316 'payment_methods' => array_values( $payment_methods ),
4317 'buy_link_text' => $buyNowLinkText,
4318 'buy_link_onclick' => $buyNowLinkOnClick,
4319 'buy_link_href' => $buyNowLinkHref,
4320 'primary_payment_method' => $primary_payment_method,
4321 'show_primary_buy_link' => $show_primary_buy_link ? 1 : 0,
4322 'form_html' => $form,
4323 'md5_nonce' => $md5_nonce,
4324 'locale' => $locale,
4325 'preview_label' => $preview_label,
4326 'ebook' => $ebook,
4327 'paypal_currency' => $paypal_currency,
4328 'currency_symbol' => $c->getSymbol($paypal_currency),
4329 'price_display' => $c->getSymbol($paypal_currency) . @number_format(ebook_store_price_plus_vat($ebook['ebook_price']),2),
4330 'disable_cover_buy_now' => (int) get_option('ebook_store_disable_cover_buy_now',0),
4331 'hide_buy_now' => (int) get_option('ebook_store_hide_buy_now',0),
4332 'buy_link_font_size' => get_option('ebook_store_buy_link_font_size', 0.65),
4333 'title_font_size' => get_option('ebook_store_title_font_size', 1.8),
4334 'form_scale' => get_option('form_scale', 1),
4335 );
4336 endwhile;
4337 wp_reset_postdata();
4338 if ( $template === '' ) {
4339 $template = ebook_store_resolve_form_template( '' );
4340 }
4341
4342 if ($buyNowOnly == true) {
4343 if ( ! $show_primary_buy_link ) {
4344 return '';
4345 }
4346 $ga_attributes = '';
4347 if ( ebook_store_ga4_is_enabled() && $primary_payment_method !== '' ) {
4348 $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']) ) . '"';
4349 }
4350 return $form . '<button title="' . esc_attr( $buyNowLinkText ) . '" aria-label="' . esc_attr( $buyNowLinkText ) . '"' . $ga_attributes . ' onClick="' . $buyNowLinkOnClick . '">' . esc_html( $buyNowLinkText ) . '</button>';
4351 }
4352 if ( $template === 'legacy' ) {
4353 wp_enqueue_style( 'ebookstore-bookblock', plugins_url('css/bookblock.css',__FILE__) );
4354 wp_enqueue_style( 'ebookstore-component', plugins_url('css/component.css',__FILE__) );
4355 wp_enqueue_script( 'ebookstore-modernizr', plugins_url('js/modernizr.custom.js',__FILE__), array(), null, false );
4356 wp_enqueue_script( 'ebookstore-bookblock', plugins_url('js/bookblock.min.js',__FILE__), array(), null, true );
4357 wp_enqueue_script( 'ebookstore-classie', plugins_url('js/classie.js',__FILE__), array(), null, true );
4358 wp_enqueue_script( 'ebookstore-bookshelf', plugins_url('js/bookshelf.js',__FILE__), array('ebookstore-bookblock','ebookstore-classie'), null, true );
4359 }
4360
4361 $context = array(
4362 'items' => $template_items,
4363 'template' => $template,
4364 'plugin_url' => plugins_url('', __FILE__),
4365 'locale' => $locale,
4366 );
4367
4368 return ebook_store_render_form_template( $template, $context ) . ebook_store_ga4_render_view_item_tracking( $template_items );
4369 } catch ( Throwable $e ) {
4370 error_log(
4371 sprintf(
4372 'Ebook Store render error for ebook %d: %s in %s:%d',
4373 (int) $atts['ebook_id'],
4374 $e->getMessage(),
4375 $e->getFile(),
4376 (int) $e->getLine()
4377 )
4378 );
4379
4380 return '<div class="ebook-store-alert ebook-store-alert-error">' . esc_html__( 'The ebook order form could not be loaded.', 'ebook-store' ) . '</div>';
4381 }
4382 }
4383 function custom_enter_title_author( $input ) {
4384 global $post_type;
4385
4386 if ( is_admin() && 'ebook_author' == $post_type )
4387 return __( 'Enter Author Name' );
4388
4389 return $input;
4390 }
4391
4392 function custom_enter_title_publisher( $input ) {
4393 global $post_type;
4394
4395 if ( is_admin() && 'ebook_publisher' == $post_type )
4396 return __( 'Enter Publisher Name' );
4397
4398 return $input;
4399 }
4400
4401 class Currencies {
4402
4403 public $currencies = array(
4404
4405 'AUD' => array('name' => "Australian Dollar", 'symbol' => "A$", 'ASCII' => "A&#36;"),
4406
4407 'CAD' => array('name' => "Canadian Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4408
4409 'CZK' => array('name' => "Czech Koruna", 'symbol' => "kr", 'ASCII' => "kr"),
4410
4411 'DKK' => array('name' => "Danish Krone", 'symbol' => "Kr", 'ASCII' => ""),
4412
4413 'EUR' => array('name' => "Euro", 'symbol' => "€", 'ASCII' => "&#128;"),
4414
4415 'HKD' => array('name' => "Hong Kong Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4416
4417 'HUF' => array('name' => "Hungarian Forint", 'symbol' => "Ft", 'ASCII' => "Ft"),
4418
4419 'ILS' => array('name' => "Israeli New Sheqel", 'symbol' => "₪", 'ASCII' => "&#x20AA;"),
4420
4421 'INR' => array('name' => "Indian Rupee", 'symbol' => "₹", 'ASCII' => "&#8377;"),
4422
4423 'JPY' => array('name' => "Japanese Yen", 'symbol' => "¥", 'ASCII' => "&#165;"),
4424
4425 'MXN' => array('name' => "Mexican Peso", 'symbol' => "$", 'ASCII' => "&#36;"),
4426
4427 'NOK' => array('name' => "Norwegian Krone", 'symbol' => "Kr", 'ASCII' => ""),
4428
4429 'NZD' => array('name' => "New Zealand Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4430
4431 'PHP' => array('name' => "Philippine Peso", 'symbol' => "₱", 'ASCII' => ""),
4432
4433 'PLN' => array('name' => "Polish Zloty", 'symbol' => "zł‚", 'ASCII' => "zł"),
4434
4435 'GBP' => array('name' => "Pound Sterling", 'symbol' => "£", 'ASCII' => "&#163;"),
4436
4437 'SGD' => array('name' => "Singapore Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4438
4439 'SEK' => array('name' => "Swedish Krona", 'symbol' => "kr", 'ASCII' => ""),
4440
4441 'CHF' => array('name' => "Swiss Franc", 'symbol' => "CHF", 'ASCII' => "Fr"),
4442
4443 'TWD' => array('name' => "Taiwan New Dollar", 'symbol' => "NT$", 'ASCII' => "NT&#36;"),
4444
4445 'THB' => array('name' => "Thai Baht", 'symbol' => "฿", 'ASCII' => "&#3647;"),
4446
4447 'USD' => array('name' => "U.S. Dollar", 'symbol' => "$", 'ASCII' => "&#36;"),
4448
4449 'BRL' => array('name' => "Brazilian Real", 'symbol' => "R$", 'ASCII' => "&#x0024;"),
4450
4451 'TRY' => array('name' => "Turkish Lira", 'symbol' => "TL", 'ASCII' => "TL"),
4452
4453 'MYR' => array('name' => "Malaysian Ringgit", 'symbol' => "RM", 'ASCII' => "RM"),
4454
4455 'NGN' => array('name' => "Nigerian naira", 'symbol' => "₦", 'ASCII' => "₦"),
4456
4457 'RON' => array('name' => "Romanian Leu", 'symbol' => "Lei", 'ASCII' => "Lei"),
4458
4459 'ZAR' => array('name' => "South African Rands", 'symbol' => "R", 'ASCII' => "R"),
4460 );
4461
4462 public function getSymbol($code = 'USD') {
4463
4464 if (!empty($this->currencies[$code]['ASCII'])) {
4465
4466 return (string) $this->currencies[$code]['ASCII'];
4467
4468 }
4469
4470 return (string) @$this->currencies[$code]['symbol'];
4471
4472 }
4473
4474 }
4475
4476 function order_columns($columns)
4477 {
4478 $columns = array(
4479 'cb' => '<input type="checkbox" />',
4480 'order_id' => __('Order', 'ebook-store'),
4481 'title' => __('Buyer', 'ebook-store'),
4482 'product' => __('Product', 'ebook-store'),
4483 'amount' => __('Amount', 'ebook-store'),
4484 'paypal' => __('Payment', 'ebook-store'),
4485 'downloadlink' => __('Delivery', 'ebook-store'),
4486 'formData' => __('Form Data', 'ebook-store'),
4487 'date' => __('Date', 'ebook-store'),
4488 );
4489 return $columns;
4490 }
4491 function order_custom_columns($column)
4492 {
4493 global $post;
4494 $c = new Currencies();
4495 switch ($column) {
4496 case 'order_id':
4497 $payment_status = get_post_meta($post->ID,'payment_status',true);
4498 $downloads = intval(get_post_meta($post->ID,'downloads',true));
4499 echo '<div class="ebook-order-list">';
4500 echo '<div class="ebook-order-list__title">#' . esc_html($post->ID) . '</div>';
4501 echo '<div class="ebook-order-list__badges">';
4502 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status ? $payment_status : __('Unknown', 'ebook-store'), ebook_store_order_status_class($payment_status) );
4503 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), number_format_i18n($downloads) );
4504 echo '</div>';
4505 $txn_id = get_post_meta($post->ID,'txn_id',true);
4506 if ($txn_id) {
4507 echo '<span class="ebook-order-list__inline-code">' . esc_html($txn_id) . '</span>';
4508 }
4509 echo '</div>';
4510 break;
4511 case "title":
4512 $first_name = trim((string)get_post_meta($post->ID,'first_name',true));
4513 $last_name = trim((string)get_post_meta($post->ID,'last_name',true));
4514 $buyer_name = trim($first_name . ' ' . $last_name);
4515 $mail = trim((string)get_post_meta($post->ID,'payer_email',true));
4516 $country = trim((string)get_post_meta($post->ID,'residence_country',true));
4517 echo '<div class="ebook-order-list">';
4518 echo '<div class="ebook-order-list__title">' . esc_html($buyer_name !== '' ? $buyer_name : __('Unnamed buyer', 'ebook-store')) . '</div>';
4519 if ($mail !== '') {
4520 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4521 }
4522 echo '<div class="ebook-order-list__badges">';
4523 if ($country !== '') {
4524 echo ebook_store_render_order_badge( __('Country', 'ebook-store'), $country );
4525 }
4526 $payer_status = get_post_meta($post->ID,'payer_status',true);
4527 if ($payer_status !== '') {
4528 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status, ebook_store_order_status_class($payer_status) );
4529 }
4530 echo '</div>';
4531 echo '</div>';
4532 break;
4533 case "product":
4534 $ebook_id = intval(get_post_meta($post->ID,'ebook',true));
4535 $product_title = $ebook_id ? get_the_title($ebook_id) : '';
4536 $item_number = get_post_meta($post->ID,'item_number',true);
4537 $quantity = get_post_meta($post->ID,'quantity',true);
4538 echo '<div class="ebook-order-list">';
4539 if ($ebook_id && $product_title) {
4540 echo '<div class="ebook-order-list__title"><a href="post.php?post=' . esc_attr($ebook_id) . '&action=edit">' . esc_html($product_title) . '</a></div>';
4541 } else {
4542 echo '<div class="ebook-order-list__title">' . esc_html(get_post_meta($post->ID,'item_name',true)) . '</div>';
4543 }
4544 echo '<div class="ebook-order-list__badges">';
4545 if ($quantity !== '') {
4546 echo ebook_store_render_order_badge( __('Qty', 'ebook-store'), $quantity );
4547 }
4548 if ($item_number !== '') {
4549 echo ebook_store_render_order_badge( __('SKU', 'ebook-store'), $item_number );
4550 }
4551 echo '</div>';
4552 echo '</div>';
4553 break;
4554 case "amount":
4555 $mc_currency = get_post_meta($post->ID,'mc_currency',true);
4556 $mc_gross = get_post_meta($post->ID,'mc_gross',true);
4557 $mc_fee = get_post_meta($post->ID,'mc_fee',true);
4558 $total = floatval($mc_gross) - floatval($mc_fee);
4559 echo '<div class="ebook-order-list">';
4560 echo '<div class="ebook-order-list__title">' . esc_html( ebook_store_order_money_display( $mc_gross, $mc_currency ) ) . '</div>';
4561 echo '<div class="ebook-order-list__stack">';
4562 echo '<span>' . esc_html__( 'Fee:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $mc_fee, $mc_currency ) ) . '</span>';
4563 echo '<span>' . esc_html__( 'Net:', 'ebook-store' ) . ' ' . esc_html( ebook_store_order_money_display( $total, $mc_currency ) ) . '</span>';
4564 echo '</div>';
4565 echo '</div>';
4566 break;
4567 case "paypal":
4568 $mail = esc_attr(get_post_meta($post->ID,'payer_email',true));
4569 $payment_type = get_post_meta($post->ID,'payment_type',true);
4570 $payment_status = get_post_meta($post->ID,'payment_status',true);
4571 $refund_gateway = ebook_store_get_order_refund_gateway( $post->ID );
4572 echo '<div class="ebook-order-list">';
4573 echo '<div class="ebook-order-list__title">' . esc_html( $payment_type !== '' ? $payment_type : __('PayPal', 'ebook-store') ) . '</div>';
4574 echo '<div class="ebook-order-list__badges">';
4575 if ( $payment_status !== '' ) {
4576 echo ebook_store_render_order_badge( __('Status', 'ebook-store'), $payment_status, ebook_store_order_status_class($payment_status) );
4577 }
4578 $payer_status = get_post_meta($post->ID,'payer_status',true);
4579 if ( $payer_status !== '' ) {
4580 echo ebook_store_render_order_badge( __('Payer', 'ebook-store'), $payer_status );
4581 }
4582 echo '</div>';
4583 if ($mail !== '') {
4584 echo '<div class="ebook-order-list__meta"><a href="mailto:' . esc_attr($mail) . '">' . esc_html($mail) . '</a></div>';
4585 }
4586 if ( $refund_gateway !== '' ) {
4587 echo '<div class="ebook-order-list__actions">';
4588 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>';
4589 echo '</div>';
4590 }
4591 echo '</div>';
4592 break;
4593 case 'downloadlink':
4594 $downloadlink = ebook_download_link(
4595 array(
4596 'ebook_key' => array( get_post_meta( $post->ID, 'ebook_key', true ) ),
4597 'md5_nonce' => array( get_post_meta( $post->ID, 'md5_nonce', true ) ),
4598 'ebook' => array( get_post_meta( $post->ID, 'ebook', true ) ),
4599 'order_id' => array( $post->ID ),
4600 )
4601 );
4602 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
4603 $link = add_query_arg(array('ebook_key' => get_post_meta($post->ID,'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
4604 $password = get_post_meta($post->ID,'password',true);
4605 echo '<div class="ebook-order-list">';
4606 echo '<div class="ebook-order-list__actions">';
4607 if ($downloadlink != '') {
4608 echo '<a href="' . esc_url($downloadlink) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Download', 'ebook-store') . '</a>';
4609 } else {
4610 echo '<span class="ebook-order-list__empty">' . esc_html__('No direct link saved', 'ebook-store') . '</span>';
4611 }
4612 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>';
4613 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>';
4614 echo '<a href="' . esc_url($link) . '" class="button button-small" target="_blank" rel="noopener">' . esc_html__('Thank you page', 'ebook-store') . '</a>';
4615 echo '</div>';
4616 echo '<div class="ebook-order-list__badges">';
4617 $downloads = get_post_meta($post->ID,'downloads',true);
4618 echo ebook_store_render_order_badge( __('Downloads', 'ebook-store'), $downloads !== '' ? $downloads : '0' );
4619 if ( $password !== '' ) {
4620 echo ebook_store_render_order_badge( __('Password', 'ebook-store'), $password );
4621 }
4622 echo '</div>';
4623 echo '</div>';
4624 break;
4625 case 'formData':
4626 $md5_nonce = get_post_meta($post->ID,'md5_nonce',true);
4627 $data = get_post_meta($post->ID,'formData',true);
4628 $data = json_decode($data, true);
4629 if (is_array($data) && !empty($data)) {
4630 unset($data['md5_nonce']);
4631 echo '<div class="ebook-order-list ebook-order-list__pairs">';
4632 $count = 0;
4633 foreach ($data as $key => $value) {
4634 if ($count >= 5) {
4635 break;
4636 }
4637 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>';
4638 $count++;
4639 }
4640 if ($count === 0) {
4641 echo '<span class="ebook-order-list__empty">' . esc_html__('No form fields saved', 'ebook-store') . '</span>';
4642 }
4643 echo '</div>';
4644 } else {
4645 echo '<span class="ebook-order-list__empty">' . esc_html__('No extra form data', 'ebook-store') . '</span>';
4646 }
4647
4648 break;
4649 }
4650 }
4651 function ebook_readfile_chunked( $file, $retbytes = TRUE ) {
4652 $chunksize = 1 * (1024 * 1024);
4653 $buffer = '';
4654 $cnt = 0;
4655 $handle = fopen( $file, 'r' );
4656 header('Cache-Control: no-store');
4657
4658 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4659 return readfile($file);
4660 //if ob_clean / flush are executed on some hosts, the file is not properly downloaded.
4661
4662 if( $size = @filesize( $file ) ) header("Content-Length: " . $size );
4663 //wp_die($file);
4664 ob_clean();
4665 flush();
4666 return readfile($file);
4667
4668 if ( $handle === FALSE ) return FALSE;
4669
4670 while ( ! feof( $handle ) ) :
4671 $buffer = fread( $handle, $chunksize );
4672 echo $buffer;
4673 //ob_flush();
4674 //flush();
4675
4676 if ( $retbytes ) $cnt += strlen( $buffer );
4677 endwhile;
4678
4679 $status = fclose( $handle );
4680
4681 if ( $retbytes AND $status ) return $cnt;
4682
4683 return $status;
4684 }
4685 function ebook_get_file_ctype( $extension ) {
4686 switch( $extension ):
4687 case 'ac' : $ctype = "application/pkix-attr-cert"; break;
4688 case 'adp' : $ctype = "audio/adpcm"; break;
4689 case 'ai' : $ctype = "application/postscript"; break;
4690 case 'aif' : $ctype = "audio/x-aiff"; break;
4691 case 'aifc' : $ctype = "audio/x-aiff"; break;
4692 case 'aiff' : $ctype = "audio/x-aiff"; break;
4693 case 'air' : $ctype = "application/vnd.adobe.air-application-installer-package+zip"; break;
4694 case 'apk' : $ctype = "application/vnd.android.package-archive"; break;
4695 case 'asc' : $ctype = "application/pgp-signature"; break;
4696 case 'atom' : $ctype = "application/atom+xml"; break;
4697 case 'atomcat' : $ctype = "application/atomcat+xml"; break;
4698 case 'atomsvc' : $ctype = "application/atomsvc+xml"; break;
4699 case 'au' : $ctype = "audio/basic"; break;
4700 case 'aw' : $ctype = "application/applixware"; break;
4701 case 'avi' : $ctype = "video/x-msvideo"; break;
4702 case 'bcpio' : $ctype = "application/x-bcpio"; break;
4703 case 'bin' : $ctype = "application/octet-stream"; break;
4704 case 'bmp' : $ctype = "image/bmp"; break;
4705 case 'boz' : $ctype = "application/x-bzip2"; break;
4706 case 'bpk' : $ctype = "application/octet-stream"; break;
4707 case 'bz' : $ctype = "application/x-bzip"; break;
4708 case 'bz2' : $ctype = "application/x-bzip2"; break;
4709 case 'ccxml' : $ctype = "application/ccxml+xml"; break;
4710 case 'cdmia' : $ctype = "application/cdmi-capability"; break;
4711 case 'cdmic' : $ctype = "application/cdmi-container"; break;
4712 case 'cdmid' : $ctype = "application/cdmi-domain"; break;
4713 case 'cdmio' : $ctype = "application/cdmi-object"; break;
4714 case 'cdmiq' : $ctype = "application/cdmi-queue"; break;
4715 case 'cdf' : $ctype = "application/x-netcdf"; break;
4716 case 'cer' : $ctype = "application/pkix-cert"; break;
4717 case 'cgm' : $ctype = "image/cgm"; break;
4718 case 'class' : $ctype = "application/octet-stream"; break;
4719 case 'cpio' : $ctype = "application/x-cpio"; break;
4720 case 'cpt' : $ctype = "application/mac-compactpro"; break;
4721 case 'crl' : $ctype = "application/pkix-crl"; break;
4722 case 'csh' : $ctype = "application/x-csh"; break;
4723 case 'css' : $ctype = "text/css"; break;
4724 case 'cu' : $ctype = "application/cu-seeme"; break;
4725 case 'davmount' : $ctype = "application/davmount+xml"; break;
4726 case 'dbk' : $ctype = "application/docbook+xml"; break;
4727 case 'dcr' : $ctype = "application/x-director"; break;
4728 case 'deploy' : $ctype = "application/octet-stream"; break;
4729 case 'dif' : $ctype = "video/x-dv"; break;
4730 case 'dir' : $ctype = "application/x-director"; break;
4731 case 'dist' : $ctype = "application/octet-stream"; break;
4732 case 'distz' : $ctype = "application/octet-stream"; break;
4733 case 'djv' : $ctype = "image/vnd.djvu"; break;
4734 case 'djvu' : $ctype = "image/vnd.djvu"; break;
4735 case 'dll' : $ctype = "application/octet-stream"; break;
4736 case 'dmg' : $ctype = "application/octet-stream"; break;
4737 case 'dms' : $ctype = "application/octet-stream"; break;
4738 case 'doc' : $ctype = "application/msword"; break;
4739 case 'docx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; break;
4740 case 'dotx' : $ctype = "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; break;
4741 case 'dssc' : $ctype = "application/dssc+der"; break;
4742 case 'dtd' : $ctype = "application/xml-dtd"; break;
4743 case 'dump' : $ctype = "application/octet-stream"; break;
4744 case 'dv' : $ctype = "video/x-dv"; break;
4745 case 'dvi' : $ctype = "application/x-dvi"; break;
4746 case 'dxr' : $ctype = "application/x-director"; break;
4747 case 'ecma' : $ctype = "application/ecmascript"; break;
4748 case 'elc' : $ctype = "application/octet-stream"; break;
4749 case 'emma' : $ctype = "application/emma+xml"; break;
4750 case 'eps' : $ctype = "application/postscript"; break;
4751 case 'epub' : $ctype = "application/epub+zip"; break;
4752 case 'etx' : $ctype = "text/x-setext"; break;
4753 case 'exe' : $ctype = "application/octet-stream"; break;
4754 case 'exi' : $ctype = "application/exi"; break;
4755 case 'ez' : $ctype = "application/andrew-inset"; break;
4756 case 'f4v' : $ctype = "video/x-f4v"; break;
4757 case 'fli' : $ctype = "video/x-fli"; break;
4758 case 'flv' : $ctype = "video/x-flv"; break;
4759 case 'gif' : $ctype = "image/gif"; break;
4760 case 'gml' : $ctype = "application/srgs"; break;
4761 case 'gpx' : $ctype = "application/gml+xml"; break;
4762 case 'gram' : $ctype = "application/gpx+xml"; break;
4763 case 'grxml' : $ctype = "application/srgs+xml"; break;
4764 case 'gtar' : $ctype = "application/x-gtar"; break;
4765 case 'gxf' : $ctype = "application/gxf"; break;
4766 case 'hdf' : $ctype = "application/x-hdf"; break;
4767 case 'hqx' : $ctype = "application/mac-binhex40"; break;
4768 case 'htm' : $ctype = "text/html"; break;
4769 case 'html' : $ctype = "text/html"; break;
4770 case 'ice' : $ctype = "x-conference/x-cooltalk"; break;
4771 case 'ico' : $ctype = "image/x-icon"; break;
4772 case 'ics' : $ctype = "text/calendar"; break;
4773 case 'ief' : $ctype = "image/ief"; break;
4774 case 'ifb' : $ctype = "text/calendar"; break;
4775 case 'iges' : $ctype = "model/iges"; break;
4776 case 'igs' : $ctype = "model/iges"; break;
4777 case 'ink' : $ctype = "application/inkml+xml"; break;
4778 case 'inkml' : $ctype = "application/inkml+xml"; break;
4779 case 'ipfix' : $ctype = "application/ipfix"; break;
4780 case 'jar' : $ctype = "application/java-archive"; break;
4781 case 'jnlp' : $ctype = "application/x-java-jnlp-file"; break;
4782 case 'jp2' : $ctype = "image/jp2"; break;
4783 case 'jpe' : $ctype = "image/jpeg"; break;
4784 case 'jpeg' : $ctype = "image/jpeg"; break;
4785 case 'jpg' : $ctype = "image/jpeg"; break;
4786 case 'js' : $ctype = "application/javascript"; break;
4787 case 'json' : $ctype = "application/json"; break;
4788 case 'jsonml' : $ctype = "application/jsonml+json"; break;
4789 case 'kar' : $ctype = "audio/midi"; break;
4790 case 'latex' : $ctype = "application/x-latex"; break;
4791 case 'lha' : $ctype = "application/octet-stream"; break;
4792 case 'lrf' : $ctype = "application/octet-stream"; break;
4793 case 'lzh' : $ctype = "application/octet-stream"; break;
4794 case 'lostxml' : $ctype = "application/lost+xml"; break;
4795 case 'm3u' : $ctype = "audio/x-mpegurl"; break;
4796 case 'm4a' : $ctype = "audio/mp4a-latm"; break;
4797 case 'm4b' : $ctype = "audio/mp4a-latm"; break;
4798 case 'm4p' : $ctype = "audio/mp4a-latm"; break;
4799 case 'm4u' : $ctype = "video/vnd.mpegurl"; break;
4800 case 'm4v' : $ctype = "video/x-m4v"; break;
4801 case 'm21' : $ctype = "application/mp21"; break;
4802 case 'ma' : $ctype = "application/mathematica"; break;
4803 case 'mac' : $ctype = "image/x-macpaint"; break;
4804 case 'mads' : $ctype = "application/mads+xml"; break;
4805 case 'man' : $ctype = "application/x-troff-man"; break;
4806 case 'mar' : $ctype = "application/octet-stream"; break;
4807 case 'mathml' : $ctype = "application/mathml+xml"; break;
4808 case 'mbox' : $ctype = "application/mbox"; break;
4809 case 'me' : $ctype = "application/x-troff-me"; break;
4810 case 'mesh' : $ctype = "model/mesh"; break;
4811 case 'metalink' : $ctype = "application/metalink+xml"; break;
4812 case 'meta4' : $ctype = "application/metalink4+xml"; break;
4813 case 'mets' : $ctype = "application/mets+xml"; break;
4814 case 'mid' : $ctype = "audio/midi"; break;
4815 case 'midi' : $ctype = "audio/midi"; break;
4816 case 'mif' : $ctype = "application/vnd.mif"; break;
4817 case 'mods' : $ctype = "application/mods+xml"; break;
4818 case 'mov' : $ctype = "video/quicktime"; break;
4819 case 'movie' : $ctype = "video/x-sgi-movie"; break;
4820 case 'm1v' : $ctype = "video/mpeg"; break;
4821 case 'm2v' : $ctype = "video/mpeg"; break;
4822 case 'mp2' : $ctype = "audio/mpeg"; break;
4823 case 'mp2a' : $ctype = "audio/mpeg"; break;
4824 case 'mp21' : $ctype = "application/mp21"; break;
4825 case 'mp3' : $ctype = "audio/mpeg"; break;
4826 case 'mp3a' : $ctype = "audio/mpeg"; break;
4827 case 'mp4' : $ctype = "video/mp4"; break;
4828 case 'mp4s' : $ctype = "application/mp4"; break;
4829 case 'mpe' : $ctype = "video/mpeg"; break;
4830 case 'mpeg' : $ctype = "video/mpeg"; break;
4831 case 'mpg' : $ctype = "video/mpeg"; break;
4832 case 'mpg4' : $ctype = "video/mpeg"; break;
4833 case 'mpga' : $ctype = "audio/mpeg"; break;
4834 case 'mrc' : $ctype = "application/marc"; break;
4835 case 'mrcx' : $ctype = "application/marcxml+xml"; break;
4836 case 'ms' : $ctype = "application/x-troff-ms"; break;
4837 case 'mscml' : $ctype = "application/mediaservercontrol+xml"; break;
4838 case 'msh' : $ctype = "model/mesh"; break;
4839 case 'mxf' : $ctype = "application/mxf"; break;
4840 case 'mxu' : $ctype = "video/vnd.mpegurl"; break;
4841 case 'nc' : $ctype = "application/x-netcdf"; break;
4842 case 'oda' : $ctype = "application/oda"; break;
4843 case 'oga' : $ctype = "application/ogg"; break;
4844 case 'ogg' : $ctype = "application/ogg"; break;
4845 case 'ogx' : $ctype = "application/ogg"; break;
4846 case 'omdoc' : $ctype = "application/omdoc+xml"; break;
4847 case 'onetoc' : $ctype = "application/onenote"; break;
4848 case 'onetoc2' : $ctype = "application/onenote"; break;
4849 case 'onetmp' : $ctype = "application/onenote"; break;
4850 case 'onepkg' : $ctype = "application/onenote"; break;
4851 case 'opf' : $ctype = "application/oebps-package+xml"; break;
4852 case 'oxps' : $ctype = "application/oxps"; break;
4853 case 'p7c' : $ctype = "application/pkcs7-mime"; break;
4854 case 'p7m' : $ctype = "application/pkcs7-mime"; break;
4855 case 'p7s' : $ctype = "application/pkcs7-signature"; break;
4856 case 'p8' : $ctype = "application/pkcs8"; break;
4857 case 'p10' : $ctype = "application/pkcs10"; break;
4858 case 'pbm' : $ctype = "image/x-portable-bitmap"; break;
4859 case 'pct' : $ctype = "image/pict"; break;
4860 case 'pdb' : $ctype = "chemical/x-pdb"; break;
4861 case 'pdf' : $ctype = "application/pdf"; break;
4862 case 'pki' : $ctype = "application/pkixcmp"; break;
4863 case 'pkipath' : $ctype = "application/pkix-pkipath"; break;
4864 case 'pfr' : $ctype = "application/font-tdpfr"; break;
4865 case 'pgm' : $ctype = "image/x-portable-graymap"; break;
4866 case 'pgn' : $ctype = "application/x-chess-pgn"; break;
4867 case 'pgp' : $ctype = "application/pgp-encrypted"; break;
4868 case 'pic' : $ctype = "image/pict"; break;
4869 case 'pict' : $ctype = "image/pict"; break;
4870 case 'pkg' : $ctype = "application/octet-stream"; break;
4871 case 'png' : $ctype = "image/png"; break;
4872 case 'pnm' : $ctype = "image/x-portable-anymap"; break;
4873 case 'pnt' : $ctype = "image/x-macpaint"; break;
4874 case 'pntg' : $ctype = "image/x-macpaint"; break;
4875 case 'pot' : $ctype = "application/vnd.ms-powerpoint"; break;
4876 case 'potx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.template"; break;
4877 case 'ppm' : $ctype = "image/x-portable-pixmap"; break;
4878 case 'pps' : $ctype = "application/vnd.ms-powerpoint"; break;
4879 case 'ppsx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; break;
4880 case 'ppt' : $ctype = "application/vnd.ms-powerpoint"; break;
4881 case 'pptx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.presentation"; break;
4882 case 'prf' : $ctype = "application/pics-rules"; break;
4883 case 'ps' : $ctype = "application/postscript"; break;
4884 case 'psd' : $ctype = "image/photoshop"; break;
4885 case 'qt' : $ctype = "video/quicktime"; break;
4886 case 'qti' : $ctype = "image/x-quicktime"; break;
4887 case 'qtif' : $ctype = "image/x-quicktime"; break;
4888 case 'ra' : $ctype = "audio/x-pn-realaudio"; break;
4889 case 'ram' : $ctype = "audio/x-pn-realaudio"; break;
4890 case 'ras' : $ctype = "image/x-cmu-raster"; break;
4891 case 'rdf' : $ctype = "application/rdf+xml"; break;
4892 case 'rgb' : $ctype = "image/x-rgb"; break;
4893 case 'rm' : $ctype = "application/vnd.rn-realmedia"; break;
4894 case 'rmi' : $ctype = "audio/midi"; break;
4895 case 'roff' : $ctype = "application/x-troff"; break;
4896 case 'rss' : $ctype = "application/rss+xml"; break;
4897 case 'rtf' : $ctype = "text/rtf"; break;
4898 case 'rtx' : $ctype = "text/richtext"; break;
4899 case 'sgm' : $ctype = "text/sgml"; break;
4900 case 'sgml' : $ctype = "text/sgml"; break;
4901 case 'sh' : $ctype = "application/x-sh"; break;
4902 case 'shar' : $ctype = "application/x-shar"; break;
4903 case 'sig' : $ctype = "application/pgp-signature"; break;
4904 case 'silo' : $ctype = "model/mesh"; break;
4905 case 'sit' : $ctype = "application/x-stuffit"; break;
4906 case 'skd' : $ctype = "application/x-koan"; break;
4907 case 'skm' : $ctype = "application/x-koan"; break;
4908 case 'skp' : $ctype = "application/x-koan"; break;
4909 case 'skt' : $ctype = "application/x-koan"; break;
4910 case 'sldx' : $ctype = "application/vnd.openxmlformats-officedocument.presentationml.slide"; break;
4911 case 'smi' : $ctype = "application/smil"; break;
4912 case 'smil' : $ctype = "application/smil"; break;
4913 case 'snd' : $ctype = "audio/basic"; break;
4914 case 'so' : $ctype = "application/octet-stream"; break;
4915 case 'spl' : $ctype = "application/x-futuresplash"; break;
4916 case 'spx' : $ctype = "audio/ogg"; break;
4917 case 'src' : $ctype = "application/x-wais-source"; break;
4918 case 'stk' : $ctype = "application/hyperstudio"; break;
4919 case 'sv4cpio' : $ctype = "application/x-sv4cpio"; break;
4920 case 'sv4crc' : $ctype = "application/x-sv4crc"; break;
4921 case 'svg' : $ctype = "image/svg+xml"; break;
4922 case 'swf' : $ctype = "application/x-shockwave-flash"; break;
4923 case 't' : $ctype = "application/x-troff"; break;
4924 case 'tar' : $ctype = "application/x-tar"; break;
4925 case 'tcl' : $ctype = "application/x-tcl"; break;
4926 case 'tex' : $ctype = "application/x-tex"; break;
4927 case 'texi' : $ctype = "application/x-texinfo"; break;
4928 case 'texinfo' : $ctype = "application/x-texinfo"; break;
4929 case 'tif' : $ctype = "image/tiff"; break;
4930 case 'tiff' : $ctype = "image/tiff"; break;
4931 case 'torrent' : $ctype = "application/x-bittorrent"; break;
4932 case 'tr' : $ctype = "application/x-troff"; break;
4933 case 'tsv' : $ctype = "text/tab-separated-values"; break;
4934 case 'txt' : $ctype = "text/plain"; break;
4935 case 'ustar' : $ctype = "application/x-ustar"; break;
4936 case 'vcd' : $ctype = "application/x-cdlink"; break;
4937 case 'vrml' : $ctype = "model/vrml"; break;
4938 case 'vsd' : $ctype = "application/vnd.visio"; break;
4939 case 'vss' : $ctype = "application/vnd.visio"; break;
4940 case 'vst' : $ctype = "application/vnd.visio"; break;
4941 case 'vsw' : $ctype = "application/vnd.visio"; break;
4942 case 'vxml' : $ctype = "application/voicexml+xml"; break;
4943 case 'wav' : $ctype = "audio/x-wav"; break;
4944 case 'wbmp' : $ctype = "image/vnd.wap.wbmp"; break;
4945 case 'wbmxl' : $ctype = "application/vnd.wap.wbxml"; break;
4946 case 'wm' : $ctype = "video/x-ms-wm"; break;
4947 case 'wml' : $ctype = "text/vnd.wap.wml"; break;
4948 case 'wmlc' : $ctype = "application/vnd.wap.wmlc"; break;
4949 case 'wmls' : $ctype = "text/vnd.wap.wmlscript"; break;
4950 case 'wmlsc' : $ctype = "application/vnd.wap.wmlscriptc"; break;
4951 case 'wmv' : $ctype = "video/x-ms-wmv"; break;
4952 case 'wmx' : $ctype = "video/x-ms-wmx"; break;
4953 case 'wrl' : $ctype = "model/vrml"; break;
4954 case 'xbm' : $ctype = "image/x-xbitmap"; break;
4955 case 'xdssc' : $ctype = "application/dssc+xml"; break;
4956 case 'xer' : $ctype = "application/patch-ops-error+xml"; break;
4957 case 'xht' : $ctype = "application/xhtml+xml"; break;
4958 case 'xhtml' : $ctype = "application/xhtml+xml"; break;
4959 case 'xla' : $ctype = "application/vnd.ms-excel"; break;
4960 case 'xlam' : $ctype = "application/vnd.ms-excel.addin.macroEnabled.12"; break;
4961 case 'xlc' : $ctype = "application/vnd.ms-excel"; break;
4962 case 'xlm' : $ctype = "application/vnd.ms-excel"; break;
4963 case 'xls' : $ctype = "application/vnd.ms-excel"; break;
4964 case 'xlsx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; break;
4965 case 'xlsb' : $ctype = "application/vnd.ms-excel.sheet.binary.macroEnabled.12"; break;
4966 case 'xlt' : $ctype = "application/vnd.ms-excel"; break;
4967 case 'xltx' : $ctype = "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; break;
4968 case 'xlw' : $ctype = "application/vnd.ms-excel"; break;
4969 case 'xml' : $ctype = "application/xml"; break;
4970 case 'xpm' : $ctype = "image/x-xpixmap"; break;
4971 case 'xsl' : $ctype = "application/xml"; break;
4972 case 'xslt' : $ctype = "application/xslt+xml"; break;
4973 case 'xul' : $ctype = "application/vnd.mozilla.xul+xml"; break;
4974 case 'xwd' : $ctype = "image/x-xwindowdump"; break;
4975 case 'xyz' : $ctype = "chemical/x-xyz"; break;
4976 case 'zip' : $ctype = "application/zip"; break;
4977 default : $ctype = "application/force-download";
4978 endswitch;
4979
4980 return apply_filters( 'ebook_file_ctype', $ctype );
4981 }
4982 function ebook_process_download($data = false) {
4983 $QSWPOptions = new QSWPOptions();
4984 if ($data) {
4985 extract($data);
4986 }
4987 // Errors must not be printed into a binary file download, but the site's
4988 // error *reporting* level is the site owner's business, not this plugin's.
4989 @ini_set( 'display_errors', '0' );
4990
4991 $formats = array('mobi','txt','epub','zip', 'mp3','mp4');
4992 if (@$_GET['wc_order'] != '') {
4993 $formats[] = 'pdf';
4994 }
4995 if ((@$_REQUEST['ebook_key'] != false && $_REQUEST['action'] == 'download') || (@$_REQUEST['action'] == 'wc_order_download')) {
4996 //error_reporting(E_ALL);
4997 if( function_exists( 'apache_setenv' ) ) @apache_setenv('no-gzip', 1);
4998 @ini_set( 'zlib.output_compression', 'Off' );
4999 nocache_headers();
5000 $order_id = 0;
5001 $ebook = 0;
5002 $request_ebook_key = isset( $_REQUEST['ebook_key'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['ebook_key'] ) ) : '';
5003 $request_order_id = isset( $_REQUEST['order_id'] ) ? absint( $_REQUEST['order_id'] ) : 0;
5004
5005 if ( $request_order_id > 0 && $request_ebook_key !== '' ) {
5006 $stored_ebook_key = (string) get_post_meta( $request_order_id, 'ebook_key', true );
5007 if ( $stored_ebook_key !== '' && hash_equals( $stored_ebook_key, $request_ebook_key ) ) {
5008 $order_id = $request_order_id;
5009 $ebook = (int) get_post_meta( $order_id, 'ebook', true );
5010 }
5011 }
5012
5013 if ( ! $order_id && $request_ebook_key !== '' ) {
5014 $loop = new WP_Query(
5015 array(
5016 'post_type' => 'ebook_order',
5017 'meta_key' => 'ebook_key',
5018 'meta_value' => $request_ebook_key,
5019 'posts_per_page' => 1,
5020 'orderby' => 'ID',
5021 'order' => 'DESC',
5022 )
5023 );
5024 while ( $loop->have_posts() ) : $loop->the_post();
5025 $order_id = get_the_ID();
5026 $ebook = get_post_meta(get_the_ID(),'ebook',true);
5027 endwhile;
5028 wp_reset_postdata();
5029 }
5030
5031 if (@$_REQUEST['action'] == 'wc_order_download') {
5032 // A WooCommerce download is only authorised by the order key. Verify it
5033 // here, before any file is resolved: this branch used to trust order_id
5034 // and ebook_id straight from the query string, which handed every paid
5035 // ebook to anonymous visitors.
5036 $order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
5037 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5038
5039 if ( ! ebook_store_wc_order_key_matches( $order_id, isset( $_GET['wc_order'] ) ? wp_unslash( $_GET['wc_order'] ) : '' ) ) {
5040 wp_die( esc_html__( 'This download link is not valid for that order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5041 }
5042
5043 if ( ! ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) ) {
5044 wp_die( esc_html__( 'That ebook is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5045 }
5046
5047 $ebook = $ebook_id;
5048 }
5049 if (@$_REQUEST['action'] == 'download' && @$_REQUEST['type'] == 'bonus') {
5050 $ebook_id = (int)$_GET['ebook_id'];
5051 $ebook = $ebook_id;
5052 $ebookObj = new EbookStoreEbook($ebook);
5053 $requested_file = $ebookObj->encrypted((int)$_GET['order_id'], 'pdf');
5054
5055 }
5056 $attachment = get_post_meta($ebook,'ebook_wp_custom_attachment');
5057 //wp_die(print_r($attachment,true));
5058 if (@!$requested_file) {
5059 $requested_file = $attachment[0]['file'];
5060 }
5061
5062 if (get_post_meta($order_id,'encrypted_pdf',true) == 'in_progress') {
5063 if (!ebook_store_encryptable($requested_file)) {
5064 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>');
5065 }
5066 if (get_option('ebook_store_no_autorefresh', false) == false) {
5067 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">
5068 setInterval(\'window.location.reload()\', 15000);
5069 </script>');
5070 } else {
5071 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>
5072 ");
5073 }
5074
5075 }
5076 if (get_post_meta($order_id,'encrypted_pdf',true) != '' && $_REQUEST['type'] != 'bonus') {
5077 $requested_file = get_post_meta($order_id,'encrypted_pdf',true);
5078 }
5079 if (@in_array($_GET['format'], $formats)) {
5080 $format_meta_tag = 'ebook_wp_custom_attachment' . (@$_GET['format'] != 'pdf' ? '_' . $_GET['format'] : '');
5081 $attachment = get_post_meta($ebook,$format_meta_tag);
5082 //wp_die(print_r($attachment,true));
5083 $requested_file = $attachment[0]['file'];
5084 if ( @$_GET['format'] === 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
5085 // Standalone (non-WooCommerce) epub downloads were served raw; run
5086 // them through the watermarker too, using the resolved order id.
5087 $ebookObj = new EbookStoreEbook( $ebook );
5088 $ebookObj->order_id = $order_id;
5089 $requested_file = $ebookObj->encrypted( $order_id, 'epub' );
5090 }
5091 if (@$_GET['wc_order'] != '') {
5092 $wc_order = sanitize_text_field($_GET['wc_order']);
5093 $ebookObj = new EbookStoreEbook($ebook);
5094 $requested_file = $ebookObj->encrypted((int)$_GET['order_id'], $_GET['format']);
5095 }
5096 }
5097 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5098 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
5099 if ( $payment_status !== '' && ebook_store_is_negative_order_status( $payment_status ) ) {
5100 wp_die( esc_html__( 'Oops, this order is no longer eligible for downloads.', 'ebook-store' ) );
5101 }
5102 $downloads = (int) get_post_meta( $order_id, 'downloads', true );
5103 $downloads_limit = ebook_store_get_download_limit();
5104 //wp_die('Downloads ' . $downloads);
5105 if ( $downloads >= $downloads_limit ) {
5106 wp_die('Oops, you have reached the maximum amount of downloads for this order, you need to order again.');
5107 }
5108 // $gmt_timestamp = get_post_time();
5109 // if (!$gmt_timestamp) {
5110 // $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5111 // }
5112 //wp_die("GMT timestamp $gmt_timestamp, time " . time() . ' post id ' . get_the_ID());
5113
5114
5115 $gmt_timestamp = get_post_time('U', false, ($order_id > 0 ? $order_id : get_the_ID()));
5116 if (!$gmt_timestamp) {
5117 $gmt_timestamp = get_post_time('U', false, (int)$_GET['order_id']);
5118 }
5119
5120
5121 $link_expiration = trim( (string) get_option( 'link_expiration', $QSWPOptions->link_expiration ) );
5122 if ( $link_expiration === '' ) {
5123 $link_expiration = '1 year';
5124 }
5125
5126 $strtotime = strtotime("+" . $link_expiration,$gmt_timestamp);
5127
5128 if (WP_DEBUG == true) {
5129 //error_log("strtotime $strtotime vs time " . time() . ' vs GMT TIMESTAMP ' . $gmt_timestamp);
5130 }
5131 if ($strtotime < time()) {
5132 wp_die('Oops, the link you are using has expired, you need to order again.');
5133 }
5134
5135 if ( ! $requested_file || ! file_exists( $requested_file ) ) {
5136 wp_die( esc_html__( 'Sorry, the ebook file was not found for that order. It may have expired.', 'ebook-store' ) );
5137 }
5138
5139 header("Robots: none");
5140 header("Content-Type: " . $ctype . "");
5141 header("Content-Description: File Transfer");
5142 header("Content-Disposition: attachment; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5143 header("Content-Transfer-Encoding: binary");
5144 ebook_readfile_chunked($requested_file);
5145 ebook_count_download($order_id);
5146 die();
5147 //endwhile;
5148 } else if (@$_REQUEST['action'] == 'download_free') {
5149
5150 @$formData = ebook_store_get_form($_REQUEST['md5_nonce']);
5151 if (is_object($formData)) {
5152 $marketing_emails = array_unique(
5153 array_filter(
5154 array(
5155 isset( $formData->email ) ? $formData->email : '',
5156 isset( $formData->payer_email ) ? $formData->payer_email : '',
5157 )
5158 )
5159 );
5160 foreach ( $marketing_emails as $marketing_email ) {
5161 ebook_store_subscribe_buyer_to_marketing_lists( $marketing_email );
5162 }
5163 }
5164 // Only suppress *display* of errors — this runs while streaming a file.
5165 @ini_set( 'display_errors', '0' );
5166
5167 $id = $_REQUEST['p'];
5168 $loop = new WP_Query( array ( 'post_type' => 'ebook', 'p' => $id ) );
5169
5170
5171
5172
5173 while ( $loop->have_posts() ) : $loop->the_post();
5174 $attachment = get_post_meta($id,'ebook_wp_custom_attachment');
5175 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
5176
5177 if (get_option('encrypt_pdf') == 1) {
5178 if (!ebook_store_encryptable($attachment[0]['file'])) {
5179 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>');
5180 }
5181 }
5182 //die();
5183 if ($ebook['ebook_price'] == 0) {
5184 $ebook['donate_or_download'] = 'free';
5185 }
5186 if ($ebook['ebook_price'] > 0 && $ebook['donate_or_download'] != 'free') {
5187 wp_die('You are trying to download a file that is not free - ' . get_the_title());
5188 }
5189 $status = get_post_status();
5190 erl($status);
5191
5192 if ($status != 'publish') {
5193 wp_die('You are trying to download a file that is not available.');
5194 }
5195
5196 $requested_file = $attachment[0]['file'];
5197
5198 $ctype = ebook_get_file_ctype(pathinfo($requested_file,PATHINFO_EXTENSION));
5199 $gmt_timestamp = get_post_time('U');
5200 //insert order
5201 $my_post = array(
5202 'post_title' => 'Free Download',
5203 'post_type' => 'ebook_order',
5204 'post_status' => 'publish',
5205 'post_author' => 1,
5206 'post_category' => array(8,39));
5207 $post_id = wp_insert_post( $my_post, @$wp_error );
5208 //txn_id payment_date payer_email
5209 //encrypt here.
5210 global $current_user;
5211 $current_user = wp_get_current_user();
5212 $data = array();
5213
5214 $data['txn_id'] = 'free';
5215 //$data['payment_date'] = date("F j, Y, g:i a");
5216 $data['payment_date'] = current_time('mysql');
5217
5218
5219
5220 if (get_option('encrypt_files_for_logged_in_users') == 1) {
5221 $data['payer_email'] = $current_user->user_email;
5222 } else {
5223 $data['payer_email'] = '';
5224 if (@$formData->email != '') {
5225 $data['payer_email'] = $formData->email;
5226 }
5227 }
5228
5229 if ($formData->payer_email != '') {
5230 $data['payer_email'] = $formData->payer_email;
5231 }
5232
5233 //error_log(print_r($formData,true));
5234
5235
5236 $data['first_name'] = $current_user->user_firstname;
5237 $data['last_name'] = $current_user->user_lastname;
5238 $data['mc_currency'] = ebook_store_get_store_currency();
5239 $data['tax'] = 0;
5240 $data['ip'] = $_SERVER['REMOTE_ADDR'];
5241 $data['residence_country'] = 'n/a';
5242 //error_log('data ' . print_r($data,true));
5243 if ($data['first_name'] == '') {
5244 if ($formData->name != '') {
5245 $data['first_name'] = $formData->name;
5246 $data['last_name'] = '';
5247 } else if ($formData->first_name != '') {
5248 $data['first_name'] = $formData->first_name;
5249 $data['last_name'] = $formData->last_name;
5250 } else {
5251 $data['first_name'] = 'Anonymous';
5252 $data['last_name'] = 'User';
5253
5254 }
5255 }
5256
5257
5258 //wp_die($data['payer_email']);
5259
5260 $md5_nonce = strip_tags($_REQUEST['md5_nonce']);
5261 $ebook_key = $md5_nonce;
5262
5263 $data['password'] = ebook_store_get_password_for_order($post_id, array(
5264 'password' => isset($data['password']) ? $data['password'] : '',
5265 'payer_email' => $data['payer_email'],
5266 'md5_nonce' => $md5_nonce,
5267 'ebook_key' => $ebook_key,
5268 ));
5269
5270 global $attachment;
5271 @$attachment[0]['file'] = $requested_file;
5272
5273 $fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
5274 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
5275 //ebook_encrypt_pdf($data, $post_id);
5276 $requested_file = ebook_encrypt_pdf($data, $post_id);
5277 }
5278
5279 $formData = ebook_store_get_form($md5_nonce);
5280 $formData = json_encode($formData);
5281 $item_name = get_the_title((int)$_REQUEST['p']);
5282 //die($item_name);
5283 update_post_meta($post_id,'ebook_key',$md5_nonce);
5284 update_post_meta($post_id,'downloads',1);
5285 update_post_meta($post_id,'formData',wp_slash($formData));
5286 update_post_meta($post_id, 'downloadlink', 'http://' . sanitize_text_field($_SERVER['HTTP_HOST']) . esc_url_raw($_SERVER['REQUEST_URI']));
5287 update_post_meta($post_id,'ebook',(int)$_REQUEST['p']);
5288 update_post_meta($post_id,'item_name',$item_name);
5289 update_post_meta($post_id,'payer_email',(@$data['payer_email'] != '' ? $data['payer_email'] : 'N/A'));
5290 update_post_meta($post_id,'mc_gross',0);
5291 update_post_meta($post_id,'mc_fee',0);
5292 update_post_meta($post_id,'payment_date',date("F j, Y, g:i a"));
5293 foreach ($data as $dk => $dv) {
5294 update_post_meta($post_id,$dk,$dv);
5295 }
5296 //get_post_meta($post->ID,'item_name',true)
5297 //insert order end
5298 if (headers_sent()) {
5299 die('Sorry, headers sent.');
5300 }
5301 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
5302 if (!$checkoutPage) {
5303 $checkoutPage = $post_id;
5304 }
5305 update_post_meta($post_id,'encrypted_pdf',wp_slash($requested_file));
5306 header("Location: ". add_query_arg(array('ebook_key' => $md5_nonce, 'action' => 'thank_you'),get_permalink($checkoutPage)));
5307
5308 // header("Robots: none");
5309 // header("Content-Type: " . $ctype . "");
5310 // header("Content-Description: File Transfer");
5311 // header("Content-Disposition: attachment; filename=\"" . apply_filters( 'ebook_requested_file_name', basename( $requested_file ) ) . "\";");
5312 // header("Content-Transfer-Encoding: binary");
5313 // ebook_readfile_chunked($requested_file);
5314 endwhile;
5315 die();
5316 } else if ($_REQUEST['action'] == 'ebook_bonus_download') {
5317 $ebook_id = isset( $_GET['ebook_id'] ) ? absint( $_GET['ebook_id'] ) : 0;
5318 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]/', '', (string) wp_unslash( $_GET['ebook_key'] ) ) : '';
5319
5320 // The key must resolve to a real order before we do anything. Without this
5321 // an anonymous GET with a bogus key created a junk ebook_order (null email,
5322 // garbage details) and could trigger a marketing subscribe + PDF encrypt.
5323 $ebook_order_meta = $ebook_key !== '' ? ebook_get_order( 'ebook_key', $ebook_key ) : null;
5324 if ( empty( $ebook_order_meta['order_id'][0] ) ) {
5325 wp_die( esc_html__( 'This download link is not valid.', 'ebook-store' ), '', array( 'response' => 403 ) );
5326 }
5327
5328 // The requested bonus book must actually be a bonus of that order's ebook.
5329 $parent_ebook = isset( $ebook_order_meta['ebook'][0] ) ? absint( $ebook_order_meta['ebook'][0] ) : 0;
5330 $allowed_bonus = $parent_ebook ? (array) get_post_meta( $parent_ebook, 'ebook_bonus', true ) : array();
5331 $allowed_bonus = array_map( 'absint', $allowed_bonus );
5332 if ( ! in_array( $ebook_id, $allowed_bonus, true ) ) {
5333 wp_die( esc_html__( 'That bonus book is not part of this order.', 'ebook-store' ), '', array( 'response' => 403 ) );
5334 }
5335
5336 $data['ebook'] = get_post_meta( $ebook_id, 'ebook_bonus', true );
5337 $data['txn_id'] = 'Bonus Download (' . ( isset( $ebook_order_meta['item_name'][0] ) ? $ebook_order_meta['item_name'][0] : '' ) . ')';
5338 $data['payer_email'] = isset( $ebook_order_meta['payer_email'][0] ) ? $ebook_order_meta['payer_email'][0] : '';
5339 $data['first_name'] = 'Bonus';
5340 $data['last_name'] = 'Download (Order #' . $ebook_order_meta['order_id'][0] . ')';
5341 ebook_store_add_order($data, true);
5342 @$ebook_order['ebook_key'][0] = $ebook_key;
5343 @$ebook_order['ebook'][0] = $data['ebook'];
5344 $downloadlink = ebook_download_link($ebook_order, 0, 1);
5345 header("Location: " . $downloadlink); //add proper ebook id here and allow it.
5346 //wp_die($downloadlink);
5347 }
5348 }
5349 function ebook_count_download($order_id) {
5350 $downloads = get_post_meta($order_id,'downloads',true);
5351 return update_post_meta($order_id,'downloads',floatval($downloads) + 1);
5352 }
5353 function ebook_get_order($key = 'ebook_key',$val = '') {
5354 $loop = new WP_Query( array ( 'post_type' => 'ebook_order', 'meta_key' => $key, 'meta_value' => $val ) );
5355 while ( $loop->have_posts() ) : $loop->the_post();
5356 $meta = get_post_meta(get_the_ID(),null,true);
5357 $meta['order_id'][0] = get_the_ID();
5358 $gross = isset( $meta['mc_gross'][0] ) ? (float) $meta['mc_gross'][0] : 0;
5359 $tax = isset( $meta['tax'][0] ) ? (float) $meta['tax'][0] : 0;
5360 $meta['total'][0] = number_format( round( $gross + $tax, 2 ), 2 );
5361 return $meta;
5362 endwhile;
5363 }
5364
5365 function toMoney($val,$symbol='',$r=2)
5366 {
5367 if ( is_array( $val ) ) {
5368 $val = reset( $val );
5369 }
5370
5371 return number_format( (float) $val, 2 );
5372
5373 }
5374
5375 function ebook_store_normalize_email_log_headers( $headers ) {
5376 if ( empty( $headers ) ) {
5377 return array();
5378 }
5379
5380 if ( is_string( $headers ) ) {
5381 $headers = preg_split( '/\r\n|\r|\n/', $headers );
5382 }
5383
5384 if ( ! is_array( $headers ) ) {
5385 $headers = array( (string) $headers );
5386 }
5387
5388 $normalized = array();
5389
5390 foreach ( $headers as $header ) {
5391 $header = trim( wp_strip_all_tags( (string) $header ) );
5392 if ( $header === '' ) {
5393 continue;
5394 }
5395 $normalized[] = $header;
5396 }
5397
5398 return array_values( array_unique( $normalized ) );
5399 }
5400
5401 function ebook_store_normalize_email_log_attachments( $attachments ) {
5402 if ( empty( $attachments ) ) {
5403 return array();
5404 }
5405
5406 if ( is_string( $attachments ) ) {
5407 $attachments = array( $attachments );
5408 }
5409
5410 if ( ! is_array( $attachments ) ) {
5411 return array();
5412 }
5413
5414 $normalized = array();
5415
5416 foreach ( $attachments as $attachment ) {
5417 if ( is_array( $attachment ) && isset( $attachment['file'] ) ) {
5418 $attachment = $attachment['file'];
5419 }
5420
5421 $attachment = trim( (string) $attachment );
5422
5423 if ( $attachment === '' ) {
5424 continue;
5425 }
5426
5427 $normalized[] = $attachment;
5428 }
5429
5430 return array_values( array_unique( $normalized ) );
5431 }
5432
5433 function ebook_store_json_encode_for_log( $value ) {
5434 $encoded = wp_json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
5435
5436 return $encoded ? $encoded : '';
5437 }
5438
5439 function ebook_store_get_first_meta_value( $value ) {
5440 if ( is_array( $value ) ) {
5441 $value = reset( $value );
5442 }
5443
5444 return $value;
5445 }
5446
5447 function ebook_store_create_email_log_entry( $args = array() ) {
5448 $defaults = array(
5449 'to' => '',
5450 'subject' => '',
5451 'body' => '',
5452 'headers' => array(),
5453 'attachments' => array(),
5454 'result' => false,
5455 'source' => 'standalone',
5456 'related_order_type' => '',
5457 'related_order_id' => 0,
5458 'ebook_id' => 0,
5459 'ebook_key' => '',
5460 'gateway' => '',
5461 'order' => array(),
5462 'context' => array(),
5463 'parent_log_id' => 0,
5464 );
5465
5466 $args = wp_parse_args( $args, $defaults );
5467
5468 $recipient = trim( (string) $args['to'] );
5469 $subject = wp_strip_all_tags( (string) $args['subject'] );
5470 $sent_at = current_time( 'mysql' );
5471 $sent_at_gmt = current_time( 'mysql', true );
5472 $status = $args['result'] ? 'sent' : 'failed';
5473 $order = is_array( $args['order'] ) ? $args['order'] : array();
5474 $context = is_array( $args['context'] ) ? $args['context'] : array();
5475 $headers = ebook_store_normalize_email_log_headers( $args['headers'] );
5476 $attachments = ebook_store_normalize_email_log_attachments( $args['attachments'] );
5477 $ebook_id = absint( ebook_store_get_first_meta_value( $args['ebook_id'] ) );
5478 $related_order_id = absint( ebook_store_get_first_meta_value( $args['related_order_id'] ) );
5479 $parent_log_id = absint( $args['parent_log_id'] );
5480 $gateway = sanitize_key( (string) $args['gateway'] );
5481 $source = sanitize_key( (string) $args['source'] );
5482 $related_order_type = sanitize_key( (string) $args['related_order_type'] );
5483
5484 if ( $ebook_id < 1 && ! empty( $order['ebook'] ) ) {
5485 $ebook_id = absint( ebook_store_get_first_meta_value( $order['ebook'] ) );
5486 }
5487
5488 if ( $related_order_id < 1 && ! empty( $order['order_id'] ) ) {
5489 $related_order_id = absint( ebook_store_get_first_meta_value( $order['order_id'] ) );
5490 }
5491
5492 if ( $related_order_type === '' ) {
5493 $related_order_type = $source === 'woocommerce' ? 'shop_order' : 'ebook_order';
5494 }
5495
5496 $title_bits = array();
5497
5498 if ( $recipient !== '' ) {
5499 $title_bits[] = $recipient;
5500 }
5501
5502 if ( $subject !== '' ) {
5503 $title_bits[] = $subject;
5504 }
5505
5506 if ( empty( $title_bits ) ) {
5507 $title_bits[] = __( 'Order email', 'ebook-store' );
5508 }
5509
5510 $post_id = wp_insert_post(
5511 array(
5512 'post_type' => 'ebook_email_log',
5513 'post_status' => 'publish',
5514 'post_title' => implode( ' - ', array_slice( $title_bits, 0, 2 ) ),
5515 ),
5516 true
5517 );
5518
5519 if ( is_wp_error( $post_id ) || ! $post_id ) {
5520 return 0;
5521 }
5522
5523 update_post_meta( $post_id, 'status', $status );
5524 update_post_meta( $post_id, 'sent_at', $sent_at );
5525 update_post_meta( $post_id, 'sent_at_gmt', $sent_at_gmt );
5526 update_post_meta( $post_id, 'recipient', $recipient );
5527 update_post_meta( $post_id, 'subject', (string) $args['subject'] );
5528 update_post_meta( $post_id, 'body_html', (string) $args['body'] );
5529 update_post_meta( $post_id, 'headers', $headers );
5530 update_post_meta( $post_id, 'attachments', $attachments );
5531 update_post_meta( $post_id, 'source', $source );
5532 update_post_meta( $post_id, 'related_order_type', $related_order_type );
5533 update_post_meta( $post_id, 'related_order_id', $related_order_id );
5534 update_post_meta( $post_id, 'ebook_id', $ebook_id );
5535 update_post_meta( $post_id, 'ebook_key', isset( $args['ebook_key'] ) ? sanitize_text_field( (string) ebook_store_get_first_meta_value( $args['ebook_key'] ) ) : '' );
5536 update_post_meta( $post_id, 'gateway', $gateway );
5537 update_post_meta( $post_id, 'order_snapshot', ebook_store_json_encode_for_log( $order ) );
5538 update_post_meta( $post_id, 'context_snapshot', ebook_store_json_encode_for_log( $context ) );
5539
5540 if ( $parent_log_id > 0 ) {
5541 update_post_meta( $post_id, 'parent_log_id', $parent_log_id );
5542 }
5543
5544 return $post_id;
5545 }
5546
5547 function ebook_store_get_email_log_order_edit_link( $log_id ) {
5548 $order_id = (int) get_post_meta( $log_id, 'related_order_id', true );
5549 $order_type = (string) get_post_meta( $log_id, 'related_order_type', true );
5550
5551 if ( $order_id < 1 ) {
5552 return '';
5553 }
5554
5555 if ( $order_type === 'ebook_order' && get_post_type( $order_id ) === 'ebook_order' ) {
5556 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5557 }
5558
5559 if ( $order_type === 'shop_order' || get_post_type( $order_id ) === 'shop_order' ) {
5560 return admin_url( 'post.php?post=' . $order_id . '&action=edit' );
5561 }
5562
5563 return '';
5564 }
5565
5566 function ebook_store_get_email_log_ebook_edit_link( $log_id ) {
5567 $ebook_id = (int) get_post_meta( $log_id, 'ebook_id', true );
5568
5569 if ( $ebook_id < 1 || get_post_type( $ebook_id ) !== 'ebook' ) {
5570 return '';
5571 }
5572
5573 return admin_url( 'post.php?post=' . $ebook_id . '&action=edit' );
5574 }
5575
5576 function ebook_store_get_email_log_resend_url( $log_id ) {
5577 return wp_nonce_url(
5578 admin_url( 'admin-post.php?action=ebook_store_resend_email_log&log_id=' . absint( $log_id ) ),
5579 'ebook_store_resend_email_log_' . absint( $log_id )
5580 );
5581 }
5582
5583 function ebook_store_get_order_reset_downloads_url( $order_id ) {
5584 return wp_nonce_url(
5585 admin_url( 'admin-post.php?action=ebook_store_reset_order_downloads&order_id=' . absint( $order_id ) ),
5586 'ebook_store_reset_order_downloads_' . absint( $order_id )
5587 );
5588 }
5589
5590 function ebook_store_get_order_resend_delivery_url( $order_id ) {
5591 return wp_nonce_url(
5592 admin_url( 'admin-post.php?action=ebook_store_resend_order_delivery&order_id=' . absint( $order_id ) ),
5593 'ebook_store_resend_order_delivery_' . absint( $order_id )
5594 );
5595 }
5596
5597 function ebook_store_get_order_refund_url( $order_id ) {
5598 return wp_nonce_url(
5599 admin_url( 'admin-post.php?action=ebook_store_refund_order&order_id=' . absint( $order_id ) ),
5600 'ebook_store_refund_order_' . absint( $order_id )
5601 );
5602 }
5603
5604 function ebook_store_get_order_refund_gateway( $order_id ) {
5605 $order_id = absint( $order_id );
5606 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
5607 return '';
5608 }
5609
5610 $payment_status = strtolower( trim( (string) get_post_meta( $order_id, 'payment_status', true ) ) );
5611 if ( ebook_store_is_negative_order_status( $payment_status ) ) {
5612 return '';
5613 }
5614
5615 if ( get_post_meta( $order_id, 'refund_gateway_id', true ) !== '' ) {
5616 return '';
5617 }
5618
5619 $payment_type = strtolower( trim( (string) get_post_meta( $order_id, 'payment_type', true ) ) );
5620
5621 if (
5622 $payment_type === 'stripe'
5623 && trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) ) !== ''
5624 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== ''
5625 ) {
5626 return 'stripe';
5627 }
5628
5629 if (
5630 $payment_type === 'adyen'
5631 && trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) ) !== ''
5632 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
5633 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
5634 ) {
5635 return 'adyen';
5636 }
5637
5638 return '';
5639 }
5640
5641 function ebook_store_get_email_log_status_badge( $status ) {
5642 $status = sanitize_key( (string) $status );
5643 $label = $status === 'sent' ? __( 'Sent', 'ebook-store' ) : __( 'Failed', 'ebook-store' );
5644
5645 return '<span class="ebook-email-log-badge ebook-email-log-badge--' . esc_attr( $status === 'sent' ? 'sent' : 'failed' ) . '">' . esc_html( $label ) . '</span>';
5646 }
5647
5648 function ebook_store_render_email_log_box( $post ) {
5649 $recipient = (string) get_post_meta( $post->ID, 'recipient', true );
5650 $subject = (string) get_post_meta( $post->ID, 'subject', true );
5651 $body_html = (string) get_post_meta( $post->ID, 'body_html', true );
5652 $headers = get_post_meta( $post->ID, 'headers', true );
5653 $attachments = get_post_meta( $post->ID, 'attachments', true );
5654 $status = (string) get_post_meta( $post->ID, 'status', true );
5655 $source = (string) get_post_meta( $post->ID, 'source', true );
5656 $sent_at = (string) get_post_meta( $post->ID, 'sent_at', true );
5657 $order_snapshot = (string) get_post_meta( $post->ID, 'order_snapshot', true );
5658 $context_snapshot = (string) get_post_meta( $post->ID, 'context_snapshot', true );
5659 $ebook_key = (string) get_post_meta( $post->ID, 'ebook_key', true );
5660 $resend_count = (int) get_post_meta( $post->ID, 'resend_count', true );
5661 $last_resend_at = (string) get_post_meta( $post->ID, 'last_resend_at', true );
5662 $last_resend_status = (string) get_post_meta( $post->ID, 'last_resend_status', true );
5663 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
5664 $ebook_link = ebook_store_get_email_log_ebook_edit_link( $post->ID );
5665
5666 if ( ! is_array( $headers ) ) {
5667 $headers = array();
5668 }
5669
5670 if ( ! is_array( $attachments ) ) {
5671 $attachments = array();
5672 }
5673
5674 echo '<div class="ebook-email-log">';
5675 echo '<div class="ebook-email-log__actions">';
5676 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>';
5677 if ( $order_link !== '' ) {
5678 echo '<a class="button button-secondary" href="' . esc_url( $order_link ) . '">' . esc_html__( 'View related order', 'ebook-store' ) . '</a>';
5679 }
5680 if ( $ebook_link !== '' ) {
5681 echo '<a class="button button-secondary" href="' . esc_url( $ebook_link ) . '">' . esc_html__( 'Edit ebook', 'ebook-store' ) . '</a>';
5682 }
5683 echo '</div>';
5684
5685 echo '<div class="ebook-email-log__summary">';
5686 echo '<div><span>' . esc_html__( 'Status', 'ebook-store' ) . '</span><strong>' . wp_kses_post( ebook_store_get_email_log_status_badge( $status ) ) . '</strong></div>';
5687 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>';
5688 echo '<div><span>' . esc_html__( 'Source', 'ebook-store' ) . '</span><strong>' . esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
5689 echo '<div><span>' . esc_html__( 'Sent at', 'ebook-store' ) . '</span><strong>' . esc_html( $sent_at !== '' ? $sent_at : __( 'Unknown', 'ebook-store' ) ) . '</strong></div>';
5690 echo '<div><span>' . esc_html__( 'Ebook key', 'ebook-store' ) . '</span><strong>' . esc_html( $ebook_key !== '' ? $ebook_key : __( 'Not set', 'ebook-store' ) ) . '</strong></div>';
5691 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>';
5692 echo '</div>';
5693
5694 echo '<div class="ebook-email-log__section">';
5695 echo '<h2>' . esc_html__( 'Message', 'ebook-store' ) . '</h2>';
5696 echo '<p><strong>' . esc_html__( 'Subject', 'ebook-store' ) . ':</strong> ' . esc_html( $subject ) . '</p>';
5697 echo '<div class="ebook-email-log__preview">' . wp_kses_post( $body_html ) . '</div>';
5698 echo '<textarea class="widefat ebook-email-log__code" rows="10" readonly>' . esc_textarea( $body_html ) . '</textarea>';
5699 echo '</div>';
5700
5701 echo '<div class="ebook-email-log__section">';
5702 echo '<h2>' . esc_html__( 'Headers and attachments', 'ebook-store' ) . '</h2>';
5703 echo '<p><strong>' . esc_html__( 'Headers', 'ebook-store' ) . ':</strong></p>';
5704 echo '<textarea class="widefat ebook-email-log__code" rows="4" readonly>' . esc_textarea( implode( "\n", $headers ) ) . '</textarea>';
5705 echo '<p><strong>' . esc_html__( 'Attachments', 'ebook-store' ) . ':</strong></p>';
5706 if ( empty( $attachments ) ) {
5707 echo '<p>' . esc_html__( 'No attachments were sent with this email.', 'ebook-store' ) . '</p>';
5708 } else {
5709 echo '<ul class="ebook-email-log__attachments">';
5710 foreach ( $attachments as $attachment ) {
5711 $label = basename( (string) $attachment );
5712 echo '<li><code>' . esc_html( $label ) . '</code><span>' . esc_html( (string) $attachment ) . '</span></li>';
5713 }
5714 echo '</ul>';
5715 }
5716 echo '</div>';
5717
5718 echo '<div class="ebook-email-log__section">';
5719 echo '<h2>' . esc_html__( 'Raw order snapshot', 'ebook-store' ) . '</h2>';
5720 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $order_snapshot ) . '</textarea>';
5721 echo '</div>';
5722
5723 echo '<div class="ebook-email-log__section">';
5724 echo '<h2>' . esc_html__( 'Raw email context', 'ebook-store' ) . '</h2>';
5725 echo '<textarea class="widefat ebook-email-log__code" rows="12" readonly>' . esc_textarea( $context_snapshot ) . '</textarea>';
5726 echo '</div>';
5727 echo '</div>';
5728 }
5729
5730 /**
5731 * Build the list of files to attach to a delivery email.
5732 *
5733 * Always keeps the core file the caller already resolved (e.g. the encrypted
5734 * PDF), and additionally attaches a per-buyer watermarked ePub when the ebook
5735 * offers one and ePub watermarking is enabled. So a buyer who opts into email
5736 * attachments gets the ePub attached AND watermarked, for every gateway — not
5737 * only via the download link. encrypted() caches per order, so this is idempotent.
5738 *
5739 * @param string|null $core_file Path already chosen for attachment (may be null).
5740 * @param int $order_id Order post ID.
5741 * @param int $ebook_id Ebook post ID.
5742 * @return string|array|null A single path, an array of paths, or null.
5743 */
5744 function ebook_store_collect_delivery_attachments( $core_file, $order_id, $ebook_id ) {
5745 $order_id = (int) $order_id;
5746 $ebook_id = (int) $ebook_id;
5747 $files = array();
5748
5749 if ( is_string( $core_file ) && $core_file !== '' && file_exists( $core_file ) ) {
5750 $files[] = $core_file;
5751 }
5752
5753 if ( $ebook_id > 0 && get_option( 'ebook_store_epub_watermark' ) && function_exists( 'ebook_store_watermark_epub' ) ) {
5754 $obj = new EbookStoreEbook( $ebook_id );
5755 if ( ! empty( $obj->files['epub'] ) && file_exists( $obj->files['epub'] ) ) {
5756 $obj->order_id = $order_id;
5757 $epub = $obj->encrypted( $order_id, 'epub' );
5758 if ( is_string( $epub ) && $epub !== '' && file_exists( $epub ) && ! in_array( $epub, $files, true ) ) {
5759 $files[] = $epub;
5760 }
5761 }
5762 }
5763
5764 if ( empty( $files ) ) {
5765 return null;
5766 }
5767
5768 return count( $files ) === 1 ? $files[0] : $files;
5769 }
5770
5771 function ebook_email_delivery($post_id = null) {
5772 $QSWPOptions = new QSWPOptions();
5773 global $ebook_email_delivery, $formData;
5774 if (@$ebook_email_delivery['done'] == 1) {
5775 return;
5776 }
5777 $ebook_email_delivery['done'] = 1;
5778
5779 if ( isset( $formData ) ) {
5780 if ( is_string( $formData ) && $formData !== '' ) {
5781 $decoded_form_data = json_decode( $formData, true );
5782 $formData = is_array( $decoded_form_data ) ? $decoded_form_data : array();
5783 } elseif ( is_array( $formData ) ) {
5784 $formData = $formData;
5785 } else {
5786 $formData = array();
5787 }
5788 } else {
5789 $formData = array();
5790 }
5791
5792 // error_log(var_export(debug_backtrace(),true));
5793 $ebook_email_delivery['order']['download_links'] = implode("<br />",ebook_download_links($ebook_email_delivery['order']));
5794 $ebook_email_delivery['order']['downloadlinks'] = implode("<br />",ebook_download_links($ebook_email_delivery['order']));
5795 $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'));
5796
5797 $order_total = isset( $ebook_email_delivery['order']['total'] ) ? $ebook_email_delivery['order']['total'] : false;
5798 $order_tax = isset( $ebook_email_delivery['order']['tax'] ) ? $ebook_email_delivery['order']['tax'] : 0;
5799 $order_gross = isset( $ebook_email_delivery['order']['mc_gross'] ) ? $ebook_email_delivery['order']['mc_gross'] : 0;
5800
5801 if ( is_array( $order_total ) ) {
5802 $order_total = reset( $order_total );
5803 }
5804 if ( is_array( $order_tax ) ) {
5805 $order_tax = reset( $order_tax );
5806 }
5807 if ( is_array( $order_gross ) ) {
5808 $order_gross = reset( $order_gross );
5809 }
5810
5811 if ( $order_total === false || $order_total === '' ) {
5812 $order_total = (float) $order_gross + (float) $order_tax;
5813 }
5814
5815 $ebook_email_delivery['order']['total'] = toMoney( $order_total );
5816 $ebook_email_delivery['order']['tax'] = toMoney( $order_tax );
5817
5818 if (!isset($ebook_email_delivery['order']['password'])) {
5819 $ebook_email_delivery['order']['password'] = ebook_store_get_password_for_order(
5820 isset($ebook_email_delivery['order']['order_id']) ? (int)$ebook_email_delivery['order']['order_id'] : 0,
5821 array(
5822 'payer_email' => isset($ebook_email_delivery['order']['payer_email']) ? $ebook_email_delivery['order']['payer_email'] : '',
5823 'password' => '',
5824 'md5_nonce' => isset($ebook_email_delivery['order']['md5_nonce']) ? $ebook_email_delivery['order']['md5_nonce'] : '',
5825 'ebook_key' => isset($ebook_email_delivery['order']['ebook_key']) ? $ebook_email_delivery['order']['ebook_key'] : '',
5826 )
5827 );
5828 }
5829
5830 @$attachmentFile = $ebook_email_delivery['attachment'][0]['file'];
5831 $ebook_email_delivery['order']['filesize'] = ( $attachmentFile && file_exists( $attachmentFile ) ) ? ebook_store_human_filesize(filesize($attachmentFile)) : '';
5832 if (get_option('attach_files') != 1) {
5833 $attachmentFile = null;
5834 } else {
5835 // Attachments are on: attach the core file AND a watermarked ePub if the
5836 // ebook offers one, for every gateway (not only the download link).
5837 $ebook_attach_order = isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : $post_id;
5838 if ( is_array( $ebook_attach_order ) ) { $ebook_attach_order = reset( $ebook_attach_order ); }
5839 if ( ! $ebook_attach_order ) { $ebook_attach_order = $post_id; }
5840 $ebook_attach_id = isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0;
5841 if ( is_array( $ebook_attach_id ) ) { $ebook_attach_id = reset( $ebook_attach_id ); }
5842 $attachmentFile = ebook_store_collect_delivery_attachments( $attachmentFile, (int) $ebook_attach_order, (int) $ebook_attach_id );
5843 }
5844
5845
5846 //error_reporting(E_ALL);
5847 foreach ($ebook_email_delivery['order'] as $k => $v) {
5848 if (is_array($v)) {
5849 $v = $v[0];
5850 }
5851 $ebook_email_delivery['text'] = str_replace('%%' . $k . '%%', $v, $ebook_email_delivery['text']);
5852 }
5853
5854
5855 $ebook_email_delivery['text'] = nl2br($ebook_email_delivery['text']);
5856 //$ebook_email_delivery['text'] = apply_filters('the_content',$ebook_email_delivery['text']);
5857 //error_log('ebook_email_delivery ' . print_r($ebook_email_delivery,true));
5858 add_filter( 'wp_mail_content_type', 'ebook_set_content_type' );
5859 $ebook_email_delivery['mail_result'] = false;
5860 if (floatval(get_option('email_delivery',$QSWPOptions->email_delivery)) == 1) {
5861 //echo "Sending mail, data: " . implode("; ", $ebook_email_delivery);
5862 if (floatval(get_option('kindleDelivery',$QSWPOptions->kindleDelivery)) == 1) {
5863 if ( ! empty( $formData['kindle_email'] ) && strpos($formData['kindle_email'], '@kindle.com')) {
5864 // if (1) {
5865 $formData['kindle_email'] = filter_var($formData['kindle_email'], FILTER_VALIDATE_EMAIL);
5866 $ebook_email_delivery['to'] = $formData['kindle_email'];
5867 } else {
5868 return true; //not a kind
5869 }
5870 }
5871 if ( ! empty( $formData['gift_email'] ) ) {
5872 $formData['gift_email'] = filter_var($formData['gift_email'], FILTER_VALIDATE_EMAIL);
5873 $ebook_email_delivery['to'] = $formData['gift_email'];
5874 }
5875 $headers = array('Content-Type: text/html; charset=UTF-8');
5876
5877 $mail_result = wp_mail($ebook_email_delivery['to'],$ebook_email_delivery['subject'], $ebook_email_delivery['text'],$headers,$attachmentFile);
5878 $ebook_email_delivery['mail_result'] = (bool) $mail_result;
5879
5880 ebook_store_create_email_log_entry(
5881 array(
5882 'to' => isset( $ebook_email_delivery['to'] ) ? $ebook_email_delivery['to'] : '',
5883 'subject' => isset( $ebook_email_delivery['subject'] ) ? $ebook_email_delivery['subject'] : '',
5884 'body' => isset( $ebook_email_delivery['text'] ) ? $ebook_email_delivery['text'] : '',
5885 'headers' => $headers,
5886 'attachments' => $attachmentFile,
5887 'result' => $mail_result,
5888 'source' => isset( $ebook_email_delivery['source'] ) ? $ebook_email_delivery['source'] : 'standalone',
5889 'related_order_type' => isset( $ebook_email_delivery['related_order_type'] ) ? $ebook_email_delivery['related_order_type'] : '',
5890 'related_order_id' => isset( $ebook_email_delivery['order']['order_id'] ) ? $ebook_email_delivery['order']['order_id'] : 0,
5891 'ebook_id' => isset( $ebook_email_delivery['order']['ebook'] ) ? $ebook_email_delivery['order']['ebook'] : 0,
5892 'ebook_key' => isset( $ebook_email_delivery['order']['ebook_key'] ) ? $ebook_email_delivery['order']['ebook_key'] : '',
5893 'gateway' => isset( $ebook_email_delivery['order']['gateway'] ) ? $ebook_email_delivery['order']['gateway'] : '',
5894 'order' => isset( $ebook_email_delivery['order'] ) ? $ebook_email_delivery['order'] : array(),
5895 'context' => $ebook_email_delivery,
5896 )
5897 );
5898 }
5899 }
5900
5901 function ebook_set_content_type( $content_type ){
5902 return 'text/html';
5903 }
5904 function ebook_attachment($post_id, $ignoreSetting = false) {
5905 if (get_option('attach_files') == 1 || $ignoreSetting == true) {
5906 $attachment = get_post_meta($post_id,'ebook_wp_custom_attachment');
5907 if (WP_DEBUG == true) error_log('attachment - ' . print_r($attachment,true));
5908 return $attachment;
5909 }
5910 }
5911
5912 function ebook_store_set_email_log_columns( $columns ) {
5913 return array(
5914 'cb' => '<input type="checkbox" />',
5915 'title' => __( 'Email', 'ebook-store' ),
5916 'recipient' => __( 'Recipient', 'ebook-store' ),
5917 'source' => __( 'Source', 'ebook-store' ),
5918 'related_order' => __( 'Order', 'ebook-store' ),
5919 'ebook_reference' => __( 'Ebook', 'ebook-store' ),
5920 'status' => __( 'Status', 'ebook-store' ),
5921 'resends' => __( 'Resends', 'ebook-store' ),
5922 'date' => __( 'Logged', 'ebook-store' ),
5923 );
5924 }
5925
5926 function ebook_store_email_log_columns_output( $column, $post_id ) {
5927 switch ( $column ) {
5928 case 'recipient':
5929 $recipient = (string) get_post_meta( $post_id, 'recipient', true );
5930 echo $recipient !== '' ? '<a href="mailto:' . esc_attr( $recipient ) . '">' . esc_html( $recipient ) . '</a>' : '&mdash;';
5931 break;
5932 case 'source':
5933 $source = (string) get_post_meta( $post_id, 'source', true );
5934 echo esc_html( $source !== '' ? ucfirst( str_replace( '_', ' ', $source ) ) : __( 'Unknown', 'ebook-store' ) );
5935 break;
5936 case 'related_order':
5937 $order_id = (int) get_post_meta( $post_id, 'related_order_id', true );
5938 $link = ebook_store_get_email_log_order_edit_link( $post_id );
5939 if ( $order_id < 1 ) {
5940 echo '&mdash;';
5941 break;
5942 }
5943 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">#' . esc_html( $order_id ) . '</a>' : '#' . esc_html( $order_id );
5944 break;
5945 case 'ebook_reference':
5946 $ebook_id = (int) get_post_meta( $post_id, 'ebook_id', true );
5947 $link = ebook_store_get_email_log_ebook_edit_link( $post_id );
5948 if ( $ebook_id < 1 ) {
5949 echo '&mdash;';
5950 break;
5951 }
5952 $title = get_the_title( $ebook_id );
5953 echo $link !== '' ? '<a href="' . esc_url( $link ) . '">' . esc_html( $title !== '' ? $title : '#' . $ebook_id ) . '</a>' : esc_html( $title !== '' ? $title : '#' . $ebook_id );
5954 break;
5955 case 'status':
5956 echo wp_kses_post( ebook_store_get_email_log_status_badge( get_post_meta( $post_id, 'status', true ) ) );
5957 break;
5958 case 'resends':
5959 $resend_count = (int) get_post_meta( $post_id, 'resend_count', true );
5960 $last_resend_at = (string) get_post_meta( $post_id, 'last_resend_at', true );
5961 echo '<strong>' . esc_html( number_format_i18n( $resend_count ) ) . '</strong>';
5962 if ( $last_resend_at !== '' ) {
5963 echo '<div class="ebook-email-log-list__meta">' . esc_html( $last_resend_at ) . '</div>';
5964 }
5965 break;
5966 }
5967 }
5968
5969 function ebook_store_email_log_row_actions( $actions, $post ) {
5970 if ( ! $post || $post->post_type !== 'ebook_email_log' ) {
5971 return $actions;
5972 }
5973
5974 $actions['resend_email'] = '<a href="' . esc_url( ebook_store_get_email_log_resend_url( $post->ID ) ) . '">' . esc_html__( 'Resend', 'ebook-store' ) . '</a>';
5975
5976 $order_link = ebook_store_get_email_log_order_edit_link( $post->ID );
5977 if ( $order_link !== '' ) {
5978 $actions['view_related_order'] = '<a href="' . esc_url( $order_link ) . '">' . esc_html__( 'View order', 'ebook-store' ) . '</a>';
5979 }
5980
5981 return $actions;
5982 }
5983
5984 function ebook_store_remove_email_log_submit_box() {
5985 remove_meta_box( 'submitdiv', 'ebook_email_log', 'side' );
5986 }
5987
5988 function ebook_store_cleanup_email_log_submenu() {
5989 remove_submenu_page( 'edit.php?post_type=ebook', 'post-new.php?post_type=ebook_email_log' );
5990 }
5991
5992 function ebook_store_redirect_new_email_log_entry() {
5993 global $pagenow;
5994
5995 if ( ! is_admin() || $pagenow !== 'post-new.php' ) {
5996 return;
5997 }
5998
5999 if ( empty( $_GET['post_type'] ) || wp_unslash( $_GET['post_type'] ) !== 'ebook_email_log' ) {
6000 return;
6001 }
6002
6003 wp_safe_redirect( admin_url( 'edit.php?post_type=ebook_email_log' ) );
6004 exit;
6005 }
6006
6007 function ebook_store_render_email_log_admin_styles() {
6008 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6009 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6010 return;
6011 }
6012
6013 echo '<style>
6014 .post-type-ebook_email_log #titlediv,
6015 .post-type-ebook_email_log #minor-publishing,
6016 .post-type-ebook_email_log #misc-publishing-actions,
6017 .post-type-ebook_email_log #major-publishing-actions .open-if-no-js {
6018 display: none;
6019 }
6020 .post-type-ebook_email_log .page-title-action {
6021 display: none;
6022 }
6023 .post-type-ebook_email_log #major-publishing-actions {
6024 padding: 0;
6025 border-top: 0;
6026 }
6027 .ebook-email-log {
6028 display: grid;
6029 gap: 16px;
6030 }
6031 .ebook-email-log__actions {
6032 display: flex;
6033 flex-wrap: wrap;
6034 gap: 10px;
6035 }
6036 .ebook-email-log__summary {
6037 display: grid;
6038 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
6039 gap: 12px;
6040 }
6041 .ebook-email-log__summary > div,
6042 .ebook-email-log__section {
6043 background: #fff;
6044 border: 1px solid #dcdcde;
6045 border-radius: 14px;
6046 padding: 16px;
6047 }
6048 .ebook-email-log__summary span {
6049 display: block;
6050 margin-bottom: 6px;
6051 color: #646970;
6052 font-size: 12px;
6053 text-transform: uppercase;
6054 letter-spacing: .04em;
6055 }
6056 .ebook-email-log__summary strong {
6057 display: block;
6058 font-size: 14px;
6059 line-height: 1.5;
6060 }
6061 .ebook-email-log__summary small {
6062 display: block;
6063 margin-top: 6px;
6064 color: #646970;
6065 font-weight: 400;
6066 }
6067 .ebook-email-log__section h2 {
6068 margin: 0 0 12px;
6069 font-size: 15px;
6070 }
6071 .ebook-email-log__preview {
6072 max-height: 360px;
6073 overflow: auto;
6074 padding: 16px;
6075 background: #f6f7f7;
6076 border: 1px solid #dcdcde;
6077 border-radius: 12px;
6078 margin-bottom: 12px;
6079 }
6080 .ebook-email-log__code {
6081 font-family: Consolas, Monaco, monospace;
6082 font-size: 12px;
6083 }
6084 .ebook-email-log__attachments {
6085 margin: 0;
6086 padding: 0;
6087 list-style: none;
6088 display: grid;
6089 gap: 10px;
6090 }
6091 .ebook-email-log__attachments li {
6092 display: grid;
6093 gap: 4px;
6094 padding: 10px 12px;
6095 background: #f6f7f7;
6096 border-radius: 10px;
6097 }
6098 .ebook-email-log-badge {
6099 display: inline-flex;
6100 align-items: center;
6101 padding: 4px 10px;
6102 border-radius: 999px;
6103 font-size: 12px;
6104 font-weight: 600;
6105 }
6106 .ebook-email-log-badge--sent {
6107 background: #e7f7ed;
6108 color: #116b38;
6109 }
6110 .ebook-email-log-badge--failed {
6111 background: #fdeaea;
6112 color: #b42318;
6113 }
6114 .ebook-email-log-list__meta {
6115 color: #646970;
6116 font-size: 12px;
6117 margin-top: 4px;
6118 }
6119 </style>';
6120 }
6121
6122 function ebook_store_email_log_admin_notice() {
6123 if ( ! is_admin() || empty( $_GET['ebook_store_email_log_notice'] ) ) {
6124 return;
6125 }
6126
6127 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6128 if ( ! $screen || $screen->post_type !== 'ebook_email_log' ) {
6129 return;
6130 }
6131
6132 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_email_log_notice'] ) );
6133 if ( ! in_array( $notice, array( 'resent', 'resend_failed' ), true ) ) {
6134 return;
6135 }
6136
6137 $class = $notice === 'resent' ? 'notice-success' : 'notice-error';
6138 $message = $notice === 'resent'
6139 ? __( 'The email was resent and logged as a new entry.', 'ebook-store' )
6140 : __( 'The resend attempt failed. Check your mail configuration and the attachment paths.', 'ebook-store' );
6141
6142 echo '<div class="notice ' . esc_attr( $class ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6143 }
6144
6145 function ebook_store_order_admin_notice() {
6146 if ( ! is_admin() || empty( $_GET['ebook_store_order_notice'] ) ) {
6147 return;
6148 }
6149
6150 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
6151 if ( ! $screen || $screen->post_type !== 'ebook_order' ) {
6152 return;
6153 }
6154
6155 $notice = sanitize_key( wp_unslash( $_GET['ebook_store_order_notice'] ) );
6156 $messages = array(
6157 'downloads_reset' => array(
6158 'class' => 'notice-success',
6159 'text' => __( 'The order download counter was reset to 0.', 'ebook-store' ),
6160 ),
6161 'delivery_resent' => array(
6162 'class' => 'notice-success',
6163 'text' => __( 'The delivery email was sent again and logged as a new email entry.', 'ebook-store' ),
6164 ),
6165 'delivery_failed' => array(
6166 'class' => 'notice-error',
6167 'text' => __( 'The delivery email resend failed. Check the order email address, attachment file, and mail configuration.', 'ebook-store' ),
6168 ),
6169 'refund_succeeded' => array(
6170 'class' => 'notice-success',
6171 'text' => __( 'The payment was refunded successfully.', 'ebook-store' ),
6172 ),
6173 'refund_failed' => array(
6174 'class' => 'notice-error',
6175 'text' => __( 'The payment refund failed.', 'ebook-store' ),
6176 ),
6177 );
6178
6179 if ( ! isset( $messages[ $notice ] ) ) {
6180 return;
6181 }
6182
6183 $message = $messages[ $notice ]['text'];
6184 if ( ! empty( $_GET['ebook_store_order_message'] ) ) {
6185 $message = sanitize_text_field( wp_unslash( $_GET['ebook_store_order_message'] ) );
6186 }
6187
6188 echo '<div class="notice ' . esc_attr( $messages[ $notice ]['class'] ) . ' is-dismissible"><p>' . esc_html( $message ) . '</p></div>';
6189 }
6190
6191 function ebook_store_get_order_admin_list_url( $notice = '', $args = array() ) {
6192 $args = array(
6193 'post_type' => 'ebook_order',
6194 ) + (array) $args;
6195
6196 if ( $notice !== '' ) {
6197 $args['ebook_store_order_notice'] = $notice;
6198 }
6199
6200 return add_query_arg( $args, admin_url( 'edit.php' ) );
6201 }
6202
6203 function ebook_store_mark_order_refunded( $order_id, $gateway, $refund_id, $refund_status, $amount = '', $currency = '' ) {
6204 $order_id = absint( $order_id );
6205 if ( $order_id < 1 ) {
6206 return;
6207 }
6208
6209 update_post_meta( $order_id, 'payment_status', 'Refunded' );
6210 update_post_meta( $order_id, 'refund_gateway', sanitize_key( (string) $gateway ) );
6211 update_post_meta( $order_id, 'refund_gateway_id', sanitize_text_field( (string) $refund_id ) );
6212 update_post_meta( $order_id, 'refund_gateway_status', sanitize_text_field( (string) $refund_status ) );
6213 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
6214
6215 if ( $amount !== '' && is_numeric( $amount ) ) {
6216 update_post_meta( $order_id, 'refund_amount', (string) $amount );
6217 }
6218
6219 if ( $currency !== '' ) {
6220 update_post_meta( $order_id, 'refund_currency', strtoupper( sanitize_text_field( (string) $currency ) ) );
6221 }
6222 }
6223
6224 function ebook_store_refund_stripe_order( $order_id ) {
6225 $payment_intent_id = trim( (string) get_post_meta( $order_id, 'stripe_payment_intent_id', true ) );
6226 if ( $payment_intent_id === '' ) {
6227 return new WP_Error( 'ebook_store_missing_stripe_payment_intent', __( 'This Stripe order is missing its payment intent ID.', 'ebook-store' ) );
6228 }
6229
6230 $bootstrap = ebook_store_bootstrap_stripe_sdk();
6231 if ( is_wp_error( $bootstrap ) ) {
6232 return $bootstrap;
6233 }
6234
6235 try {
6236 $refund = \Stripe\Refund::create(
6237 array(
6238 'payment_intent' => $payment_intent_id,
6239 'reason' => 'requested_by_customer',
6240 'metadata' => array(
6241 'ebook_order_id' => (string) $order_id,
6242 ),
6243 )
6244 );
6245 } catch ( Exception $e ) {
6246 return new WP_Error( 'ebook_store_stripe_refund_failed', $e->getMessage() );
6247 }
6248
6249 $refund_amount = isset( $refund->amount ) ? ((float) $refund->amount / 100) : (float) get_post_meta( $order_id, 'mc_gross', true );
6250 $refund_currency = isset( $refund->currency ) ? strtoupper( (string) $refund->currency ) : (string) get_post_meta( $order_id, 'mc_currency', true );
6251 $refund_status = isset( $refund->status ) ? (string) $refund->status : 'succeeded';
6252 $refund_id = isset( $refund->id ) ? (string) $refund->id : '';
6253
6254 ebook_store_mark_order_refunded( $order_id, 'stripe', $refund_id, $refund_status, $refund_amount, $refund_currency );
6255
6256 return array(
6257 'gateway' => 'stripe',
6258 'refund_id' => $refund_id,
6259 'status' => $refund_status,
6260 'amount' => $refund_amount,
6261 'currency' => $refund_currency,
6262 );
6263 }
6264
6265 function ebook_store_refund_adyen_order( $order_id ) {
6266 $psp_reference = trim( (string) get_post_meta( $order_id, 'adyen_psp_reference', true ) );
6267 if ( $psp_reference === '' ) {
6268 return new WP_Error( 'ebook_store_missing_adyen_psp_reference', __( 'This Adyen order is missing its PSP reference.', 'ebook-store' ) );
6269 }
6270
6271 $amount = (float) get_post_meta( $order_id, 'mc_gross', true );
6272 $currency = strtoupper( trim( (string) get_post_meta( $order_id, 'mc_currency', true ) ) );
6273 if ( $amount <= 0 || $currency === '' ) {
6274 return new WP_Error( 'ebook_store_invalid_adyen_refund_amount', __( 'This Adyen order is missing a valid refund amount or currency.', 'ebook-store' ) );
6275 }
6276
6277 $response = ebook_store_adyen_request(
6278 'POST',
6279 'payments/' . rawurlencode( $psp_reference ) . '/refunds',
6280 array(
6281 'merchantAccount' => trim( (string) get_option( 'adyen_merchant_account', '' ) ),
6282 'amount' => array(
6283 'currency' => $currency,
6284 'value' => (int) round( $amount * 100 ),
6285 ),
6286 'reference' => 'refund-' . $order_id . '-' . wp_generate_password( 8, false, false ),
6287 ),
6288 array(
6289 'Idempotency-Key' => wp_generate_uuid4(),
6290 )
6291 );
6292
6293 if ( is_wp_error( $response ) ) {
6294 return $response;
6295 }
6296
6297 $refund_id = ! empty( $response['pspReference'] ) ? (string) $response['pspReference'] : '';
6298 ebook_store_mark_order_refunded( $order_id, 'adyen', $refund_id, 'received', $amount, $currency );
6299
6300 return array(
6301 'gateway' => 'adyen',
6302 'refund_id' => $refund_id,
6303 'status' => 'received',
6304 'amount' => $amount,
6305 'currency' => $currency,
6306 );
6307 }
6308
6309 function ebook_store_process_order_refund( $order_id ) {
6310 $order_id = absint( $order_id );
6311 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6312 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6313 }
6314
6315 $gateway = ebook_store_get_order_refund_gateway( $order_id );
6316 if ( $gateway === '' ) {
6317 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this order or payment method.', 'ebook-store' ) );
6318 }
6319
6320 if ( $gateway === 'stripe' ) {
6321 return ebook_store_refund_stripe_order( $order_id );
6322 }
6323
6324 if ( $gateway === 'adyen' ) {
6325 return ebook_store_refund_adyen_order( $order_id );
6326 }
6327
6328 return new WP_Error( 'ebook_store_refund_not_supported', __( 'Refund is not available for this payment method.', 'ebook-store' ) );
6329 }
6330
6331 function ebook_store_prepare_order_delivery_email( $order_id ) {
6332 $order_id = absint( $order_id );
6333 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6334 return new WP_Error( 'ebook_store_invalid_order', __( 'Invalid ebook order.', 'ebook-store' ) );
6335 }
6336
6337 $order_meta = get_post_meta( $order_id, null, true );
6338 if ( ! is_array( $order_meta ) || empty( $order_meta['ebook'][0] ) ) {
6339 return new WP_Error( 'ebook_store_invalid_order_meta', __( 'This order is missing ebook delivery data.', 'ebook-store' ) );
6340 }
6341
6342 $ebook_id = absint( $order_meta['ebook'][0] );
6343 if ( $ebook_id < 1 ) {
6344 return new WP_Error( 'ebook_store_invalid_order_ebook', __( 'This order is not linked to a valid ebook.', 'ebook-store' ) );
6345 }
6346
6347 $QSWPOptions = new QSWPOptions();
6348 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_pdf', true );
6349 if ( $attachment_file === '' ) {
6350 // Fall back to a watermarked EPUB copy if one was generated for this order.
6351 $attachment_file = (string) get_post_meta( $order_id, 'encrypted_epub', true );
6352 }
6353 if ( $attachment_file === '' || ! file_exists( $attachment_file ) ) {
6354 $attachment = ebook_attachment( $ebook_id, true );
6355 $attachment_file = isset( $attachment[0]['file'] ) ? (string) $attachment[0]['file'] : '';
6356 }
6357
6358 $order_meta['order_id'][0] = $order_id;
6359 $order_meta['downloadlink'][0] = ebook_download_link( $order_meta );
6360
6361 // The delivery-email template substitutes %%downloadlink_html%%. The checkout
6362 // path builds it, but the admin resend path (which rebuilds the order array
6363 // from post meta) did not — so a resent email showed the raw placeholder and
6364 // the buyer had no way to download. Build it here too.
6365 $delivery_ebook = new EbookStoreEbook( $ebook_id );
6366 // Plugin order: give link() the ebook_key credentials, otherwise the non-PDF
6367 // format links come out as wc_order_download URLs with no order key, which
6368 // the download endpoint (rightly) refuses.
6369 $delivery_ebook->ebook_key = isset( $order_meta['ebook_key'][0] ) ? (string) $order_meta['ebook_key'][0] : '';
6370 $delivery_ebook->md5_nonce = isset( $order_meta['md5_nonce'][0] ) ? (string) $order_meta['md5_nonce'][0] : '';
6371 $delivery_ebook->order_id = $order_id;
6372 $delivery_ebook->setLink['pdf'] = $order_meta['downloadlink'][0];
6373 $order_meta['downloadlink_html'][0] = $delivery_ebook->format_links();
6374
6375 $recipient = isset( $order_meta['payer_email'][0] ) ? sanitize_email( (string) $order_meta['payer_email'][0] ) : '';
6376 if ( $recipient === '' ) {
6377 return new WP_Error( 'ebook_store_missing_order_email', __( 'This order does not have a valid delivery email address.', 'ebook-store' ) );
6378 }
6379 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_id );
6380
6381 return array(
6382 'to' => $recipient,
6383 'subject' => $email_template['subject'],
6384 'text' => $email_template['text'],
6385 'attachment' => array(
6386 array(
6387 'file' => $attachment_file,
6388 ),
6389 ),
6390 'order' => $order_meta,
6391 'source' => 'admin_resend',
6392 'related_order_type' => 'ebook_order',
6393 );
6394 }
6395
6396 function ebook_store_reset_order_downloads() {
6397 if ( ! current_user_can( 'manage_options' ) ) {
6398 wp_die( esc_html__( 'You are not allowed to reset order downloads.', 'ebook-store' ) );
6399 }
6400
6401 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6402 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6403 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6404 }
6405
6406 check_admin_referer( 'ebook_store_reset_order_downloads_' . $order_id );
6407 update_post_meta( $order_id, 'downloads', 0 );
6408
6409 wp_safe_redirect( ebook_store_get_order_admin_list_url( 'downloads_reset' ) );
6410 exit;
6411 }
6412
6413 function ebook_store_resend_order_delivery() {
6414 if ( ! current_user_can( 'manage_options' ) ) {
6415 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
6416 }
6417
6418 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6419 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6420 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6421 }
6422
6423 check_admin_referer( 'ebook_store_resend_order_delivery_' . $order_id );
6424
6425 $email_context = ebook_store_prepare_order_delivery_email( $order_id );
6426 if ( is_wp_error( $email_context ) ) {
6427 wp_die( esc_html( $email_context->get_error_message() ) );
6428 }
6429
6430 global $ebook_email_delivery, $formData;
6431 $previous_email_delivery = isset( $ebook_email_delivery ) ? $ebook_email_delivery : null;
6432 $previous_form_data = isset( $formData ) ? $formData : null;
6433 $previous_email_delivery_setting = get_option( 'email_delivery', null );
6434
6435 $ebook_email_delivery = $email_context;
6436 unset( $ebook_email_delivery['done'] );
6437 $formData = null;
6438 update_option( 'email_delivery', 1 );
6439
6440 ob_start();
6441 ebook_email_delivery( $order_id );
6442 ob_end_clean();
6443
6444 $resend_succeeded = ! empty( $ebook_email_delivery['mail_result'] );
6445
6446 if ( null === $previous_email_delivery_setting ) {
6447 delete_option( 'email_delivery' );
6448 } else {
6449 update_option( 'email_delivery', $previous_email_delivery_setting );
6450 }
6451 $ebook_email_delivery = $previous_email_delivery;
6452 $formData = $previous_form_data;
6453
6454 wp_safe_redirect( ebook_store_get_order_admin_list_url( $resend_succeeded ? 'delivery_resent' : 'delivery_failed' ) );
6455 exit;
6456 }
6457
6458 function ebook_store_refund_order() {
6459 if ( ! current_user_can( 'manage_options' ) ) {
6460 wp_die( esc_html__( 'You are not allowed to refund ebook orders.', 'ebook-store' ) );
6461 }
6462
6463 $order_id = isset( $_GET['order_id'] ) ? absint( wp_unslash( $_GET['order_id'] ) ) : 0;
6464 if ( $order_id < 1 || get_post_type( $order_id ) !== 'ebook_order' ) {
6465 wp_die( esc_html__( 'Invalid ebook order.', 'ebook-store' ) );
6466 }
6467
6468 check_admin_referer( 'ebook_store_refund_order_' . $order_id );
6469
6470 $result = ebook_store_process_order_refund( $order_id );
6471 if ( is_wp_error( $result ) ) {
6472 wp_safe_redirect(
6473 ebook_store_get_order_admin_list_url(
6474 'refund_failed',
6475 array(
6476 'ebook_store_order_message' => $result->get_error_message(),
6477 )
6478 )
6479 );
6480 exit;
6481 }
6482
6483 $gateway_label = ucfirst( (string) $result['gateway'] );
6484 $amount_label = ebook_store_order_money_display(
6485 isset( $result['amount'] ) ? $result['amount'] : '',
6486 isset( $result['currency'] ) ? $result['currency'] : ''
6487 );
6488 $message = sprintf(
6489 /* translators: 1: payment gateway, 2: refunded amount */
6490 __( '%1$s refund created successfully for %2$s.', 'ebook-store' ),
6491 $gateway_label,
6492 $amount_label
6493 );
6494
6495 wp_safe_redirect(
6496 ebook_store_get_order_admin_list_url(
6497 'refund_succeeded',
6498 array(
6499 'ebook_store_order_message' => $message,
6500 )
6501 )
6502 );
6503 exit;
6504 }
6505
6506 function ebook_store_resend_email_log() {
6507 if ( ! current_user_can( 'manage_options' ) ) {
6508 wp_die( esc_html__( 'You are not allowed to resend order emails.', 'ebook-store' ) );
6509 }
6510
6511 $log_id = isset( $_GET['log_id'] ) ? absint( wp_unslash( $_GET['log_id'] ) ) : 0;
6512 if ( $log_id < 1 || get_post_type( $log_id ) !== 'ebook_email_log' ) {
6513 wp_die( esc_html__( 'Invalid email log entry.', 'ebook-store' ) );
6514 }
6515
6516 check_admin_referer( 'ebook_store_resend_email_log_' . $log_id );
6517
6518 $recipient = (string) get_post_meta( $log_id, 'recipient', true );
6519 $subject = (string) get_post_meta( $log_id, 'subject', true );
6520 $body_html = (string) get_post_meta( $log_id, 'body_html', true );
6521 $headers = ebook_store_normalize_email_log_headers( get_post_meta( $log_id, 'headers', true ) );
6522 $attachments = ebook_store_normalize_email_log_attachments( get_post_meta( $log_id, 'attachments', true ) );
6523 $order_json = (string) get_post_meta( $log_id, 'order_snapshot', true );
6524 $context_json = (string) get_post_meta( $log_id, 'context_snapshot', true );
6525 $source = (string) get_post_meta( $log_id, 'source', true );
6526 $order = json_decode( $order_json, true );
6527 $context = json_decode( $context_json, true );
6528
6529 if ( ! is_array( $order ) ) {
6530 $order = array();
6531 }
6532
6533 if ( ! is_array( $context ) ) {
6534 $context = array();
6535 }
6536
6537 $attachments = array_values(
6538 array_filter(
6539 $attachments,
6540 static function ( $path ) {
6541 return is_string( $path ) && $path !== '' && file_exists( $path );
6542 }
6543 )
6544 );
6545
6546 $mail_result = wp_mail( $recipient, $subject, $body_html, $headers, $attachments );
6547
6548 $resend_count = (int) get_post_meta( $log_id, 'resend_count', true );
6549 update_post_meta( $log_id, 'resend_count', $resend_count + 1 );
6550 update_post_meta( $log_id, 'last_resend_at', current_time( 'mysql' ) );
6551 update_post_meta( $log_id, 'last_resend_status', $mail_result ? 'sent' : 'failed' );
6552
6553 $new_log_id = ebook_store_create_email_log_entry(
6554 array(
6555 'to' => $recipient,
6556 'subject' => $subject,
6557 'body' => $body_html,
6558 'headers' => $headers,
6559 'attachments' => $attachments,
6560 'result' => $mail_result,
6561 'source' => 'resend',
6562 'related_order_type' => get_post_meta( $log_id, 'related_order_type', true ),
6563 'related_order_id' => get_post_meta( $log_id, 'related_order_id', true ),
6564 'ebook_id' => get_post_meta( $log_id, 'ebook_id', true ),
6565 'ebook_key' => get_post_meta( $log_id, 'ebook_key', true ),
6566 'gateway' => get_post_meta( $log_id, 'gateway', true ),
6567 'order' => $order,
6568 'context' => array_merge(
6569 $context,
6570 array(
6571 'resend_of' => $log_id,
6572 'original_source' => $source,
6573 )
6574 ),
6575 'parent_log_id' => $log_id,
6576 )
6577 );
6578
6579 if ( $new_log_id > 0 ) {
6580 update_post_meta( $log_id, 'last_resend_log_id', $new_log_id );
6581 }
6582
6583 wp_safe_redirect(
6584 add_query_arg(
6585 array(
6586 'post' => $log_id,
6587 'action' => 'edit',
6588 'ebook_store_email_log_notice' => $mail_result ? 'resent' : 'resend_failed',
6589 ),
6590 admin_url( 'post.php' )
6591 )
6592 );
6593 exit;
6594 }
6595
6596 add_filter( 'manage_edit-ebook_email_log_columns', 'ebook_store_set_email_log_columns' );
6597 add_action( 'manage_ebook_email_log_posts_custom_column', 'ebook_store_email_log_columns_output', 10, 2 );
6598 add_filter( 'post_row_actions', 'ebook_store_email_log_row_actions', 10, 2 );
6599 add_action( 'add_meta_boxes', 'ebook_store_remove_email_log_submit_box', 20 );
6600 add_action( 'admin_menu', 'ebook_store_cleanup_email_log_submenu', 999 );
6601 add_action( 'admin_init', 'ebook_store_redirect_new_email_log_entry' );
6602 add_action( 'admin_head', 'ebook_store_render_email_log_admin_styles' );
6603 add_action( 'admin_notices', 'ebook_store_email_log_admin_notice' );
6604 add_action( 'admin_notices', 'ebook_store_order_admin_notice' );
6605 add_action( 'admin_post_ebook_store_resend_email_log', 'ebook_store_resend_email_log' );
6606 add_action( 'admin_post_ebook_store_reset_order_downloads', 'ebook_store_reset_order_downloads' );
6607 add_action( 'admin_post_ebook_store_resend_order_delivery', 'ebook_store_resend_order_delivery' );
6608 add_action( 'admin_post_ebook_store_refund_order', 'ebook_store_refund_order' );
6609
6610 function ebook_store_get_order_verification_url( $ebook_key ) {
6611 $ebook_key = preg_replace( '/[^a-zA-Z0-9]+/', '', (string) $ebook_key );
6612
6613 return add_query_arg(
6614 array(
6615 'ebook_action' => 'verify_order',
6616 'ebook_key' => $ebook_key,
6617 ),
6618 home_url( '/' )
6619 );
6620 }
6621
6622 /**
6623 * The QR-verification key used for a Test-button copy, so the verification page
6624 * can recognise it as a test (there is no real order behind it) instead of
6625 * showing "record not found". Kept uppercase so it can never collide with a real
6626 * order's md5 (lowercase-hex) key.
6627 *
6628 * @param int $ebook_id Ebook post ID.
6629 * @return string
6630 */
6631 function ebook_store_test_verification_key( $ebook_id ) {
6632 return 'EBOOKSTORETEST' . (int) $ebook_id;
6633 }
6634
6635 /**
6636 * If a verification key is a Test-button key, return the ebook ID it was made
6637 * from; otherwise false.
6638 *
6639 * @param string $key Sanitised (alphanumeric) verification key.
6640 * @return int|false
6641 */
6642 function ebook_store_parse_test_verification_key( $key ) {
6643 $prefix = 'EBOOKSTORETEST';
6644 if ( strpos( (string) $key, $prefix ) !== 0 ) {
6645 return false;
6646 }
6647 return (int) substr( (string) $key, strlen( $prefix ) );
6648 }
6649
6650 function ebook_store_render_public_order_field( $label, $value ) {
6651 if ( $value === '' || $value === null ) {
6652 return '';
6653 }
6654
6655 return '<div class="ebook-store-verify__field"><span>' . esc_html( $label ) . '</span><strong>' . esc_html( $value ) . '</strong></div>';
6656 }
6657
6658 function ebook_store_render_public_order_verification_page() {
6659 if ( ! isset( $_GET['ebook_action'] ) || sanitize_text_field( wp_unslash( $_GET['ebook_action'] ) ) !== 'verify_order' ) {
6660 return;
6661 }
6662
6663 $ebook_key = isset( $_GET['ebook_key'] ) ? preg_replace( '/[^a-zA-Z0-9]+/', '', wp_unslash( $_GET['ebook_key'] ) ) : '';
6664
6665 // A QR scanned from a Test-button copy carries no real order, so recognise it
6666 // and show a "test ebook" confirmation rather than a scary "not found".
6667 $test_ebook_id = ebook_store_parse_test_verification_key( $ebook_key );
6668 $is_test = ( $test_ebook_id !== false );
6669
6670 if ( $is_test ) {
6671 $order = false;
6672 } elseif ( $ebook_key === '' ) {
6673 $order = false;
6674 } else {
6675 $order = ebook_get_order( 'ebook_key', $ebook_key );
6676 if ( ! $order && get_option( 'stripe_integration_enabled', 0 ) ) {
6677 $recovered_order_id = ebook_store_recover_stripe_order_by_ebook_key( $ebook_key );
6678 if ( ! is_wp_error( $recovered_order_id ) && $recovered_order_id ) {
6679 $order = ebook_get_order( 'ebook_key', $ebook_key );
6680 }
6681 }
6682 }
6683
6684 $found_order = is_array( $order ) && ! empty( $order['ebook'][0] );
6685 status_header( ( $is_test || $found_order ) ? 200 : 404 );
6686 nocache_headers();
6687
6688 $site_name = wp_strip_all_tags( get_bloginfo( 'name' ) );
6689 if ( $is_test ) {
6690 $page_title = __( 'Test Copy', 'ebook-store' );
6691 $status_label = __( 'Test copy', 'ebook-store' );
6692 $status_class = 'is-test';
6693 } else {
6694 $page_title = $found_order ? __( 'Ebook Copy Verification', 'ebook-store' ) : __( 'Verification Record Not Found', 'ebook-store' );
6695 $status_label = $found_order
6696 ? ( ! empty( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : __( 'Completed', 'ebook-store' ) )
6697 : __( 'Missing', 'ebook-store' );
6698 $status_class = $found_order ? 'is-valid' : 'is-missing';
6699 }
6700
6701 $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>
6702 :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)}
6703 *{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)}
6704 a{color:inherit} .ebook-store-verify{max-width:980px;margin:0 auto;padding:32px 20px 56px}
6705 .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}
6706 .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))}
6707 .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}
6708 .ebook-store-verify__hero h1{margin:0 0 10px;font-size:clamp(2rem,4vw,3.5rem);line-height:.98}
6709 .ebook-store-verify__hero p{margin:0;max-width:62ch;font:400 16px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
6710 .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}
6711 .ebook-store-verify__status.is-valid{background:var(--ebook-verify-green-soft);color:var(--ebook-verify-green)}
6712 .ebook-store-verify__status.is-missing{background:var(--ebook-verify-red-soft);color:var(--ebook-verify-red)}
6713 .ebook-store-verify__status.is-test{background:#fbf1de;color:#7a4d00}
6714 .ebook-store-verify__grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px;margin-top:20px}
6715 .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)}
6716 .ebook-store-verify__card h2{margin:0 0 14px;font-size:1.1rem}
6717 .ebook-store-verify__field{display:flex;justify-content:space-between;gap:18px;padding:12px 0;border-top:1px solid #edf1eb}
6718 .ebook-store-verify__field:first-child{border-top:0;padding-top:0}
6719 .ebook-store-verify__field span{font:600 12px/1.3 Arial,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:var(--ebook-verify-muted)}
6720 .ebook-store-verify__field strong{font:600 15px/1.4 Arial,sans-serif;text-align:right;max-width:65%}
6721 .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)}
6722 .ebook-store-verify__footer{margin-top:16px;font:400 13px/1.6 Arial,sans-serif;color:var(--ebook-verify-muted)}
6723 @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}}
6724 </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>';
6725
6726 if ( $is_test ) {
6727 $test_title = $test_ebook_id ? get_the_title( $test_ebook_id ) : '';
6728 $content .= '<section class="ebook-store-verify__notice"><strong>' . esc_html__( 'This is a test ebook.', 'ebook-store' ) . '</strong> ';
6729 $content .= esc_html__( 'You are verifying a copy generated with the Test button in the store admin, not a real purchase.', 'ebook-store' );
6730 if ( $test_title !== '' ) {
6731 $content .= ' ' . esc_html(
6732 sprintf(
6733 /* translators: 1: ebook title, 2: ebook ID. */
6734 __( 'It was made from "%1$s" (ebook #%2$d).', 'ebook-store' ),
6735 $test_title,
6736 $test_ebook_id
6737 )
6738 );
6739 }
6740 $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>';
6741 } elseif ( $found_order ) {
6742 $ebook_id = isset( $order['ebook'][0] ) ? (int) $order['ebook'][0] : 0;
6743 $ebook_title = $ebook_id ? get_the_title( $ebook_id ) : '';
6744 $order_id = isset( $order['order_id'] ) ? (int) $order['order_id'] : 0;
6745 $buyer_name = trim( ( isset( $order['first_name'][0] ) ? $order['first_name'][0] : '' ) . ' ' . ( isset( $order['last_name'][0] ) ? $order['last_name'][0] : '' ) );
6746 $payer_email = isset( $order['payer_email'][0] ) ? (string) $order['payer_email'][0] : '';
6747 $payment_date = isset( $order['payment_date'][0] ) ? (string) $order['payment_date'][0] : '';
6748 $payment_type = isset( $order['payment_type'][0] ) ? (string) $order['payment_type'][0] : '';
6749 $payment_status = isset( $order['payment_status'][0] ) ? (string) $order['payment_status'][0] : '';
6750 $txn_id = isset( $order['txn_id'][0] ) ? (string) $order['txn_id'][0] : '';
6751 $gross = isset( $order['mc_gross'][0] ) ? (string) $order['mc_gross'][0] : '';
6752 $currency = isset( $order['mc_currency'][0] ) && $order['mc_currency'][0] !== '' ? (string) $order['mc_currency'][0] : ebook_store_get_store_currency();
6753 $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] : '' );
6754 $city = isset( $order['address_city'][0] ) ? (string) $order['address_city'][0] : '';
6755 $state = isset( $order['address_state'][0] ) ? (string) $order['address_state'][0] : '';
6756 $zip = isset( $order['address_zip'][0] ) ? (string) $order['address_zip'][0] : '';
6757 $address_street = isset( $order['address_street'][0] ) ? (string) $order['address_street'][0] : '';
6758
6759 $content .= '<section class="ebook-store-verify__grid">';
6760 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Order Record', 'ebook-store' ) . '</h2>';
6761 $content .= ebook_store_render_public_order_field( __( 'Order ID', 'ebook-store' ), $order_id ? '#' . $order_id : '' );
6762 $content .= ebook_store_render_public_order_field( __( 'Ebook', 'ebook-store' ), $ebook_title );
6763 $content .= ebook_store_render_public_order_field( __( 'Ebook ID', 'ebook-store' ), $ebook_id ? (string) $ebook_id : '' );
6764 $content .= ebook_store_render_public_order_field( __( 'Payment date', 'ebook-store' ), $payment_date );
6765 $content .= ebook_store_render_public_order_field( __( 'Gateway', 'ebook-store' ), $payment_type !== '' ? $payment_type : __( 'PayPal', 'ebook-store' ) );
6766 $content .= ebook_store_render_public_order_field( __( 'Status', 'ebook-store' ), $payment_status );
6767 $content .= ebook_store_render_public_order_field( __( 'Transaction', 'ebook-store' ), $txn_id );
6768 $content .= ebook_store_render_public_order_field( __( 'Amount', 'ebook-store' ), trim( $gross . ' ' . $currency ) );
6769 $content .= '</article>';
6770 $content .= '<article class="ebook-store-verify__card"><h2>' . esc_html__( 'Buyer Details', 'ebook-store' ) . '</h2>';
6771 $content .= ebook_store_render_public_order_field( __( 'Name', 'ebook-store' ), $buyer_name );
6772 $content .= ebook_store_render_public_order_field( __( 'Email', 'ebook-store' ), $payer_email );
6773 $content .= ebook_store_render_public_order_field( __( 'Street', 'ebook-store' ), $address_street );
6774 $content .= ebook_store_render_public_order_field( __( 'City', 'ebook-store' ), $city );
6775 $content .= ebook_store_render_public_order_field( __( 'State', 'ebook-store' ), $state );
6776 $content .= ebook_store_render_public_order_field( __( 'Postal code', 'ebook-store' ), $zip );
6777 $content .= ebook_store_render_public_order_field( __( 'Country', 'ebook-store' ), $country );
6778 $content .= ebook_store_render_public_order_field( __( 'Order key', 'ebook-store' ), $ebook_key );
6779 $content .= '</article>';
6780 $content .= '</section>';
6781 $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>';
6782 } else {
6783 $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>';
6784 }
6785
6786 $content .= '<div class="ebook-store-verify__footer">' . esc_html( $site_name ) . '</div></main></body></html>';
6787
6788 echo $content;
6789 exit;
6790 }
6791
6792 /**
6793 * Clean a string that is about to be drawn into a PDF watermark.
6794 *
6795 * PDF text is NOT HTML, so esc_attr()/esc_html() are wrong here — they turn
6796 * "Tom & Jerry" into "Tom &amp; Jerry" inside the document. Just strip control
6797 * characters and collapse whitespace.
6798 *
6799 * @param string $text Raw text (may contain %%merge%% codes before substitution).
6800 * @return string
6801 */
6802 function ebook_store_sanitize_pdf_text( $text ) {
6803 $text = (string) $text;
6804 // Drop ASCII control characters except tab/newline, then trim.
6805 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text );
6806
6807 return trim( (string) $text );
6808 }
6809
6810 /**
6811 * Resolve the buyer watermark text for an order, substituting the %%merge%%
6812 * codes. Shared by the PDF and EPUB watermark paths so the tokens resolve the
6813 * same way: WooCommerce-style meta first, then the built-in order meta.
6814 *
6815 * @param int $order_id Order post ID.
6816 * @param string $template Raw template, e.g. "Licensed to %%first_name%% %%last_name%%".
6817 * @return string Resolved, control-char-stripped text (NOT HTML-escaped).
6818 */
6819 function ebook_store_resolve_buyer_watermark( $order_id, $template ) {
6820 $order_id = (int) $order_id;
6821 $meta = $order_id > 0 ? get_post_meta( $order_id ) : array();
6822 $meta = is_array( $meta ) ? $meta : array();
6823 $mv = function ( $key ) use ( $meta ) {
6824 return isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
6825 };
6826
6827 $tokens = array(
6828 'payer_email' => $mv( '_billing_email' ),
6829 'payment_date' => $mv( '_paid_date' ),
6830 'txn_id' => $mv( '_transaction_id' ),
6831 'first_name' => $mv( '_billing_first_name' ),
6832 'last_name' => $mv( '_billing_last_name' ),
6833 'residence_country' => $mv( '_billing_country' ),
6834 );
6835
6836 // Built-in (non-Woo) orders store the buyer under their own keys. Read each
6837 // field from its own key (an earlier copy of this logic took first_name from
6838 // the last_name meta — fixed here).
6839 if ( $tokens['payer_email'] === '' ) {
6840 $tokens['payer_email'] = $mv( 'payer_email' );
6841 $tokens['payment_date'] = $mv( 'payment_date' );
6842 $tokens['txn_id'] = $mv( 'txn_id' );
6843 $tokens['first_name'] = $mv( 'first_name' );
6844 $tokens['last_name'] = $mv( 'last_name' );
6845 $tokens['residence_country'] = $mv( 'residence_country' );
6846 }
6847
6848 $text = ebook_store_sanitize_pdf_text( (string) $template );
6849 foreach ( $tokens as $key => $value ) {
6850 if ( is_scalar( $value ) ) {
6851 $text = str_replace( '%%' . $key . '%%', ebook_store_sanitize_pdf_text( (string) $value ), $text );
6852 }
6853 }
6854
6855 return $text;
6856 }
6857
6858 /**
6859 * Build the visible watermark HTML fragment injected into EPUB content.
6860 *
6861 * Inline CSS only, so no stylesheet or manifest edit is needed. dir="auto" lets
6862 * the reader lay the buyer line out right-to-left for Hebrew/Arabic names.
6863 *
6864 * @param string $text Buyer text (raw, not yet escaped).
6865 * @param string $style 'subtle' | 'box'.
6866 * @return string XML-well-formed XHTML fragment.
6867 */
6868 function ebook_store_epub_watermark_fragment( $text, $style ) {
6869 $wm = htmlspecialchars( (string) $text, ENT_QUOTES, 'UTF-8' );
6870 if ( $style === 'box' ) {
6871 $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';
6872 } else {
6873 $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';
6874 }
6875
6876 return '<div class="ebook-store-wm" dir="auto" style="' . $css . '">' . $wm . '</div>';
6877 }
6878
6879 /**
6880 * Insert an XHTML fragment into a content document via string ops, NOT DOM.
6881 *
6882 * Real content XHTML routinely contains named entities like &nbsp; that a strict
6883 * DOM parser rejects, so we never DOM-parse content documents — we splice the
6884 * fragment next to the <body> tag and leave everything else byte-for-byte intact.
6885 *
6886 * @param string $html The XHTML document.
6887 * @param string $fragment The fragment to insert.
6888 * @param string $position 'top' | 'bottom'.
6889 * @return string|null Modified document, or null if no <body> boundary was found.
6890 */
6891 function ebook_store_epub_inject_fragment( $html, $fragment, $position ) {
6892 $html = (string) $html;
6893
6894 if ( $position === 'top' ) {
6895 if ( preg_match( '/<body\b[^>]*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
6896 $at = $m[0][1] + strlen( $m[0][0] );
6897 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
6898 }
6899 return null;
6900 }
6901
6902 if ( preg_match( '/<\/body\s*>/i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
6903 $at = $m[0][1];
6904 return substr( $html, 0, $at ) . $fragment . substr( $html, $at );
6905 }
6906
6907 return null;
6908 }
6909
6910 /**
6911 * Relative path from a directory inside the EPUB to a file inside it, so an
6912 * <img> in a content document (or a manifest href) can point at the QR image
6913 * wherever the two happen to sit in the archive.
6914 *
6915 * @param string $from_dir Directory of the referencing file (archive path).
6916 * @param string $to_path Target file (archive path).
6917 * @return string Relative reference.
6918 */
6919 function ebook_store_epub_relative_path( $from_dir, $to_path ) {
6920 $from_dir = ( $from_dir === '.' || $from_dir === '/' ) ? '' : trim( (string) $from_dir, '/' );
6921 $to_path = ltrim( (string) $to_path, '/' );
6922 $from = $from_dir === '' ? array() : explode( '/', $from_dir );
6923 $to = explode( '/', $to_path );
6924 while ( ! empty( $from ) && ! empty( $to ) && $from[0] === $to[0] ) {
6925 array_shift( $from );
6926 array_shift( $to );
6927 }
6928 $rel = str_repeat( '../', count( $from ) ) . implode( '/', $to );
6929 return $rel === '' ? basename( $to_path ) : $rel;
6930 }
6931
6932 /**
6933 * Render a QR code to PNG bytes on a solid white background with a quiet zone,
6934 * so it stays scannable on any reader background (including night mode).
6935 *
6936 * Uses the bundled TCPDF 2D-barcode generator, loaded narrowly so the EPUB path
6937 * never pulls in the full PDF engine. Returns '' on any problem (fail-open: the
6938 * book is still delivered, just without a QR).
6939 *
6940 * @param string $text The URL/text to encode.
6941 * @param int $module_px Pixels per QR module.
6942 * @param int $quiet Quiet-zone width in modules.
6943 * @return string PNG bytes, or '' if a QR could not be produced.
6944 */
6945 function ebook_store_generate_qr_png( $text, $module_px = 8, $quiet = 4 ) {
6946 $text = (string) $text;
6947 if ( $text === '' || ! function_exists( 'imagecreatetruecolor' ) || ! function_exists( 'imagepng' ) ) {
6948 return '';
6949 }
6950
6951 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
6952 $base = __DIR__ . '/fpdi/vendor/tecnickcom/tcpdf';
6953 $qr = $base . '/include/barcodes/qrcode.php';
6954 $bc = $base . '/tcpdf_barcodes_2d.php';
6955 if ( is_readable( $qr ) && is_readable( $bc ) ) {
6956 require_once $qr;
6957 require_once $bc;
6958 }
6959 }
6960 if ( ! class_exists( 'TCPDF2DBarcode' ) ) {
6961 return '';
6962 }
6963
6964 try {
6965 $obj = new TCPDF2DBarcode( $text, 'QRCODE,M' );
6966 $arr = $obj->getBarcodeArray();
6967 if ( empty( $arr['num_rows'] ) || empty( $arr['num_cols'] ) || empty( $arr['bcode'] ) ) {
6968 return '';
6969 }
6970 $rows = (int) $arr['num_rows'];
6971 $cols = (int) $arr['num_cols'];
6972 $w = ( $cols + 2 * $quiet ) * $module_px;
6973 $h = ( $rows + 2 * $quiet ) * $module_px;
6974
6975 $img = imagecreatetruecolor( $w, $h );
6976 $white = imagecolorallocate( $img, 255, 255, 255 );
6977 $black = imagecolorallocate( $img, 0, 0, 0 );
6978 imagefilledrectangle( $img, 0, 0, $w - 1, $h - 1, $white );
6979 for ( $r = 0; $r < $rows; $r++ ) {
6980 for ( $c = 0; $c < $cols; $c++ ) {
6981 if ( ! empty( $arr['bcode'][ $r ][ $c ] ) ) {
6982 $x0 = ( $c + $quiet ) * $module_px;
6983 $y0 = ( $r + $quiet ) * $module_px;
6984 imagefilledrectangle( $img, $x0, $y0, $x0 + $module_px - 1, $y0 + $module_px - 1, $black );
6985 }
6986 }
6987 }
6988 ob_start();
6989 imagepng( $img );
6990 $data = ob_get_clean();
6991 imagedestroy( $img );
6992
6993 return is_string( $data ) ? $data : '';
6994 } catch ( \Throwable $e ) {
6995 return '';
6996 }
6997 }
6998
6999 /**
7000 * The XHTML fragment that shows the QR image, pointing at a relative src.
7001 *
7002 * @param string $src Relative image reference from the referencing document.
7003 * @return string Well-formed XHTML fragment.
7004 */
7005 function ebook_store_epub_qr_fragment( $src ) {
7006 $src = htmlspecialchars( (string) $src, ENT_QUOTES, 'UTF-8' );
7007 $alt = htmlspecialchars( __( 'Scan to verify this copy', 'ebook-store' ), ENT_QUOTES, 'UTF-8' );
7008 return '<div class="ebook-store-qr" style="text-align:center;margin:1.2em 0">'
7009 . '<img src="' . $src . '" alt="' . $alt . '" style="width:120px;height:120px;max-width:40%"/></div>';
7010 }
7011
7012 /**
7013 * Watermark an EPUB with the buyer's identity (social DRM) while keeping it a
7014 * valid, reader-compatible book.
7015 *
7016 * EPUB cannot be password-encrypted and still open in e-readers, so instead the
7017 * buyer line is injected into the content and (optionally) the OPF metadata, and
7018 * the archive is rewritten with "mimetype" kept FIRST and STORED. This is a plain
7019 * function that never loads the PDF stack, and it FAILS OPEN: on any problem it
7020 * returns the untouched $src_path so a paid download is never blocked. It never
7021 * throws.
7022 *
7023 * @param string $src_path Absolute path to the source .epub.
7024 * @param string $dest_path Absolute path to write the watermarked copy to.
7025 * @param string $watermark_text Buyer text, already merge-resolved (raw, not escaped).
7026 * @param array $opts scope|position|style|stamp_metadata|order_id.
7027 * @return string $dest_path on success, else $src_path.
7028 */
7029 function ebook_store_watermark_epub( $src_path, $dest_path, $watermark_text, $opts = array() ) {
7030 if ( ! class_exists( 'ZipArchive' ) || ! class_exists( 'DOMDocument' ) || ! is_readable( $src_path ) ) {
7031 return $src_path;
7032 }
7033
7034 $log = function ( $message, $context = array() ) {
7035 if ( function_exists( 'ebook_store_log' ) ) {
7036 ebook_store_log( 'epub', $message, $context );
7037 }
7038 };
7039
7040 // The archive is held in memory ~twice while repacking, and an OOM is
7041 // uncatchable, so refuse very large books rather than 500 the download.
7042 $max = (int) apply_filters( 'ebook_store_epub_watermark_max_bytes', 64 * 1024 * 1024 );
7043 $size = @filesize( $src_path );
7044 if ( $size !== false && $size > $max ) {
7045 $log( 'too large, serving source', array( 'bytes' => $size ) );
7046 return $src_path;
7047 }
7048 if ( function_exists( 'ebook_store_raise_pdf_memory' ) ) {
7049 ebook_store_raise_pdf_memory();
7050 }
7051
7052 try {
7053 $scope = in_array( isset( $opts['scope'] ) ? $opts['scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['scope'] : 'first';
7054 $position = in_array( isset( $opts['position'] ) ? $opts['position'] : '', array( 'top', 'bottom' ), true ) ? $opts['position'] : 'bottom';
7055 $style = in_array( isset( $opts['style'] ) ? $opts['style'] : '', array( 'subtle', 'box' ), true ) ? $opts['style'] : 'subtle';
7056 $stamp = ! empty( $opts['stamp_metadata'] );
7057 $order_id = isset( $opts['order_id'] ) ? (int) $opts['order_id'] : 0;
7058
7059 // QR code carries its own placement so the buyer can put it where they like.
7060 $qr_scope = in_array( isset( $opts['qr_scope'] ) ? $opts['qr_scope'] : '', array( 'first', 'every', 'colophon' ), true ) ? $opts['qr_scope'] : 'first';
7061 $qr_position = in_array( isset( $opts['qr_position'] ) ? $opts['qr_position'] : '', array( 'top', 'bottom' ), true ) ? $opts['qr_position'] : 'bottom';
7062 $qr_text = isset( $opts['qr_text'] ) ? trim( (string) $opts['qr_text'] ) : '';
7063 $qr_enabled = ! empty( $opts['qr'] ) && $qr_text !== '';
7064
7065 $text = ebook_store_sanitize_pdf_text( (string) $watermark_text );
7066 if ( $text === '' && ! $qr_enabled && ! $stamp ) {
7067 return $src_path; // nothing to add
7068 }
7069
7070 // --- STEP 1: read the whole archive into memory, then close it. ---
7071 $in = new ZipArchive();
7072 if ( $in->open( $src_path ) !== true ) {
7073 return $src_path;
7074 }
7075 $entries = array();
7076 $order = array();
7077 for ( $i = 0; $i < $in->numFiles; $i++ ) {
7078 $stat = $in->statIndex( $i );
7079 if ( $stat === false ) {
7080 $in->close();
7081 return $src_path;
7082 }
7083 $name = $stat['name'];
7084 if ( $name === '' || substr( $name, -1 ) === '/' ) {
7085 continue; // directory marker
7086 }
7087 $content = $in->getFromIndex( $i );
7088 if ( $content === false ) {
7089 $in->close();
7090 return $src_path; // corrupt entry: never ship a hollow book
7091 }
7092 if ( ! isset( $entries[ $name ] ) ) {
7093 $order[] = $name;
7094 }
7095 $entries[ $name ] = $content;
7096 }
7097 $in->close();
7098
7099 if ( ! isset( $entries['mimetype'] ) || trim( $entries['mimetype'] ) !== 'application/epub+zip' ) {
7100 return $src_path; // not a real EPUB, don't touch it
7101 }
7102
7103 // --- STEP 2: never rewrite signed or DRM-encrypted containers. ---
7104 if ( isset( $entries['META-INF/signatures.xml'] ) ) {
7105 $log( 'signed container, serving source' );
7106 return $src_path;
7107 }
7108 if ( isset( $entries['META-INF/encryption.xml'] ) ) {
7109 $enc = $entries['META-INF/encryption.xml'];
7110 $font_obfuscation_only = ( strpos( $enc, 'http://www.idpf.org/2008/embedding' ) !== false || strpos( $enc, 'http://ns.adobe.com/pdf/enc#RC' ) !== false )
7111 && stripos( $enc, 'xmlenc#aes' ) === false
7112 && stripos( $enc, 'adept' ) === false;
7113 if ( ! $font_obfuscation_only ) {
7114 $log( 'encrypted/DRM container, serving source' );
7115 return $src_path;
7116 }
7117 // Font obfuscation: every entry name/bytes are preserved verbatim and
7118 // dc:identifier is never touched, so the deobfuscation key still derives.
7119 }
7120
7121 // --- STEP 3: container.xml -> OPF path (clean XML, DOM-safe). ---
7122 $opf_path = '';
7123 if ( isset( $entries['META-INF/container.xml'] ) ) {
7124 $cx = new DOMDocument();
7125 $prev = libxml_use_internal_errors( true );
7126 $loaded = $cx->loadXML( $entries['META-INF/container.xml'], LIBXML_NONET );
7127 libxml_clear_errors();
7128 libxml_use_internal_errors( $prev );
7129 if ( $loaded ) {
7130 foreach ( $cx->getElementsByTagName( 'rootfile' ) as $rf ) {
7131 $fp = $rf->getAttribute( 'full-path' );
7132 if ( $fp !== '' ) {
7133 $opf_path = $fp;
7134 break;
7135 }
7136 }
7137 }
7138 }
7139 if ( $opf_path === '' || ! isset( $entries[ $opf_path ] ) ) {
7140 return $src_path;
7141 }
7142 $opf_dir = trim( dirname( $opf_path ), '/.' );
7143 $resolve = function ( $href ) use ( $opf_dir ) {
7144 $href = preg_replace( '/[?#].*$/', '', (string) $href );
7145 $path = ( $opf_dir === '' ? '' : $opf_dir . '/' ) . $href;
7146 $out = array();
7147 foreach ( explode( '/', $path ) as $seg ) {
7148 if ( $seg === '' || $seg === '.' ) {
7149 continue;
7150 }
7151 if ( $seg === '..' ) {
7152 array_pop( $out );
7153 continue;
7154 }
7155 $out[] = $seg;
7156 }
7157 return implode( '/', $out );
7158 };
7159
7160 // --- STEP 4: parse the OPF (clean XML), read version, manifest, spine. ---
7161 $opf = new DOMDocument();
7162 $prev = libxml_use_internal_errors( true );
7163 $opf_loaded = $opf->loadXML( $entries[ $opf_path ], LIBXML_NONET );
7164 libxml_clear_errors();
7165 libxml_use_internal_errors( $prev );
7166 if ( ! $opf_loaded ) {
7167 return $src_path;
7168 }
7169
7170 $manifest_href = array();
7171 $manifest_media = array();
7172 foreach ( $opf->getElementsByTagName( 'item' ) as $item ) {
7173 $id = $item->getAttribute( 'id' );
7174 if ( $id === '' ) {
7175 continue;
7176 }
7177 $manifest_href[ $id ] = $resolve( $item->getAttribute( 'href' ) );
7178 $manifest_media[ $id ] = $item->getAttribute( 'media-type' );
7179 }
7180 $spine_docs = array();
7181 foreach ( $opf->getElementsByTagName( 'itemref' ) as $iref ) {
7182 $idref = $iref->getAttribute( 'idref' );
7183 if ( $idref === '' || ! isset( $manifest_href[ $idref ] ) ) {
7184 continue;
7185 }
7186 $href = $manifest_href[ $idref ];
7187 $media = isset( $manifest_media[ $idref ] ) ? $manifest_media[ $idref ] : '';
7188 if ( $media === 'application/xhtml+xml' && isset( $entries[ $href ] ) ) {
7189 $spine_docs[] = $href;
7190 }
7191 }
7192
7193 $applied = false;
7194 $opf_dirty = false;
7195
7196 $manifest_node = $opf->getElementsByTagName( 'manifest' )->item( 0 );
7197 $spine_node = $opf->getElementsByTagName( 'spine' )->item( 0 );
7198
7199 // Which documents a scope targets: a list of content docs, or the string
7200 // 'colophon' (a dedicated page we append).
7201 $targets_for = function ( $target_scope ) use ( $spine_docs ) {
7202 if ( $target_scope === 'colophon' ) {
7203 return 'colophon';
7204 }
7205 if ( empty( $spine_docs ) ) {
7206 return array();
7207 }
7208 return $target_scope === 'every' ? $spine_docs : array( $spine_docs[0] );
7209 };
7210
7211 // Prepare the QR image up front (fail-open: no QR if it can't be made).
7212 $qr_png = '';
7213 $qr_abs = '';
7214 if ( $qr_enabled ) {
7215 $qr_png = ebook_store_generate_qr_png( $qr_text );
7216 if ( $qr_png === '' ) {
7217 $qr_enabled = false;
7218 $log( 'QR unavailable (no GD/barcode support), delivering without QR' );
7219 } else {
7220 $n = 0;
7221 do {
7222 $qr_rel = 'ebook-store-qr' . ( $n === 0 ? '' : '-' . $n ) . '.png';
7223 $qr_abs = $resolve( $qr_rel );
7224 $n++;
7225 } while ( isset( $entries[ $qr_abs ] ) && $n < 50 );
7226 }
7227 }
7228
7229 $text_targets = $text !== '' ? $targets_for( $scope ) : array();
7230 $qr_targets = $qr_enabled ? $targets_for( $qr_scope ) : array();
7231 $qr_referenced = false;
7232
7233 // --- STEP 5: inject text and/or QR into existing content documents. ---
7234 $content_targets = array();
7235 if ( is_array( $text_targets ) ) {
7236 $content_targets = array_merge( $content_targets, $text_targets );
7237 }
7238 if ( is_array( $qr_targets ) ) {
7239 $content_targets = array_merge( $content_targets, $qr_targets );
7240 }
7241 $content_targets = array_values( array_unique( $content_targets ) );
7242
7243 foreach ( $content_targets as $doc ) {
7244 $html = $entries[ $doc ];
7245 $changed = false;
7246
7247 if ( is_array( $text_targets ) && in_array( $doc, $text_targets, true ) ) {
7248 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_watermark_fragment( $text, $style ), $position );
7249 if ( $new !== null ) {
7250 $html = $new;
7251 $changed = true;
7252 } else {
7253 $log( 'no <body> boundary, doc left unmarked', array( 'doc' => $doc ) );
7254 }
7255 }
7256
7257 if ( is_array( $qr_targets ) && in_array( $doc, $qr_targets, true ) ) {
7258 $qr_src = ebook_store_epub_relative_path( dirname( $doc ), $qr_abs );
7259 $new = ebook_store_epub_inject_fragment( $html, ebook_store_epub_qr_fragment( $qr_src ), $qr_position );
7260 if ( $new !== null ) {
7261 $html = $new;
7262 $changed = true;
7263 $qr_referenced = true;
7264 } else {
7265 $log( 'no <body> boundary, QR not placed', array( 'doc' => $doc ) );
7266 }
7267 }
7268
7269 if ( $changed ) {
7270 $entries[ $doc ] = $html;
7271 $applied = true;
7272 }
7273 }
7274
7275 // --- STEP 6: colophon page carrying whatever targets 'colophon'. ---
7276 if ( ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) && $manifest_node && $spine_node ) {
7277 $n = 0;
7278 do {
7279 $rel = 'ebook-store-licence' . ( $n === 0 ? '' : '-' . $n ) . '.xhtml';
7280 $abs = $resolve( $rel );
7281 $n++;
7282 } while ( isset( $entries[ $abs ] ) && $n < 50 );
7283
7284 $body = '';
7285 if ( $text_targets === 'colophon' && $text !== '' ) {
7286 $body .= ebook_store_epub_watermark_fragment( $text, 'box' );
7287 }
7288 if ( $qr_targets === 'colophon' ) {
7289 $body .= ebook_store_epub_qr_fragment( ebook_store_epub_relative_path( dirname( $abs ), $qr_abs ) );
7290 $qr_referenced = true;
7291 }
7292
7293 $page = '<?xml version="1.0" encoding="UTF-8"?>' . "\n"
7294 . '<!DOCTYPE html>' . "\n"
7295 . '<html xmlns="http://www.w3.org/1999/xhtml"><head><meta charset="utf-8"/><title>'
7296 . htmlspecialchars( __( 'Licence', 'ebook-store' ), ENT_QUOTES, 'UTF-8' )
7297 . '</title></head><body>' . $body . '</body></html>';
7298
7299 $mid = 'ebook-store-licence';
7300 $existing_ids = array_keys( $manifest_href );
7301 $k = 0;
7302 while ( in_array( $mid, $existing_ids, true ) ) {
7303 $k++;
7304 $mid = 'ebook-store-licence-' . $k;
7305 }
7306
7307 $entries[ $abs ] = $page;
7308 $order[] = $abs;
7309
7310 $item_el = $opf->createElement( 'item' );
7311 $item_el->setAttribute( 'id', $mid );
7312 $item_el->setAttribute( 'href', $rel );
7313 $item_el->setAttribute( 'media-type', 'application/xhtml+xml' );
7314 $manifest_node->appendChild( $item_el );
7315
7316 $iref_el = $opf->createElement( 'itemref' );
7317 $iref_el->setAttribute( 'idref', $mid );
7318 $spine_node->appendChild( $iref_el );
7319
7320 $applied = true;
7321 $opf_dirty = true;
7322 } elseif ( $text_targets === 'colophon' || $qr_targets === 'colophon' ) {
7323 $log( 'no manifest/spine, cannot add colophon' );
7324 }
7325
7326 // --- Register the QR image in the archive + manifest (once, if used). ---
7327 if ( $qr_referenced && $manifest_node ) {
7328 $entries[ $qr_abs ] = $qr_png;
7329 $order[] = $qr_abs;
7330
7331 $qr_id = 'ebook-store-qr';
7332 $existing_ids = array();
7333 foreach ( $opf->getElementsByTagName( 'item' ) as $it ) {
7334 $existing_ids[] = $it->getAttribute( 'id' );
7335 }
7336 $k = 0;
7337 while ( in_array( $qr_id, $existing_ids, true ) ) {
7338 $k++;
7339 $qr_id = 'ebook-store-qr-' . $k;
7340 }
7341 $qr_item = $opf->createElement( 'item' );
7342 $qr_item->setAttribute( 'id', $qr_id );
7343 $qr_item->setAttribute( 'href', ebook_store_epub_relative_path( $opf_dir, $qr_abs ) );
7344 $qr_item->setAttribute( 'media-type', 'image/png' );
7345 $manifest_node->appendChild( $qr_item );
7346
7347 $opf_dirty = true;
7348 }
7349
7350 // --- STEP 7: metadata stamp (DOM auto-escapes; pass raw text). ---
7351 if ( $stamp && $text !== '' ) {
7352 $metadata = $opf->getElementsByTagName( 'metadata' )->item( 0 );
7353 if ( $metadata ) {
7354 $dc_ns = 'http://purl.org/dc/elements/1.1/';
7355
7356 // Stamp the resolved buyer line verbatim; it already reads as a
7357 // licence statement, so we don't prepend our own prefix (which
7358 // would double up on the default "Licensed to ..." template).
7359 $contributor = $opf->createElementNS( $dc_ns, 'dc:contributor' );
7360 $contributor->appendChild( $opf->createTextNode( $text ) );
7361 $metadata->appendChild( $contributor );
7362
7363 $has_rights = false;
7364 foreach ( $metadata->getElementsByTagName( 'rights' ) as $unused ) {
7365 $has_rights = true;
7366 break;
7367 }
7368 if ( ! $has_rights ) {
7369 $rights = $opf->createElementNS( $dc_ns, 'dc:rights' );
7370 $rights->appendChild( $opf->createTextNode( $text ) );
7371 $metadata->appendChild( $rights );
7372 }
7373
7374 $meta_wm = $opf->createElement( 'meta' );
7375 $meta_wm->setAttribute( 'name', 'ebook-store:watermark' );
7376 $meta_wm->setAttribute( 'content', $text );
7377 $metadata->appendChild( $meta_wm );
7378
7379 if ( $order_id > 0 ) {
7380 $meta_order = $opf->createElement( 'meta' );
7381 $meta_order->setAttribute( 'name', 'ebook-store:order' );
7382 $meta_order->setAttribute( 'content', (string) $order_id );
7383 $metadata->appendChild( $meta_order );
7384 }
7385
7386 $applied = true;
7387 $opf_dirty = true;
7388 }
7389 }
7390
7391 // Re-serialize the OPF only if we changed it.
7392 if ( $opf_dirty ) {
7393 $xml = $opf->saveXML();
7394 if ( $xml !== false && $xml !== '' ) {
7395 $entries[ $opf_path ] = $xml; // saveXML()===false must never blank the OPF
7396 }
7397 }
7398
7399 if ( ! $applied ) {
7400 return $src_path; // nothing actually changed; don't rewrite
7401 }
7402
7403 // --- STEP 8: repack, mimetype FIRST + STORED (the make-or-break rule). ---
7404 $out = new ZipArchive();
7405 if ( $out->open( $dest_path, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== true ) {
7406 return $src_path;
7407 }
7408 $out->addFromString( 'mimetype', 'application/epub+zip' );
7409 // Class constant only — the global ZIP_CM_STORE is undefined on this build.
7410 $out->setCompressionName( 'mimetype', ZipArchive::CM_STORE );
7411 foreach ( $order as $name ) {
7412 if ( $name === 'mimetype' || ! isset( $entries[ $name ] ) || ! is_string( $entries[ $name ] ) ) {
7413 continue;
7414 }
7415 if ( ! $out->addFromString( $name, $entries[ $name ] ) ) {
7416 $out->close();
7417 @unlink( $dest_path );
7418 return $src_path;
7419 }
7420 }
7421 if ( $out->close() !== true || ! file_exists( $dest_path ) ) {
7422 @unlink( $dest_path );
7423 return $src_path;
7424 }
7425
7426 // --- STEP 9: verify the invariant held, else discard and fail open. ---
7427 $verify = new ZipArchive();
7428 if ( $verify->open( $dest_path ) !== true ) {
7429 @unlink( $dest_path );
7430 return $src_path;
7431 }
7432 $first = $verify->statIndex( 0 );
7433 $verify->close();
7434 if ( ! $first || $first['name'] !== 'mimetype' || (int) $first['comp_method'] !== ZipArchive::CM_STORE ) {
7435 @unlink( $dest_path );
7436 return $src_path;
7437 }
7438
7439 return $dest_path;
7440 } catch ( \Throwable $e ) {
7441 if ( file_exists( $dest_path ) ) {
7442 @unlink( $dest_path );
7443 }
7444 $log( 'watermark failed', array( 'err' => $e->getMessage() ) );
7445 return $src_path;
7446 }
7447 }
7448
7449 function ebook_encrypt_pdf($r = null, $post_id = null) {
7450 //@ini_set('display_errors',1);
7451 //@error_reporting(E_ALL);
7452 if ($r == false) {
7453 $r = $_REQUEST;
7454 }
7455 global $ebook_email_delivery, $ebook_qr_text, $ebook_png_path, $ebook_pngname, $attachment, $pdfHeaderText;
7456 require_once('fpdi/qrcode.class.php');
7457
7458 // Resolve the order this encryption belongs to first, so the output can go in
7459 // that order's protected cache directory (in uploads, HTTP-denied) rather than
7460 // the web-accessible plugin folder — and so a missing folder can't fatal.
7461 $target_order_id = 0;
7462 if ( ! empty( $r['order_id'] ) ) {
7463 $target_order_id = (int) $r['order_id'];
7464 } elseif ( ! empty( $ebook_email_delivery['order']['order_id'] ) ) {
7465 $target_order_id = (int) $ebook_email_delivery['order']['order_id'];
7466 } elseif ( ! empty( $post_id ) ) {
7467 $target_order_id = (int) $post_id;
7468 }
7469
7470 $cache_dir = ebook_store_get_cache_dir( $target_order_id );
7471 if ( $cache_dir === '' ) {
7472 // Uploads unusable — fall back to the system temp dir, never the web root.
7473 $cache_dir = untrailingslashit( get_temp_dir() );
7474 }
7475
7476 $ebook_qr_text = ebook_store_get_order_verification_url( isset( $r['ebook_key'] ) ? $r['ebook_key'] : '' );
7477 // Watermark text is drawn into a PDF, not HTML — esc_attr() would leak entities
7478 // ("Tom & Jerry" -> "Tom &amp; Jerry"). Strip control chars instead.
7479 $pdfHeaderText = ebook_store_sanitize_pdf_text( (string) get_option('buyer_info_text') );
7480
7481 foreach ($r as $k => $v) {
7482 if ( is_scalar( $v ) ) {
7483 $pdfHeaderText = str_replace("%%$k%%", ebook_store_sanitize_pdf_text( (string) $v ), $pdfHeaderText);
7484 }
7485 }
7486 // The QR code is now drawn natively by TCPDF as vectors; no PNG is fetched
7487 // from an external service, and no GD / allow_url_fopen is needed.
7488 if (@is_array($attachment)) {
7489 @$file = $attachment[0]['file'];
7490 }
7491 if (@!$file) {
7492 $file = $ebook_email_delivery['attachment'][0]['file'];
7493 }
7494 // ($target_order_id was resolved at the top so the cache dir could be built.)
7495 $password = ebook_store_get_password_for_order($target_order_id, array(
7496 'password' => isset($r['password']) ? $r['password'] : '',
7497 'payer_email' => isset($r['payer_email']) ? $r['payer_email'] : '',
7498 'md5_nonce' => isset($r['md5_nonce']) ? $r['md5_nonce'] : '',
7499 'ebook_key' => isset($r['ebook_key']) ? $r['ebook_key'] : '',
7500 ));
7501
7502 if (isset($ebook_email_delivery['order']) && is_array($ebook_email_delivery['order'])) {
7503 $ebook_email_delivery['order']['password'] = $password;
7504 }
7505
7506 //free files encrypt with user email?
7507
7508
7509 //$password = $r['payer_email'];
7510 // if (get_option('ebook_store_random_password') == 1) {
7511 // if ($ebook_store_random_password != '') {
7512 // $r['password'] = $ebook_store_random_password;
7513 // }
7514 // if (@$r['password'] != '') {
7515 // $password = $r['password'];
7516 // }
7517 // }
7518
7519 // if (get_option('ebook_store_blank_password') == 1) {
7520 // $r['password'] = '';
7521 // $password = '';
7522 // }
7523
7524 //random password for non logged in users
7525 // if (!is_user_logged_in() && get_option('formEnabled') < 1) {
7526 // if (get_option('ebook_store_random_password') == 1) {
7527 // $password = substr(md5(rand()),0,8);
7528 // //$password = 'proba';
7529 // $ebook_store_random_password = $password;
7530 // if ($post_id != null) {
7531 // @update_post_meta($post_id,'password',$password);
7532 // }
7533
7534 // // Additional code to handle the generated password
7535 // }
7536 //}
7537 // if (is_user_logged_in()) {
7538 // if (get_option('ebook_store_random_password') == 1) {
7539 // $password = substr(md5(rand()),0,8);
7540 // //$password = 'proba';
7541 // $ebook_store_random_password = $password;
7542 // if ($post_id != null) {
7543 // @update_post_meta($post_id,'password',$password);
7544 // }
7545
7546 // // Additional code to handle the generated password
7547 // } else {
7548 // if (get_option('ebook_store_blank_password') < 1) {
7549 // $current_user = wp_get_current_user();
7550 // $password = $current_user->user_email;
7551 // if ($post_id != null) {
7552 // @update_post_meta($post_id,'password',$password);
7553 // }
7554 // }
7555 // }
7556 // }
7557
7558 //wp_die($password . ' post id ' . $post_id);
7559 //error_log('password ' . $password);
7560
7561 $owner_password = (string) get_option('ebook_store_owner_password');
7562
7563 // Write the finished, buyer-watermarked PDF into the protected cache dir (in
7564 // uploads, HTTP-denied), NOT the plugin folder — those files carry the buyer's
7565 // email/IP in the watermark and were previously fetchable, since the plugin
7566 // .htaccess denies .txt/.log but not .pdf.
7567 $destfile = $cache_dir . '/' . basename( $file );
7568
7569
7570 if ($target_order_id) {
7571 update_post_meta($target_order_id,'encrypted_pdf','in_progress');
7572 }
7573 ebook_store_raise_pdf_memory();
7574 $pdf = new QRPDF();
7575 $pdf->watermarkText = $pdfHeaderText;
7576 $pdf->qrText = $ebook_qr_text;
7577 $pdf->stampCount = 0;
7578 try {
7579 $pagecount = $pdf->setSourceFile($file);
7580 } catch (Exception $e) {
7581 wp_die( esc_html( sprintf(
7582 /* translators: %s: PDF import error. */
7583 __( 'This PDF could not be processed for protection. Please re-save it as a standard PDF or turn encryption off. Details: %s', 'ebook-store' ),
7584 $e->getMessage()
7585 ) ) );
7586 }
7587
7588 for ($loop = 1; $loop <= $pagecount; $loop++) {
7589 $tplidx = $pdf->importPage($loop);
7590 $size = $pdf->getTemplateSize($tplidx);
7591 $pdf->AddPage($size['orientation'], array($size['width'], $size['height']));
7592 $pdf->useTemplate($tplidx);
7593 $pdf->stampCurrentPage();
7594 }
7595
7596 $pdf->applyProtection($password, $owner_password);
7597 $pdf->Output($destfile, 'F');
7598 if ($target_order_id) {
7599 update_post_meta($target_order_id,'encrypted_pdf',wp_slash($destfile));
7600 }
7601 //make sure enc file is attached
7602 //file_put_contents('ebook-store-password-debug.txt', $password . "\n" . print_r($r, true), FILE_APPEND);
7603
7604 $ebook_email_delivery['attachment'][0]['file'] = $destfile;
7605 $isPdf = true;
7606 return $destfile;
7607 }
7608 function ebook_store_missing_gd() {
7609 ?>
7610 <div class="updated">
7611 <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>
7612 </div>
7613 <?php
7614 }
7615 function ebook_store_admin_notice() {
7616 ?>
7617 <div class="updated">
7618 <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.
7619 <br />
7620 <p class="submit"><input type="button" name="button" id="button" class="button button-primary" value="Fix automatically" onClick="window.location = \'options-general.php?page=ebook_options.php&task=fixThankYouPage\';"></p>
7621 ', 'ebook-store' ); ?></p>
7622 </div>
7623 <?php
7624 }
7625 function ebook_store_admin_notice_paypal() {
7626 if ( ! ebook_store_is_paypal_enabled() ) {
7627 return;
7628 }
7629 ?>
7630 <div class="updated">
7631 <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>
7632 </div>
7633 <?php
7634 }
7635 function ebook_store_admin_notice_autocompleteorders() {
7636 ?>
7637 <div class="updated">
7638 <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>
7639 </div>
7640 <?php
7641 }
7642
7643 function ebook_store_set_messages($messages) {
7644 global $post, $post_ID;
7645 $post_type = get_post_type( $post_ID );
7646
7647 $obj = get_post_type_object($post_type);
7648 $singular = $obj->labels->singular_name;
7649 if ($post_type == 'ebook') {
7650 $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 . '"]\'>';
7651 }
7652 return $messages;
7653 }
7654 function ebook_store_remove_parmelink( $return ) {
7655 if (@$post_id == false) {
7656 $post_id = get_the_ID();
7657 }
7658 if ('ebook' === get_post_type( $post_id )) {
7659 $return = '';
7660 }
7661 return $return;
7662 }
7663
7664 function ebook_admin_css() {
7665 global $post_type;
7666 $post_types = array(
7667 /* set post types */
7668 'ebook',
7669 );
7670 if(in_array($post_type, $post_types))
7671 echo '<style type="text/css">#post-preview, #view-post-btn{display: none;}</style>';
7672 }
7673 function ebook_store_set_columns( $columns ) {
7674
7675 $columns = array(
7676 'cb' => '<input type="checkbox" />',
7677 'cover' => __( 'Cover', 'ebook-store' ),
7678 'title' => __( 'Ebook', 'ebook-store' ),
7679 'date' => __( 'Date', 'ebook-store' ),
7680 'price' => __( 'Price', 'ebook-store' ),
7681 'formats' => __( 'Formats', 'ebook-store' ),
7682 'sales' => __( 'Sales', 'ebook-store' ),
7683 'embed_code' => __('Shortcode & Links', 'ebook-store')
7684 );
7685
7686 return $columns;
7687 }
7688
7689 function ebook_store_columns_output( $column, $post_id ) {
7690 global $post;
7691 $ebook = new EbookStoreEbook($post_id);
7692 switch( $column ) {
7693 case 'embed_code':
7694 $shortcode_id = 'ebook-shortcode-' . $post_id;
7695 $link_id = 'ebook-direct-link-' . $post_id;
7696 $shortcode = '[ebook_store ebook_id="' . $post_id . '"]';
7697 $order_url = add_query_arg( 'task', 'direct_order', get_permalink( $post_id ) );
7698
7699 echo '<div class="ebook-list-shortcuts">';
7700 echo '<div class="ebook-list-shortcuts__field">';
7701 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Shortcode', 'ebook-store' ) . '</span>';
7702 echo '<div class="ebook-list-shortcuts__control">';
7703 echo '<input type="text" id="' . esc_attr( $shortcode_id ) . '" readonly value="' . esc_attr( $shortcode ) . '" />';
7704 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>';
7705 echo '</div>';
7706 echo '</div>';
7707 echo '<div class="ebook-list-shortcuts__field">';
7708 echo '<span class="ebook-list-shortcuts__label">' . esc_html__( 'Direct order link', 'ebook-store' ) . '</span>';
7709 echo '<div class="ebook-list-shortcuts__control">';
7710 echo '<input type="text" id="' . esc_attr( $link_id ) . '" readonly value="' . esc_attr( $order_url ) . '" />';
7711 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>';
7712 echo '</div>';
7713 echo '</div>';
7714 echo '</div>';
7715 break;
7716 case 'sales':
7717 $sales_index = ebook_store_get_ebook_sales_index();
7718 $sales = isset( $sales_index[ $post_id ] ) ? $sales_index[ $post_id ] : array(
7719 'orders' => 0,
7720 'total' => 0.0,
7721 'last_7d' => 0.0,
7722 'last_30d' => 0.0,
7723 'latest_order_gmt' => '',
7724 );
7725 $currency = ebook_store_get_store_currency();
7726 $latest_order = $sales['latest_order_gmt'] !== '' ? human_time_diff( strtotime( $sales['latest_order_gmt'] . ' UTC' ), current_time( 'timestamp', true ) ) : __( 'No orders yet', 'ebook-store' );
7727
7728 echo '<div class="ebook-list-sales">';
7729 echo '<strong class="ebook-list-sales__amount">' . esc_html( ebook_store_order_money_display( $sales['total'], $currency ) ) . '</strong>';
7730 echo '<div class="ebook-list-sales__pills">';
7731 echo '<span class="ebook-list-pill is-green">' . esc_html( sprintf( __( '%s orders', 'ebook-store' ), number_format_i18n( $sales['orders'] ) ) ) . '</span>';
7732 echo '<span class="ebook-list-pill">' . esc_html( sprintf( __( '7d %s', 'ebook-store' ), ebook_store_order_money_display( $sales['last_7d'], $currency ) ) ) . '</span>';
7733 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>';
7734 echo '</div>';
7735 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>';
7736 echo '</div>';
7737 break;
7738 case 'price':
7739 $ebook = get_post_meta( get_the_ID(), 'ebook', true );
7740 $price = 0.0;
7741 if ( ! is_array( $ebook ) ) {
7742 echo '<div class="ebook-list-price"><strong class="ebook-list-price__amount">-</strong></div>';
7743 break;
7744 }
7745 if ( isset( $ebook['ebook_price'] ) ) {
7746 $price = (float) $ebook['ebook_price'];
7747 }
7748 echo '<div class="ebook-list-price">';
7749 echo '<strong class="ebook-list-price__amount">' . esc_html( ebook_store_order_money_display( $price, ebook_store_get_store_currency() ) ) . '</strong>';
7750 echo '<div class="ebook-list-price__pills">';
7751 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>';
7752 if ( ! empty( $ebook['donate_or_download'] ) && $ebook['donate_or_download'] === 'donate' ) {
7753 echo '<span class="ebook-list-pill">' . esc_html__( 'Donation mode', 'ebook-store' ) . '</span>';
7754 }
7755 echo '</div>';
7756 echo '</div>';
7757 break;
7758 case 'cover':
7759 $cover = get_post_meta( get_the_ID(), 'ebook_wp_custom_attachment_cover', true );
7760 $cover = is_array( $cover ) ? $cover : array();
7761
7762 echo '<div class="ebook-list-cover">';
7763 echo '<div class="ebook-list-cover__frame">';
7764 if ( ! empty( $cover['url'] ) ) {
7765 echo '<img src="' . esc_url( $cover['url'] ) . '" alt="" />';
7766 } else {
7767 echo '<span class="ebook-list-cover__placeholder">' . esc_html__( 'No cover uploaded', 'ebook-store' ) . '</span>';
7768 }
7769 echo '</div>';
7770 echo '<span class="ebook-list-status">' . esc_html( get_post_status( $post_id ) ) . '</span>';
7771 echo '</div>';
7772
7773 break;
7774 case 'formats':
7775 $formats_found = array();
7776 if ( is_array( $ebook->files ) ) {
7777 foreach ( array_keys( $ebook->files ) as $key => $value ) {
7778 if ( strpos( $value, '_size' ) === false ) {
7779 $format = $value;
7780 $icon_url = plugins_url( 'img/' . $value . '.png', __FILE__ );
7781 $title = basename( $ebook->files[ $format ] ) . ' ' . $ebook->human_filesize( $ebook->files[ $format . '_size' ] );
7782 $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>';
7783 }
7784 }
7785 echo '<div class="ebook-list-formats">';
7786 echo implode( '', $formats_found );
7787 if ( empty( $formats_found ) ) {
7788 echo '<span class="ebook-list-pill">' . esc_html__( 'No files attached', 'ebook-store' ) . '</span>';
7789 }
7790 echo '</div>';
7791 $testable = array();
7792 if ( ! empty( $ebook->files['pdf'] ) && file_exists( $ebook->files['pdf'] ) ) {
7793 $testable['pdf'] = __( 'PDF', 'ebook-store' );
7794 }
7795 if ( ! empty( $ebook->files['epub'] ) && file_exists( $ebook->files['epub'] ) ) {
7796 $testable['epub'] = __( 'ePub', 'ebook-store' );
7797 }
7798 if ( ! empty( $testable ) ) {
7799 $ebook_test_link = function ( $format ) use ( $post_id ) {
7800 return wp_nonce_url(
7801 admin_url( 'admin-post.php?action=ebook_store_test_encryption&ebook_id=' . $post_id . '&format=' . $format ),
7802 'ebook_store_test_encryption_' . $post_id
7803 );
7804 };
7805 if ( count( $testable ) === 1 ) {
7806 $only = array_key_first( $testable );
7807 $label = $only === 'epub'
7808 ? __( 'Test watermark', 'ebook-store' )
7809 : __( 'Test encryption (password: test)', 'ebook-store' );
7810 echo '<div class="ebook-format-test-link"><a class="button button-small" href="' . esc_url( $ebook_test_link( $only ) ) . '">' . esc_html( $label ) . '</a></div>';
7811 } else {
7812 // Both formats present: one Test control that asks which file to use.
7813 echo '<details class="ebook-format-test">';
7814 echo '<summary class="button button-small">' . esc_html__( 'Test protection…', 'ebook-store' ) . '</summary>';
7815 echo '<div class="ebook-format-test__menu">';
7816 echo '<span class="ebook-format-test__prompt">' . esc_html__( 'Test with which file?', 'ebook-store' ) . '</span>';
7817 foreach ( $testable as $format => $flabel ) {
7818 /* translators: %s: file format, e.g. PDF or ePub. */
7819 echo '<a class="button button-small" href="' . esc_url( $ebook_test_link( $format ) ) . '">' . esc_html( sprintf( __( 'Test %s', 'ebook-store' ), $flabel ) ) . '</a>';
7820 }
7821 echo '</div></details>';
7822 }
7823 }
7824 }
7825 break;
7826
7827 /* Just break out of the switch statement for everything else. */
7828 default :
7829 break;
7830 }
7831 }
7832 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
7833 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
7834 function ebook_store_pre_get_shortlink( $false, $post_id ) {
7835 return 'ebook' === get_post_type( $post_id ) ? '' : $false;
7836 }
7837 function ebook_mime_types($mime_types){
7838 // These have to be the real types. fileinfo reports an ePub as
7839 // application/epub+zip, and when the map here said octet-stream instead,
7840 // wp_check_filetype_and_ext() saw a mismatch and returned ext=false and
7841 // type=false for a perfectly valid book — which is what made the uploader
7842 // answer "that file type is not allowed here" for every .epub.
7843 $mime_types['epub'] = 'application/epub+zip';
7844 $mime_types['mobi'] = 'application/x-mobipocket-ebook';
7845 $mime_types['txt'] = 'text/plain';
7846 $mime_types['mp3'] = 'audio/mpeg';
7847
7848 return $mime_types;
7849 }
7850 /**
7851 * The directory that holds captured checkout-form data.
7852 *
7853 * @return string Absolute path with a trailing slash, or '' on failure.
7854 */
7855 function ebook_store_get_form_data_dir() {
7856 static $dir = null;
7857
7858 if ( $dir !== null ) {
7859 return $dir;
7860 }
7861
7862 $uploads = wp_upload_dir();
7863 if ( ! empty( $uploads['error'] ) ) {
7864 $dir = '';
7865 return $dir;
7866 }
7867
7868 $dir = trailingslashit( $uploads['basedir'] ) . 'ebook_store_data/';
7869
7870 if ( ! is_dir( $dir ) && ! wp_mkdir_p( $dir ) ) {
7871 $dir = '';
7872 return $dir;
7873 }
7874
7875 // Buyer names and email addresses live here; they must not be fetchable.
7876 if ( ! file_exists( $dir . '.htaccess' ) ) {
7877 @file_put_contents( $dir . '.htaccess', "Require all denied\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n" );
7878 }
7879 if ( ! file_exists( $dir . 'index.php' ) ) {
7880 @file_put_contents( $dir . 'index.php', "<?php\n// Silence is golden.\n" );
7881 }
7882
7883 return $dir;
7884 }
7885
7886 /**
7887 * Turn a request-supplied nonce into a safe, fixed-shape filename.
7888 *
7889 * The read side used to interpolate this value straight into a path, giving an
7890 * arbitrary-file-read primitive via ?md5_nonce=../../../etc/passwd.
7891 *
7892 * @param string $md5_nonce Value from the request.
7893 * @return string Filename, or '' when the value is not a plausible token.
7894 */
7895 function ebook_store_get_form_filename( $md5_nonce ) {
7896 $md5_nonce = preg_replace( '/[^a-zA-Z0-9]/', '', (string) $md5_nonce );
7897
7898 if ( $md5_nonce === '' || strlen( $md5_nonce ) > 64 ) {
7899 return '';
7900 }
7901
7902 return 'ebook_store_form_' . $md5_nonce . '.json';
7903 }
7904
7905 function ebook_store_formContent() {
7906 $task = isset( $_REQUEST['task'] ) ? sanitize_key( wp_unslash( $_REQUEST['task'] ) ) : '';
7907
7908 if ( $task === 'formContent' ) {
7909 // The stored markup is authored by an administrator, but it is served to
7910 // anonymous visitors, so it still goes through wp_kses_post().
7911 echo wp_kses_post( get_option( 'formContent' ) );
7912 die();
7913 }
7914
7915 if ( $task === 'ebook_store_form_input' ) {
7916 ebook_store_save_form( isset( $_POST['md5_nonce'] ) ? wp_unslash( $_POST['md5_nonce'] ) : '', $_POST );
7917 }
7918 }
7919 /**
7920 * Persist the buyer details captured by the checkout form.
7921 *
7922 * Called from an unauthenticated request (the buyer has not logged in), so it
7923 * is deliberately narrow: a fixed filename shape, a size cap, a field cap, and
7924 * a rate limit so it cannot be used to fill the disk.
7925 *
7926 * @param string $md5_nonce Order token.
7927 * @param array $data Posted fields.
7928 */
7929 function ebook_store_save_form( $md5_nonce, $data ) {
7930 $filename = ebook_store_get_form_filename( $md5_nonce );
7931 $dir = ebook_store_get_form_data_dir();
7932
7933 if ( $filename === '' || $dir === '' ) {
7934 status_header( 400 );
7935 die( esc_html__( 'Invalid request.', 'ebook-store' ) );
7936 }
7937
7938 // One write per token per minute is plenty for a real checkout, and it stops
7939 // this endpoint being used as an unlimited disk-fill primitive.
7940 $rate_key = 'ebook_store_form_rl_' . md5( $filename );
7941 if ( get_transient( $rate_key ) ) {
7942 status_header( 429 );
7943 die( esc_html__( 'Please wait a moment and try again.', 'ebook-store' ) );
7944 }
7945 set_transient( $rate_key, 1, MINUTE_IN_SECONDS );
7946
7947 $json = array();
7948 $fields = 0;
7949 $skip = array( 'task', 'md5_nonce' );
7950
7951 foreach ( (array) $data as $k => $v ) {
7952 if ( ++$fields > 50 ) {
7953 break;
7954 }
7955 $key = sanitize_key( $k );
7956 if ( $key === '' || in_array( $key, $skip, true ) || ! is_scalar( $v ) ) {
7957 continue;
7958 }
7959 $json[ $key ] = sanitize_text_field( substr( (string) $v, 0, 500 ) );
7960 }
7961
7962 if ( file_put_contents( $dir . $filename, wp_json_encode( $json ), LOCK_EX ) === false ) {
7963 status_header( 500 );
7964 die( esc_html__( 'The form data could not be saved.', 'ebook-store' ) );
7965 }
7966
7967 status_header( 200 );
7968 die( esc_html__( 'Form data saved successfully.', 'ebook-store' ) );
7969 }
7970
7971 /**
7972 * Read back the buyer details captured at checkout.
7973 *
7974 * This used to read from get_temp_dir() with no '.json' suffix, i.e. a
7975 * different directory and a different filename than ebook_store_save_form()
7976 * writes, so it always returned false and every order fell back to
7977 * "Anonymous User".
7978 *
7979 * @param string $md5_nonce Order token.
7980 * @return object|false
7981 */
7982 function ebook_store_get_form( $md5_nonce ) {
7983 $filename = ebook_store_get_form_filename( $md5_nonce );
7984 $dir = ebook_store_get_form_data_dir();
7985
7986 if ( $filename === '' || $dir === '' ) {
7987 return false;
7988 }
7989
7990 $path = $dir . $filename;
7991 if ( ! is_readable( $path ) ) {
7992 return false;
7993 }
7994
7995 $form_data = file_get_contents( $path );
7996 if ( ! $form_data ) {
7997 return false;
7998 }
7999
8000 $decoded = json_decode( $form_data );
8001
8002 return $decoded === null ? false : $decoded;
8003 }
8004 function ebook_store_get_mailchimp_lists() {
8005 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8006 if ( $api_key === '' ) {
8007 return false;
8008 }
8009
8010 $response = ebook_store_mailchimp_request(
8011 'GET',
8012 '/lists',
8013 array(
8014 'count' => 1000,
8015 'fields' => 'lists.id,lists.name,total_items',
8016 )
8017 );
8018
8019 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8020 return false;
8021 }
8022
8023 return array_map(
8024 static function( $list ) {
8025 return (object) array(
8026 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8027 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8028 );
8029 },
8030 $response['lists']
8031 );
8032 }
8033 function ebook_store_get_brevo_lists() {
8034 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8035 if ( $api_key === '' ) {
8036 return false;
8037 }
8038
8039 $lists = array();
8040 $offset = 0;
8041 $limit = 50;
8042
8043 do {
8044 $response = ebook_store_brevo_request(
8045 'GET',
8046 '/contacts/lists',
8047 array(
8048 'limit' => $limit,
8049 'offset' => $offset,
8050 )
8051 );
8052
8053 if ( is_wp_error( $response ) || ! isset( $response['lists'] ) || ! is_array( $response['lists'] ) ) {
8054 return false;
8055 }
8056
8057 $batch = $response['lists'];
8058 $lists = array_merge( $lists, $batch );
8059 $offset += count( $batch );
8060 $total = isset( $response['count'] ) ? (int) $response['count'] : count( $lists );
8061 } while ( ! empty( $batch ) && $offset < $total );
8062
8063 return array_map(
8064 static function( $list ) {
8065 return (object) array(
8066 'id' => isset( $list['id'] ) ? (string) $list['id'] : '',
8067 'name' => isset( $list['name'] ) ? (string) $list['name'] : '',
8068 );
8069 },
8070 $lists
8071 );
8072 }
8073 function ebook_store_get_mailchimp_subscribe($email) {
8074 //error_log('MAILCHIMP Subscribing ' . $email);
8075 $list_id = trim( (string) get_option( 'mailchimp_lists' ) );
8076 $integration_enabled = (string) get_option( 'mailchimp_integration_enabled', $list_id !== '' ? '1' : '' );
8077 if ( $integration_enabled !== '1' ) {
8078 return false;
8079 }
8080
8081 if ( $list_id === '' ) {
8082 return false;
8083 }
8084
8085 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8086 if ( $api_key === '' ) {
8087 return false;
8088 }
8089
8090 $email = sanitize_email( (string) $email );
8091 if ( $email === '' ) {
8092 return false;
8093 }
8094
8095 $subscriber_hash = md5( strtolower( $email ) );
8096
8097 return ebook_store_mailchimp_request(
8098 'PUT',
8099 '/lists/' . rawurlencode( $list_id ) . '/members/' . $subscriber_hash,
8100 array(),
8101 array(
8102 'email_address' => $email,
8103 'status_if_new' => 'subscribed',
8104 'status' => 'subscribed',
8105 )
8106 );
8107 }
8108 function ebook_store_get_brevo_subscribe( $email ) {
8109 $list_id = trim( (string) get_option( 'brevo_lists' ) );
8110 $integration_enabled = (string) get_option( 'brevo_integration_enabled', $list_id !== '' ? '1' : '' );
8111 if ( $integration_enabled !== '1' ) {
8112 return false;
8113 }
8114
8115 if ( $list_id === '' || ! ctype_digit( $list_id ) ) {
8116 return false;
8117 }
8118
8119 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8120 if ( $api_key === '' ) {
8121 return false;
8122 }
8123
8124 $email = sanitize_email( (string) $email );
8125 if ( $email === '' ) {
8126 return false;
8127 }
8128
8129 return ebook_store_brevo_request(
8130 'POST',
8131 '/contacts',
8132 array(),
8133 array(
8134 'email' => $email,
8135 'listIds' => array( (int) $list_id ),
8136 'emailBlacklisted' => false,
8137 'smsBlacklisted' => false,
8138 'updateEnabled' => true,
8139 )
8140 );
8141 }
8142
8143 function ebook_store_subscribe_buyer_to_marketing_lists( $email ) {
8144 $email = sanitize_email( (string) $email );
8145 if ( $email === '' ) {
8146 return false;
8147 }
8148
8149 $results = array();
8150 $results['mailchimp'] = ebook_store_get_mailchimp_subscribe( $email );
8151 $results['brevo'] = ebook_store_get_brevo_subscribe( $email );
8152
8153 return $results;
8154 }
8155
8156 function ebook_store_get_mailchimp_server_prefix( $api_key ) {
8157 $api_key = trim( (string) $api_key );
8158 if ( $api_key === '' || strpos( $api_key, '-' ) === false ) {
8159 return '';
8160 }
8161
8162 $parts = explode( '-', $api_key );
8163 $server_prefix = strtolower( trim( (string) end( $parts ) ) );
8164
8165 return preg_match( '/^[a-z0-9]+$/', $server_prefix ) ? $server_prefix : '';
8166 }
8167
8168 function ebook_store_mailchimp_request( $method, $path, $query = array(), $body = null ) {
8169 $api_key = trim( (string) get_option( 'mailchimp_api_key' ) );
8170 if ( $api_key === '' ) {
8171 return new WP_Error( 'mailchimp_missing_api_key', 'Mailchimp API key not configured.' );
8172 }
8173
8174 $server_prefix = ebook_store_get_mailchimp_server_prefix( $api_key );
8175 if ( $server_prefix === '' ) {
8176 return new WP_Error( 'mailchimp_invalid_api_key', 'Mailchimp API key is missing a valid server prefix.' );
8177 }
8178
8179 $url = 'https://' . $server_prefix . '.api.mailchimp.com/3.0' . $path;
8180 if ( ! empty( $query ) ) {
8181 $url = add_query_arg( $query, $url );
8182 }
8183
8184 $args = array(
8185 'method' => strtoupper( (string) $method ),
8186 'timeout' => 20,
8187 'headers' => array(
8188 'Authorization' => 'Basic ' . base64_encode( 'anystring:' . $api_key ),
8189 'Accept' => 'application/json',
8190 ),
8191 );
8192
8193 if ( null !== $body ) {
8194 $args['headers']['Content-Type'] = 'application/json';
8195 $args['body'] = wp_json_encode( $body );
8196 }
8197
8198 $response = wp_remote_request( $url, $args );
8199 if ( is_wp_error( $response ) ) {
8200 return $response;
8201 }
8202
8203 $status_code = (int) wp_remote_retrieve_response_code( $response );
8204 $response_body = wp_remote_retrieve_body( $response );
8205 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8206
8207 if ( $status_code < 200 || $status_code >= 300 ) {
8208 $message = is_array( $data ) && ! empty( $data['detail'] ) ? (string) $data['detail'] : 'Mailchimp request failed.';
8209 return new WP_Error( 'mailchimp_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8210 }
8211
8212 return is_array( $data ) ? $data : array();
8213 }
8214 function ebook_store_brevo_request( $method, $path, $query = array(), $body = null ) {
8215 $api_key = trim( (string) get_option( 'brevo_api_key' ) );
8216 if ( $api_key === '' ) {
8217 return new WP_Error( 'brevo_missing_api_key', 'Brevo API key not configured.' );
8218 }
8219
8220 $url = 'https://api.brevo.com/v3' . $path;
8221 if ( ! empty( $query ) ) {
8222 $url = add_query_arg( $query, $url );
8223 }
8224
8225 $args = array(
8226 'method' => strtoupper( (string) $method ),
8227 'timeout' => 20,
8228 'headers' => array(
8229 'api-key' => $api_key,
8230 'Accept' => 'application/json',
8231 ),
8232 );
8233
8234 if ( null !== $body ) {
8235 $args['headers']['Content-Type'] = 'application/json';
8236 $args['body'] = wp_json_encode( $body );
8237 }
8238
8239 $response = wp_remote_request( $url, $args );
8240 if ( is_wp_error( $response ) ) {
8241 return $response;
8242 }
8243
8244 $status_code = (int) wp_remote_retrieve_response_code( $response );
8245 $response_body = wp_remote_retrieve_body( $response );
8246 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
8247
8248 if ( $status_code < 200 || $status_code >= 300 ) {
8249 $message = 'Brevo request failed.';
8250 if ( is_array( $data ) ) {
8251 if ( ! empty( $data['message'] ) ) {
8252 $message = (string) $data['message'];
8253 } elseif ( ! empty( $data['code'] ) ) {
8254 $message = (string) $data['code'];
8255 }
8256 }
8257
8258 return new WP_Error( 'brevo_api_error', $message, array( 'status' => $status_code, 'response' => $data ) );
8259 }
8260
8261 return is_array( $data ) ? $data : array();
8262 }
8263 function ebook_wp_embed_ebook() {
8264 wp_enqueue_script( 'ebook_store_settings', plugins_url( '/js/ebook_store_settings.js' , __FILE__ ), array(), '1.0.0', true );
8265 ?>
8266 <div class="ebook_store_embed_all_items" style=" display:inline-block; clear:both; max-height:200px; overflow-x:auto; width:100%;">
8267 <?php
8268 $new = new WP_Query('post_type=ebook');
8269 while ($new->have_posts()) : $new->the_post();
8270 @$ebooks_found++;
8271 $img_cover = get_post_meta(get_the_ID(), 'ebook_wp_custom_attachment_cover', true);
8272 $ebook = get_post_meta(get_the_ID(), 'ebook', true);
8273 $c = new Currencies();
8274 $price = $c->getSymbol(get_option('paypal_currency','USD')) . number_format((float)$ebook['ebook_price'],2)
8275 ?>
8276 <div class="ebook_store_embed_ebook_item" style="width:100%; clear:both;">
8277 <?php
8278 if (!is_array($img_cover)) {
8279 $img_cover = array();
8280 }
8281 if (@$img_cover['url'] != '') {
8282 ?><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;" />
8283 <?php
8284 } else {
8285 ?>
8286 <div style="background:gray; width:45px; height:65px; float: left; margin:10px;" onClick="ebook_store_embed_code(<?php echo get_the_ID(); ?>);"></div>
8287 <?php
8288 }
8289
8290 ?><span class="ebook_store_embed_ebook_item_title" style="margin-top: 11px;display: inline-block;">
8291 <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>
8292 <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>
8293 <br />Form: <input onClick="this.select()" readonly type="text" size="29" value='[ebook_store ebook_id="<?php echo get_the_ID(); ?>"]' />
8294 <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>
8295
8296 </span>
8297 <p>
8298 Price: <?php echo $price; ?> Download type: <?php echo $ebook['donate_or_download']; ?>
8299
8300 </p>
8301 </div>
8302 <?php
8303 endwhile;
8304 if (@$ebooks_found == 0) {
8305 echo "<p>You have not created any ebooks yet, please click <a href='post-new.php?post_type=ebook'>here</a> to start.</p>";
8306 }
8307 ?>
8308 </div>
8309 <?php
8310 }
8311 /**
8312 * Kept as a no-op shim: third-party code and older templates call this directly.
8313 * Registration now happens once in includes/ebook-store-assets.php and the
8314 * stylesheet is enqueued only where store markup is actually rendered.
8315 */
8316 function ebookstorestylesheet() {
8317 ebook_store_enqueue_frontend_styles();
8318 }
8319 function ebook_store_post_type_view( $content ) {
8320 // is_singular() asks about the main query, not about the post whose content
8321 // is being filtered. Without the in_the_loop()/is_main_query() guard, a
8322 // related-posts widget or an excerpt on a single-ebook page also had its
8323 // content replaced by the buy form.
8324 if ( ! in_the_loop() || ! is_main_query() ) {
8325 return $content;
8326 }
8327
8328 if ( get_post_type() !== 'ebook' ) {
8329 return $content;
8330 }
8331
8332 return ebook_store( get_the_ID() );
8333 }
8334 function ebook_download_links($ebook_order, $free = 0) {
8335 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
8336 $locale = ebook_store_get_locale_strings();
8337 $formats_locale = ebook_store_get_format_labels();
8338 if ($free == 0) {
8339 $action_name = 'download';
8340 } else {
8341 $action_name = 'download_free';
8342 }
8343 $ebook_id = $ebook_order['ebook'][0];
8344 $loop = array('mobi','txt','epub','zip', 'mp3', 'mp4');
8345 $meta = get_post_meta($ebook_id);
8346 $links = array();
8347 $post = get_post($ebook_order['ebook'][0]);
8348 @$slug = $post->post_name;
8349 foreach ($loop as $l) {
8350 if (@is_array($meta['ebook_wp_custom_attachment_' . $l])) {
8351 $book = unserialize($meta['ebook_wp_custom_attachment_' . $l][0]);
8352 $file = $book['file'];
8353 $ebook_key = (strlen(@$ebook_order['ebook_key'][0]) == 32 ? @$ebook_order['ebook_key'][0] : @$ebook_order['ebook_key_array'][0]);
8354 $order_id = isset( $ebook_order['order_id'][0] ) ? absint( $ebook_order['order_id'][0] ) : ( isset( $ebook_order['order_id'] ) ? absint( $ebook_order['order_id'] ) : 0 );
8355 $link_args = array(
8356 'format' => $l,
8357 'ebook_key' => $ebook_key,
8358 'action' => $action_name,
8359 'md5_nonce' => @$ebook_order['md5_nonce'][0],
8360 );
8361 if ( $order_id > 0 ) {
8362 $link_args['order_id'] = $order_id;
8363 }
8364
8365 $link = add_query_arg($link_args,get_permalink($ebook_order['ebook'][0]));
8366 $link = remove_query_arg('p',$link);
8367 $link = add_query_arg(array('ebook' => $slug),$link);
8368
8369 @$links[] = "<a href=\"$link\">" . $formats_locale[$l] . " Format</a>";
8370 }
8371 }
8372 if (count($links) == 0) {
8373 $links[] = 'No other file formats are available for this ebook.';
8374 }
8375 //error_log($link);
8376 return $links;
8377 }
8378 function ebook_download_links_bonus($ebook_order, $free = 0) {
8379 if (@is_array($ebook_order['ebook_key']) == false) {
8380 foreach ($ebook_order as $k => $v) {
8381 if (is_array($v)) {
8382 $v = $v[0];
8383 }
8384 @$new_order[$k][0] = $v;
8385 }
8386 $ebook_order = $new_order;
8387 }
8388 //error_log(__LINE__ . ' ' . print_r($ebook_order,true));
8389
8390 $ebook_id = $ebook_order['ebook'][0];
8391 $ebook_bonus = get_post_meta($ebook_id, 'ebook', true);
8392 $ebook_bonus = @$ebook_bonus['ebook_bonus'];
8393 $links = array();
8394 if (is_array($ebook_bonus)) {
8395 if (count($ebook_bonus) > 0) {
8396 foreach ($ebook_bonus as $ebook_id) {
8397 $post = get_post($ebook_id);
8398 $ebook_key = (strlen($ebook_order['ebook_key'][0]) == 32 ? $ebook_order['ebook_key'][0] : $ebook_order['ebook_key_array'][0]);
8399 // $link //= add_query_arg(array('ebook_key' => $ebook_key,'ebook_id' => $ebook_id,'action' => 'ebook_bonus_download', 'md5_nonce' => @$ebook_order['md5_nonce'][0]),get_permalink($ebook_id));
8400 $link = ebook_download_link($ebook_order, 0, 1, $ebook_id);
8401 $links[] = "<a href=\"$link\" target=\"_blank\">" . $post->post_title ."</a>";
8402 }
8403 }
8404
8405 }
8406 //$links[] = '<pre>'.print_r($ebook_order,true) . '</pre>';
8407 return $links;
8408 }
8409 function ebook_store_stats($ebook_id, $days) {
8410 $seconds = 86400 * $days;
8411 if ($seconds == 0) {
8412 $seconds = 157680000;
8413 }
8414 $args = array(
8415 'post_type' => 'ebook_order',
8416 'meta_query' => array(
8417 array(
8418 'key' => 'ebook',
8419 'value' => $ebook_id,
8420 )
8421 ),
8422 'date_query' => array(
8423 array(
8424 'column' => 'post_date_gmt',
8425 'after' => $seconds . ' seconds ago',
8426 ),
8427 ));
8428 // The Query
8429 $the_query = new WP_Query( $args );
8430 $out = 0;
8431 // The Loop
8432 if ( $the_query->have_posts() ) {
8433 // echo '<ul>';
8434 while ( $the_query->have_posts() ) {
8435 $the_query->the_post();
8436 $id = get_the_ID();
8437 //echo "$id<br/>";
8438 $meta = get_post_meta($id);
8439 //print_r($meta);
8440 $out += $meta['mc_gross'][0];
8441 // echo '<li>' . get_the_title() . '</li>';
8442 }
8443 // echo '</ul>';
8444 } else {
8445 // no posts found
8446 }
8447 /* Restore original Post Data */
8448 wp_reset_postdata();
8449 return $out;
8450 }
8451
8452 /**
8453 * Sales totals bucketed by date, computed in SQL.
8454 *
8455 * Both report functions used to run WP_Query with posts_per_page => -1 and then
8456 * call get_post_meta() three times per row inside the loop. Because the PayPal
8457 * IPN handler writes one meta row per field, a store with 10k orders hydrated
8458 * ~10k WP_Post objects plus several hundred thousand meta rows in a single
8459 * request. That does not get slow, it exhausts the memory limit and white-screens
8460 * the reports page — and the cache is flushed on every incoming order, so the
8461 * expensive path is the normal path on a busy store.
8462 *
8463 * @param int $ebook_id Limit to one ebook, or 0 for the whole store.
8464 * @param string $format 'Y-m-d' for daily buckets, 'Y-m' for monthly.
8465 * @param string $since Anything strtotime() understands, e.g. '30 days ago'.
8466 * @return array bucket => total.
8467 */
8468 function ebook_store_get_sales_buckets( $ebook_id, $format, $since ) {
8469 global $wpdb;
8470
8471 $ebook_id = absint( $ebook_id );
8472 $after = gmdate( 'Y-m-d H:i:s', strtotime( $since, current_time( 'timestamp', true ) ) );
8473
8474 // MySQL's DATE_FORMAT equivalents of the PHP formats used by the callers.
8475 $sql_format = $format === 'Y-m' ? '%Y-%m' : '%Y-%m-%d';
8476
8477 // Prefer the gateway's payment_date when present, fall back to the post date,
8478 // so an order recorded late still lands in the right bucket.
8479 $join = " INNER JOIN {$wpdb->postmeta} gross ON ( gross.post_id = p.ID AND gross.meta_key = 'mc_gross' )";
8480 $join .= " LEFT JOIN {$wpdb->postmeta} pdate ON ( pdate.post_id = p.ID AND pdate.meta_key = 'payment_date' )";
8481 $join .= " LEFT JOIN {$wpdb->postmeta} pstatus ON ( pstatus.post_id = p.ID AND pstatus.meta_key = 'payment_status' )";
8482
8483 $where = $wpdb->prepare( " WHERE p.post_type = 'ebook_order' AND p.post_status = 'publish' AND p.post_date_gmt >= %s", $after );
8484 $where .= " AND ( pstatus.meta_value IS NULL OR LOWER( pstatus.meta_value ) NOT IN ( 'refunded', 'reversed', 'canceled_reversal', 'denied', 'failed', 'voided', 'expired', 'partially_refunded' ) )";
8485
8486 if ( $ebook_id > 0 ) {
8487 $join .= " INNER JOIN {$wpdb->postmeta} ebook ON ( ebook.post_id = p.ID AND ebook.meta_key = 'ebook' )";
8488 $where .= $wpdb->prepare( ' AND ebook.meta_value = %d', $ebook_id );
8489 }
8490
8491 // payment_date is stored in whatever shape the gateway sent: PayPal uses
8492 // "11:51:05 Apr 02, 2026 PDT", the built-in checkout writes "04/02/2026 11:51:05".
8493 // Try both, and fall back to post_date_gmt, which is always well formed.
8494 $paid_at = "COALESCE(
8495 STR_TO_DATE( pdate.meta_value, '%H:%i:%s %b %d, %Y' ),
8496 STR_TO_DATE( pdate.meta_value, '%m/%d/%Y %H:%i:%s' ),
8497 p.post_date_gmt
8498 )";
8499
8500 $bucket = "DATE_FORMAT( {$paid_at}, '{$sql_format}' )";
8501
8502 $rows = $wpdb->get_results(
8503 "SELECT {$bucket} AS bucket, SUM( gross.meta_value + 0 ) AS total
8504 FROM {$wpdb->posts} p
8505 {$join}
8506 {$where}
8507 GROUP BY bucket",
8508 ARRAY_A
8509 );
8510
8511 $out = array();
8512 foreach ( (array) $rows as $row ) {
8513 if ( ! empty( $row['bucket'] ) ) {
8514 $out[ $row['bucket'] ] = (float) $row['total'];
8515 }
8516 }
8517
8518 return $out;
8519 }
8520
8521 function ebook_store_stats_report_30days( $ebook_id = 0 ) {
8522 $out = array();
8523
8524 for ( $i = 29; $i >= 0; $i-- ) {
8525 $timestamp = current_time( 'timestamp', true ) - ( DAY_IN_SECONDS * $i );
8526 $out[ gmdate( 'Y-m-d', $timestamp ) ] = 0;
8527 }
8528
8529 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m-d', '30 days ago' ) as $bucket => $total ) {
8530 $out[ $bucket ] = $total;
8531 }
8532
8533 return $out;
8534 }
8535
8536 function ebook_store_stats_report_2years( $ebook_id = 0 ) {
8537 $out = array();
8538
8539 for ( $i = 23; $i >= 0; $i-- ) {
8540 $timestamp = strtotime( gmdate( 'Y-m-01 00:00:00', current_time( 'timestamp', true ) ) . " -{$i} months" );
8541 $out[ gmdate( 'Y-m', $timestamp ) ] = 0;
8542 }
8543
8544 foreach ( ebook_store_get_sales_buckets( $ebook_id, 'Y-m', '24 months ago' ) as $bucket => $total ) {
8545 $out[ $bucket ] = $total;
8546 }
8547
8548 return $out;
8549 }
8550
8551 function ebook_store_get_reports_data( $ebook_id = 0 ) {
8552 $ebook_id = absint( $ebook_id );
8553 $cache_key = ebook_store_get_reports_cache_key( $ebook_id );
8554 $cached = get_transient( $cache_key );
8555 if ( false !== $cached && is_array( $cached ) ) {
8556 return $cached;
8557 }
8558
8559 global $wpdb;
8560
8561 $query = "
8562 SELECT
8563 p.ID,
8564 p.post_date_gmt,
8565 status.meta_value AS payment_status,
8566 gross.meta_value AS mc_gross,
8567 fee.meta_value AS mc_fee,
8568 currency.meta_value AS mc_currency,
8569 ebook.meta_value AS ebook_id,
8570 item.meta_value AS item_name,
8571 quantity.meta_value AS quantity,
8572 payment_type.meta_value AS payment_type,
8573 payer_email.meta_value AS payer_email
8574 FROM {$wpdb->posts} p
8575 LEFT JOIN {$wpdb->postmeta} status ON (p.ID = status.post_id AND status.meta_key = 'payment_status')
8576 LEFT JOIN {$wpdb->postmeta} gross ON (p.ID = gross.post_id AND gross.meta_key = 'mc_gross')
8577 LEFT JOIN {$wpdb->postmeta} fee ON (p.ID = fee.post_id AND fee.meta_key = 'mc_fee')
8578 LEFT JOIN {$wpdb->postmeta} currency ON (p.ID = currency.post_id AND currency.meta_key = 'mc_currency')
8579 LEFT JOIN {$wpdb->postmeta} ebook ON (p.ID = ebook.post_id AND ebook.meta_key = 'ebook')
8580 LEFT JOIN {$wpdb->postmeta} item ON (p.ID = item.post_id AND item.meta_key = 'item_name')
8581 LEFT JOIN {$wpdb->postmeta} quantity ON (p.ID = quantity.post_id AND quantity.meta_key = 'quantity')
8582 LEFT JOIN {$wpdb->postmeta} payment_type ON (p.ID = payment_type.post_id AND payment_type.meta_key = 'payment_type')
8583 LEFT JOIN {$wpdb->postmeta} payer_email ON (p.ID = payer_email.post_id AND payer_email.meta_key = 'payer_email')
8584 WHERE p.post_type = 'ebook_order'
8585 AND p.post_status = 'publish'
8586 ";
8587
8588 if ( $ebook_id > 0 ) {
8589 $query .= $wpdb->prepare( " AND ebook.meta_value = %d", $ebook_id );
8590 }
8591
8592 $query .= " ORDER BY p.post_date_gmt DESC";
8593
8594 $rows = $wpdb->get_results( $query, ARRAY_A );
8595
8596 $default_currency = ebook_store_get_store_currency();
8597 $currency = $default_currency;
8598 $gateway_totals = array();
8599 $status_totals = array();
8600 $currency_totals = array();
8601 $recent_orders = array();
8602 $top_ebooks = array();
8603 $item_totals = array();
8604 $currencies_seen = array();
8605 $now_gmt = current_time( 'timestamp', true );
8606 $last_24h_cutoff = $now_gmt - DAY_IN_SECONDS;
8607 $last_7d_cutoff = $now_gmt - ( 7 * DAY_IN_SECONDS );
8608 $summary = array(
8609 'total_sales' => 0.0,
8610 'total_profit' => 0.0,
8611 'last_24h_sales' => 0.0,
8612 'last_7d_sales' => 0.0,
8613 'total_orders' => count( $rows ),
8614 'reportable_orders' => 0,
8615 'paid_orders' => 0,
8616 'free_orders' => 0,
8617 'average_order' => 0.0,
8618 'latest_order_gmt' => '',
8619 'latest_order_id' => 0,
8620 'latest_order_amount' => 0.0,
8621 'currency' => $default_currency,
8622 'mixed_currencies' => false,
8623 'currency_count' => 0,
8624 'top_item' => array(
8625 'label' => __( 'No ebook sales yet', 'ebook-store' ),
8626 'units' => 0,
8627 'orders' => 0,
8628 'revenue' => 0.0,
8629 'ebook_id' => 0,
8630 ),
8631 );
8632
8633 foreach ( $rows as $row ) {
8634 $status = trim( (string) $row['payment_status'] );
8635 $status_key = $status !== '' ? sanitize_key( $status ) : 'unknown';
8636 if ( ! isset( $status_totals[ $status_key ] ) ) {
8637 $status_totals[ $status_key ] = array(
8638 'label' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
8639 'count' => 0,
8640 );
8641 }
8642 $status_totals[ $status_key ]['count']++;
8643
8644 if ( ebook_store_is_negative_order_status( $status ) ) {
8645 continue;
8646 }
8647
8648 $summary['reportable_orders']++;
8649
8650 $gross = isset( $row['mc_gross'] ) ? (float) $row['mc_gross'] : 0.0;
8651 $fee = isset( $row['mc_fee'] ) ? (float) $row['mc_fee'] : 0.0;
8652 $gateway = sanitize_key( (string) $row['payment_type'] );
8653 $gateway_label = $gateway !== '' ? ucfirst( str_replace( '_', ' ', $gateway ) ) : __( 'Manual / unknown', 'ebook-store' );
8654 $order_currency = trim( (string) $row['mc_currency'] );
8655 $order_currency = $order_currency !== '' ? $order_currency : $currency;
8656 $timestamp = ! empty( $row['post_date_gmt'] ) ? strtotime( $row['post_date_gmt'] . ' UTC' ) : false;
8657 $quantity = isset( $row['quantity'] ) && is_numeric( $row['quantity'] ) && (float) $row['quantity'] > 0 ? (float) $row['quantity'] : 1.0;
8658 $row_ebook_id = isset( $row['ebook_id'] ) ? absint( $row['ebook_id'] ) : 0;
8659 $item_name = trim( isset( $row['item_name'] ) ? (string) $row['item_name'] : '' );
8660
8661 $summary['total_sales'] += $gross;
8662 $summary['total_profit'] += ( $gross - $fee );
8663
8664 if ( $gross > 0 ) {
8665 $summary['paid_orders']++;
8666 } else {
8667 $summary['free_orders']++;
8668 }
8669
8670 if ( $order_currency !== '' ) {
8671 $currencies_seen[ $order_currency ] = true;
8672 if ( $summary['currency'] === '' || $summary['currency'] === $default_currency ) {
8673 $summary['currency'] = $order_currency;
8674 }
8675 $currency = $summary['currency'];
8676 }
8677
8678 if ( $timestamp ) {
8679 if ( $timestamp >= $last_24h_cutoff ) {
8680 $summary['last_24h_sales'] += $gross;
8681 }
8682 if ( $timestamp >= $last_7d_cutoff ) {
8683 $summary['last_7d_sales'] += $gross;
8684 }
8685 if ( $summary['latest_order_gmt'] === '' || strtotime( $summary['latest_order_gmt'] . ' UTC' ) < $timestamp ) {
8686 $summary['latest_order_gmt'] = gmdate( 'Y-m-d H:i:s', $timestamp );
8687 $summary['latest_order_id'] = (int) $row['ID'];
8688 $summary['latest_order_amount'] = $gross;
8689 }
8690 }
8691
8692 $item_key = $row_ebook_id > 0 ? 'ebook:' . $row_ebook_id : 'item:' . md5( $item_name !== '' ? $item_name : __( 'Unknown ebook', 'ebook-store' ) );
8693 if ( ! isset( $item_totals[ $item_key ] ) ) {
8694 $item_totals[ $item_key ] = array(
8695 'ebook_id' => $row_ebook_id,
8696 'title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : '',
8697 'orders' => 0,
8698 'total' => 0.0,
8699 'last_7d' => 0.0,
8700 'last_30d' => 0.0,
8701 'units' => 0.0,
8702 'edit_url' => $row_ebook_id > 0 ? get_edit_post_link( $row_ebook_id, 'raw' ) : '',
8703 );
8704 if ( $item_totals[ $item_key ]['title'] === '' ) {
8705 $item_totals[ $item_key ]['title'] = $item_name !== '' ? $item_name : __( 'Untitled ebook', 'ebook-store' );
8706 }
8707 }
8708
8709 $item_totals[ $item_key ]['orders']++;
8710 $item_totals[ $item_key ]['total'] += $gross;
8711 $item_totals[ $item_key ]['units'] += $quantity;
8712 if ( $timestamp && $timestamp >= $last_7d_cutoff ) {
8713 $item_totals[ $item_key ]['last_7d'] += $gross;
8714 }
8715 if ( $timestamp && $timestamp >= ( $now_gmt - ( 30 * DAY_IN_SECONDS ) ) ) {
8716 $item_totals[ $item_key ]['last_30d'] += $gross;
8717 }
8718
8719 if ( ! isset( $gateway_totals[ $gateway ?: 'manual' ] ) ) {
8720 $gateway_totals[ $gateway ?: 'manual' ] = array(
8721 'label' => $gateway_label,
8722 'count' => 0,
8723 'revenue' => 0.0,
8724 );
8725 }
8726
8727 $gateway_totals[ $gateway ?: 'manual' ]['count']++;
8728 $gateway_totals[ $gateway ?: 'manual' ]['revenue'] += $gross;
8729
8730 if ( ! isset( $currency_totals[ $order_currency ] ) ) {
8731 $currency_totals[ $order_currency ] = array(
8732 'label' => $order_currency,
8733 'count' => 0,
8734 'revenue' => 0.0,
8735 );
8736 }
8737
8738 $currency_totals[ $order_currency ]['count']++;
8739 $currency_totals[ $order_currency ]['revenue'] += $gross;
8740
8741 if ( count( $recent_orders ) < 8 ) {
8742 $recent_orders[] = array(
8743 'order_id' => (int) $row['ID'],
8744 'date_gmt' => (string) $row['post_date_gmt'],
8745 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
8746 'amount' => $gross,
8747 'fee' => $fee,
8748 'currency' => $order_currency,
8749 'email' => (string) $row['payer_email'],
8750 'gateway' => $gateway_label,
8751 'ebook_id' => $row_ebook_id,
8752 'ebook_title' => $row_ebook_id > 0 ? get_the_title( $row_ebook_id ) : ( (string) $row['item_name'] !== '' ? (string) $row['item_name'] : __( 'Unknown ebook', 'ebook-store' ) ),
8753 'edit_url' => get_edit_post_link( (int) $row['ID'], 'raw' ),
8754 );
8755 }
8756 }
8757
8758 if ( $summary['paid_orders'] > 0 ) {
8759 $summary['average_order'] = $summary['total_sales'] / $summary['paid_orders'];
8760 }
8761
8762 $summary['currency_count'] = count( $currencies_seen );
8763 $summary['mixed_currencies'] = $summary['currency_count'] > 1;
8764
8765 if ( ! empty( $item_totals ) ) {
8766 uasort(
8767 $item_totals,
8768 static function ( $left, $right ) {
8769 if ( $left['total'] === $right['total'] ) {
8770 return $right['orders'] <=> $left['orders'];
8771 }
8772 return $right['total'] <=> $left['total'];
8773 }
8774 );
8775 $top_ebooks = array_values( $item_totals );
8776 $summary['top_item'] = array(
8777 'label' => $top_ebooks[0]['title'],
8778 'units' => $top_ebooks[0]['units'],
8779 'orders' => $top_ebooks[0]['orders'],
8780 'revenue' => $top_ebooks[0]['total'],
8781 'ebook_id' => $top_ebooks[0]['ebook_id'],
8782 );
8783 if ( $ebook_id === 0 ) {
8784 $top_ebooks = array_slice( $top_ebooks, 0, 8 );
8785 }
8786 }
8787
8788 uasort(
8789 $gateway_totals,
8790 static function ( $left, $right ) {
8791 if ( $left['revenue'] === $right['revenue'] ) {
8792 return $right['count'] <=> $left['count'];
8793 }
8794 return $right['revenue'] <=> $left['revenue'];
8795 }
8796 );
8797
8798 uasort(
8799 $status_totals,
8800 static function ( $left, $right ) {
8801 return $right['count'] <=> $left['count'];
8802 }
8803 );
8804
8805 uasort(
8806 $currency_totals,
8807 static function ( $left, $right ) {
8808 return $right['revenue'] <=> $left['revenue'];
8809 }
8810 );
8811
8812 $daily = ebook_store_stats_report_30days( $ebook_id );
8813 $monthly = ebook_store_stats_report_2years( $ebook_id );
8814 if ( is_array( $daily ) ) {
8815 ksort( $daily );
8816 }
8817 if ( is_array( $monthly ) ) {
8818 ksort( $monthly );
8819 }
8820
8821 $data = array(
8822 'summary' => $summary,
8823 'currency' => $currency,
8824 'ebook_id' => $ebook_id,
8825 'ebook_title' => $ebook_id > 0 ? get_the_title( $ebook_id ) : '',
8826 'daily' => is_array( $daily ) ? $daily : array(),
8827 'monthly' => is_array( $monthly ) ? $monthly : array(),
8828 'gateways' => array_values( $gateway_totals ),
8829 'statuses' => array_values( $status_totals ),
8830 'currencies' => array_values( $currency_totals ),
8831 'top_ebooks' => $top_ebooks,
8832 'recent_orders' => $recent_orders,
8833 );
8834
8835 set_transient( $cache_key, $data, 10 * MINUTE_IN_SECONDS );
8836
8837 return $data;
8838 }
8839
8840 function ebook_store_get_setup_wizard_url( $args = array() ) {
8841 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-setup-wizard' );
8842
8843 if ( ! empty( $args ) ) {
8844 $url = add_query_arg( $args, $url );
8845 }
8846
8847 return $url;
8848 }
8849
8850 function ebook_store_get_template_editor_url( $args = array() ) {
8851 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-template-editor' );
8852
8853 if ( ! empty( $args ) ) {
8854 $url = add_query_arg( $args, $url );
8855 }
8856
8857 return $url;
8858 }
8859
8860 function ebook_store_get_add_issue_url( $args = array() ) {
8861 $url = admin_url( 'edit.php?post_type=ebook&page=ebook-store-add-issue-page' );
8862
8863 if ( ! empty( $args ) ) {
8864 $url = add_query_arg( $args, $url );
8865 }
8866
8867 return $url;
8868 }
8869
8870 function ebook_store_sanitize_issue_purchase_period( $raw_period, $default = '1 year' ) {
8871 $period = strtolower( trim( preg_replace( '/[^0-9a-z\s]/i', '', (string) $raw_period ) ) );
8872
8873 if ( $period === '' ) {
8874 $period = $default;
8875 }
8876
8877 $timestamp = strtotime( $period . ' ago', current_time( 'timestamp', true ) );
8878 if ( false === $timestamp ) {
8879 $period = $default;
8880 }
8881
8882 return $period;
8883 }
8884
8885 function ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period ) {
8886 $source_ebook_id = absint( $source_ebook_id );
8887 $target_ebook_id = absint( $target_ebook_id );
8888 $purchase_period = ebook_store_sanitize_issue_purchase_period( $purchase_period );
8889 $cutoff_timestamp = strtotime( $purchase_period . ' ago', current_time( 'timestamp', true ) );
8890
8891 $result = array(
8892 'purchase_period' => $purchase_period,
8893 'cutoff_timestamp' => $cutoff_timestamp,
8894 'orders' => array(),
8895 'eligible' => array(),
8896 'skipped_existing' => array(),
8897 'skipped_invalid' => array(),
8898 'skipped_negative' => array(),
8899 'counts' => array(
8900 'total' => 0,
8901 'eligible' => 0,
8902 'skipped_existing' => 0,
8903 'skipped_invalid' => 0,
8904 'skipped_negative' => 0,
8905 ),
8906 );
8907
8908 if ( $source_ebook_id < 1 ) {
8909 return $result;
8910 }
8911
8912 $query = new WP_Query(
8913 array(
8914 'post_type' => 'ebook_order',
8915 'post_status' => 'publish',
8916 'posts_per_page' => -1,
8917 'orderby' => 'date',
8918 'order' => 'DESC',
8919 'meta_query' => array(
8920 'relation' => 'AND',
8921 array(
8922 'key' => 'ebook',
8923 'value' => $source_ebook_id,
8924 ),
8925 array(
8926 'relation' => 'OR',
8927 array(
8928 'key' => 'issue_delivery_kind',
8929 'compare' => 'NOT EXISTS',
8930 ),
8931 array(
8932 'key' => 'issue_delivery_kind',
8933 'value' => 'new_issue',
8934 'compare' => '!=',
8935 ),
8936 ),
8937 ),
8938 'date_query' => array(
8939 array(
8940 'column' => 'post_date_gmt',
8941 'after' => gmdate( 'Y-m-d H:i:s', $cutoff_timestamp ),
8942 'inclusive' => true,
8943 ),
8944 ),
8945 )
8946 );
8947
8948 $orders = array();
8949 $source_order_ids = array();
8950
8951 if ( $query->have_posts() ) {
8952 while ( $query->have_posts() ) {
8953 $query->the_post();
8954 $order_id = get_the_ID();
8955 $status = (string) get_post_meta( $order_id, 'payment_status', true );
8956 $email = sanitize_email( (string) get_post_meta( $order_id, 'payer_email', true ) );
8957 $item_name = (string) get_post_meta( $order_id, 'item_name', true );
8958 $order_date = (string) get_post_meta( $order_id, 'payment_date', true );
8959 $order_timestamp = $order_date !== '' ? strtotime( $order_date ) : false;
8960 if ( ! $order_timestamp ) {
8961 $order_timestamp = strtotime( get_post_field( 'post_date_gmt', $order_id ) . ' UTC' );
8962 }
8963
8964 $order = array(
8965 'source_order_id' => $order_id,
8966 'first_name' => (string) get_post_meta( $order_id, 'first_name', true ),
8967 'last_name' => (string) get_post_meta( $order_id, 'last_name', true ),
8968 'payer_email' => $email,
8969 'user_id' => absint( get_post_meta( $order_id, 'user_id', true ) ),
8970 'residence_country' => (string) get_post_meta( $order_id, 'residence_country', true ),
8971 'status' => $status !== '' ? $status : __( 'Unknown', 'ebook-store' ),
8972 'item_name' => $item_name !== '' ? $item_name : get_the_title( $source_ebook_id ),
8973 'order_date' => $order_date,
8974 'order_timestamp' => $order_timestamp,
8975 'order_edit_url' => get_edit_post_link( $order_id, 'raw' ),
8976 'already_sent' => false,
8977 'existing_issue_order_id' => 0,
8978 );
8979
8980 $orders[ $order_id ] = $order;
8981 $source_order_ids[] = $order_id;
8982 }
8983 }
8984 wp_reset_postdata();
8985
8986 $existing_map = array();
8987 if ( $target_ebook_id > 0 && ! empty( $source_order_ids ) ) {
8988 $existing_query = new WP_Query(
8989 array(
8990 'post_type' => 'ebook_order',
8991 'post_status' => 'publish',
8992 'posts_per_page' => -1,
8993 'fields' => 'ids',
8994 'meta_query' => array(
8995 array(
8996 'key' => 'ebook',
8997 'value' => $target_ebook_id,
8998 ),
8999 array(
9000 'key' => 'issue_delivery_kind',
9001 'value' => 'new_issue',
9002 ),
9003 array(
9004 'key' => 'issue_source_order_id',
9005 'value' => $source_order_ids,
9006 'compare' => 'IN',
9007 ),
9008 ),
9009 )
9010 );
9011
9012 if ( $existing_query->have_posts() ) {
9013 foreach ( $existing_query->posts as $issue_order_id ) {
9014 $source_order_id = absint( get_post_meta( $issue_order_id, 'issue_source_order_id', true ) );
9015 if ( $source_order_id > 0 ) {
9016 $existing_map[ $source_order_id ] = (int) $issue_order_id;
9017 }
9018 }
9019 }
9020 wp_reset_postdata();
9021 }
9022
9023 foreach ( $orders as $order_id => $order ) {
9024 $result['counts']['total']++;
9025
9026 if ( ebook_store_is_negative_order_status( $order['status'] ) ) {
9027 $result['skipped_negative'][] = $order;
9028 $result['counts']['skipped_negative']++;
9029 continue;
9030 }
9031
9032 if ( $order['payer_email'] === '' ) {
9033 $result['skipped_invalid'][] = $order;
9034 $result['counts']['skipped_invalid']++;
9035 continue;
9036 }
9037
9038 if ( isset( $existing_map[ $order_id ] ) ) {
9039 $order['already_sent'] = true;
9040 $order['existing_issue_order_id'] = $existing_map[ $order_id ];
9041 $result['skipped_existing'][] = $order;
9042 $result['counts']['skipped_existing']++;
9043 continue;
9044 }
9045
9046 $result['eligible'][] = $order;
9047 $result['counts']['eligible']++;
9048 }
9049
9050 $result['orders'] = array_values( $orders );
9051
9052 return $result;
9053 }
9054
9055 function ebook_store_process_new_issue_batch( $source_ebook_id, $target_ebook_id, $purchase_period, $offset = 0, $limit = 20 ) {
9056 $source_ebook_id = absint( $source_ebook_id );
9057 $target_ebook_id = absint( $target_ebook_id );
9058 $offset = max( 0, absint( $offset ) );
9059 $limit = max( 1, absint( $limit ) );
9060 $preview = ebook_store_get_issue_candidates( $source_ebook_id, $target_ebook_id, $purchase_period );
9061 $eligible = isset( $preview['eligible'] ) ? $preview['eligible'] : array();
9062 $batch = array_slice( $eligible, $offset, $limit );
9063 $created = array();
9064 $failed = array();
9065
9066 foreach ( $batch as $order ) {
9067 $new_order_data = array(
9068 'first_name' => $order['first_name'],
9069 'last_name' => $order['last_name'],
9070 'mc_gross' => 0,
9071 'ebook' => $target_ebook_id,
9072 'payer_email' => $order['payer_email'],
9073 'mc_fee' => 0,
9074 'item_name' => get_the_title( $target_ebook_id ),
9075 'payment_date' => date( 'm/d/Y H:i:s' ),
9076 'txn_id' => 'Issue Delivery',
9077 'residence_country' => $order['residence_country'],
9078 'md5_nonce' => md5( microtime() . mt_rand( 1, 99999999 ) . NONCE_KEY ),
9079 'user_id' => isset( $order['user_id'] ) ? $order['user_id'] : 0,
9080 'payment_status' => 'Completed',
9081 'payment_type' => 'new_issue',
9082 );
9083
9084 $created_order_id = ebook_store_add_order( $new_order_data, true, false );
9085
9086 if ( $created_order_id ) {
9087 update_post_meta( $created_order_id, 'issue_delivery_kind', 'new_issue' );
9088 update_post_meta( $created_order_id, 'issue_source_order_id', $order['source_order_id'] );
9089 update_post_meta( $created_order_id, 'issue_source_ebook_id', $source_ebook_id );
9090 update_post_meta( $created_order_id, 'issue_target_ebook_id', $target_ebook_id );
9091 update_post_meta( $created_order_id, 'issue_purchase_period', $preview['purchase_period'] );
9092 $created[] = array(
9093 'new_order_id' => $created_order_id,
9094 'source_order_id' => $order['source_order_id'],
9095 'payer_email' => $order['payer_email'],
9096 );
9097 } else {
9098 $failed[] = $order;
9099 }
9100 }
9101
9102 return array(
9103 'preview' => $preview,
9104 'offset' => $offset,
9105 'limit' => $limit,
9106 'processed' => count( $batch ),
9107 'created' => $created,
9108 'failed' => $failed,
9109 'next_offset' => $offset + count( $batch ),
9110 'completed' => ( $offset + count( $batch ) ) >= count( $eligible ),
9111 );
9112 }
9113
9114 function ebook_store_is_wordfence_active() {
9115 if ( ! function_exists( 'is_plugin_active' ) ) {
9116 require_once ABSPATH . 'wp-admin/includes/plugin.php';
9117 }
9118
9119 return function_exists( 'is_plugin_active' ) && is_plugin_active( 'wordfence/wordfence.php' );
9120 }
9121
9122 function ebook_store_get_template_directory_path() {
9123 return trailingslashit( plugin_dir_path( __FILE__ ) ) . 'templates/';
9124 }
9125
9126 function ebook_store_get_legacy_custom_template_directory_path() {
9127 return ebook_store_get_template_directory_path();
9128 }
9129
9130 function ebook_store_get_custom_template_directory_path() {
9131 $uploads = wp_upload_dir();
9132 if ( ! empty( $uploads['basedir'] ) ) {
9133 return trailingslashit( $uploads['basedir'] ) . 'ebook-store/templates/';
9134 }
9135
9136 return trailingslashit( WP_CONTENT_DIR ) . 'uploads/ebook-store/templates/';
9137 }
9138
9139 function ebook_store_ensure_custom_template_directory() {
9140 $directory = ebook_store_get_custom_template_directory_path();
9141 if ( is_dir( $directory ) ) {
9142 return $directory;
9143 }
9144
9145 if ( wp_mkdir_p( $directory ) ) {
9146 return $directory;
9147 }
9148
9149 return '';
9150 }
9151
9152 function ebook_store_sync_custom_template_name( $template_code, $template_name ) {
9153 $template_code = (string) $template_code;
9154 $template_name = trim( (string) $template_name );
9155 if ( $template_name === '' ) {
9156 return $template_code;
9157 }
9158
9159 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
9160 return (string) preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
9161 }
9162
9163 return "/*\nTemplate Name: " . $template_name . "\n*/\n" . $template_code;
9164 }
9165
9166 function ebook_store_get_unique_custom_template_identity( $preferred_name, $preferred_key = '' ) {
9167 $preferred_name = trim( (string) $preferred_name );
9168 $preferred_key = sanitize_key( (string) $preferred_key );
9169 if ( $preferred_name === '' ) {
9170 $preferred_name = __( 'Custom template', 'ebook-store' );
9171 }
9172 if ( $preferred_key === '' ) {
9173 $preferred_key = sanitize_title( $preferred_name );
9174 }
9175 if ( $preferred_key === '' ) {
9176 $preferred_key = 'custom-template';
9177 }
9178
9179 $editable = ebook_store_get_template_editor_files();
9180 $directory = ebook_store_get_custom_template_directory_path();
9181 $name = $preferred_name;
9182 $key = $preferred_key;
9183 $index = 2;
9184
9185 while ( isset( $editable[ $key ] ) || file_exists( $directory . $key . '.php' ) ) {
9186 $key = $preferred_key . '-' . $index;
9187 $name = $preferred_name . ' ' . $index;
9188 $index++;
9189 }
9190
9191 return array(
9192 'name' => $name,
9193 'key' => $key,
9194 );
9195 }
9196
9197 function ebook_store_create_custom_template_from_code( $template_name, $template_code, $preferred_key = '' ) {
9198 $directory = ebook_store_ensure_custom_template_directory();
9199 if ( $directory === '' ) {
9200 return new WP_Error( 'ebook_store_template_directory_unavailable', __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) );
9201 }
9202
9203 $identity = ebook_store_get_unique_custom_template_identity( $template_name, $preferred_key );
9204 $template_file = $directory . $identity['key'] . '.php';
9205 $template_code = ebook_store_sync_custom_template_name( $template_code, $identity['name'] );
9206 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
9207 if ( is_wp_error( $syntax_check ) ) {
9208 return $syntax_check;
9209 }
9210
9211 $result = @file_put_contents( $template_file, $template_code );
9212 if ( $result === false ) {
9213 return new WP_Error( 'ebook_store_template_create_failed', __( 'WordPress could not save the custom template file.', 'ebook-store' ) );
9214 }
9215
9216 return array(
9217 'key' => $identity['key'],
9218 'name' => $identity['name'],
9219 'file' => $template_file,
9220 'code' => $template_code,
9221 );
9222 }
9223
9224 function ebook_store_migrate_legacy_custom_templates() {
9225 static $migrated = false;
9226 if ( $migrated ) {
9227 return;
9228 }
9229 $migrated = true;
9230
9231 $legacy_dir = ebook_store_get_legacy_custom_template_directory_path();
9232 $custom_dir = ebook_store_ensure_custom_template_directory();
9233 if ( $custom_dir === '' || ! is_dir( $legacy_dir ) ) {
9234 return;
9235 }
9236
9237 $core_files = array_map( 'basename', wp_list_pluck( ebook_store_get_core_form_templates(), 'file' ) );
9238 foreach ( (array) glob( $legacy_dir . '*.php' ) as $file ) {
9239 $basename = basename( $file );
9240 if ( in_array( $basename, $core_files, true ) ) {
9241 continue;
9242 }
9243
9244 $destination = $custom_dir . $basename;
9245 if ( file_exists( $destination ) ) {
9246 continue;
9247 }
9248
9249 @copy( $file, $destination );
9250 }
9251 }
9252
9253 function ebook_store_get_core_form_templates() {
9254 return array(
9255 'modern' => array(
9256 'label' => __( 'Modern', 'ebook-store' ),
9257 'file' => ebook_store_get_template_directory_path() . 'modern.php',
9258 'core' => true,
9259 ),
9260 '2026' => array(
9261 'label' => __( '2026', 'ebook-store' ),
9262 'file' => ebook_store_get_template_directory_path() . '2026.php',
9263 'core' => true,
9264 ),
9265 'hello-elementor-2026' => array(
9266 'label' => __( 'Hello Elementor 2026', 'ebook-store' ),
9267 'file' => ebook_store_get_template_directory_path() . 'hello-elementor-2026.php',
9268 'core' => true,
9269 ),
9270 'legacy' => array(
9271 'label' => __( 'Legacy', 'ebook-store' ),
9272 'file' => ebook_store_get_template_directory_path() . 'current.php',
9273 'core' => true,
9274 ),
9275 );
9276 }
9277
9278 function ebook_store_get_core_template_keys() {
9279 return array_keys( ebook_store_get_core_form_templates() );
9280 }
9281
9282 function ebook_store_template_editor_humanize_key( $key ) {
9283 $key = trim( (string) $key );
9284 if ( $key === '' ) {
9285 return __( 'Custom template', 'ebook-store' );
9286 }
9287
9288 return ucwords( str_replace( array( '-', '_' ), ' ', $key ) );
9289 }
9290
9291 function ebook_store_can_access_template_editor() {
9292 if ( is_multisite() ) {
9293 return is_super_admin();
9294 }
9295
9296 return current_user_can( 'manage_options' );
9297 }
9298
9299 function ebook_store_get_template_header_label( $file ) {
9300 if ( ! file_exists( $file ) ) {
9301 return '';
9302 }
9303
9304 $contents = (string) @file_get_contents( $file, false, null, 0, 2048 );
9305 if ( $contents === '' ) {
9306 return '';
9307 }
9308
9309 if ( preg_match( '/Template Name:\s*(.+)$/mi', $contents, $matches ) ) {
9310 return trim( $matches[1] );
9311 }
9312
9313 return '';
9314 }
9315
9316 function ebook_store_get_custom_form_templates() {
9317 ebook_store_migrate_legacy_custom_templates();
9318 $template_dir = ebook_store_get_custom_template_directory_path();
9319 $core_templates = ebook_store_get_core_form_templates();
9320 $core_files = array_map( 'basename', wp_list_pluck( $core_templates, 'file' ) );
9321 $templates = array();
9322
9323 foreach ( (array) glob( $template_dir . '*.php' ) as $file ) {
9324 $basename = basename( $file );
9325 if ( in_array( $basename, $core_files, true ) ) {
9326 continue;
9327 }
9328
9329 $key = sanitize_key( basename( $basename, '.php' ) );
9330 if ( $key === '' ) {
9331 continue;
9332 }
9333
9334 $label = ebook_store_get_template_header_label( $file );
9335 if ( $label === '' ) {
9336 $label = ebook_store_template_editor_humanize_key( $key );
9337 }
9338
9339 $templates[ $key ] = array(
9340 'label' => $label,
9341 'file' => $file,
9342 'core' => false,
9343 );
9344 }
9345
9346 ksort( $templates );
9347
9348 return $templates;
9349 }
9350
9351 function ebook_store_get_template_usage_stats( $template_key ) {
9352 $template_key = sanitize_key( (string) $template_key );
9353 $stats = array(
9354 'ebooks' => 0,
9355 'default' => false,
9356 );
9357
9358 if ( $template_key === '' ) {
9359 return $stats;
9360 }
9361
9362 $ebook_ids = get_posts(
9363 array(
9364 'post_type' => 'ebook',
9365 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
9366 'posts_per_page' => -1,
9367 'fields' => 'ids',
9368 'no_found_rows' => true,
9369 )
9370 );
9371
9372 foreach ( (array) $ebook_ids as $ebook_id ) {
9373 $ebook_meta = get_post_meta( (int) $ebook_id, 'ebook', true );
9374 $current_template = '';
9375 if ( is_array( $ebook_meta ) && ! empty( $ebook_meta['form_template'] ) ) {
9376 $current_template = (string) $ebook_meta['form_template'];
9377 }
9378
9379 if ( function_exists( 'ebook_store_resolve_form_template' ) && ebook_store_resolve_form_template( $current_template ) === $template_key ) {
9380 $stats['ebooks']++;
9381 }
9382 }
9383
9384 $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' ) );
9385 $stats['default'] = $default_template === $template_key;
9386
9387 return $stats;
9388 }
9389
9390 function ebook_store_reassign_template_usage( $from_template, $to_template ) {
9391 $from_template = sanitize_key( (string) $from_template );
9392 $to_template = sanitize_key( (string) $to_template );
9393 $result = array(
9394 'ebook_count' => 0,
9395 'default_updated' => false,
9396 );
9397
9398 if ( $from_template === '' || $to_template === '' || $from_template === $to_template ) {
9399 return $result;
9400 }
9401
9402 $ebook_ids = get_posts(
9403 array(
9404 'post_type' => 'ebook',
9405 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
9406 'posts_per_page' => -1,
9407 'fields' => 'ids',
9408 'no_found_rows' => true,
9409 )
9410 );
9411
9412 foreach ( (array) $ebook_ids as $ebook_id ) {
9413 $ebook_id = (int) $ebook_id;
9414 $ebook_meta = get_post_meta( $ebook_id, 'ebook', true );
9415 if ( ! is_array( $ebook_meta ) ) {
9416 $ebook_meta = array();
9417 }
9418
9419 $current_template = isset( $ebook_meta['form_template'] ) ? (string) $ebook_meta['form_template'] : '';
9420 $resolved_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( $current_template ) : sanitize_key( $current_template );
9421 if ( $resolved_template !== $from_template ) {
9422 continue;
9423 }
9424
9425 $ebook_meta['form_template'] = $to_template;
9426 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
9427 $result['ebook_count']++;
9428 }
9429
9430 $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' ) );
9431 if ( $default_template === $from_template ) {
9432 update_option( 'ebook_store_form_template', $to_template );
9433 $result['default_updated'] = true;
9434 }
9435
9436 return $result;
9437 }
9438
9439 function ebook_store_is_core_template( $template_key ) {
9440 return in_array( sanitize_key( (string) $template_key ), ebook_store_get_core_template_keys(), true );
9441 }
9442
9443 function ebook_store_get_template_editor_files() {
9444 $templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
9445 $editable = array();
9446
9447 foreach ( $templates as $template_key => $template ) {
9448 if ( empty( $template['file'] ) || ! file_exists( $template['file'] ) ) {
9449 continue;
9450 }
9451
9452 $editable[ $template_key ] = array(
9453 'key' => (string) $template_key,
9454 'label' => isset( $template['label'] ) ? $template['label'] : ucfirst( $template_key ),
9455 'file' => $template['file'],
9456 'filename' => basename( $template['file'] ),
9457 'core' => ! empty( $template['core'] ),
9458 );
9459 }
9460
9461 return $editable;
9462 }
9463
9464 function ebook_store_get_template_editor_current_key( $requested = '' ) {
9465 $requested = sanitize_key( (string) $requested );
9466 $editable = ebook_store_get_template_editor_files();
9467
9468 if ( $requested !== '' && isset( $editable[ $requested ] ) ) {
9469 return $requested;
9470 }
9471
9472 $keys = array_keys( $editable );
9473
9474 return ! empty( $keys ) ? $keys[0] : '';
9475 }
9476
9477 function ebook_store_get_empty_template_scaffold( $template_name ) {
9478 $template_name = trim( (string) $template_name );
9479
9480 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";
9481 }
9482
9483 function ebook_store_validate_template_php_syntax( $template_code ) {
9484 $template_code = (string) $template_code;
9485
9486 if ( trim( $template_code ) === '' ) {
9487 return new WP_Error( 'ebook_store_template_empty', __( 'Template code cannot be empty.', 'ebook-store' ) );
9488 }
9489
9490 if ( strpos( $template_code, '<?php' ) === false ) {
9491 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' ) );
9492 }
9493
9494 if ( ! function_exists( 'shell_exec' ) ) {
9495 return true;
9496 }
9497
9498 $php_binary = defined( 'PHP_BINARY' ) && PHP_BINARY ? PHP_BINARY : '';
9499 if ( ! $php_binary || ! is_executable( $php_binary ) ) {
9500 $php_binary = trim( (string) @shell_exec( 'command -v php 2>/dev/null' ) );
9501 }
9502
9503 if ( $php_binary === '' ) {
9504 return true;
9505 }
9506
9507 $temp_file = wp_tempnam( 'ebook-store-template-lint' );
9508 if ( ! $temp_file ) {
9509 return true;
9510 }
9511
9512 @file_put_contents( $temp_file, $template_code );
9513 $command = escapeshellarg( $php_binary ) . ' -l ' . escapeshellarg( $temp_file ) . ' 2>&1';
9514 $output = @shell_exec( $command );
9515 @unlink( $temp_file );
9516
9517 if ( ! is_string( $output ) ) {
9518 return true;
9519 }
9520
9521 if ( stripos( $output, 'No syntax errors detected' ) !== false ) {
9522 return true;
9523 }
9524
9525 $message = trim( preg_replace( '/\s+in\s+.+?on line\s+/i', ' on line ', $output ) );
9526 if ( $message === '' ) {
9527 $message = __( 'PHP syntax validation failed for the template code.', 'ebook-store' );
9528 }
9529
9530 return new WP_Error( 'ebook_store_template_syntax_error', $message );
9531 }
9532
9533 function ebook_store_template_editor_create_template() {
9534 $template_name = isset( $_POST['new_template_name'] ) ? sanitize_text_field( wp_unslash( $_POST['new_template_name'] ) ) : '';
9535 $template_source = isset( $_POST['new_template_source'] ) ? sanitize_key( wp_unslash( $_POST['new_template_source'] ) ) : '';
9536 $template_key = sanitize_title( $template_name );
9537 $editable = ebook_store_get_template_editor_files();
9538
9539 if ( $template_name === '' ) {
9540 return array(
9541 'errors' => array( __( 'Please enter a template name.', 'ebook-store' ) ),
9542 );
9543 }
9544
9545 if ( $template_key === '' ) {
9546 return array(
9547 'errors' => array( __( 'The template name must contain letters or numbers.', 'ebook-store' ) ),
9548 );
9549 }
9550
9551 if ( isset( $editable[ $template_key ] ) ) {
9552 return array(
9553 'errors' => array( __( 'A template with that name already exists.', 'ebook-store' ) ),
9554 );
9555 }
9556
9557 $template_dir = ebook_store_ensure_custom_template_directory();
9558 if ( $template_dir === '' ) {
9559 return array(
9560 'errors' => array( __( 'WordPress could not create the custom template storage folder inside uploads.', 'ebook-store' ) ),
9561 );
9562 }
9563
9564 $template_file = $template_dir . $template_key . '.php';
9565 if ( file_exists( $template_file ) ) {
9566 return array(
9567 'errors' => array( __( 'A template file with that slug already exists.', 'ebook-store' ) ),
9568 );
9569 }
9570
9571 $template_code = '';
9572 if ( $template_source !== '' && $template_source !== 'none' && isset( $editable[ $template_source ] ) && file_exists( $editable[ $template_source ]['file'] ) ) {
9573 $template_code = (string) file_get_contents( $editable[ $template_source ]['file'] );
9574 if ( preg_match( '/Template Name:\s*(.+)$/mi', $template_code ) ) {
9575 $template_code = preg_replace( '/Template Name:\s*(.+)$/mi', 'Template Name: ' . $template_name, $template_code, 1 );
9576 }
9577 } else {
9578 $template_code = ebook_store_get_empty_template_scaffold( $template_name );
9579 }
9580
9581 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
9582 if ( is_wp_error( $syntax_check ) ) {
9583 return array(
9584 'errors' => array( $syntax_check->get_error_message() ),
9585 );
9586 }
9587
9588 $result = @file_put_contents( $template_file, $template_code );
9589 if ( $result === false ) {
9590 return array(
9591 'errors' => array( __( 'WordPress could not create the new template file.', 'ebook-store' ) ),
9592 );
9593 }
9594
9595 wp_safe_redirect(
9596 ebook_store_get_template_editor_url(
9597 array(
9598 'template' => $template_key,
9599 'created' => 1,
9600 )
9601 )
9602 );
9603 exit;
9604 }
9605
9606 function ebook_store_template_editor_delete_template() {
9607 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
9608 $editable = ebook_store_get_template_editor_files();
9609
9610 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
9611 return array(
9612 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
9613 );
9614 }
9615
9616 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
9617 return array(
9618 'errors' => array( __( 'Built-in templates cannot be deleted.', 'ebook-store' ) ),
9619 'template_key' => $template_key,
9620 );
9621 }
9622
9623 if ( ! file_exists( $editable[ $template_key ]['file'] ) || ! is_writable( $editable[ $template_key ]['file'] ) ) {
9624 return array(
9625 'errors' => array( __( 'That template file could not be deleted because it is missing or not writable.', 'ebook-store' ) ),
9626 'template_key' => $template_key,
9627 );
9628 }
9629
9630 $result = @unlink( $editable[ $template_key ]['file'] );
9631 if ( ! $result ) {
9632 return array(
9633 'errors' => array( __( 'WordPress could not delete that template file.', 'ebook-store' ) ),
9634 'template_key' => $template_key,
9635 );
9636 }
9637
9638 wp_safe_redirect(
9639 ebook_store_get_template_editor_url(
9640 array(
9641 'template' => 'modern',
9642 'deleted' => 1,
9643 )
9644 )
9645 );
9646 exit;
9647 }
9648
9649 function ebook_store_template_editor_save_template() {
9650 $template_key = ebook_store_get_template_editor_current_key( isset( $_POST['template_key'] ) ? wp_unslash( $_POST['template_key'] ) : '' );
9651 $editable = ebook_store_get_template_editor_files();
9652
9653 if ( $template_key === '' || ! isset( $editable[ $template_key ] ) ) {
9654 return array(
9655 'errors' => array( __( 'The selected template could not be found.', 'ebook-store' ) ),
9656 );
9657 }
9658
9659 $template_code = isset( $_POST['template_code'] ) ? wp_unslash( $_POST['template_code'] ) : '';
9660
9661 $syntax_check = ebook_store_validate_template_php_syntax( $template_code );
9662 if ( is_wp_error( $syntax_check ) ) {
9663 return array(
9664 'errors' => array( $syntax_check->get_error_message() ),
9665 'template_key' => $template_key,
9666 'template_code' => $template_code,
9667 );
9668 }
9669
9670 if ( ! empty( $editable[ $template_key ]['core'] ) ) {
9671 $preferred_name = sprintf( __( '%s Custom', 'ebook-store' ), $editable[ $template_key ]['label'] );
9672 $preferred_key = sanitize_title( $template_key . '-custom' );
9673 $created_template = ebook_store_create_custom_template_from_code( $preferred_name, $template_code, $preferred_key );
9674 if ( is_wp_error( $created_template ) ) {
9675 return array(
9676 'errors' => array( $created_template->get_error_message() ),
9677 'template_key' => $template_key,
9678 'template_code' => $template_code,
9679 );
9680 }
9681
9682 $reassign = ! empty( $_POST['template_editor_reassign_books'] );
9683 $assignment = array(
9684 'ebook_count' => 0,
9685 'default_updated' => false,
9686 );
9687 if ( $reassign ) {
9688 $assignment = ebook_store_reassign_template_usage( $template_key, $created_template['key'] );
9689 }
9690
9691 wp_safe_redirect(
9692 ebook_store_get_template_editor_url(
9693 array(
9694 'template' => $created_template['key'],
9695 'created' => 1,
9696 'forked_from_core' => 1,
9697 'copied_from' => $template_key,
9698 'reassigned' => $reassign ? 1 : 0,
9699 'reassigned_ebooks' => isset( $assignment['ebook_count'] ) ? (int) $assignment['ebook_count'] : 0,
9700 'reassigned_default' => ! empty( $assignment['default_updated'] ) ? 1 : 0,
9701 )
9702 )
9703 );
9704 exit;
9705 }
9706
9707 $template_file = $editable[ $template_key ]['file'];
9708 if ( ! is_writable( $template_file ) ) {
9709 return array(
9710 'errors' => array( __( 'That template file is not writable on the server.', 'ebook-store' ) ),
9711 'template_key' => $template_key,
9712 'template_code' => $template_code,
9713 );
9714 }
9715
9716 $result = @file_put_contents( $template_file, $template_code );
9717
9718 if ( $result === false ) {
9719 return array(
9720 'errors' => array( __( 'WordPress could not save the updated template code.', 'ebook-store' ) ),
9721 'template_key' => $template_key,
9722 'template_code' => $template_code,
9723 );
9724 }
9725
9726 wp_safe_redirect(
9727 ebook_store_get_template_editor_url(
9728 array(
9729 'template' => $template_key,
9730 'saved' => 1,
9731 )
9732 )
9733 );
9734 exit;
9735 }
9736
9737 function ebook_store_template_editor_handle_submission() {
9738 if ( empty( $_POST['ebook_store_template_editor_action'] ) ) {
9739 return null;
9740 }
9741
9742 if ( ! ebook_store_can_access_template_editor() ) {
9743 return array(
9744 'errors' => array( __( 'You do not have permission to edit Ebook Store templates.', 'ebook-store' ) ),
9745 );
9746 }
9747
9748 check_admin_referer( 'ebook_store_template_editor' );
9749
9750 $action_type = isset( $_POST['template_editor_action_type'] ) ? sanitize_key( wp_unslash( $_POST['template_editor_action_type'] ) ) : 'save';
9751
9752 if ( $action_type === 'create' ) {
9753 return ebook_store_template_editor_create_template();
9754 }
9755
9756 if ( $action_type === 'delete' ) {
9757 return ebook_store_template_editor_delete_template();
9758 }
9759
9760 return ebook_store_template_editor_save_template();
9761 }
9762
9763 function ebook_store_template_editor_page_callback() {
9764 if ( ! ebook_store_can_access_template_editor() ) {
9765 wp_die(
9766 esc_html__( 'You do not have permission to access the Ebook Store template editor.', 'ebook-store' ),
9767 esc_html__( 'Access denied', 'ebook-store' ),
9768 array( 'response' => 403 )
9769 );
9770 }
9771
9772 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
9773 $locale = ebook_store_get_locale_strings();
9774 $formats_locale = ebook_store_get_format_labels();
9775 wp_enqueue_style( 'ebookstorestylesheet' );
9776
9777 $code_editor_settings = wp_enqueue_code_editor(
9778 array(
9779 'type' => 'application/x-httpd-php',
9780 'codemirror' => array(
9781 'indentUnit' => 4,
9782 'tabSize' => 4,
9783 'lineNumbers' => true,
9784 'lineWrapping' => false,
9785 'mode' => 'application/x-httpd-php',
9786 ),
9787 )
9788 );
9789
9790 wp_enqueue_script( 'code-editor' );
9791 wp_enqueue_script( 'wp-theme-plugin-editor' );
9792 wp_enqueue_style( 'code-editor' );
9793
9794 $submission = null;
9795 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_template_editor_action'] ) ) {
9796 $submission = ebook_store_template_editor_handle_submission();
9797 }
9798
9799 $editable = ebook_store_get_template_editor_files();
9800 $current_template = ebook_store_get_template_editor_current_key( isset( $_REQUEST['template'] ) ? wp_unslash( $_REQUEST['template'] ) : '' );
9801 if ( $submission && ! empty( $submission['template_key'] ) ) {
9802 $current_template = ebook_store_get_template_editor_current_key( $submission['template_key'] );
9803 }
9804
9805 $current = $current_template !== '' && isset( $editable[ $current_template ] ) ? $editable[ $current_template ] : null;
9806 $current_code = '';
9807
9808 if ( $submission && array_key_exists( 'template_code', (array) $submission ) ) {
9809 $current_code = (string) $submission['template_code'];
9810 } elseif ( $current && file_exists( $current['file'] ) ) {
9811 $current_code = (string) file_get_contents( $current['file'] );
9812 }
9813
9814 $saved = isset( $_GET['saved'] ) && (int) $_GET['saved'] === 1;
9815 $created = isset( $_GET['created'] ) && (int) $_GET['created'] === 1;
9816 $deleted = isset( $_GET['deleted'] ) && (int) $_GET['deleted'] === 1;
9817 $forked_from_core = isset( $_GET['forked_from_core'] ) && (int) $_GET['forked_from_core'] === 1;
9818 $copied_from = isset( $_GET['copied_from'] ) ? sanitize_key( wp_unslash( $_GET['copied_from'] ) ) : '';
9819 $reassigned = isset( $_GET['reassigned'] ) && (int) $_GET['reassigned'] === 1;
9820 $reassigned_ebooks = isset( $_GET['reassigned_ebooks'] ) ? max( 0, (int) $_GET['reassigned_ebooks'] ) : 0;
9821 $reassigned_default = isset( $_GET['reassigned_default'] ) && (int) $_GET['reassigned_default'] === 1;
9822 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
9823 $wordfence_active = ebook_store_is_wordfence_active();
9824 $current_usage = $current ? ebook_store_get_template_usage_stats( $current['key'] ) : array(
9825 'ebooks' => 0,
9826 'default' => false,
9827 );
9828
9829 ob_start();
9830 ?>
9831 <div class="wrap ebook-template-editor">
9832 <div class="ebook-template-editor__hero">
9833 <div class="ebook-template-editor__hero-copy">
9834 <span class="ebook-template-editor__eyebrow"><?php esc_html_e( 'Developer tools', 'ebook-store' ); ?></span>
9835 <h1><?php esc_html_e( 'Button templates', 'ebook-store' ); ?></h1>
9836 <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>
9837 <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>
9838 </div>
9839 <div class="ebook-template-editor__hero-meta">
9840 <span class="ebook-template-editor__pill"><?php esc_html_e( 'PHP-safe editing', 'ebook-store' ); ?></span>
9841 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Update-safe custom copies', 'ebook-store' ); ?></span>
9842 <span class="ebook-template-editor__pill"><?php esc_html_e( 'Template-scoped access only', 'ebook-store' ); ?></span>
9843 </div>
9844 </div>
9845
9846 <?php if ( $saved && $current ) : ?>
9847 <div class="notice notice-success is-dismissible">
9848 <p>
9849 <strong><?php esc_html_e( 'Template saved.', 'ebook-store' ); ?></strong>
9850 <?php
9851 printf(
9852 esc_html__( '%s was updated successfully.', 'ebook-store' ),
9853 esc_html( $current['label'] )
9854 );
9855 ?>
9856 </p>
9857 </div>
9858 <?php endif; ?>
9859
9860 <?php if ( $created && $current && ! $forked_from_core ) : ?>
9861 <div class="notice notice-success is-dismissible">
9862 <p>
9863 <strong><?php esc_html_e( 'Template created.', 'ebook-store' ); ?></strong>
9864 <?php
9865 printf(
9866 esc_html__( '%s is now available in the template selector and stored separately from plugin updates.', 'ebook-store' ),
9867 esc_html( $current['label'] )
9868 );
9869 ?>
9870 </p>
9871 </div>
9872 <?php endif; ?>
9873
9874 <?php if ( $created && $current && $forked_from_core ) : ?>
9875 <div class="notice notice-success is-dismissible">
9876 <p>
9877 <strong><?php esc_html_e( 'Custom template created.', 'ebook-store' ); ?></strong>
9878 <?php
9879 printf(
9880 esc_html__( '%1$s was saved as the new custom template %2$s, so it will survive plugin updates.', 'ebook-store' ),
9881 esc_html( $copied_from !== '' && isset( $editable[ $copied_from ] ) ? $editable[ $copied_from ]['label'] : __( 'Built-in template', 'ebook-store' ) ),
9882 esc_html( $current['label'] )
9883 );
9884 ?>
9885 </p>
9886 <?php if ( $reassigned ) : ?>
9887 <p>
9888 <?php
9889 printf(
9890 esc_html__( '%1$d ebook(s) were reassigned to use this custom template.%2$s', 'ebook-store' ),
9891 (int) $reassigned_ebooks,
9892 $reassigned_default ? ' ' . esc_html__( 'The default template setting was updated too.', 'ebook-store' ) : ''
9893 );
9894 ?>
9895 </p>
9896 <?php endif; ?>
9897 </div>
9898 <?php endif; ?>
9899
9900 <?php if ( $deleted ) : ?>
9901 <div class="notice notice-success is-dismissible">
9902 <p><strong><?php esc_html_e( 'Template deleted.', 'ebook-store' ); ?></strong> <?php esc_html_e( 'The custom template file was removed.', 'ebook-store' ); ?></p>
9903 </div>
9904 <?php endif; ?>
9905
9906 <?php if ( ! empty( $errors ) ) : ?>
9907 <div class="notice notice-error">
9908 <p><strong><?php esc_html_e( 'The template could not be saved yet.', 'ebook-store' ); ?></strong></p>
9909 <ul class="ebook-template-editor__notice-list">
9910 <?php foreach ( $errors as $error ) : ?>
9911 <li><?php echo esc_html( $error ); ?></li>
9912 <?php endforeach; ?>
9913 </ul>
9914 </div>
9915 <?php endif; ?>
9916
9917 <?php if ( $wordfence_active ) : ?>
9918 <div class="notice notice-warning">
9919 <p>
9920 <strong><?php esc_html_e( 'Wordfence note:', 'ebook-store' ); ?></strong>
9921 <?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' ); ?>
9922 </p>
9923 <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>
9924 </div>
9925 <?php endif; ?>
9926
9927 <div class="ebook-template-editor__layout">
9928 <aside class="ebook-template-editor__sidebar">
9929 <div class="ebook-template-editor__nav-card">
9930 <div class="ebook-template-editor__nav-header">
9931 <h2><?php esc_html_e( 'Templates', 'ebook-store' ); ?></h2>
9932 <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>
9933 </div>
9934 <div class="ebook-template-editor__nav">
9935 <?php foreach ( $editable as $template ) : ?>
9936 <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'] ) ) ); ?>">
9937 <strong><?php echo esc_html( $template['label'] ); ?></strong>
9938 <small><?php echo esc_html( $template['filename'] ); ?></small>
9939 </a>
9940 <?php endforeach; ?>
9941 </div>
9942 </div>
9943 </aside>
9944
9945 <div class="ebook-template-editor__main">
9946 <?php if ( $current ) : ?>
9947 <form method="post" class="ebook-template-editor__form">
9948 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
9949 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
9950 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
9951
9952 <div class="ebook-template-editor__panel">
9953 <div class="ebook-template-editor__panel-header">
9954 <div>
9955 <h2><?php echo esc_html( $current['label'] ); ?></h2>
9956 <p>
9957 <?php
9958 echo ! empty( $current['core'] )
9959 ? esc_html__( 'Built-in templates are update-protected. Saving here creates a new custom template copy instead of overwriting the plugin file.', 'ebook-store' )
9960 : esc_html__( 'Editing the custom PHP template file used by Ebook Store for this layout.', 'ebook-store' );
9961 ?>
9962 </p>
9963 </div>
9964 <div class="ebook-template-editor__header-side">
9965 <div class="ebook-template-editor__file-meta">
9966 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $current['filename'] ); ?></span>
9967 <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>
9968 </div>
9969 <?php if ( empty( $current['core'] ) ) : ?>
9970 <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>
9971 <?php endif; ?>
9972 </div>
9973 </div>
9974
9975 <div class="ebook-template-editor__editor-wrap">
9976 <textarea id="ebook_store_template_code" name="template_code" class="large-text code" rows="28" spellcheck="false"><?php echo esc_textarea( $current_code ); ?></textarea>
9977 </div>
9978
9979 <?php if ( ! empty( $current['core'] ) ) : ?>
9980 <div class="ebook-template-editor__field">
9981 <label>
9982 <input type="checkbox" name="template_editor_reassign_books" value="1" />
9983 <?php
9984 printf(
9985 esc_html__( 'Also assign the new custom template automatically to ebooks currently using %s.', 'ebook-store' ),
9986 esc_html( $current['label'] )
9987 );
9988 ?>
9989 </label>
9990 <p class="description">
9991 <?php
9992 printf(
9993 esc_html__( 'Right now %1$d ebook(s)%2$s resolve to this template.', 'ebook-store' ),
9994 (int) $current_usage['ebooks'],
9995 ! empty( $current_usage['default'] ) ? ' ' . esc_html__( 'The global default template also points to it.', 'ebook-store' ) : ''
9996 );
9997 ?>
9998 </p>
9999 </div>
10000 <?php endif; ?>
10001
10002 <div class="ebook-template-editor__actions">
10003 <input type="hidden" name="template_editor_action_type" value="save" />
10004 <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>
10005 <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>
10006 </div>
10007 <?php if ( $wordfence_active ) : ?>
10008 <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>
10009 <?php endif; ?>
10010 </div>
10011 </form>
10012 <?php else : ?>
10013 <div class="ebook-template-editor__panel">
10014 <p><?php esc_html_e( 'No editable templates were found.', 'ebook-store' ); ?></p>
10015 </div>
10016 <?php endif; ?>
10017 </div>
10018 </div>
10019 </div>
10020 <div class="ebook-template-editor__modal" data-template-modal="create" hidden>
10021 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10022 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-create-title">
10023 <form method="post" class="ebook-template-editor__modal-form">
10024 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10025 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10026 <input type="hidden" name="template_editor_action_type" value="create" />
10027 <div class="ebook-template-editor__modal-header">
10028 <h2 id="ebook-template-editor-create-title"><?php esc_html_e( 'Create new template', 'ebook-store' ); ?></h2>
10029 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10030 </div>
10031 <div class="ebook-template-editor__modal-body">
10032 <div class="ebook-template-editor__field">
10033 <label for="new_template_name"><?php esc_html_e( 'Template name', 'ebook-store' ); ?></label>
10034 <input type="text" id="new_template_name" name="new_template_name" required />
10035 </div>
10036 <div class="ebook-template-editor__field">
10037 <label for="new_template_source"><?php esc_html_e( 'Copy code from', 'ebook-store' ); ?></label>
10038 <select id="new_template_source" name="new_template_source">
10039 <option value="none"><?php esc_html_e( 'None (start with an empty scaffold)', 'ebook-store' ); ?></option>
10040 <?php foreach ( $editable as $template ) : ?>
10041 <option value="<?php echo esc_attr( $template['key'] ); ?>"><?php echo esc_html( $template['label'] ); ?></option>
10042 <?php endforeach; ?>
10043 </select>
10044 <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>
10045 </div>
10046 </div>
10047 <div class="ebook-template-editor__modal-actions">
10048 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10049 <button type="submit" class="button button-primary"><?php esc_html_e( 'Create template', 'ebook-store' ); ?></button>
10050 </div>
10051 </form>
10052 </div>
10053 </div>
10054 <?php if ( $current && empty( $current['core'] ) ) : ?>
10055 <div class="ebook-template-editor__modal" data-template-modal="delete" hidden>
10056 <div class="ebook-template-editor__modal-backdrop" data-template-modal-close></div>
10057 <div class="ebook-template-editor__modal-dialog" role="dialog" aria-modal="true" aria-labelledby="ebook-template-editor-delete-title">
10058 <form method="post" class="ebook-template-editor__modal-form">
10059 <?php wp_nonce_field( 'ebook_store_template_editor' ); ?>
10060 <input type="hidden" name="ebook_store_template_editor_action" value="1" />
10061 <input type="hidden" name="template_editor_action_type" value="delete" />
10062 <input type="hidden" name="template_key" value="<?php echo esc_attr( $current['key'] ); ?>" />
10063 <div class="ebook-template-editor__modal-header">
10064 <h2 id="ebook-template-editor-delete-title"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></h2>
10065 <button type="button" class="button-link" data-template-modal-close><?php esc_html_e( 'Close', 'ebook-store' ); ?></button>
10066 </div>
10067 <div class="ebook-template-editor__modal-body">
10068 <p><?php printf( esc_html__( 'Delete the custom template “%s”? This removes the saved custom template file.', 'ebook-store' ), esc_html( $current['label'] ) ); ?></p>
10069 <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>
10070 </div>
10071 <div class="ebook-template-editor__modal-actions">
10072 <button type="button" class="button button-secondary" data-template-modal-close><?php esc_html_e( 'Cancel', 'ebook-store' ); ?></button>
10073 <button type="submit" class="button button-primary button-link-delete"><?php esc_html_e( 'Delete template', 'ebook-store' ); ?></button>
10074 </div>
10075 </form>
10076 </div>
10077 </div>
10078 <?php endif; ?>
10079 <?php
10080
10081 if ( $code_editor_settings && $current ) {
10082 wp_add_inline_script(
10083 'code-editor',
10084 'jQuery(function(){wp.codeEditor.initialize("ebook_store_template_code",' . wp_json_encode( $code_editor_settings ) . ');});'
10085 );
10086 }
10087
10088 wp_add_inline_script(
10089 'code-editor',
10090 '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);}});});'
10091 );
10092
10093 $output = ob_get_clean();
10094 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
10095 }
10096
10097 function ebook_store_setup_wizard_menu_title() {
10098 return sprintf(
10099 '%s <span class="update-plugins count-1"><span class="plugin-count">%s</span></span>',
10100 esc_html__( 'Setup wizard', 'ebook-store' ),
10101 esc_html__( 'new', 'ebook-store' )
10102 );
10103 }
10104
10105 function ebook_store_setup_wizard_is_pdf( $file ) {
10106 if ( empty( $file['name'] ) || empty( $file['tmp_name'] ) ) {
10107 return false;
10108 }
10109
10110 $filetype = wp_check_filetype( basename( $file['name'] ) );
10111 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
10112 $mime_type = isset( $filetype['type'] ) ? strtolower( (string) $filetype['type'] ) : '';
10113
10114 return $extension === 'pdf' || $mime_type === 'application/pdf';
10115 }
10116
10117 function ebook_store_setup_wizard_human_mode_label( $mode ) {
10118 if ( $mode === 'woocommerce' ) {
10119 return __( 'WooCommerce integration', 'ebook-store' );
10120 }
10121
10122 return __( 'Standalone ebook sales', 'ebook-store' );
10123 }
10124
10125 function ebook_store_is_paypal_enabled() {
10126 return (string) get_option( 'paypal_integration_enabled', '1' ) === '1';
10127 }
10128
10129 function ebook_store_get_standalone_payment_method_state() {
10130 $paypal_ready = ebook_store_is_paypal_enabled() && trim( (string) get_option( 'paypal_account', '' ) ) !== '';
10131 $stripe_ready = (string) get_option( 'stripe_integration_enabled', '0' ) === '1'
10132 && trim( (string) get_option( 'stripe_publishable_key', '' ) ) !== ''
10133 && trim( (string) get_option( 'stripe_secret_key', '' ) ) !== '';
10134 $adyen_environment = (string) get_option( 'adyen_environment', 'test' );
10135 $adyen_ready = (string) get_option( 'adyen_integration_enabled', '0' ) === '1'
10136 && trim( (string) get_option( 'adyen_api_key', '' ) ) !== ''
10137 && trim( (string) get_option( 'adyen_merchant_account', '' ) ) !== ''
10138 && trim( (string) get_option( 'adyen_hmac_key', '' ) ) !== ''
10139 && ( $adyen_environment !== 'live' || trim( (string) get_option( 'adyen_live_prefix', '' ) ) !== '' );
10140
10141 return array(
10142 'paypal' => array(
10143 'enabled' => ebook_store_is_paypal_enabled(),
10144 'ready' => $paypal_ready,
10145 ),
10146 'stripe' => array(
10147 'enabled' => (string) get_option( 'stripe_integration_enabled', '0' ) === '1',
10148 'ready' => $stripe_ready,
10149 ),
10150 'adyen' => array(
10151 'enabled' => (string) get_option( 'adyen_integration_enabled', '0' ) === '1',
10152 'ready' => $adyen_ready,
10153 ),
10154 );
10155 }
10156
10157 function ebook_store_has_ready_standalone_payment_method() {
10158 $methods = ebook_store_get_standalone_payment_method_state();
10159
10160 foreach ( $methods as $method ) {
10161 if ( ! empty( $method['ready'] ) ) {
10162 return true;
10163 }
10164 }
10165
10166 return false;
10167 }
10168
10169 function ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ) {
10170 if ( ! class_exists( 'Imagick' ) || ! file_exists( $pdf_path ) ) {
10171 return 0;
10172 }
10173
10174 try {
10175 $imagick = new Imagick();
10176 $imagick->pingImage( $pdf_path );
10177 $page_count = (int) $imagick->getNumberImages();
10178 $imagick->clear();
10179 $imagick->destroy();
10180
10181 return max( 0, $page_count );
10182 } catch ( Exception $exception ) {
10183 return 0;
10184 }
10185 }
10186
10187 function ebook_store_setup_wizard_unescape_pdf_literal( $value ) {
10188 $value = preg_replace_callback(
10189 '/\\\\([0-7]{1,3})/',
10190 function ( $matches ) {
10191 return chr( octdec( $matches[1] ) );
10192 },
10193 (string) $value
10194 );
10195
10196 $replacements = array(
10197 '\\n' => "\n",
10198 '\\r' => "\r",
10199 '\\t' => "\t",
10200 '\\b' => "\b",
10201 '\\f' => "\f",
10202 '\\(' => '(',
10203 '\\)' => ')',
10204 '\\\\' => '\\',
10205 );
10206
10207 return strtr( $value, $replacements );
10208 }
10209
10210 function ebook_store_setup_wizard_clean_pdf_text( $text ) {
10211 $text = html_entity_decode( (string) $text, ENT_QUOTES, 'UTF-8' );
10212 $text = preg_replace( "/\r\n?/", "\n", $text );
10213 $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', ' ', $text );
10214 $text = preg_replace( '/[ \t]+/', ' ', $text );
10215 $text = preg_replace( "/\n{3,}/", "\n\n", $text );
10216
10217 $lines = preg_split( "/\n+/", trim( $text ) );
10218 $clean_lines = array();
10219
10220 foreach ( $lines as $line ) {
10221 $line = trim( wp_strip_all_tags( $line ) );
10222 $line = preg_replace( '/\s+/', ' ', $line );
10223
10224 if ( strlen( $line ) < 3 ) {
10225 continue;
10226 }
10227
10228 if ( preg_match( '/^[^A-Za-z\\p{L}\\p{N}]+$/u', $line ) ) {
10229 continue;
10230 }
10231
10232 if ( in_array( $line, $clean_lines, true ) ) {
10233 continue;
10234 }
10235
10236 $clean_lines[] = $line;
10237 }
10238
10239 return trim( implode( "\n", $clean_lines ) );
10240 }
10241
10242 function ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path ) {
10243 if ( ! function_exists( 'shell_exec' ) || ! file_exists( $pdf_path ) ) {
10244 return '';
10245 }
10246
10247 $ghostscript_path = trim( (string) @shell_exec( 'command -v gs 2>/dev/null' ) );
10248 if ( $ghostscript_path === '' ) {
10249 return '';
10250 }
10251
10252 $command = escapeshellarg( $ghostscript_path ) . ' -q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=txtwrite -sOutputFile=- ' . escapeshellarg( $pdf_path ) . ' 2>/dev/null';
10253 $output = @shell_exec( $command );
10254
10255 return is_string( $output ) ? ebook_store_setup_wizard_clean_pdf_text( $output ) : '';
10256 }
10257
10258 function ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path ) {
10259 if ( ! file_exists( $pdf_path ) ) {
10260 return '';
10261 }
10262
10263 $pdf_contents = @file_get_contents( $pdf_path );
10264 if ( ! is_string( $pdf_contents ) || $pdf_contents === '' ) {
10265 return '';
10266 }
10267
10268 $matches = array();
10269 preg_match_all( '/stream[\r\n]+(.*?)endstream/s', $pdf_contents, $matches );
10270 if ( empty( $matches[1] ) ) {
10271 return '';
10272 }
10273
10274 $chunks = array();
10275
10276 foreach ( array_slice( $matches[1], 0, 12 ) as $stream ) {
10277 $candidates = array( $stream );
10278
10279 if ( function_exists( 'gzuncompress' ) ) {
10280 $decoded = @gzuncompress( $stream );
10281 if ( is_string( $decoded ) && $decoded !== '' ) {
10282 $candidates[] = $decoded;
10283 }
10284 }
10285
10286 if ( function_exists( 'gzinflate' ) ) {
10287 $decoded = @gzinflate( $stream );
10288 if ( is_string( $decoded ) && $decoded !== '' ) {
10289 $candidates[] = $decoded;
10290 }
10291
10292 $decoded = @gzinflate( substr( $stream, 2 ) );
10293 if ( is_string( $decoded ) && $decoded !== '' ) {
10294 $candidates[] = $decoded;
10295 }
10296 }
10297
10298 foreach ( $candidates as $candidate ) {
10299 $text_matches = array();
10300 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)\s*Tj/s', $candidate, $text_matches );
10301 foreach ( $text_matches[0] as $text_match ) {
10302 if ( preg_match( '/^\((.*)\)\s*Tj$/s', $text_match, $literal_match ) ) {
10303 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( $literal_match[1] );
10304 }
10305 }
10306
10307 $array_matches = array();
10308 preg_match_all( '/\[(.*?)\]\s*TJ/s', $candidate, $array_matches );
10309 foreach ( $array_matches[1] as $array_match ) {
10310 $literal_matches = array();
10311 preg_match_all( '/\((?:\\\\.|[^\\\\)])*\)/s', $array_match, $literal_matches );
10312 foreach ( $literal_matches[0] as $literal ) {
10313 $chunks[] = ebook_store_setup_wizard_unescape_pdf_literal( trim( $literal, '()' ) );
10314 }
10315 }
10316 }
10317 }
10318
10319 return ebook_store_setup_wizard_clean_pdf_text( implode( "\n", $chunks ) );
10320 }
10321
10322 function ebook_store_setup_wizard_extract_pdf_details( $pdf_path, $fallback_title = '' ) {
10323 $details = array(
10324 'title' => '',
10325 'description' => '',
10326 'author' => '',
10327 'pages' => ebook_store_setup_wizard_get_pdf_page_count( $pdf_path ),
10328 );
10329
10330 if ( ! file_exists( $pdf_path ) ) {
10331 return $details;
10332 }
10333
10334 $pdf_contents = @file_get_contents( $pdf_path );
10335 if ( is_string( $pdf_contents ) && $pdf_contents !== '' ) {
10336 if ( preg_match( '/\/Title\s*\((.*?)\)/s', $pdf_contents, $title_match ) ) {
10337 $details['title'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $title_match[1] ) );
10338 }
10339
10340 if ( preg_match( '/\/Subject\s*\((.*?)\)/s', $pdf_contents, $subject_match ) ) {
10341 $details['description'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $subject_match[1] ) );
10342 }
10343
10344 if ( preg_match( '/\/Author\s*\((.*?)\)/s', $pdf_contents, $author_match ) ) {
10345 $details['author'] = trim( ebook_store_setup_wizard_unescape_pdf_literal( $author_match[1] ) );
10346 }
10347 }
10348
10349 $text = ebook_store_setup_wizard_extract_pdf_text_via_ghostscript( $pdf_path );
10350 if ( $text === '' ) {
10351 $text = ebook_store_setup_wizard_extract_pdf_text_via_streams( $pdf_path );
10352 }
10353
10354 $lines = $text !== '' ? array_values( array_filter( preg_split( "/\n+/", $text ) ) ) : array();
10355 if ( $details['title'] === '' && ! empty( $lines ) ) {
10356 foreach ( $lines as $line ) {
10357 $line = trim( $line );
10358 if ( strlen( $line ) >= 4 && strlen( $line ) <= 140 ) {
10359 $details['title'] = $line;
10360 break;
10361 }
10362 }
10363 }
10364
10365 if ( $details['description'] === '' && count( $lines ) > 1 ) {
10366 $description_lines = array();
10367 foreach ( $lines as $line ) {
10368 $line = trim( $line );
10369 if ( $line === '' ) {
10370 continue;
10371 }
10372 if ( $details['title'] !== '' && strcasecmp( $line, $details['title'] ) === 0 ) {
10373 continue;
10374 }
10375 $description_lines[] = $line;
10376 if ( strlen( implode( ' ', $description_lines ) ) >= 280 ) {
10377 break;
10378 }
10379 }
10380 $details['description'] = trim( implode( ' ', array_slice( $description_lines, 0, 4 ) ) );
10381 }
10382
10383 if ( $details['title'] === '' ) {
10384 $details['title'] = $fallback_title;
10385 }
10386
10387 if ( $details['description'] !== '' ) {
10388 $details['description'] = wp_trim_words( $details['description'], 48, '...' );
10389 }
10390
10391 return $details;
10392 }
10393
10394 function ebook_store_setup_wizard_upload_pdf( $ebook_id, $file ) {
10395 if ( empty( $file['tmp_name'] ) || ! file_exists( $file['tmp_name'] ) ) {
10396 return new WP_Error( 'ebook_store_wizard_upload_missing', __( 'The uploaded PDF file could not be read.', 'ebook-store' ) );
10397 }
10398
10399 // The wizard accepted whatever it was handed. Run the same content check the
10400 // rest of the plugin uses, so a renamed file cannot be parked here.
10401 if ( function_exists( 'ebook_store_validate_uploaded_ebook' ) ) {
10402 $validated = ebook_store_validate_uploaded_ebook( $file['tmp_name'], 'pdf' );
10403
10404 if ( is_wp_error( $validated ) ) {
10405 return $validated;
10406 }
10407 }
10408
10409 // This used to be wp_upload_bits( ..., file_get_contents( $tmp ) ), which
10410 // pulled the whole book into memory — a 60 MB PDF blew memory_limit before it
10411 // ever reached disk. Moving the temp file is constant-memory.
10412 if ( function_exists( 'ebook_store_store_uploaded_ebook' ) ) {
10413 $upload = ebook_store_store_uploaded_ebook( $file['tmp_name'], $file['name'] );
10414 } else {
10415 add_filter( 'upload_dir', 'ebook_set_upload_dir' );
10416 $upload = wp_upload_bits( $file['name'], null, file_get_contents( $file['tmp_name'] ) );
10417 remove_filter( 'upload_dir', 'ebook_set_upload_dir' );
10418 }
10419
10420 if ( is_wp_error( $upload ) ) {
10421 return $upload;
10422 }
10423
10424 if ( isset( $upload['error'] ) && $upload['error'] ) {
10425 return new WP_Error( 'ebook_store_wizard_upload_failed', $upload['error'] );
10426 }
10427
10428 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment', wp_slash( $upload ) );
10429 update_post_meta( $ebook_id, 'ebook_wp_custom_attachment_pdf', wp_slash( $upload ) );
10430 ebook_store_generate_cover_from_pdf( $ebook_id );
10431
10432 return $upload;
10433 }
10434
10435 function ebook_store_setup_wizard_create_ebook( $args ) {
10436 $title = isset( $args['title'] ) ? sanitize_text_field( $args['title'] ) : '';
10437 $description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : '';
10438 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
10439 $currency = isset( $args['currency'] ) && $args['currency'] !== '' ? sanitize_text_field( $args['currency'] ) : ebook_store_get_store_currency();
10440 $pdf_file = isset( $args['pdf_file'] ) ? $args['pdf_file'] : array();
10441 $pdf_pages = isset( $args['pages'] ) ? (int) $args['pages'] : 0;
10442 $author = isset( $args['author'] ) ? sanitize_text_field( $args['author'] ) : '';
10443 $template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( '' ) : 'modern';
10444 $delivery_mode = $price > 0 ? 'paid' : 'free';
10445
10446 if ( $title === '' ) {
10447 $title = __( 'Imported ebook', 'ebook-store' );
10448 }
10449
10450 $ebook_post = array(
10451 'post_title' => $title,
10452 'post_content' => $description,
10453 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $description ), 40, '...' ),
10454 'post_status' => 'draft',
10455 'post_type' => 'ebook',
10456 );
10457
10458 $ebook_id = wp_insert_post( $ebook_post, true );
10459 if ( is_wp_error( $ebook_id ) ) {
10460 return $ebook_id;
10461 }
10462
10463 $ebook_meta = array(
10464 'ebook_price' => number_format( $price, 2, '.', '' ),
10465 'paypal_currency' => $currency,
10466 'donate_or_download' => $delivery_mode,
10467 'form_template' => $template,
10468 );
10469
10470 if ( $pdf_pages > 0 ) {
10471 $ebook_meta['ebook_pages'] = $pdf_pages;
10472 }
10473
10474 update_post_meta( $ebook_id, 'ebook', $ebook_meta );
10475 update_post_meta( $ebook_id, 'ebook_bonus', array() );
10476
10477 if ( $author !== '' ) {
10478 update_post_meta( $ebook_id, 'ebook_store_wizard_author_name', $author );
10479 }
10480
10481 $upload = ebook_store_setup_wizard_upload_pdf( $ebook_id, $pdf_file );
10482 if ( is_wp_error( $upload ) ) {
10483 wp_delete_post( $ebook_id, true );
10484 return $upload;
10485 }
10486
10487 return array(
10488 'ebook_id' => $ebook_id,
10489 'title' => $title,
10490 'description' => $description,
10491 'pages' => $pdf_pages,
10492 );
10493 }
10494
10495 function ebook_store_setup_wizard_create_woocommerce_bundle( $args ) {
10496 if ( ! class_exists( 'WooCommerce' ) ) {
10497 return new WP_Error( 'ebook_store_wizard_woocommerce_missing', __( 'WooCommerce is not active. Activate WooCommerce first, then run this setup path again.', 'ebook-store' ) );
10498 }
10499
10500 $ebook_result = ebook_store_setup_wizard_create_ebook( $args );
10501 if ( is_wp_error( $ebook_result ) ) {
10502 return $ebook_result;
10503 }
10504
10505 $product_name = isset( $args['product_name'] ) ? sanitize_text_field( $args['product_name'] ) : $ebook_result['title'];
10506 $product_description = isset( $args['description'] ) ? wp_kses_post( $args['description'] ) : $ebook_result['description'];
10507 $price = isset( $args['price'] ) ? (float) $args['price'] : 0.0;
10508
10509 $product_post = array(
10510 'post_title' => $product_name,
10511 'post_content' => $product_description,
10512 'post_excerpt' => wp_trim_words( wp_strip_all_tags( $product_description ), 40, '...' ),
10513 'post_status' => 'draft',
10514 'post_type' => 'product',
10515 );
10516
10517 $product_id = wp_insert_post( $product_post, true );
10518 if ( is_wp_error( $product_id ) ) {
10519 wp_delete_post( $ebook_result['ebook_id'], true );
10520 return $product_id;
10521 }
10522
10523 wp_set_object_terms( $product_id, 'ebookstore', 'product_type' );
10524 update_post_meta( $product_id, '_regular_price', number_format( $price, 2, '.', '' ) );
10525 update_post_meta( $product_id, '_price', number_format( $price, 2, '.', '' ) );
10526 update_post_meta( $product_id, '_virtual', 'yes' );
10527 update_post_meta( $product_id, '_downloadable', 'yes' );
10528 update_post_meta( $product_id, '_enable_ebook_store_option', 'yes' );
10529 update_post_meta( $product_id, 'ebook_store_book_id', $ebook_result['ebook_id'] );
10530 update_post_meta( $ebook_result['ebook_id'], 'woocommerce_product_id', $product_id );
10531
10532 $license_key = trim( (string) get_option( 'ebook_store_license_key', '' ) );
10533 $integration_enabled = (string) get_option( 'ebook_store_woocommerce_integration', '' ) === '1';
10534 $integration_auto_enabled = false;
10535
10536 if ( ! $integration_enabled && $license_key !== '' ) {
10537 update_option( 'ebook_store_woocommerce_integration', '1' );
10538 $integration_enabled = true;
10539 $integration_auto_enabled = true;
10540 }
10541
10542 if ( function_exists( 'wc_delete_product_transients' ) ) {
10543 wc_delete_product_transients( $product_id );
10544 }
10545
10546 return array(
10547 'ebook_id' => $ebook_result['ebook_id'],
10548 'product_id' => $product_id,
10549 'integration_enabled' => $integration_enabled,
10550 'integration_auto_enabled' => $integration_auto_enabled,
10551 );
10552 }
10553
10554 function ebook_store_setup_wizard_handle_submission() {
10555 if ( empty( $_POST['ebook_store_setup_wizard_action'] ) ) {
10556 return null;
10557 }
10558
10559 if ( ! current_user_can( 'manage_options' ) ) {
10560 return array(
10561 'errors' => array( __( 'You do not have permission to run the setup wizard.', 'ebook-store' ) ),
10562 );
10563 }
10564
10565 check_admin_referer( 'ebook_store_setup_wizard' );
10566
10567 $mode = sanitize_key( (string) wp_unslash( $_POST['wizard_mode'] ) );
10568 $price = isset( $_POST['wizard_price'] ) ? (float) wp_unslash( $_POST['wizard_price'] ) : 0.0;
10569 $product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
10570 $wizard_enable_paypal = isset( $_POST['wizard_enable_paypal'] ) && (string) wp_unslash( $_POST['wizard_enable_paypal'] ) === '1' ? '1' : '';
10571 $wizard_enable_stripe = isset( $_POST['wizard_enable_stripe'] ) && (string) wp_unslash( $_POST['wizard_enable_stripe'] ) === '1' ? '1' : '';
10572 $wizard_enable_adyen = isset( $_POST['wizard_enable_adyen'] ) && (string) wp_unslash( $_POST['wizard_enable_adyen'] ) === '1' ? '1' : '';
10573 $wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : '';
10574 $wizard_stripe_publishable_key = isset( $_POST['wizard_stripe_publishable_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_publishable_key'] ) ) : '';
10575 $wizard_stripe_secret_key = isset( $_POST['wizard_stripe_secret_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_stripe_secret_key'] ) ) : '';
10576 $wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : 'test';
10577 $wizard_adyen_api_key = isset( $_POST['wizard_adyen_api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_api_key'] ) ) : '';
10578 $wizard_adyen_merchant_account = isset( $_POST['wizard_adyen_merchant_account'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_merchant_account'] ) ) : '';
10579 $wizard_adyen_hmac_key = isset( $_POST['wizard_adyen_hmac_key'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_hmac_key'] ) ) : '';
10580 $wizard_adyen_live_prefix = isset( $_POST['wizard_adyen_live_prefix'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_adyen_live_prefix'] ) ) : '';
10581 $pdf_file = isset( $_FILES['wizard_pdf_file'] ) ? $_FILES['wizard_pdf_file'] : array();
10582 $errors = array();
10583
10584 if ( ! in_array( $mode, array( 'standalone', 'woocommerce' ), true ) ) {
10585 $errors[] = __( 'Please choose how you want to set up Ebook Store first.', 'ebook-store' );
10586 }
10587
10588 if ( empty( $pdf_file['tmp_name'] ) ) {
10589 $errors[] = __( 'Please upload the PDF file you want to sell.', 'ebook-store' );
10590 } elseif ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
10591 $errors[] = __( 'The uploaded file must be a PDF.', 'ebook-store' );
10592 }
10593
10594 if ( $mode === 'woocommerce' && $product_name === '' ) {
10595 $errors[] = __( 'Please enter the WooCommerce product name.', 'ebook-store' );
10596 }
10597
10598 if ( $mode === 'standalone' ) {
10599 $wizard_ready_payment_methods = 0;
10600
10601 if ( $wizard_enable_paypal === '1' ) {
10602 if ( $wizard_paypal_account === '' ) {
10603 $errors[] = __( 'Enter the PayPal account email or turn off PayPal in Payment method setup.', 'ebook-store' );
10604 } else {
10605 $wizard_ready_payment_methods++;
10606 }
10607 }
10608
10609 if ( $wizard_enable_stripe === '1' ) {
10610 if ( $wizard_stripe_publishable_key === '' || $wizard_stripe_secret_key === '' ) {
10611 $errors[] = __( 'Enter both Stripe API keys or turn off Stripe in Payment method setup.', 'ebook-store' );
10612 } else {
10613 $wizard_ready_payment_methods++;
10614 }
10615 }
10616
10617 if ( $wizard_enable_adyen === '1' ) {
10618 if ( ! in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ) {
10619 $wizard_adyen_environment = 'test';
10620 }
10621 if ( $wizard_adyen_api_key === '' || $wizard_adyen_merchant_account === '' || $wizard_adyen_hmac_key === '' ) {
10622 $errors[] = __( 'Enter the Adyen API key, merchant account, and webhook HMAC key or turn off Adyen in Payment method setup.', 'ebook-store' );
10623 } elseif ( $wizard_adyen_environment === 'live' && $wizard_adyen_live_prefix === '' ) {
10624 $errors[] = __( 'Enter the Adyen live URL prefix or switch Adyen to Test mode in Payment method setup.', 'ebook-store' );
10625 } else {
10626 $wizard_ready_payment_methods++;
10627 }
10628 }
10629
10630 if ( $price > 0 && $wizard_ready_payment_methods < 1 ) {
10631 $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' );
10632 }
10633 }
10634
10635 if ( ! empty( $errors ) ) {
10636 return array(
10637 'errors' => $errors,
10638 'mode' => $mode,
10639 );
10640 }
10641
10642 if ( $mode === 'standalone' ) {
10643 update_option( 'paypal_integration_enabled', $wizard_enable_paypal );
10644 update_option( 'paypal_account', $wizard_paypal_account );
10645 update_option( 'stripe_integration_enabled', $wizard_enable_stripe );
10646 update_option( 'stripe_publishable_key', $wizard_stripe_publishable_key );
10647 update_option( 'stripe_secret_key', $wizard_stripe_secret_key );
10648 update_option( 'adyen_integration_enabled', $wizard_enable_adyen );
10649 update_option( 'adyen_environment', in_array( $wizard_adyen_environment, array( 'test', 'live' ), true ) ? $wizard_adyen_environment : 'test' );
10650 update_option( 'adyen_api_key', $wizard_adyen_api_key );
10651 update_option( 'adyen_merchant_account', $wizard_adyen_merchant_account );
10652 update_option( 'adyen_hmac_key', $wizard_adyen_hmac_key );
10653 update_option( 'adyen_live_prefix', $wizard_adyen_live_prefix );
10654 }
10655
10656 $fallback_title = $mode === 'woocommerce' ? $product_name : preg_replace( '/\.[^.]+$/', '', sanitize_file_name( $pdf_file['name'] ) );
10657 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $pdf_file['tmp_name'], $fallback_title );
10658 $title = $mode === 'woocommerce' ? $product_name : $pdf_details['title'];
10659 $description = $pdf_details['description'];
10660
10661 if ( $title === '' ) {
10662 $title = $fallback_title !== '' ? $fallback_title : __( 'Imported ebook', 'ebook-store' );
10663 }
10664
10665 $shared_args = array(
10666 'title' => $title,
10667 'product_name'=> $product_name,
10668 'description' => $description,
10669 'price' => $price,
10670 'currency' => ebook_store_get_store_currency(),
10671 'pdf_file' => $pdf_file,
10672 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
10673 'author' => isset( $pdf_details['author'] ) ? $pdf_details['author'] : '',
10674 );
10675
10676 $result = $mode === 'woocommerce'
10677 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
10678 : ebook_store_setup_wizard_create_ebook( $shared_args );
10679
10680 if ( is_wp_error( $result ) ) {
10681 return array(
10682 'errors' => array( $result->get_error_message() ),
10683 'mode' => $mode,
10684 );
10685 }
10686
10687 $redirect_args = array(
10688 'wizard_mode' => $mode,
10689 'wizard_created' => 1,
10690 'ebook_id' => (int) $result['ebook_id'],
10691 );
10692
10693 if ( ! empty( $result['product_id'] ) ) {
10694 $redirect_args['product_id'] = (int) $result['product_id'];
10695 }
10696
10697 if ( isset( $result['integration_enabled'] ) ) {
10698 $redirect_args['integration_enabled'] = $result['integration_enabled'] ? 1 : 0;
10699 }
10700
10701 if ( isset( $result['integration_auto_enabled'] ) ) {
10702 $redirect_args['integration_auto_enabled'] = $result['integration_auto_enabled'] ? 1 : 0;
10703 }
10704
10705 wp_safe_redirect( ebook_store_get_setup_wizard_url( $redirect_args ) );
10706 exit;
10707 }
10708
10709 function ebook_store_setup_wizard_page_callback() {
10710 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
10711 $locale = ebook_store_get_locale_strings();
10712 $formats_locale = ebook_store_get_format_labels();
10713 wp_enqueue_style( 'ebookstorestylesheet' );
10714
10715 $submission = null;
10716 if ( 'POST' === $_SERVER['REQUEST_METHOD'] && isset( $_POST['ebook_store_setup_wizard_action'] ) ) {
10717 $submission = ebook_store_setup_wizard_handle_submission();
10718 }
10719
10720 $wizard_mode = isset( $_REQUEST['wizard_mode'] ) ? sanitize_key( (string) wp_unslash( $_REQUEST['wizard_mode'] ) ) : '';
10721 if ( $submission && ! empty( $submission['mode'] ) ) {
10722 $wizard_mode = $submission['mode'];
10723 }
10724
10725 $errors = $submission && ! empty( $submission['errors'] ) ? $submission['errors'] : array();
10726 $wizard_created = isset( $_GET['wizard_created'] ) && (int) $_GET['wizard_created'] === 1;
10727 $ebook_id = isset( $_GET['ebook_id'] ) ? (int) $_GET['ebook_id'] : 0;
10728 $product_id = isset( $_GET['product_id'] ) ? (int) $_GET['product_id'] : 0;
10729 $integration_enabled = isset( $_GET['integration_enabled'] ) ? (int) $_GET['integration_enabled'] === 1 : null;
10730 $integration_auto_enabled = isset( $_GET['integration_auto_enabled'] ) ? (int) $_GET['integration_auto_enabled'] === 1 : false;
10731 $current_currency = ebook_store_get_store_currency();
10732 $current_price = isset( $_POST['wizard_price'] ) ? (string) wp_unslash( $_POST['wizard_price'] ) : '5.00';
10733 $current_product_name = isset( $_POST['wizard_product_name'] ) ? sanitize_text_field( wp_unslash( $_POST['wizard_product_name'] ) ) : '';
10734 $payment_method_state = ebook_store_get_standalone_payment_method_state();
10735 $has_ready_standalone_payment = ebook_store_has_ready_standalone_payment_method();
10736 $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' : '' );
10737 $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' : '' );
10738 $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' : '' );
10739 $current_wizard_paypal_account = isset( $_POST['wizard_paypal_account'] ) ? sanitize_email( wp_unslash( $_POST['wizard_paypal_account'] ) ) : (string) get_option( 'paypal_account', '' );
10740 $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', '' );
10741 $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', '' );
10742 $current_wizard_adyen_environment = isset( $_POST['wizard_adyen_environment'] ) ? sanitize_key( (string) wp_unslash( $_POST['wizard_adyen_environment'] ) ) : (string) get_option( 'adyen_environment', 'test' );
10743 $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', '' );
10744 $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', '' );
10745 $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', '' );
10746 $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', '' );
10747
10748 ob_start();
10749 ?>
10750 <div class="wrap ebook-setup-wizard">
10751 <div class="ebook-setup-wizard__hero">
10752 <div class="ebook-setup-wizard__hero-copy">
10753 <span class="ebook-setup-wizard__eyebrow"><?php esc_html_e( 'New onboarding flow', 'ebook-store' ); ?></span>
10754 <h1><?php esc_html_e( 'Setup wizard', 'ebook-store' ); ?></h1>
10755 <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>
10756 </div>
10757 <div class="ebook-setup-wizard__hero-meta">
10758 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Creates draft items', 'ebook-store' ); ?></span>
10759 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Extracts title and summary', 'ebook-store' ); ?></span>
10760 <span class="ebook-setup-wizard__pill"><?php esc_html_e( 'Builds first-page cover', 'ebook-store' ); ?></span>
10761 </div>
10762 </div>
10763
10764 <?php if ( ! empty( $errors ) ) : ?>
10765 <div class="notice notice-error">
10766 <p><strong><?php esc_html_e( 'The wizard could not finish yet.', 'ebook-store' ); ?></strong></p>
10767 <ul class="ebook-setup-wizard__notice-list">
10768 <?php foreach ( $errors as $error ) : ?>
10769 <li><?php echo esc_html( $error ); ?></li>
10770 <?php endforeach; ?>
10771 </ul>
10772 </div>
10773 <?php endif; ?>
10774
10775 <?php if ( $wizard_created && $ebook_id > 0 ) : ?>
10776 <div class="notice notice-success is-dismissible">
10777 <p>
10778 <strong><?php esc_html_e( 'Setup complete.', 'ebook-store' ); ?></strong>
10779 <?php
10780 printf(
10781 esc_html__( '%1$s was created as a draft so you can review it before going live.', 'ebook-store' ),
10782 esc_html( ebook_store_setup_wizard_human_mode_label( $wizard_mode ) )
10783 );
10784 ?>
10785 </p>
10786 <p class="ebook-setup-wizard__success-links">
10787 <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>
10788 <?php if ( $product_id > 0 ) : ?>
10789 <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>
10790 <?php endif; ?>
10791 <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>
10792 </p>
10793 <?php if ( $product_id > 0 && $integration_enabled === false ) : ?>
10794 <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>
10795 <?php elseif ( $product_id > 0 && $integration_auto_enabled ) : ?>
10796 <p class="description"><?php esc_html_e( 'WooCommerce integration was enabled automatically so the linked product can use Ebook Store delivery.', 'ebook-store' ); ?></p>
10797 <?php endif; ?>
10798 </div>
10799 <?php endif; ?>
10800
10801 <?php if ( $wizard_mode === 'standalone' ) : ?>
10802 <div class="ebook-setup-wizard__panel">
10803 <div class="ebook-setup-wizard__panel-header">
10804 <div>
10805 <h2><?php esc_html_e( 'Standalone ebook sales', 'ebook-store' ); ?></h2>
10806 <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>
10807 </div>
10808 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
10809 </div>
10810 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
10811 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
10812 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
10813 <input type="hidden" name="wizard_mode" value="standalone" />
10814 <div class="ebook-setup-wizard__grid">
10815 <div class="ebook-setup-wizard__field">
10816 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
10817 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
10818 <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>
10819 </div>
10820 <div class="ebook-setup-wizard__field">
10821 <label for="wizard_price"><?php esc_html_e( 'Selling price', 'ebook-store' ); ?></label>
10822 <div class="ebook-setup-wizard__input-group">
10823 <span><?php echo esc_html( $current_currency ); ?></span>
10824 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
10825 </div>
10826 <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>
10827 </div>
10828 </div>
10829 <div class="ebook-setup-wizard__gateway-panel">
10830 <div class="ebook-setup-wizard__gateway-header">
10831 <h3><?php esc_html_e( 'Payment method setup', 'ebook-store' ); ?></h3>
10832 <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>
10833 <?php if ( ! $has_ready_standalone_payment && (float) $current_price > 0 ) : ?>
10834 <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>
10835 <?php endif; ?>
10836 </div>
10837 <div class="ebook-settings-toggle-grid ebook-settings-toggle-grid--wizard">
10838 <label class="ebook-settings-toggle-card">
10839 <input type="hidden" name="wizard_enable_paypal" value="" />
10840 <input type="checkbox" name="wizard_enable_paypal" value="1" <?php checked( $current_wizard_paypal_enabled, '1' ); ?> />
10841 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
10842 <span class="ebook-settings-toggle-card__content">
10843 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable PayPal', 'ebook-store' ); ?></span>
10844 <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>
10845 </span>
10846 </label>
10847 <label class="ebook-settings-toggle-card">
10848 <input type="hidden" name="wizard_enable_stripe" value="" />
10849 <input type="checkbox" name="wizard_enable_stripe" value="1" <?php checked( $current_wizard_stripe_enabled, '1' ); ?> />
10850 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
10851 <span class="ebook-settings-toggle-card__content">
10852 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Stripe', 'ebook-store' ); ?></span>
10853 <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>
10854 </span>
10855 </label>
10856 <label class="ebook-settings-toggle-card">
10857 <input type="hidden" name="wizard_enable_adyen" value="" />
10858 <input type="checkbox" name="wizard_enable_adyen" value="1" <?php checked( $current_wizard_adyen_enabled, '1' ); ?> />
10859 <span class="ebook-settings-toggle-card__control" aria-hidden="true"></span>
10860 <span class="ebook-settings-toggle-card__content">
10861 <span class="ebook-settings-toggle-card__title"><?php esc_html_e( 'Enable Adyen', 'ebook-store' ); ?></span>
10862 <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>
10863 </span>
10864 </label>
10865 </div>
10866 <div class="ebook-setup-wizard__grid">
10867 <div class="ebook-setup-wizard__field">
10868 <label for="wizard_paypal_account"><?php esc_html_e( 'PayPal account email', 'ebook-store' ); ?></label>
10869 <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' ); ?>" />
10870 <p class="description"><?php esc_html_e( 'Required when PayPal is enabled. Payments are sent to this PayPal account.', 'ebook-store' ); ?></p>
10871 </div>
10872 <div class="ebook-setup-wizard__field">
10873 <label for="wizard_stripe_publishable_key"><?php esc_html_e( 'Stripe publishable key', 'ebook-store' ); ?></label>
10874 <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" />
10875 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
10876 </div>
10877 <div class="ebook-setup-wizard__field">
10878 <label for="wizard_stripe_secret_key"><?php esc_html_e( 'Stripe secret key', 'ebook-store' ); ?></label>
10879 <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" />
10880 <p class="description"><?php esc_html_e( 'Required when Stripe is enabled.', 'ebook-store' ); ?></p>
10881 </div>
10882 <div class="ebook-setup-wizard__field">
10883 <label for="wizard_adyen_environment"><?php esc_html_e( 'Adyen environment', 'ebook-store' ); ?></label>
10884 <select id="wizard_adyen_environment" name="wizard_adyen_environment">
10885 <option value="test" <?php selected( $current_wizard_adyen_environment, 'test' ); ?>><?php esc_html_e( 'Test', 'ebook-store' ); ?></option>
10886 <option value="live" <?php selected( $current_wizard_adyen_environment, 'live' ); ?>><?php esc_html_e( 'Live', 'ebook-store' ); ?></option>
10887 </select>
10888 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
10889 </div>
10890 <div class="ebook-setup-wizard__field">
10891 <label for="wizard_adyen_merchant_account"><?php esc_html_e( 'Adyen merchant account', 'ebook-store' ); ?></label>
10892 <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" />
10893 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
10894 </div>
10895 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
10896 <label for="wizard_adyen_api_key"><?php esc_html_e( 'Adyen API key', 'ebook-store' ); ?></label>
10897 <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" />
10898 <p class="description"><?php esc_html_e( 'Required when Adyen is enabled.', 'ebook-store' ); ?></p>
10899 </div>
10900 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
10901 <label for="wizard_adyen_hmac_key"><?php esc_html_e( 'Adyen webhook HMAC key', 'ebook-store' ); ?></label>
10902 <input type="text" id="wizard_adyen_hmac_key" name="wizard_adyen_hmac_key" value="<?php echo esc_attr( $current_wizard_adyen_hmac_key ); ?>" placeholder="base64-hmac-key" class="ebook-setup-wizard__code-input" />
10903 <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>
10904 </div>
10905 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
10906 <label for="wizard_adyen_live_prefix"><?php esc_html_e( 'Adyen live URL prefix', 'ebook-store' ); ?></label>
10907 <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" />
10908 <p class="description"><?php esc_html_e( 'Required only when Adyen is enabled in Live mode.', 'ebook-store' ); ?></p>
10909 </div>
10910 </div>
10911 </div>
10912 <div class="ebook-setup-wizard__actions">
10913 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create ebook draft', 'ebook-store' ); ?></button>
10914 </div>
10915 </form>
10916 </div>
10917 <?php elseif ( $wizard_mode === 'woocommerce' ) : ?>
10918 <div class="ebook-setup-wizard__panel">
10919 <div class="ebook-setup-wizard__panel-header">
10920 <div>
10921 <h2><?php esc_html_e( 'WooCommerce integration', 'ebook-store' ); ?></h2>
10922 <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>
10923 </div>
10924 <a class="button button-secondary" href="<?php echo esc_url( ebook_store_get_setup_wizard_url() ); ?>"><?php esc_html_e( 'Back', 'ebook-store' ); ?></a>
10925 </div>
10926 <form method="post" enctype="multipart/form-data" class="ebook-setup-wizard__form">
10927 <?php wp_nonce_field( 'ebook_store_setup_wizard' ); ?>
10928 <input type="hidden" name="ebook_store_setup_wizard_action" value="1" />
10929 <input type="hidden" name="wizard_mode" value="woocommerce" />
10930 <div class="ebook-setup-wizard__grid">
10931 <div class="ebook-setup-wizard__field">
10932 <label for="wizard_product_name"><?php esc_html_e( 'Product name', 'ebook-store' ); ?></label>
10933 <input type="text" id="wizard_product_name" name="wizard_product_name" value="<?php echo esc_attr( $current_product_name ); ?>" required />
10934 <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>
10935 </div>
10936 <div class="ebook-setup-wizard__field">
10937 <label for="wizard_price"><?php esc_html_e( 'Product price', 'ebook-store' ); ?></label>
10938 <div class="ebook-setup-wizard__input-group">
10939 <span><?php echo esc_html( $current_currency ); ?></span>
10940 <input type="number" id="wizard_price" name="wizard_price" min="0" step="0.01" value="<?php echo esc_attr( $current_price ); ?>" required />
10941 </div>
10942 <p class="description"><?php esc_html_e( 'The same amount is written to the Ebook Store item and the WooCommerce product.', 'ebook-store' ); ?></p>
10943 </div>
10944 <div class="ebook-setup-wizard__field ebook-setup-wizard__field--full">
10945 <label for="wizard_pdf_file"><?php esc_html_e( 'PDF file', 'ebook-store' ); ?></label>
10946 <input type="file" id="wizard_pdf_file" name="wizard_pdf_file" accept="application/pdf,.pdf" required />
10947 <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>
10948 </div>
10949 </div>
10950 <div class="ebook-setup-wizard__actions">
10951 <button type="submit" class="button button-primary button-hero"><?php esc_html_e( 'Create linked draft items', 'ebook-store' ); ?></button>
10952 </div>
10953 </form>
10954 </div>
10955 <?php else : ?>
10956 <div class="ebook-setup-wizard__chooser">
10957 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'standalone' ) ) ); ?>">
10958 <span class="dashicons dashicons-book-alt"></span>
10959 <h2><?php esc_html_e( 'Sell with Ebook Store', 'ebook-store' ); ?></h2>
10960 <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>
10961 <strong><?php esc_html_e( 'Upload a PDF and generate the ebook item automatically', 'ebook-store' ); ?></strong>
10962 </a>
10963 <a class="ebook-setup-wizard__choice" href="<?php echo esc_url( ebook_store_get_setup_wizard_url( array( 'wizard_mode' => 'woocommerce' ) ) ); ?>">
10964 <span class="dashicons dashicons-cart"></span>
10965 <h2><?php esc_html_e( 'Use WooCommerce integration', 'ebook-store' ); ?></h2>
10966 <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>
10967 <strong><?php esc_html_e( 'Create and link both items in one guided flow', 'ebook-store' ); ?></strong>
10968 </a>
10969 </div>
10970 <?php endif; ?>
10971 </div>
10972 <?php
10973 $output = ob_get_clean();
10974 echo function_exists( 'ebook_store_translate_markup' ) ? ebook_store_translate_markup( $output ) : $output;
10975 }
10976
10977 function ebook_store_get_import_books_url() {
10978 return admin_url( 'edit.php?post_type=ebook&page=ebook-store-import-books' );
10979 }
10980
10981 function ebook_store_import_books_has_pro_access() {
10982 if ( function_exists( 'ebook_store_has_pro_license' ) ) {
10983 return ebook_store_has_pro_license();
10984 }
10985
10986 return trim( (string) get_option( 'ebook_store_license_key', '' ) ) !== '';
10987 }
10988
10989 function ebook_store_import_books_admin_assets() {
10990 if ( ! is_admin() ) {
10991 return;
10992 }
10993
10994 $page = isset( $_GET['page'] ) ? sanitize_key( (string) wp_unslash( $_GET['page'] ) ) : '';
10995 if ( $page !== 'ebook-store-import-books' ) {
10996 return;
10997 }
10998
10999 wp_enqueue_style( 'ebookstorestylesheet' );
11000 wp_enqueue_script( 'plupload-all' );
11001 wp_enqueue_script(
11002 'ebook-store-import-books',
11003 plugins_url( 'js/ebook_store_import_books.js', __FILE__ ),
11004 array( 'jquery', 'plupload-all' ),
11005 filemtime( plugin_dir_path( __FILE__ ) . 'js/ebook_store_import_books.js' ),
11006 true
11007 );
11008 wp_localize_script(
11009 'ebook-store-import-books',
11010 'ebookStoreImportBooks',
11011 array(
11012 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
11013 'nonce' => wp_create_nonce( 'ebook_store_import_books' ),
11014 'sessionId' => wp_generate_uuid4(),
11015 'hasProLicense' => ebook_store_import_books_has_pro_access(),
11016 'woocommerceAvailable' => class_exists( 'WooCommerce' ),
11017 'maxFileSize' => (string) wp_max_upload_size() . 'b',
11018 'strings' => array(
11019 'dropActive' => __( 'Drop PDFs to add them to the import queue', 'ebook-store' ),
11020 'dropIdle' => __( 'Drop PDF files here or use the button below', 'ebook-store' ),
11021 'uploading' => __( 'Uploading...', 'ebook-store' ),
11022 'uploaded' => __( 'Uploaded', 'ebook-store' ),
11023 'queued' => __( 'Queued for import', 'ebook-store' ),
11024 'importing' => __( 'Creating draft items...', 'ebook-store' ),
11025 'imported' => __( 'Imported', 'ebook-store' ),
11026 'failed' => __( 'Failed', 'ebook-store' ),
11027 'invalidMode' => __( 'Choose how imported books should be created before starting the import.', 'ebook-store' ),
11028 'invalidPrice' => __( 'Enter a valid default price before starting the import.', 'ebook-store' ),
11029 'noUploadedFiles' => __( 'Upload at least one PDF before starting the import.', 'ebook-store' ),
11030 'proRequired' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ),
11031 'woocommerceMissing' => __( 'WooCommerce is not active, so the connected product option is not available.', 'ebook-store' ),
11032 'titleFallback' => __( 'Imported ebook', 'ebook-store' ),
11033 'descriptionFallback' => __( 'No description could be extracted from the PDF.', 'ebook-store' ),
11034 'ebookLink' => __( 'Edit ebook', 'ebook-store' ),
11035 'productLink' => __( 'Edit product', 'ebook-store' ),
11036 'completedSummary' => __( 'Import finished.', 'ebook-store' ),
11037 'processingSummary' => __( 'Importing uploaded books...', 'ebook-store' ),
11038 ),
11039 )
11040 );
11041 }
11042
11043 function ebook_store_import_books_get_temp_root() {
11044 return trailingslashit( get_temp_dir() ) . 'ebook-store-imports';
11045 }
11046
11047 function ebook_store_import_books_get_session_dir( $user_id, $session_id ) {
11048 $user_id = max( 0, (int) $user_id );
11049 $session_id = preg_replace( '/[^a-z0-9_-]/i', '', (string) $session_id );
11050
11051 return trailingslashit( ebook_store_import_books_get_temp_root() ) . $user_id . '/' . $session_id;
11052 }
11053
11054 function ebook_store_import_books_ensure_directory( $dir ) {
11055 if ( $dir === '' ) {
11056 return false;
11057 }
11058
11059 if ( file_exists( $dir ) ) {
11060 return is_dir( $dir );
11061 }
11062
11063 return wp_mkdir_p( $dir );
11064 }
11065
11066 function ebook_store_import_books_cleanup_empty_dirs( $dir, $stop_dir ) {
11067 $dir = untrailingslashit( (string) $dir );
11068 $stop_dir = untrailingslashit( (string) $stop_dir );
11069
11070 while ( $dir !== '' && strpos( $dir, $stop_dir ) === 0 && $dir !== $stop_dir ) {
11071 $entries = @scandir( $dir );
11072 if ( ! is_array( $entries ) || count( $entries ) > 2 ) {
11073 break;
11074 }
11075 @rmdir( $dir );
11076 $dir = dirname( $dir );
11077 }
11078 }
11079
11080 function ebook_store_import_books_store_queue_item( $token, $data ) {
11081 $user_id = get_current_user_id();
11082 set_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token, $data, DAY_IN_SECONDS );
11083 }
11084
11085 function ebook_store_import_books_get_queue_item( $token ) {
11086 $user_id = get_current_user_id();
11087 return get_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11088 }
11089
11090 function ebook_store_import_books_delete_queue_item( $token ) {
11091 $user_id = get_current_user_id();
11092 delete_transient( 'ebook_store_import_queue_' . $user_id . '_' . $token );
11093 }
11094
11095 function ebook_store_import_books_ajax_upload() {
11096 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11097
11098 if ( ! current_user_can( 'manage_options' ) ) {
11099 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11100 }
11101
11102 if ( ! ebook_store_import_books_has_pro_access() ) {
11103 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11104 }
11105
11106 if ( empty( $_FILES['file']['tmp_name'] ) ) {
11107 wp_send_json_error( array( 'message' => __( 'No upload chunk was received.', 'ebook-store' ) ), 400 );
11108 }
11109
11110 $session_id = isset( $_REQUEST['session'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['session'] ) ) : '';
11111 $file_id = isset( $_REQUEST['file_id'] ) ? preg_replace( '/[^a-z0-9_-]/i', '', (string) wp_unslash( $_REQUEST['file_id'] ) ) : '';
11112 $original_name = isset( $_REQUEST['name'] ) ? sanitize_file_name( (string) wp_unslash( $_REQUEST['name'] ) ) : sanitize_file_name( (string) $_FILES['file']['name'] );
11113 $chunk = isset( $_REQUEST['chunk'] ) ? max( 0, (int) $_REQUEST['chunk'] ) : 0;
11114 $chunks = isset( $_REQUEST['chunks'] ) ? max( 0, (int) $_REQUEST['chunks'] ) : 0;
11115
11116 if ( $session_id === '' || $file_id === '' || $original_name === '' ) {
11117 wp_send_json_error( array( 'message' => __( 'The upload request is missing required file information.', 'ebook-store' ) ), 400 );
11118 }
11119
11120 $filetype = wp_check_filetype( $original_name );
11121 $extension = isset( $filetype['ext'] ) ? strtolower( (string) $filetype['ext'] ) : '';
11122 if ( $extension !== 'pdf' ) {
11123 wp_send_json_error( array( 'message' => __( 'Only PDF files can be imported here.', 'ebook-store' ) ), 400 );
11124 }
11125
11126 $session_dir = ebook_store_import_books_get_session_dir( get_current_user_id(), $session_id );
11127 if ( ! ebook_store_import_books_ensure_directory( $session_dir ) ) {
11128 wp_send_json_error( array( 'message' => __( 'The temporary upload directory could not be created.', 'ebook-store' ) ), 500 );
11129 }
11130
11131 $stored_name = sanitize_file_name( $file_id . '-' . $original_name );
11132 $partial_path = trailingslashit( $session_dir ) . $stored_name . '.part';
11133 $final_path = trailingslashit( $session_dir ) . $stored_name;
11134 $chunk_path = $_FILES['file']['tmp_name'];
11135
11136 $input_handle = @fopen( $chunk_path, 'rb' );
11137 if ( ! $input_handle ) {
11138 wp_send_json_error( array( 'message' => __( 'The uploaded file chunk could not be read.', 'ebook-store' ) ), 500 );
11139 }
11140
11141 $output_handle = @fopen( $partial_path, $chunk === 0 ? 'wb' : 'ab' );
11142 if ( ! $output_handle ) {
11143 @fclose( $input_handle );
11144 wp_send_json_error( array( 'message' => __( 'The temporary upload file could not be written.', 'ebook-store' ) ), 500 );
11145 }
11146
11147 while ( ! feof( $input_handle ) ) {
11148 $buffer = fread( $input_handle, 1048576 );
11149 if ( $buffer === false ) {
11150 break;
11151 }
11152 fwrite( $output_handle, $buffer );
11153 }
11154
11155 @fclose( $input_handle );
11156 @fclose( $output_handle );
11157
11158 $is_last_chunk = $chunks < 2 || ( $chunk + 1 ) >= $chunks;
11159 if ( ! $is_last_chunk ) {
11160 wp_send_json_success(
11161 array(
11162 'fileId' => $file_id,
11163 'status' => 'chunked',
11164 )
11165 );
11166 }
11167
11168 @unlink( $final_path );
11169 if ( ! @rename( $partial_path, $final_path ) ) {
11170 wp_send_json_error( array( 'message' => __( 'The uploaded PDF could not be finalized.', 'ebook-store' ) ), 500 );
11171 }
11172
11173 $pdf_file = array(
11174 'name' => $original_name,
11175 'tmp_name' => $final_path,
11176 );
11177 if ( ! ebook_store_setup_wizard_is_pdf( $pdf_file ) ) {
11178 @unlink( $final_path );
11179 wp_send_json_error( array( 'message' => __( 'The uploaded file is not a valid PDF.', 'ebook-store' ) ), 400 );
11180 }
11181
11182 $fallback_title = preg_replace( '/\.[^.]+$/', '', $original_name );
11183 $pdf_details = ebook_store_setup_wizard_extract_pdf_details( $final_path, $fallback_title );
11184 $token = str_replace( '-', '', wp_generate_uuid4() );
11185 $filesize = file_exists( $final_path ) ? (int) filesize( $final_path ) : 0;
11186
11187 ebook_store_import_books_store_queue_item(
11188 $token,
11189 array(
11190 'token' => $token,
11191 'file_id' => $file_id,
11192 'session_id' => $session_id,
11193 'file_path' => $final_path,
11194 'file_name' => $original_name,
11195 'file_size' => $filesize,
11196 'details' => $pdf_details,
11197 'uploaded_at' => time(),
11198 )
11199 );
11200
11201 wp_send_json_success(
11202 array(
11203 'fileId' => $file_id,
11204 'token' => $token,
11205 'fileName' => $original_name,
11206 'fileSize' => $filesize,
11207 'title' => isset( $pdf_details['title'] ) ? (string) $pdf_details['title'] : '',
11208 'description' => isset( $pdf_details['description'] ) ? (string) $pdf_details['description'] : '',
11209 'author' => isset( $pdf_details['author'] ) ? (string) $pdf_details['author'] : '',
11210 'pages' => isset( $pdf_details['pages'] ) ? (int) $pdf_details['pages'] : 0,
11211 )
11212 );
11213 }
11214
11215 function ebook_store_import_books_ajax_process() {
11216 check_ajax_referer( 'ebook_store_import_books', 'nonce' );
11217
11218 if ( ! current_user_can( 'manage_options' ) ) {
11219 wp_send_json_error( array( 'message' => __( 'You do not have permission to import books.', 'ebook-store' ) ), 403 );
11220 }
11221
11222 if ( ! ebook_store_import_books_has_pro_access() ) {
11223 wp_send_json_error( array( 'message' => __( 'Import books is available in Ebook Store Pro only.', 'ebook-store' ) ), 403 );
11224 }
11225
11226 $token = isset( $_POST['token'] ) ? sanitize_key( (string) wp_unslash( $_POST['token'] ) ) : '';
11227 $mode = isset( $_POST['import_mode'] ) ? sanitize_key( (string) wp_unslash( $_POST['import_mode'] ) ) : 'ebook';
11228 $import_price = isset( $_POST['import_price'] ) && is_scalar( $_POST['import_price'] ) ? (float) str_replace( ',', '.', (string) wp_unslash( $_POST['import_price'] ) ) : 0.0;
11229 $import_price = max( 0, $import_price );
11230
11231 if ( $token === '' ) {
11232 wp_send_json_error( array( 'message' => __( 'The uploaded file token is missing.', 'ebook-store' ) ), 400 );
11233 }
11234
11235 if ( ! in_array( $mode, array( 'ebook', 'woocommerce' ), true ) ) {
11236 wp_send_json_error( array( 'message' => __( 'The selected import mode is invalid.', 'ebook-store' ) ), 400 );
11237 }
11238
11239 $queue_item = ebook_store_import_books_get_queue_item( $token );
11240 if ( ! is_array( $queue_item ) || empty( $queue_item['file_path'] ) ) {
11241 wp_send_json_error( array( 'message' => __( 'The uploaded file could not be found. Please upload it again.', 'ebook-store' ) ), 404 );
11242 }
11243
11244 $file_path = (string) $queue_item['file_path'];
11245 if ( ! file_exists( $file_path ) ) {
11246 ebook_store_import_books_delete_queue_item( $token );
11247 wp_send_json_error( array( 'message' => __( 'The uploaded PDF is no longer available. Please upload it again.', 'ebook-store' ) ), 404 );
11248 }
11249
11250 $details = isset( $queue_item['details'] ) && is_array( $queue_item['details'] ) ? $queue_item['details'] : array();
11251 $file_name = isset( $queue_item['file_name'] ) ? (string) $queue_item['file_name'] : __( 'uploaded PDF', 'ebook-store' );
11252 $fallback_title = preg_replace( '/\.[^.]+$/', '', (string) $queue_item['file_name'] );
11253 $title = isset( $details['title'] ) && $details['title'] !== '' ? (string) $details['title'] : $fallback_title;
11254 $description = isset( $details['description'] ) ? (string) $details['description'] : '';
11255
11256 $shared_args = array(
11257 'title' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11258 'product_name' => $title !== '' ? $title : __( 'Imported ebook', 'ebook-store' ),
11259 'description' => $description,
11260 'price' => $import_price,
11261 'currency' => ebook_store_get_store_currency(),
11262 'pdf_file' => array(
11263 'name' => $file_name,
11264 'tmp_name' => $file_path,
11265 ),
11266 'pages' => isset( $details['pages'] ) ? (int) $details['pages'] : 0,
11267 'author' => isset( $details['author'] ) ? (string) $details['author'] : '',
11268 );
11269
11270 try {
11271 $result = $mode === 'woocommerce'
11272 ? ebook_store_setup_wizard_create_woocommerce_bundle( $shared_args )
11273 : ebook_store_setup_wizard_create_ebook( $shared_args );
11274 } catch ( Throwable $throwable ) {
11275 wp_send_json_error(
11276 array(
11277 'message' => sprintf(
11278 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
11279 $file_name,
11280 $throwable->getMessage()
11281 ),
11282 ),
11283 500
11284 );
11285 }
11286
11287 if ( is_wp_error( $result ) ) {
11288 wp_send_json_error(
11289 array(
11290 'message' => sprintf(
11291 __( 'Import failed for "%1$s": %2$s', 'ebook-store' ),
11292 $file_name,
11293 $result->get_error_message()
11294 ),
11295 ),
11296 400
11297 );
11298 }
11299
11300 ebook_store_import_books_delete_queue_item( $token );
11301 @unlink( $file_path );
11302 ebook_store_import_books_cleanup_empty_dirs( dirname( $file_path ), ebook_store_import_books_get_temp_root() );
11303
11304 wp_send_json_success(
11305 array(
11306 'title' => $shared_args['title'],
11307 'ebookId' => isset( $result['ebook_id'] ) ? (int) $result['ebook_id'] : 0,
11308 'ebookEditUrl' => ! empty( $result['ebook_id'] ) ? get_edit_post_link( (int) $result['ebook_id'], 'raw' ) : '',
11309 'productId' => ! empty( $result['product_id'] ) ? (int) $result['product_id'] : 0,
11310 'productEditUrl' => ! empty( $result['product_id'] ) ? get_edit_post_link( (int) $result['product_id'], 'raw' ) : '',
11311 )
11312 );
11313 }
11314
11315 function ebook_store_register_my_custom_submenu_page() {
11316
11317 add_submenu_page(
11318 'edit.php?post_type=ebook' //or 'options.php'
11319 , __('Orders - Add New', 'ebook-store')
11320 , __('Orders - Add New', 'ebook-store')
11321 , 'manage_options'
11322 , 'ebook-store-add-order-page'
11323 , 'ebook_store_add_order_page_callback'
11324 );
11325
11326 add_submenu_page(
11327 'edit.php?post_type=ebook' //or 'options.php'
11328 , __('Orders - Reports', 'ebook-store')
11329 , __('Orders - Reports', 'ebook-store')
11330 , 'manage_options'
11331 , 'ebook-store-order_reports'
11332 , 'ebook_store_order_reports_callback'
11333 );
11334
11335 add_submenu_page(
11336 'edit.php?post_type=ebook' //or 'options.php'
11337 , __('Orders - New Issue', 'ebook-store')
11338 , __('Orders - New Issue', 'ebook-store')
11339 , 'manage_options'
11340 , 'ebook-store-add-issue-page'
11341 , 'ebook_store_add_issue_page_callback'
11342 );
11343
11344 add_submenu_page(
11345 'edit.php?post_type=ebook',
11346 __( 'Setup wizard', 'ebook-store' ),
11347 ebook_store_setup_wizard_menu_title(),
11348 'manage_options',
11349 'ebook-store-setup-wizard',
11350 'ebook_store_setup_wizard_page_callback'
11351 );
11352
11353 add_submenu_page(
11354 'edit.php?post_type=ebook',
11355 __( 'Import books', 'ebook-store' ),
11356 __( 'Import books', 'ebook-store' ),
11357 'manage_options',
11358 'ebook-store-import-books',
11359 'ebook_store_import_books_page_callback'
11360 );
11361
11362 add_submenu_page(
11363 'edit.php?post_type=ebook',
11364 __( 'Button templates', 'ebook-store' ),
11365 __( 'Button templates', 'ebook-store' ),
11366 'manage_options',
11367 'ebook-store-template-editor',
11368 'ebook_store_template_editor_page_callback'
11369 );
11370
11371 if ( ! ebook_store_can_access_template_editor() ) {
11372 remove_submenu_page( 'edit.php?post_type=ebook', 'ebook-store-template-editor' );
11373 }
11374
11375 add_submenu_page(
11376 'edit.php?post_type=ebook',
11377 'Settings',
11378 __('Settings', 'ebook-store'),
11379 'manage_options',
11380 'settings-page',
11381 'ebook_store_settings_callback' );
11382
11383 }
11384 function ebook_store_settings_callback() {
11385 echo '<script>window.location = "options-general.php?page=ebook_options.php"; </script>';
11386 }
11387 function ebook_store_add_query_vars_filter( $vars ){
11388 $vars[] = "new_issue_purchase_period";
11389 $vars[] = "new_issue_ebook_id";
11390 $vars[] = "woocommerce_product_id";
11391 return $vars;
11392 }
11393
11394 //Add custom query vars
11395
11396 function ebook_store_add_issue_page_callback() {
11397 require_once 'ebook_store_add_issue_page_callback.php';
11398
11399 }
11400
11401 function ebook_store_import_books_page_callback() {
11402 require_once 'ebook_store_import_books_page_callback.php';
11403 }
11404
11405 function ebook_store_add_order_page_callback() {
11406 require_once 'ebook_store_add_order_page_callback.php';
11407 }
11408 function ebook_store_add_order($data, $silent = false, $thankYouRedirect = false, $ebook_key = false) {
11409 $QSWPOptions = new QSWPOptions();
11410
11411 if (@$ebook_key == '') {
11412 $ebook_key = md5(microtime() . rand(1,10000000));
11413 } else {
11414 // An explicit key was supplied — this is a payment webhook (Stripe/Adyen)
11415 // or a recovery call. If an order already exists for this key, a retried
11416 // or duplicated webhook is calling again: return the existing order rather
11417 // than creating a duplicate order and sending a second delivery email.
11418 $existing_order = ebook_get_order( 'ebook_key', $ebook_key );
11419 if ( ! empty( $existing_order['order_id'][0] ) ) {
11420 return $silent ? (int) $existing_order['order_id'][0] : null;
11421 }
11422 }
11423
11424 $data['md5_nonce'] = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
11425 if ( ! isset( $data['mc_fee'] ) ) {
11426 $data['mc_fee'] = 0;
11427 }
11428 $data['item_name'] = get_the_title($data['ebook']);
11429 $data['payment_date'] = date("m/d/Y H:i:s");
11430 @$data['txn_id'] = (@$data['txn_id'] != '' ? @$data['txn_id'] : 'Manual Payment');
11431 $data['mc_currency'] = ebook_store_get_store_currency();
11432 if ( ! isset( $data['tax'] ) || ! is_numeric( $data['tax'] ) ) {
11433 $data['tax'] = 0;
11434 }
11435 $data['residence_country'] = 'n/a';
11436
11437 ebook_store_subscribe_buyer_to_marketing_lists($data['payer_email']);
11438 if (@$data['user_id'] == 0) {
11439 $data['user_id'] = ebook_store_silent_registration($data);
11440 }
11441
11442
11443 $my_post = array(
11444 'post_title' => $data['first_name'] . ' ' . $data['last_name'],
11445 'post_type' => 'ebook_order',
11446 'post_status' => 'publish',
11447 'post_author' => 1,
11448 //'post_category' => array(8,39),
11449 );
11450 if ($data['mc_gross'] == false) {
11451 $data['mc_gross'] = 0;
11452 }
11453 // Store a machine-readable amount with NO thousands separator. It used
11454 // to be number_format()'d twice with a ',' separator: the second pass
11455 // re-cast "1,500.00" to a float, which PHP truncates at the comma to
11456 // 1.0 — so every order of 1000+ was recorded as 1.00. Thousands
11457 // separators belong at display time only.
11458 $mc_gross = number_format( (float) $data['mc_gross'], 2, '.', '' );
11459 $ebook = get_post_meta($data['ebook'], 'ebook', true);
11460 $ebookObj = new EbookStoreEbook($data['ebook']);
11461
11462 // Note: unlike the PayPal IPN path, every caller of this function has
11463 // already verified the payment (admin-entered order, or a signed
11464 // Stripe/Adyen webhook), so there is no untrusted amount to check here.
11465 $post_id = wp_insert_post( $my_post, true );
11466 if ( ! is_wp_error( $post_id ) && $post_id ) {
11467 foreach ($data as $k => $v) {
11468 update_post_meta($post_id, $k, $v);
11469 @$order[$k] = $v;
11470 }
11471
11472 $order['order_id'] = $post_id;
11473 $order['password'] = ebook_store_get_password_for_order($post_id, array(
11474 'password' => isset($data['password']) ? $data['password'] : '',
11475 'payer_email' => isset($data['payer_email']) ? $data['payer_email'] : '',
11476 'md5_nonce' => isset($data['md5_nonce']) ? $data['md5_nonce'] : '',
11477 'ebook_key' => $ebook_key,
11478 ));
11479 $data['password'] = $order['password'];
11480
11481 global $attachment, $ebook_email_delivery;
11482 $attachment = ebook_attachment($data['ebook'],true);
11483
11484 $formData = ebook_store_get_form($data['md5_nonce']);
11485 $formData = json_encode($formData);
11486 $ebook_order['ebook_key'][0] = $ebook_key;
11487 $ebook_order['ebook'][0] = $data['ebook'];
11488 $ebook_order['order_id'][0] = $post_id;
11489 $ebook_order['md5_nonce'][0] = isset( $data['md5_nonce'] ) ? $data['md5_nonce'] : '';
11490 $order['downloadlink'] = ebook_download_link($ebook_order);
11491
11492 $ebookObj->order_id = $post_id;
11493 // Plugin order: link() needs the ebook_key credentials or the
11494 // non-PDF format links point at the WooCommerce-only endpoint.
11495 $ebookObj->ebook_key = $ebook_key;
11496 $ebookObj->md5_nonce = isset( $data['md5_nonce'] ) ? (string) $data['md5_nonce'] : '';
11497 $ebookObj->setLink['pdf'] = $order['downloadlink'];
11498
11499 $order['downloadlink_html'] = $ebookObj->format_links();
11500 $ebook_order['downloadlink_html'][0] = $ebookObj->format_links();
11501
11502 $order['ebook_key'] = $ebook_key;
11503 $order['order_id'] = $post_id;
11504 $order['md5_nonce'] = $data['md5_nonce'];
11505 update_post_meta($post_id,'ebook_key',$ebook_key);
11506 update_post_meta($post_id,'downloads',0);
11507 update_post_meta($post_id,'mc_gross',$mc_gross);
11508 update_post_meta($post_id,'formData',wp_slash($formData));
11509 update_post_meta($post_id,'downloadlink',$order['downloadlink']);
11510 update_post_meta($post_id,'ebook',$data['ebook']);
11511 //error_log('ebook_store_add_order - ' . print_r($order,true));
11512
11513 $email_template = ebook_store_get_effective_delivery_email_template( absint( $data['ebook'] ) );
11514 $ebook_email_delivery = array(
11515 'to' => $data['payer_email'],
11516 'subject' => $email_template['subject'],
11517 'text' => $email_template['text'],
11518 'attachment' => $attachment,
11519 'order' => $order,
11520 'source' => 'standalone',
11521 'related_order_type' => 'ebook_order',
11522 );
11523 //mail(get_option( 'admin_email' ), 'Ebook store for WordPress - Verified Order Received', print_r($ebook_email_delivery,true));
11524 @$fileExt = pathinfo($attachment[0]['file'],PATHINFO_EXTENSION);
11525 //wp_mail('deian@motov.net','test','test');
11526 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
11527 ebook_encrypt_pdf($data, null, $data);
11528 }
11529 if ($silent == false) {
11530 ?>
11531 <div class="updated">
11532 <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>
11533 </div><?php
11534 } ebook_email_delivery($post_id);
11535
11536 add_action( 'init', 'ebook_email_delivery', 100);
11537 if ($thankYouRedirect) {
11538 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
11539 $link = add_query_arg(array('ebook_key' => $ebook_key, 'action' => 'thank_you'),get_permalink($checkoutPage));
11540 header("Location: $link");
11541 }
11542 if ($silent) {
11543 return $post_id;
11544 }
11545 //error_log('ebook_email_delivery added to plugins_loaded');
11546 }
11547 }
11548
11549
11550
11551
11552
11553 function erl($var) {
11554 if (is_array($var)) {
11555 $varOut = print_r($varOut,true);
11556 } else {
11557 $varOut = $var;
11558 }
11559 error_log(debug_backtrace()[1]['function']);
11560 error_log($varOut);
11561 }
11562
11563
11564 function file_upload_max_size() {
11565 static $max_size = -1;
11566
11567 if ($max_size < 0) {
11568 // Start with post_max_size.
11569 $max_size = parse_size(ini_get('post_max_size'));
11570
11571 // If upload_max_size is less, then reduce. Except if upload_max_size is
11572 // zero, which indicates no limit.
11573 $upload_max = parse_size(ini_get('upload_max_filesize'));
11574 if ($upload_max > 0 && $upload_max < $max_size) {
11575 $max_size = $upload_max;
11576 }
11577 }
11578 return $max_size;
11579 }
11580
11581 function parse_size($size) {
11582 $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
11583 $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
11584 if ($unit) {
11585 // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
11586 return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
11587 }
11588 else {
11589 return round($size);
11590 }
11591 }
11592
11593
11594 function register_ebook_store_woocommerce_type() {
11595 /**
11596 * This should be in its own separate file.
11597 */
11598 // if (class_exists('WC_Product_Simple')) {
11599 // class WC_Product_Ebook_Store extends WC_Product_Simple {
11600
11601 // public function __construct( $product ) {
11602
11603 // $this->product_type = 'ebook_store';
11604
11605 // parent::__construct( $product );
11606
11607 // }
11608 // public function get_type() {
11609 // return 'ebook_store';
11610 // }
11611
11612 // }
11613 // }
11614 if (class_exists('WC_Product')) {
11615 class WC_Product_ebookstore extends WC_Product {
11616
11617 function __construct($product) {
11618 $this->supports[] = 'ajax_add_to_cart';
11619 parent::__construct($product);
11620 $this->product_type = 'ebookstore';
11621 }
11622
11623 /**
11624 * Method to read a product from the database.
11625 * @param WC_Product
11626 */
11627
11628 public function get_product_type( $product_id ) {
11629 return 'ebookstore';
11630
11631 }
11632 public function get_type() {
11633 return 'ebookstore';
11634 }
11635 }
11636
11637
11638 }
11639
11640 }
11641 function woocommerce_custom_product_tabs_for_ebook_store( $tabs) {
11642 $tabs['ebookstore'] = array(
11643 'label' => __( 'Ebook Store', 'woocommerce' ),
11644 'target' => 'ebook_store_options',
11645 'class' => array( 'show_if_simple', 'show_if_variable', 'show_if_ebookstore' ),
11646 );
11647 return $tabs;
11648 }
11649
11650
11651
11652
11653 function add_ebook_store_item( $types ){
11654
11655 // Key should be exactly the same as in the class product_type parameter
11656 //$types[ 'ebookstore' ] = __( 'Ebook Store', 'ebook-store' );
11657
11658 return $types;
11659
11660 }
11661 add_filter( 'product_type_selector', 'add_ebook_store_item' );
11662
11663 function woocommerce_ebook_store_price_field() {
11664
11665 if ( 'product' != get_post_type() ) :
11666 //return;
11667 endif;
11668
11669 ?><script type='text/javascript'>
11670 jQuery( document ).ready( function() {
11671 jQuery( '.options_group.pricing' ).addClass( 'show_if_ebook_store' ).show();
11672 jQuery( '.general_options' ).show();
11673 jQuery('label[for="_virtual"]').show().addClass('show_if_ebook_store');
11674 jQuery('label[for="_downloadable"]').show().addClass('show_if_ebook_store');
11675
11676 });
11677 jQuery('#product-type').on('change', function () {
11678 jQuery('.general_options').show();
11679 setTimeout(function () {
11680 jQuery('label[for="_virtual"], label[for="_downloadable"]').show();
11681 }, 500);
11682 });
11683
11684 </script><?php
11685 }
11686
11687
11688
11689 /*-----------------------------------*/
11690 /**
11691 * Add a custom product tab.
11692 */
11693
11694 /**
11695 * Contents of the rental options product tab.
11696 */
11697 function ebook_store_woocommerce_tab_content() {
11698 global $post;
11699 $postOrig = $post;
11700 ?><div id='ebook_store_options' class='panel woocommerce_options_panel' style=""><?php
11701 ?><div class='options_group'>
11702 <?php
11703 if (get_option('ebook_store_woocommerce_integration') == 0) {
11704 ?>
11705 <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>
11706 <?php
11707 return true;
11708 }
11709
11710 $product = function_exists( 'wc_get_product' ) ? wc_get_product( $post->ID ) : null;
11711 if ( $product && $product->is_type( 'variable' ) ) {
11712 ?>
11713 <p class="form-field">
11714 <strong><?php esc_html_e( 'Variable product note:', 'ebook-store' ); ?></strong>
11715 <?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' ); ?>
11716 </p>
11717 <?php
11718 }
11719
11720 woocommerce_wp_select(array(
11721 'id' => 'ebook_store_book_id',
11722 'label' => __( 'Select Ebook', 'woocommerce' ),
11723 'desc_tip' => 'true',
11724 'description' => __( 'The selected ebook will be delivered to your customer. Encryption and watermarking will be handled by ebook store plugin.' ),
11725 'type' => 'text',
11726 'value' => get_post_meta(get_the_ID(), 'ebook_store_book_id', true),
11727 'options' => ebook_store_get_woocommerce_options(),
11728 ) );
11729
11730 $selected_ebook_id = (int) get_post_meta( get_the_ID(), 'ebook_store_book_id', true );
11731 if ( $selected_ebook_id > 0 ) {
11732 $selected_ebook = get_post( $selected_ebook_id );
11733 if ( $selected_ebook && $selected_ebook->post_type === 'ebook' ) {
11734 $ebook_meta = get_post_meta( $selected_ebook_id, 'ebook', true );
11735 $ebook_meta = is_array( $ebook_meta ) ? $ebook_meta : array();
11736 $ebook_cover = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_cover', true );
11737 $ebook_cover = is_array( $ebook_cover ) ? $ebook_cover : array();
11738 $ebook_pdf = get_post_meta( $selected_ebook_id, 'ebook_wp_custom_attachment_pdf', true );
11739 $ebook_pdf = is_array( $ebook_pdf ) ? $ebook_pdf : array();
11740 $ebook_template = function_exists( 'ebook_store_resolve_form_template' ) ? ebook_store_resolve_form_template( isset( $ebook_meta['form_template'] ) ? $ebook_meta['form_template'] : '' ) : 'modern';
11741 $form_templates = function_exists( 'ebook_store_get_form_templates' ) ? ebook_store_get_form_templates() : array();
11742 $template_label = isset( $form_templates[ $ebook_template ]['label'] ) ? $form_templates[ $ebook_template ]['label'] : ucfirst( $ebook_template );
11743 $delivery_mode = isset( $ebook_meta['donate_or_download'] ) ? $ebook_meta['donate_or_download'] : 'paid';
11744 $delivery_label = __( 'Paid download', 'ebook-store' );
11745 if ( $delivery_mode === 'free' ) {
11746 $delivery_label = __( 'Free download', 'ebook-store' );
11747 } elseif ( $delivery_mode === 'donate' ) {
11748 $delivery_label = __( 'Donate to download', 'ebook-store' );
11749 }
11750 $currency = isset( $ebook_meta['paypal_currency'] ) && $ebook_meta['paypal_currency'] !== '' ? $ebook_meta['paypal_currency'] : ebook_store_get_store_currency();
11751 $price = isset( $ebook_meta['ebook_price'] ) ? (float) $ebook_meta['ebook_price'] : 0.0;
11752 $pages = isset( $ebook_meta['ebook_pages'] ) ? (int) $ebook_meta['ebook_pages'] : 0;
11753 $cover_url = ! empty( $ebook_cover['url'] ) ? $ebook_cover['url'] : '';
11754 $pdf_name = ! empty( $ebook_pdf['file'] ) ? basename( $ebook_pdf['file'] ) : '';
11755 $edit_ebook_link = get_edit_post_link( $selected_ebook_id, 'raw' );
11756 ?>
11757 <div class="ebook-woocommerce-linked-ebook">
11758 <div class="ebook-woocommerce-linked-ebook__media">
11759 <div class="ebook-woocommerce-linked-ebook__cover<?php echo $cover_url ? ' has-image' : ' is-empty'; ?>">
11760 <?php if ( $cover_url ) : ?>
11761 <img src="<?php echo esc_url( $cover_url ); ?>" alt="<?php echo esc_attr( get_the_title( $selected_ebook_id ) ); ?>" />
11762 <?php else : ?>
11763 <span><?php esc_html_e( 'No cover', 'ebook-store' ); ?></span>
11764 <?php endif; ?>
11765 </div>
11766 </div>
11767 <div class="ebook-woocommerce-linked-ebook__content">
11768 <div class="ebook-woocommerce-linked-ebook__header">
11769 <div>
11770 <h4><?php echo esc_html( get_the_title( $selected_ebook_id ) ); ?></h4>
11771 <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>
11772 </div>
11773 <?php if ( $edit_ebook_link ) : ?>
11774 <a class="button button-secondary" href="<?php echo esc_url( $edit_ebook_link ); ?>"><?php esc_html_e( 'Edit ebook', 'ebook-store' ); ?></a>
11775 <?php endif; ?>
11776 </div>
11777 <div class="ebook-woocommerce-linked-ebook__meta">
11778 <span><strong><?php esc_html_e( 'Status:', 'ebook-store' ); ?></strong> <?php echo esc_html( ucfirst( get_post_status( $selected_ebook_id ) ) ); ?></span>
11779 <span><strong><?php esc_html_e( 'Price:', 'ebook-store' ); ?></strong> <?php echo esc_html( ebook_store_order_money_display( $price, $currency ) ); ?></span>
11780 <span><strong><?php esc_html_e( 'Delivery:', 'ebook-store' ); ?></strong> <?php echo esc_html( $delivery_label ); ?></span>
11781 <span><strong><?php esc_html_e( 'Template:', 'ebook-store' ); ?></strong> <?php echo esc_html( $template_label ); ?></span>
11782 <?php if ( $pages > 0 ) : ?>
11783 <span><strong><?php esc_html_e( 'Pages:', 'ebook-store' ); ?></strong> <?php echo esc_html( number_format_i18n( $pages ) ); ?></span>
11784 <?php endif; ?>
11785 <?php if ( $pdf_name !== '' ) : ?>
11786 <span><strong><?php esc_html_e( 'File:', 'ebook-store' ); ?></strong> <?php echo esc_html( $pdf_name ); ?></span>
11787 <?php endif; ?>
11788 </div>
11789 <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>
11790 </div>
11791 </div>
11792 <?php
11793 }
11794 }
11795
11796 ?></div>
11797
11798 </div><?php
11799 $post = $postOrig;
11800 }
11801 /**
11802 * Save the custom fields.
11803 */
11804 function save_woocommerce_ebook_store_data( $post_id ) {
11805
11806 $ebook_store_ebook_id = isset( $_POST['_enable_ebook_store_option'] ) ? 'yes' : 'no';
11807 update_post_meta( $post_id, '_enable_ebook_store_option', $ebook_store_ebook_id );
11808
11809 if ( isset( $_POST['ebook_store_book_id'] ) ) {
11810 //first reset the old ebook store item to 0 in case id of the assigned woocommerce prduct is changed
11811 $ebook_store_book_id = get_post_meta($post_id, 'ebook_store_book_id', true);
11812 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', 0 );
11813 //then update to the currently set it.
11814 $ebook_store_book_id = sanitize_text_field( $_POST['ebook_store_book_id'] );
11815 update_post_meta( $post_id, 'ebook_store_book_id', $ebook_store_book_id );
11816 update_post_meta( $ebook_store_book_id, 'woocommerce_product_id', $post_id );
11817 //wp_die('woocommerce_product_id - ' . get_post_meta($ebook_store_book_id, 'woocommerce_product_id', true));
11818 }
11819
11820 }
11821 function ebook_store_get_woocommerce_options() {
11822 $new = new WP_Query(
11823 array(
11824 'posts_per_page' => -1,
11825 'post_type' => 'ebook',
11826 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
11827 'orderby' => 'title',
11828 'order' => 'ASC',
11829 'no_found_rows' => true,
11830 )
11831 );
11832 $ebooks = array();
11833 $ebooks[0] = '-- Please select --';
11834 while ($new->have_posts()) : $new->the_post();
11835 $ebook_title = get_the_title(get_the_ID());
11836 if ($ebook_title == '') {
11837 $ebook_title = '(no title)';
11838 }
11839 $ebook_status = get_post_status( get_the_ID() );
11840 if ( $ebook_status && $ebook_status !== 'publish' ) {
11841 $ebook_title .= ' [' . ucfirst( $ebook_status ) . ']';
11842 }
11843 $ebooks[get_the_ID()] = $ebook_title;
11844 endwhile;
11845 wp_reset_postdata();
11846
11847 return $ebooks;
11848 }
11849
11850 function ebook_store_woocommerce_email_delivery_pending($order_id) {
11851 if ('pending' != esc_attr(get_option('ebook_store_woocommerce_required_order_status'))) return;
11852 ebook_store_woocommerce_email_delivery($order_id);
11853 }
11854
11855 function ebook_store_woocommerce_email_delivery_processing($order_id) {
11856 if ('processing' != esc_attr(get_option('ebook_store_woocommerce_required_order_status'))) return;
11857 ebook_store_woocommerce_email_delivery($order_id);
11858 }
11859
11860 function ebook_store_woocommerce_email_delivery_on_hold($order_id) {
11861 if ('on-hold' != esc_attr(get_option('ebook_store_woocommerce_required_order_status'))) return;
11862 ebook_store_woocommerce_email_delivery($order_id);
11863 }
11864
11865 function ebook_store_woocommerce_email_delivery_cancelled($order_id) {
11866 if ('cancelled' != esc_attr(get_option('ebook_store_woocommerce_required_order_status'))) return;
11867 ebook_store_woocommerce_email_delivery($order_id);
11868 }
11869 function ebook_store_woocommerce_email_delivery_completed($order_id) {
11870 if ('completed' != esc_attr(get_option('ebook_store_woocommerce_required_order_status'))) return;
11871 ebook_store_woocommerce_email_delivery($order_id);
11872 }
11873
11874 function ebook_store_woocommerce_email_delivery($order_id = 0, $event = 'completed' ) {
11875
11876 if (is_numeric($order_status)) { //fix for using the order completed status hook
11877 $order_id = $order_status;
11878 $order_status = 'completed';
11879 }
11880 global $ebook_email_delivery;
11881 // erl('triggered completed');
11882 //error_reporting(E_ALL);
11883 $ebook_email_delivery = array();
11884 $order = new WC_Order( $order_id );
11885 $order_items = $order->get_items();
11886 $order->address = $order->get_address();
11887 // error_log('WC ORDER' . print_r($order, true));
11888
11889 foreach ($order_items as $id => $item) {
11890 // error_log(print_r($item,true));
11891 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
11892 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
11893 if ($ebook_store_book_id == 0) {
11894 continue;
11895 }
11896 $email_template = ebook_store_get_effective_delivery_email_template( $ebook_store_book_id );
11897 $ebook_email_delivery = array(
11898 'to' => $order->address['email'],
11899 'subject' => $email_template['subject'],
11900 'text' => $email_template['text'],
11901 'source' => 'woocommerce',
11902 'related_order_type' => 'shop_order',
11903 'woocommerce_event' => sanitize_key( (string) $event ),
11904 );
11905 //create new order array to make email delivery work
11906 $ebook = new EbookStoreEbook($ebook_store_book_id);
11907 $ebook->order_id = $order_id;
11908 $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.
11909 $taxes = $order->get_taxes();
11910
11911 // error_log('taxes' . print_r($taxes,true));
11912 // foreach ($taxes as $tax) {
11913 // $total_taxes = $tax->get_tax_total();
11914 // error_log('total_taxes' . print_r($total_taxes,true));
11915 // //break;
11916 // }
11917 $vat = ebook_store_get_vat_percent();
11918 if ($vat > 0) {
11919 $tax = $ebook->price * ($vat / 100);
11920 } else {
11921 $tax = 0;
11922 }
11923 $md5_nonce = md5(microtime() . mt_rand(1,99999999) . NONCE_KEY);
11924 $ebook_key = md5($md5_nonce);
11925 update_post_meta( $order_id, 'md5_nonce', $md5_nonce );
11926 update_post_meta( $order_id, 'ebook_key', $ebook_key );
11927
11928 $ebook_email_delivery['order'] = array(
11929 'first_name' => $order->address['first_name'],
11930 'last_name' => $order->address['last_name'],
11931 'mc_gross' => $item['line_subtotal'],
11932 'ebook' => $ebook_store_book_id,
11933 'payer_email' => $order->address['email'],
11934 'md5_nonce' => $md5_nonce,
11935 'ebook_key' => $ebook_key,
11936 'mc_fee' => '',
11937 'item_name' => get_the_title($ebook_store_book_id),
11938 'payment_date' => $order->get_date_created()->__toString(),
11939 'mc_currency' => $order->get_currency(),
11940 'residence_country' => $order->address['country'],
11941 'order_id' => $order_id,
11942 'password' => $ebook->password(),
11943 'txn_id' => $order->get_transaction_id(),
11944 'downloadlink' => $ebook->format_links(),
11945 'downloadlink_html' => $ebook->format_links(),
11946 'downloadlinks' => $ebook->format_links(),
11947 'tax' => $tax,
11948 'ip' => $_SERVER['REMOTE_ADDR'],
11949 );
11950
11951
11952 $ebook_email_delivery['attachment'][0]['file'] = $ebook->file;
11953 $ebook_email_delivery['attachment'][0]['file'] = $ebook_email_delivery['attachment'][0]['file']['file'];
11954
11955 @$fileExt = pathinfo($ebook_email_delivery['attachment'][0]['file'],PATHINFO_EXTENSION);
11956
11957 //wp_mail('deian@motov.net','test','test');
11958 if ($fileExt == 'pdf' && get_option('encrypt_pdf')) {
11959 $ebook_email_delivery['attachment'][0]['file'] = ebook_encrypt_pdf($ebook_email_delivery['order']);
11960 }
11961 if ( $fileExt == 'epub' && get_option( 'ebook_store_epub_watermark' ) ) {
11962 $ebook_email_delivery['attachment'][0]['file'] = $ebook->encrypted( $order_id, 'epub' );
11963 }
11964
11965 $ebook_store_woocommerce_required_order_status = esc_attr(get_option('ebook_store_woocommerce_required_order_status', 0));
11966 // if ($order_status != $ebook_store_woocommerce_required_order_status && $ebook_store_woocommerce_required_order_status != '0') {
11967 // error_log('ORDER STATUS ' . $ebook_store_woocommerce_required_order_status . ' VS ' . $order_status);
11968 // erl('will return now');
11969 // return $order_status;
11970 // }
11971
11972 ebook_email_delivery();
11973 // error_log('ebook id ' . $ebook_store_book_id);
11974 # code...
11975 }
11976 // if ( 'processing' == $order_status && ( 'on-hold' == $order->status || 'pending' == $order->status || 'failed' == $order->status ) ) {
11977 // return 'completed';
11978 // }
11979 return true;
11980 }
11981
11982 function ebook_store_woocommerce_order_details($order, $skipHeaders = false){
11983 $order_items = $order->get_items();
11984 $order_status = $order->get_status();
11985 $order_id = 0;
11986 if (is_object($order)) {
11987 if (method_exists($order, 'get_id')) {
11988 $order_id = $order->get_id();
11989 } elseif (isset($order->id)) {
11990 $order_id = $order->id;
11991 }
11992 }
11993 $ebook_store_woocommerce_required_order_status = esc_attr(get_option('ebook_store_woocommerce_required_order_status', 0));
11994 if ($order_status != $ebook_store_woocommerce_required_order_status && $ebook_store_woocommerce_required_order_status != '0') {
11995 return true;
11996 }
11997 if ($skipHeaders == false) {
11998 @$out .= "<h2>". __('Downloads', 'ebook-store') . "</h2>";
11999 @$out .= '<table class="shop_table order_downloads_ebook_store">
12000 <thead>
12001 <tr>
12002 <th class="product-name">'. __('Ebook (digital copy)', 'ebook-store') . '</th>
12003 <th class="product-total">'. __('Download', 'ebook-store') . '</th>
12004 </tr>
12005 </thead>
12006 <tbody>';
12007
12008 }
12009 // echo "<pre>";
12010 // print_r($order_items);
12011 // echo "<pre>";
12012 foreach ($order_items as $item_id => $item) {
12013 $item_mapping = ebook_store_get_wc_item_ebook_mapping( $item );
12014 $woocommerce_product_id = $item->get_product_id();
12015 $product = $item->get_product();
12016 $title = $product->get_title();
12017
12018 //wp_die("ID is $woocommerce_product_id");
12019 //$woocommerce_product_id = 100;
12020 $ebook_store_book_id = isset( $item_mapping['ebook_id'] ) ? (int) $item_mapping['ebook_id'] : 0;
12021
12022 if ($ebook_store_book_id != 0) {
12023 @$ebooks++;
12024 }
12025
12026 $ebook = new EbookStoreEbook($ebook_store_book_id);
12027 $ebook->order_id = $order_id;
12028 $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.
12029 $cell = '<span class="amount">' . $ebook->format_links() . '</span><br /><small>' . $ebook->password_desc() . '</small>';
12030 if (get_option( 'ebook_store_woocommerce_pdf_reader', false )) {
12031 $cell = ebook_pdf_reader(false, $ebook_store_book_id);
12032 }
12033
12034 if ($ebook_store_book_id > 0) {
12035 @$out .= '
12036 <tr class="order_item">
12037 <td class="product-name">
12038 ' . $title . '</td>
12039 <td class="product-total">
12040 ' . $cell . '
12041 </td>
12042 </tr>';
12043
12044 }
12045 }
12046 if ($skipHeaders == false) {
12047 @$out .= '
12048 </tbody>
12049 </table>';
12050 }
12051
12052 if (@$ebooks > 0) {
12053 echo $out;
12054 }
12055
12056 // echo '<pre>' . print_r($order,true) . '</pre>';
12057 // echo '<pre>' . print_r($ord,true) . '</pre>';
12058
12059 // echo '<p><strong>'.__('Pickup Location').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12060 // echo '<p><strong>'.__('Pickup Date').':</strong> ' . get_post_meta( $order->id, 'ebook_store_book_id', true ). '</p>';
12061 }
12062 /**
12063 * The order key stored against a WooCommerce order.
12064 *
12065 * Reads through the WooCommerce CRUD layer so the lookup keeps working under
12066 * High-Performance Order Storage, where orders no longer live in wp_posts and
12067 * get_post_meta() returns nothing.
12068 *
12069 * @param int $order_id WooCommerce order ID.
12070 * @return string Order key, or '' when the order does not exist.
12071 */
12072 function ebook_store_get_wc_order_key( $order_id ) {
12073 $order_id = absint( $order_id );
12074 if ( $order_id < 1 ) {
12075 return '';
12076 }
12077
12078 if ( function_exists( 'wc_get_order' ) ) {
12079 $order = wc_get_order( $order_id );
12080 if ( $order ) {
12081 return (string) $order->get_order_key();
12082 }
12083
12084 return '';
12085 }
12086
12087 return (string) get_post_meta( $order_id, '_order_key', true );
12088 }
12089
12090 /**
12091 * Does the supplied key authorise access to this WooCommerce order?
12092 *
12093 * @param int $order_id WooCommerce order ID.
12094 * @param string $order_key Key supplied in the request.
12095 * @return bool
12096 */
12097 function ebook_store_wc_order_key_matches( $order_id, $order_key ) {
12098 $order_key = sanitize_text_field( (string) $order_key );
12099 if ( $order_key === '' ) {
12100 return false;
12101 }
12102
12103 $stored = ebook_store_get_wc_order_key( $order_id );
12104
12105 return $stored !== '' && hash_equals( $stored, $order_key );
12106 }
12107
12108 /**
12109 * Is this ebook actually one of the items in that WooCommerce order?
12110 *
12111 * Without this check a valid order key for a $1 book would unlock every other
12112 * ebook in the catalogue.
12113 *
12114 * @param int $order_id WooCommerce order ID.
12115 * @param int $ebook_id Ebook post ID.
12116 * @return bool
12117 */
12118 function ebook_store_wc_order_contains_ebook( $order_id, $ebook_id ) {
12119 $ebook_id = absint( $ebook_id );
12120 if ( $ebook_id < 1 ) {
12121 return false;
12122 }
12123
12124 if ( ! function_exists( 'wc_get_order' ) ) {
12125 // Without WooCommerce there is nothing to cross-check against; the order
12126 // key match above is the only gate.
12127 return true;
12128 }
12129
12130 $order = wc_get_order( absint( $order_id ) );
12131 if ( ! $order ) {
12132 return false;
12133 }
12134
12135 foreach ( $order->get_items() as $item ) {
12136 $product_id = (int) $item->get_product_id();
12137 $variation_id = (int) $item->get_variation_id();
12138
12139 foreach ( array( $variation_id, $product_id ) as $candidate ) {
12140 if ( $candidate < 1 ) {
12141 continue;
12142 }
12143 if ( (int) get_post_meta( $candidate, 'ebook_store_book_id', true ) === $ebook_id ) {
12144 return true;
12145 }
12146 }
12147 }
12148
12149 return (bool) apply_filters( 'ebook_store_wc_order_contains_ebook', false, $order_id, $ebook_id );
12150 }
12151
12152 function ebook_process_download_woocomerce() {
12153 if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'wc_order_download' ) {
12154 return false;
12155 }
12156
12157 // ebook_process_download() now performs the order-key and order-contents
12158 // checks itself, so this wrapper only forwards the request.
12159 ebook_process_download( isset( $_GET['wc_order'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_order'] ) ) : '' );
12160 die();
12161 }
12162
12163 function ebook_store_add_to_cart() {
12164 include_once( plugin_dir_path( EBOOK_STORE_PLUGIN_FILE ) . 'locale.php' );
12165 $locale = ebook_store_get_locale_strings();
12166 $formats_locale = ebook_store_get_format_labels();
12167
12168 if ($_GET['woocommerce_product_id'] > 0) {
12169 global $woocommerce;
12170 $woocommerce_product_id = (int)$_GET['woocommerce_product_id'];
12171 $cart_config = ebook_store_get_wc_add_to_cart_config( $woocommerce_product_id );
12172 if ( ! $cart_config ) {
12173 return;
12174 }
12175
12176 if ( ! ebook_store_cart_contains_wc_product( $cart_config ) ) {
12177 WC()->cart->add_to_cart(
12178 $cart_config['product_id'],
12179 1,
12180 $cart_config['variation_id'],
12181 $cart_config['variation']
12182 );
12183 }
12184
12185 global $ebook_store_messages;
12186
12187 if (get_option('ebook_store_woocommerce_integration_no_added_to_cart') < 1) {
12188 $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>';
12189 }
12190 }
12191 }
12192
12193
12194
12195 function ebook_store_action_links ( $links ) {
12196 $mylinks = array(
12197 '<a href="' . admin_url( 'options-general.php?page=ebook_options.php' ) . '">Settings</a>',
12198 '<a href="https://www.youtube.com/watch?v=T0mrdvrbmbc&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-" target="_blank">Video Tutorials</a>',
12199 '<a href="https://shopfiles.com/index.php/products/wordpress-ebook-store" target="_blank">Upgrade</a>',
12200 );
12201 return array_merge( $links, $mylinks );
12202 }
12203
12204 function ebook_store_payment_completed_wpam($ipn_data, $order_id)
12205 {
12206 $custom_data = $ipn_data['custom'];
12207 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - payment completed hook fired. Custom field value: '.$custom_data);
12208 $custom_values = array();
12209 parse_str($custom_data, $custom_values);
12210 if(isset($custom_values['wpam_tracking']) && !empty($custom_values['wpam_tracking']))
12211 {
12212 $tracking_value = $custom_values['wpam_tracking'];
12213 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Tracking data present. Need to track affiliate commission. Tracking value: '.$tracking_value);
12214 $purchaseLogId = $ipn_data['txn_id'];
12215 $purchaseAmount = $ipn_data['mc_gross']; //TODO - later calculate sub-total only
12216 $strRefKey = $tracking_value;
12217 $requestTracker = new WPAM_Tracking_RequestTracker();
12218 $requestTracker->handleCheckoutWithRefKey( $purchaseLogId, $purchaseAmount, $strRefKey);
12219 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Commission tracked for transaction ID: '.$purchaseLogId.'. Purchase amt: '.$purchaseAmount);
12220 }
12221 }
12222
12223 function ebook_store_payment_gateway_parameters_wpam($parameters)
12224 {
12225 if (class_exists('WPAM_PluginConfig') == false) {
12226 return;
12227 }
12228 if(isset($_COOKIE['wpam_id']))
12229 {
12230 $name = 'wpam_tracking';
12231 $value = $_COOKIE['wpam_id'];
12232 $new_val = $name.'='.$value;
12233 $current_val = $parameters['custom'];
12234 if(empty($current_val)){
12235 $parameters['custom'] = $new_val;
12236 }
12237 else{
12238 $parameters['custom'] = $current_val.'&'.$new_val;
12239 }
12240 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
12241 }
12242
12243 else if(isset($_COOKIE[WPAM_PluginConfig::$RefKey]))
12244 {
12245 $name = 'wpam_tracking';
12246 $value = $_COOKIE[WPAM_PluginConfig::$RefKey];
12247 $new_val = $name.'='.$value;
12248 $current_val = $parameters['custom'];
12249 if(empty($current_val)){
12250 $parameters['custom'] = $new_val;
12251 }
12252 else{
12253 $parameters['custom'] = $current_val.'&'.$new_val;
12254 }
12255 WPAM_Logger::log_debug('Ebook Store for WordPress Integration - Adding custom field value. New value: '.$parameters['custom']);
12256 }
12257
12258 return $parameters;
12259 }
12260
12261 function ebook_store_my_taxonomies_product() {
12262 $labels = array(
12263 'name' => _x( 'Ebook Categories', 'Ebook Categories', 'ebook-store' ),
12264 'singular_name' => _x( 'Ebook Category', 'Ebook Category', 'ebook-store' ),
12265 'search_items' => __( 'Search Ebook Categories', 'ebook-store' ),
12266 'all_items' => __( 'All Ebook Categories', 'ebook-store' ),
12267 'parent_item' => __( 'Parent Ebook Category', 'ebook-store' ),
12268 'parent_item_colon' => __( 'Parent Ebook Category:', 'ebook-store' ),
12269 'edit_item' => __( 'Edit Ebook Category', 'ebook-store' ),
12270 'update_item' => __( 'Update Ebook Category', 'ebook-store' ),
12271 'add_new_item' => __( 'Add New Ebook Category', 'ebook-store' ),
12272 'new_item_name' => __( 'New Ebook Category', 'ebook-store' ),
12273 'menu_name' => __( 'Ebook Categories', 'ebook-store' ),
12274 );
12275 $args = array(
12276 'labels' => $labels,
12277 'hierarchical' => true,
12278 );
12279 register_taxonomy( 'ebook_category', 'ebook', $args );
12280 }
12281
12282 function ebook_store_vc_before_init_actions() {
12283 class vcInfoBox extends WPBakeryShortCode {
12284
12285 // Element Init
12286 function __construct() {
12287 add_action( 'init', array( $this, 'vc_infobox_mapping' ) );
12288 add_shortcode( 'vc_infobox', array( $this, 'vc_infobox_html' ) );
12289 }
12290
12291 // Element Mapping
12292 public function vc_infobox_mapping() {
12293
12294 // Stop all if VC is not enabled
12295 if ( !defined( 'WPB_VC_VERSION' ) ) {
12296 return;
12297 }
12298
12299 // Map the block with vc_map()
12300 vc_map(
12301 array(
12302 'name' => __('Ebook Order Form', 'text-domain'),
12303 'base' => 'ebook_store',
12304 'description' => __('Inserts shortcode that will call the Ebook Order form.', 'text-domain'),
12305 'category' => __('Ebook Store', 'ebook-store'),
12306 'icon' => 'dashicons-cart', //get_template_directory_uri().'/assets/img/vc-icon.png',
12307 'params' => array(
12308
12309 array(
12310 'type' => 'dropdown',
12311 'holder' => 'h3',
12312 'class' => 'title-class',
12313 'heading' => __( 'Select Ebook', 'text-domain' ),
12314 'param_name' => 'ebook_id',
12315 'value' => vc_ebook_store_array(),
12316 'description' => __( 'You are embedding a direct order form for the Ebook via Ebook Store plugin.', 'text-domain' ),
12317 'admin_label' => false,
12318 'weight' => 0,
12319 'group' => 'Ebook Store',
12320 ),
12321
12322
12323 ),
12324 )
12325 );
12326
12327 }
12328
12329
12330 // Element HTML
12331 public function vc_infobox_html( $atts ) {
12332
12333 // Params extraction
12334 extract(
12335 shortcode_atts(
12336 array(
12337 'title' => '',
12338 'text' => '',
12339 'ebook_id' => '',
12340 ),
12341 $atts
12342 )
12343 );
12344
12345 // Fill $html var with data
12346 $html = '
12347 <div class="vc-infobox-wrap">
12348
12349 <h2 class="vc-infobox-title">' . $title . '</h2>
12350
12351 <div class="vc-infobox-text"></div>
12352
12353
12354
12355 </div>';
12356
12357 return $html;
12358
12359 }
12360
12361 } // End Element Class
12362
12363
12364 // Element Class Init
12365 new vcInfoBox();
12366 }
12367 function vc_ebook_store_array() {
12368 $args = array(
12369 'post_type' => 'ebook',
12370 );
12371 $out = array();
12372 $query = new WP_Query($args);
12373 while ($query->have_posts()) {
12374 $query->the_post();
12375 $out[get_the_title()] = get_the_ID();
12376 }
12377 return $out;
12378 }
12379 function ebook_store_order_reports_callback() {
12380 include_once('ebook_store_order_reports_callback.php');
12381 }
12382 /**
12383 * Stream the orders list as a CSV download.
12384 *
12385 * The previous version loaded every order at once, called get_post_meta() per
12386 * row, buffered the whole file in a PHP array and then die()'d with an implode.
12387 * At 10k orders that peaked several hundred megabytes and never finished. It
12388 * also took the column list from the FIRST row only, so every order with a
12389 * different meta shape emitted an undefined-index warning per missing field.
12390 *
12391 * This walks the orders in pages of 200, writes straight to the output stream,
12392 * and uses the union of all meta keys as the header row.
12393 */
12394 function ebook_store_export_orders() {
12395 if ( ! is_admin() ) {
12396 return;
12397 }
12398
12399 if ( ! isset( $_GET['export'] ) || (int) $_GET['export'] !== 1 ) {
12400 return;
12401 }
12402
12403 if ( ! current_user_can( 'manage_options' ) ) {
12404 wp_die( esc_html__( 'You are not allowed to export orders.', 'ebook-store' ) );
12405 }
12406
12407 check_admin_referer( 'ebook_store_export_orders' );
12408
12409 $selected_ebook_id = isset( $_GET['ebook_id'] ) ? absint( wp_unslash( $_GET['ebook_id'] ) ) : 0;
12410
12411 $base_args = array(
12412 'post_type' => 'ebook_order',
12413 'post_status' => 'any',
12414 'posts_per_page' => 200,
12415 'fields' => 'ids',
12416 'no_found_rows' => true,
12417 'update_post_term_cache' => false,
12418 'orderby' => 'ID',
12419 'order' => 'ASC',
12420 );
12421
12422 if ( $selected_ebook_id > 0 ) {
12423 $base_args['meta_key'] = 'ebook';
12424 $base_args['meta_value'] = $selected_ebook_id;
12425 }
12426
12427 // First pass: collect the union of meta keys so no row is missing a column.
12428 $keys = array();
12429 $page = 1;
12430 do {
12431 $args = $base_args;
12432 $args['paged'] = $page;
12433 $ids = get_posts( $args );
12434
12435 foreach ( $ids as $id ) {
12436 foreach ( array_keys( (array) get_post_meta( $id ) ) as $key ) {
12437 $keys[ $key ] = true;
12438 }
12439 }
12440
12441 $page++;
12442 } while ( count( $ids ) === $base_args['posts_per_page'] );
12443
12444 if ( empty( $keys ) ) {
12445 wp_die( esc_html__( 'There are no orders to export yet.', 'ebook-store' ) );
12446 }
12447
12448 $keys = array_keys( $keys );
12449 sort( $keys );
12450
12451 nocache_headers();
12452 header( 'Content-Type: text/csv; charset=utf-8' );
12453 header( 'Content-Disposition: attachment; filename="ebook-store-orders-' . gmdate( 'Y-m-d' ) . '.csv"' );
12454
12455 $out = fopen( 'php://output', 'w' );
12456
12457 // A UTF-8 BOM so Excel opens accented buyer names correctly.
12458 fwrite( $out, "\xEF\xBB\xBF" );
12459 fputcsv( $out, array_merge( array( 'order_id', 'order_date' ), $keys ) );
12460
12461 $page = 1;
12462 do {
12463 $args = $base_args;
12464 $args['paged'] = $page;
12465 $ids = get_posts( $args );
12466
12467 foreach ( $ids as $id ) {
12468 $meta = (array) get_post_meta( $id );
12469 $row = array( $id, get_post_field( 'post_date_gmt', $id ) );
12470
12471 foreach ( $keys as $key ) {
12472 $row[] = isset( $meta[ $key ][0] ) ? $meta[ $key ][0] : '';
12473 }
12474
12475 fputcsv( $out, $row );
12476 }
12477
12478 // Release the meta cache for the page we just wrote, otherwise the
12479 // pagination saves nothing.
12480 foreach ( $ids as $id ) {
12481 wp_cache_delete( $id, 'post_meta' );
12482 }
12483
12484 flush();
12485 $page++;
12486 } while ( count( $ids ) === $base_args['posts_per_page'] );
12487
12488 fclose( $out );
12489 exit;
12490 }
12491
12492 function ebook_store_session_end() {
12493 session_destroy ();
12494 }
12495 function ebook_store_encryptable($file) {
12496 require_once __DIR__ . '/fpdi/bootstrap.php';
12497 try {
12498 $pdf = new \setasign\Fpdi\Fpdi();
12499 $pdf->setSourceFile($file);
12500 return true;
12501 } catch (\Throwable $e) {
12502 return false;
12503 }
12504 }
12505
12506 function ebook_store_offer_tutorial() {
12507 if (ebook_store_endsWith(esc_url_raw($_SERVER['REQUEST_URI']), 'edit.php?post_type=ebook')) {
12508 $args = array(
12509 'post_type' => 'ebook',
12510 );
12511 $myquery = new WP_Query($args);
12512 if ($myquery->found_posts == 0) {
12513 ?>
12514 <div class="updated">
12515 <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 />
12516 <a href="https://www.youtube.com/watch?v=HxYzlaEHPU4&list=PL9hp_4MJ0h1sDTf05H6aguzlYqcg47QJ-&index=1" target="_blank">Watch Tutorials on YouTube</a>', 'ebooks-store' ); ?></p>
12517 </div>
12518 <?php
12519 }
12520 }
12521 }
12522 function ebook_store_not_encryptable() {
12523 $class = 'notice notice-error';
12524 $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' );
12525
12526 printf( '<div class="%1$s"><p>%2$s</p></div>', esc_attr( $class ), $message );
12527 }
12528
12529 function ebook_store_get_password_for_order($order_id = 0, $context = array()) {
12530 static $runtime_password_cache = array();
12531
12532 $random_enabled = intval(get_option('ebook_store_random_password')) === 1;
12533 $blank_enabled = intval(get_option('ebook_store_blank_password')) === 1;
12534
12535 $cache_key = '';
12536 if ($order_id > 0) {
12537 $cache_key = 'order_' . $order_id;
12538 } elseif (!empty($context['md5_nonce'])) {
12539 $cache_key = 'nonce_' . $context['md5_nonce'];
12540 } elseif (!empty($context['ebook_key'])) {
12541 $cache_key = 'ebook_key_' . $context['ebook_key'];
12542 }
12543
12544 if ($cache_key && isset($runtime_password_cache[$cache_key])) {
12545 return $runtime_password_cache[$cache_key];
12546 }
12547
12548 $stored_exists = ($order_id > 0 && function_exists('metadata_exists')) ? metadata_exists('post', $order_id, 'password') : false;
12549 if ($stored_exists) {
12550 $stored_password = get_post_meta($order_id,'password',true);
12551 if ($cache_key) {
12552 $runtime_password_cache[$cache_key] = $stored_password;
12553 }
12554 return $stored_password;
12555 }
12556
12557 $payer_email = isset($context['payer_email']) ? trim($context['payer_email']) : '';
12558 if (!$payer_email && $order_id > 0) {
12559 $payer_email = get_post_meta($order_id,'payer_email',true);
12560 }
12561 $provided_password = isset($context['password']) ? (string)$context['password'] : '';
12562 $billing_email = isset($context['billing_email']) ? trim($context['billing_email']) : '';
12563 if ($billing_email === '' && $order_id > 0) {
12564 $billing_email = get_post_meta($order_id,'_billing_email',true);
12565 $billing_email = is_string($billing_email) ? trim($billing_email) : '';
12566 }
12567
12568 if ($blank_enabled) {
12569 $password = '';
12570 } elseif ($random_enabled) {
12571 $password = $provided_password;
12572 if ($password === '') {
12573 $password = substr(md5(uniqid('ebook_pw_', true)), 0, 8);
12574 }
12575 } else {
12576 $password = $provided_password ?: $payer_email;
12577 if ($password === '') {
12578 $password = $billing_email;
12579 }
12580 if ($password === '' && !empty($context['fallback_email'])) {
12581 $password = $context['fallback_email'];
12582 }
12583 }
12584
12585 if ($order_id > 0) {
12586 update_post_meta($order_id,'password',$password);
12587 }
12588
12589 if ($cache_key) {
12590 $runtime_password_cache[$cache_key] = $password;
12591 }
12592
12593 return $password;
12594 }
12595
12596 /**
12597 * The order-detail block shared by the confirmation page and the delivery email.
12598 *
12599 * @param bool $with_vat Append the VAT figure to the total line.
12600 * @return string
12601 */
12602 function ebook_store_default_order_details_block( $with_vat = false ) {
12603 $total = $with_vat
12604 /* translators: %%mc_currency%%, %%total%% and %%tax%% are merge codes — keep them exactly as they are. */
12605 ? __( 'Total: %%mc_currency%% %%total%% (VAT %%tax%%)', 'ebook-store' )
12606 /* translators: %%mc_currency%% and %%total%% are merge codes — keep them exactly as they are. */
12607 : __( 'Total: %%mc_currency%% %%total%%', 'ebook-store' );
12608
12609 return '<h3>' . __( 'Details for your order:', 'ebook-store' ) . '</h3>' . "\n"
12610 /* translators: %%order_id%% is a merge code — keep it exactly as it is. */
12611 . __( 'Order #: %%order_id%%', 'ebook-store' ) . "\n"
12612 /* translators: %%txn_id%% is a merge code — keep it exactly as it is. */
12613 . __( 'Transaction: %%txn_id%%', 'ebook-store' ) . "\n"
12614 /* translators: %%mc_currency%% and %%mc_gross%% are merge codes — keep them exactly as they are. */
12615 . __( 'Amount: %%mc_currency%% %%mc_gross%%', 'ebook-store' ) . "\n"
12616 . $total . "\n\n"
12617 /* translators: %%password%% is a merge code — keep it exactly as it is. */
12618 . '<strong>' . __( 'Your password is: %%password%%', 'ebook-store' ) . '</strong>' . "\n\n"
12619 . __( 'Thank you!', 'ebook-store' );
12620 }
12621
12622 /**
12623 * Default content of the order confirmation ("thank you") page.
12624 *
12625 * This used to be a hardcoded English string on QSWPOptions, so a shop selling
12626 * in German or Arabic showed its buyers an English confirmation page until
12627 * somebody rewrote it by hand. Built from translated pieces instead, so a store
12628 * gets the page in its own language out of the box.
12629 *
12630 * The %%...%% merge codes are substituted later and must survive translation.
12631 *
12632 * @return string
12633 */
12634 function ebook_store_default_thankyou_page() {
12635 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
12636 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
12637 . __( 'You have successfully completed the order process! Please use the link(s) below to download your copy:', 'ebook-store' ) . "\n\n"
12638 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
12639 . '<strong>' . __( 'Use the link(s) to start the download:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
12640 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
12641 . __( 'Bonus ebook download links:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n"
12642 . ebook_store_default_order_details_block( false );
12643 }
12644
12645 /**
12646 * Default body of the delivery email.
12647 *
12648 * Same reasoning as the confirmation page: it shipped in English only.
12649 *
12650 * @return string
12651 */
12652 function ebook_store_default_email_delivery_text() {
12653 /* translators: %%first_name%% and %%last_name%% are merge codes — keep them exactly as they are. */
12654 return '<h1>' . __( 'Thank you for your order %%first_name%% %%last_name%%!', 'ebook-store' ) . '</h1>' . "\n"
12655 . __( 'You have successfully completed the order process!', 'ebook-store' ) . "\n\n"
12656 /* translators: %%downloadlink_html%% is a merge code — keep it exactly as it is. */
12657 . '<strong>' . __( 'Please use the link(s) below to download your copy:', 'ebook-store' ) . '</strong> %%downloadlink_html%%' . "\n\n"
12658 /* translators: %%ebook_bonus%% is a merge code — keep it exactly as it is. */
12659 . __( 'Bonus ebooks:', 'ebook-store' ) . ' %%ebook_bonus%%' . "\n\n"
12660 . ebook_store_default_order_details_block( true );
12661 }
12662
12663 /**
12664 * Default subject line of the delivery email.
12665 *
12666 * @return string
12667 */
12668 function ebook_store_default_email_delivery_subject() {
12669 return __( 'Email Delivery - Order complete!', 'ebook-store' );
12670 }
12671
12672 /**
12673 * Mark the admin body when this site holds a valid Pro licence.
12674 *
12675 * The "Pro" badge is drawn by CSS on any element carrying .goPro2, and several
12676 * of those classes are printed unconditionally — the delivery options, the bonus
12677 * books picker, the importer. A paying customer was still shown a locked-looking
12678 * screen covered in upsell badges. One body class lets the stylesheet stand every
12679 * one of them down, including any added later.
12680 *
12681 * @param string $classes Existing body classes.
12682 * @return string
12683 */
12684 function ebook_store_admin_body_class( $classes ) {
12685 if ( function_exists( 'ebook_store_has_pro_license' ) && ebook_store_has_pro_license() ) {
12686 $classes .= ' ebook-store-pro-active';
12687 }
12688
12689 return $classes;
12690 }
12691 add_filter( 'admin_body_class', 'ebook_store_admin_body_class' );
12692
12693 function ebook_store_file_formats_form($ebook_id) {
12694 $formats = array('pdf','zip','mobi','txt','mp3','epub','mp4');
12695 $ebook = new EbookStoreEbook($ebook_id);
12696 $ebook->formats = is_array($ebook->formats) ? $ebook->formats : array();
12697
12698 // Everything but PDF is a Pro format, but the "Pro" badge should only appear
12699 // to someone who does not have Pro. It used to be keyed on the format index
12700 // alone, so a paying licence holder still saw every format tab stamped and
12701 // styled as locked.
12702 $pro_unlocked = function_exists('ebook_store_has_pro_license') ? ebook_store_has_pro_license() : false;
12703
12704 $entries = array();
12705
12706 foreach ($formats as $index => $format) {
12707 if ($format == 'pdf' && get_option('encrypt_pdf')) {
12708 if (!ebook_store_encryptable(@$ebook->files[$format]) && @$ebook->files[$format] != '') {
12709 ebook_store_not_encryptable();
12710 }
12711 }
12712
12713 $has_file = @in_array($format, $ebook->formats);
12714 $filename = ($has_file && !empty($ebook->files[$format])) ? basename($ebook->files[$format]) : '';
12715 $size_label = @$ebook->human_filesize(@$ebook->files[$format . '_size']);
12716 $entries[] = array(
12717 'slug' => $format,
12718 'label' => strtoupper($format),
12719 'has_file' => $has_file,
12720 'filename' => $filename,
12721 'size_label' => $size_label,
12722 'exists_class'=> $has_file ? '' : 'hiddenEbookStore',
12723 'is_pro' => ($index > 0) && ! $pro_unlocked,
12724 );
12725 }
12726 ?>
12727 <div class="ebook_store_file_formats_form">
12728 <h3><?php _e('Upload eBook Files', 'ebook-store'); ?></h3>
12729 <p class="description"><?php _e('Upload your eBook in different formats. Supported formats are grouped by format tabs below.', 'ebook-store'); ?></p>
12730
12731 <div class="ebook-format-tabs" id="ebook-format-tabs-<?php echo esc_attr($ebook_id); ?>">
12732 <div class="ebook-format-tabs__nav" role="tablist">
12733 <?php foreach ($entries as $idx => $entry) :
12734 $panel_id = 'ebook-format-panel-' . $entry['slug'];
12735 $status_icon = $entry['has_file'] ? 'dashicons-yes-alt' : 'dashicons-minus';
12736 $nav_classes = array('ebook-format-tabs__nav-item');
12737 if ($entry['is_pro']) {
12738 $nav_classes[] = 'goPro2';
12739 }
12740 if ($entry['has_file']) {
12741 $nav_classes[] = 'has-file';
12742 }
12743 if ($idx === 0) {
12744 $nav_classes[] = 'is-active';
12745 }
12746 ?>
12747 <button type="button"
12748 class="<?php echo esc_attr(implode(' ', $nav_classes)); ?>"
12749 data-format="<?php echo esc_attr($entry['slug']); ?>"
12750 data-target="#<?php echo esc_attr($panel_id); ?>"
12751 role="tab"
12752 aria-controls="<?php echo esc_attr($panel_id); ?>"
12753 aria-selected="<?php echo $idx === 0 ? 'true' : 'false'; ?>">
12754 <span class="ebook-format-tab__icon">
12755 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?>" />
12756 </span>
12757 <span class="ebook-format-tab__label"><?php echo esc_html($entry['label']); ?></span>
12758 <span class="ebook-format-tab__status dashicons <?php echo esc_attr($status_icon); ?>"></span>
12759 </button>
12760 <?php endforeach; ?>
12761 </div>
12762 <div class="ebook-format-tabs__panels">
12763 <?php foreach ($entries as $idx => $entry) :
12764 $panel_id = 'ebook-format-panel-' . $entry['slug'];
12765 $panel_classes = array('ebook-format-tab-panel');
12766 if ($entry['is_pro']) {
12767 $panel_classes[] = 'goPro2';
12768 }
12769 if ($entry['has_file']) {
12770 $panel_classes[] = 'has-file';
12771 }
12772 if ($idx === 0) {
12773 $panel_classes[] = 'is-active';
12774 }
12775 ?>
12776 <div
12777 id="<?php echo esc_attr($panel_id); ?>"
12778 class="<?php echo esc_attr(implode(' ', $panel_classes)); ?>"
12779 data-tab-panel="<?php echo esc_attr($entry['slug']); ?>"
12780 <?php echo $idx === 0 ? '' : 'hidden'; ?>>
12781 <div class="ebook_store_file_format_item<?php echo $entry['is_pro'] ? ' goPro2' : ''; ?>">
12782 <div class="ebook_store_file_format_item_left">
12783 <span class="ebook_store_icon">
12784 <img src="<?php echo esc_url(plugins_url('img/'.$entry['slug'].'.png', __FILE__)); ?>" alt="<?php echo esc_attr($entry['label']); ?> format" width="64" />
12785 </span>
12786 <span class="ebook_store_size <?php echo esc_attr($entry['exists_class']); ?>"><?php echo esc_html($entry['size_label']); ?></span>
12787 </div>
12788 <div class="ebook_store_file_format_item_right">
12789 <div class="ebook_store_filename">
12790 <?php if ($entry['has_file']) : ?>
12791 <a href="#" title="<?php _e('Current file', 'ebook-store'); ?>"><?php echo esc_html($entry['filename']); ?></a>
12792 <?php else : ?>
12793 <span><?php _e('No file uploaded yet', 'ebook-store'); ?></span>
12794 <?php endif; ?>
12795 </div>
12796 <div class="ebook_store_upload">
12797 <input type="file"
12798 accept=".<?php echo esc_attr($entry['slug']); ?>"
12799 name="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
12800 id="ebook_wp_custom_attachment_<?php echo esc_attr($entry['slug']); ?>"
12801 data-format="<?php echo esc_attr($entry['slug']); ?>"
12802 title="<?php echo esc_attr( sprintf(__('Choose %s file', 'ebook-store'), $entry['label']) ); ?>"
12803 />
12804 </div>
12805 <div class="ebook_store_control <?php echo esc_attr($entry['exists_class']); ?>">
12806 <label title="<?php _e('Delete file', 'ebook-store'); ?>">
12807 <input type="checkbox" name="ebook_store_delete_<?php echo esc_attr($entry['slug']); ?>" />
12808 <i class="dashicons dashicons-trash"></i>
12809 <span class="screen-reader-text"><?php _e('Delete file', 'ebook-store'); ?></span>
12810 </label>
12811 </div>
12812 </div>
12813 </div>
12814 </div>
12815 <?php endforeach; ?>
12816 </div>
12817 </div>
12818 <?php
12819 // Files are sliced in the browser and streamed past the server's upload
12820 // limits, so quoting upload_max_filesize here only scared authors off
12821 // books that upload perfectly well.
12822 if ( function_exists( 'ebook_store_max_upload_bytes' ) ) :
12823 ?>
12824 <p class="description">
12825 <?php
12826 printf(
12827 /* translators: %s: maximum file size, already formatted. */
12828 esc_html__( 'Files are uploaded in small pieces, so the server upload limit does not apply. Maximum size per file: %s.', 'ebook-store' ),
12829 esc_html( size_format( ebook_store_max_upload_bytes() ) )
12830 );
12831 ?>
12832 </p>
12833 <?php else : ?>
12834 <p class="description"><?php _e('Maximum upload file size:', 'ebook-store'); ?> <?php echo ini_get('upload_max_filesize'); ?>B</p>
12835 <?php endif; ?>
12836 </div>
12837 <?php
12838 }
12839
12840 function ebook_store_price_plus_vat($price) {
12841 $vat = ebook_store_get_vat_percent();
12842 $price = (float) $price;
12843
12844 if ( $vat <= 0 ) {
12845 return $price;
12846 }
12847
12848 return $price + ( $price * ( $vat / 100 ) );
12849 }
12850
12851 function ebook_store_get_store_currency() {
12852 $currency = strtoupper( trim( (string) get_option( 'paypal_currency', 'USD' ) ) );
12853
12854 return preg_match( '/^[A-Z]{3}$/', $currency ) ? $currency : 'USD';
12855 }
12856
12857 function ebook_store_get_vat_percent() {
12858 $vat = trim( (string) get_option( 'vat_percent', '0' ) );
12859
12860 if ( $vat === '' || ! is_numeric( $vat ) ) {
12861 return 0.0;
12862 }
12863
12864 return max( 0, (float) $vat );
12865 }
12866
12867 function ebook_store_get_download_limit() {
12868 $limit = trim( (string) get_option( 'downloads_limit', '5' ) );
12869
12870 if ( $limit === '' || ! is_numeric( $limit ) ) {
12871 return 5;
12872 }
12873
12874 return max( 1, (int) $limit );
12875 }
12876
12877
12878
12879 function ebook_store_row( $atts, $content = "") {
12880 return "<div class=\"ebook_store_row ebook_store_row_".intval($atts['col'])."\">" . do_shortcode($content) . "</div>";
12881 }
12882
12883 function ebook_store_silent_registration($data) {
12884 if (get_option('ebook_store_silent_registration') != 1) {
12885 return get_current_user_id();
12886 }
12887
12888 $user_email = isset( $data['payer_email'] ) ? sanitize_email( $data['payer_email'] ) : '';
12889 if ( $user_email === '' || ! is_email( $user_email ) ) {
12890 return get_current_user_id();
12891 }
12892
12893 // The email address is the buyer's identity. If an account already uses it,
12894 // reuse that account. The username must NOT be the key: the old code keyed on
12895 // "first_last", so a second buyer who happened to share a name with an
12896 // existing customer was silently attached to that customer's account.
12897 $existing = email_exists( $user_email );
12898 if ( $existing ) {
12899 return $existing;
12900 }
12901
12902 // Build a username that does not collide with any existing account.
12903 $base = sanitize_user( trim( (string) $data['first_name'] . '_' . (string) $data['last_name'] ), true );
12904 if ( $base === '' || $base === '_' ) {
12905 $parts = explode( '@', $user_email );
12906 $base = sanitize_user( $parts[0], true );
12907 }
12908 if ( $base === '' ) {
12909 $base = 'customer';
12910 }
12911
12912 $user_name = $base;
12913 $suffix = 1;
12914 while ( username_exists( $user_name ) ) {
12915 $suffix++;
12916 $user_name = $base . '_' . $suffix;
12917 }
12918
12919 $user_id = wp_create_user( $user_name, wp_generate_password( 16, true ), $user_email );
12920
12921 // A WP_Error must never be stored as the order's user_id.
12922 if ( is_wp_error( $user_id ) ) {
12923 return get_current_user_id();
12924 }
12925
12926 wp_new_user_notification( $user_id, null, 'both' );
12927
12928 return $user_id;
12929 }
12930
12931
12932
12933 function ebook_store_downloads($atts) {
12934 $args = array(
12935 'post_type' => 'ebook_order',
12936 'meta_query' => array(
12937 array(
12938 'key' => 'user_id',
12939 'value' => get_current_user_id(),
12940 )
12941 ));
12942 // The Query
12943 $the_query = new WP_Query( $args );
12944 $out = 0;
12945 // The Loop
12946 $checkoutPage = esc_attr(get_option('ebook_store_checkout_page'));
12947 if ( $the_query->have_posts() ) {
12948
12949 $the_query->the_post();
12950 $link = add_query_arg(array('ebook_key' => get_post_meta(get_the_ID(),'ebook_key',true), 'action' => 'thank_you'),get_permalink($checkoutPage));
12951 echo '<h4><a href="' . $link . '">'.__('Order','ebook-store') . ' #' . get_the_ID().'</a></h4>';
12952 } else {
12953 echo __('No orders found.');
12954 }
12955 wp_reset_postdata();
12956 }
12957 function ebook_pdf_reader($ebook_order = false, $ebook_post_id = false) {
12958 $src = plugins_url('',__FILE__) . '/includes/ViewerJS/index.html';
12959 // /return $src;
12960 $src = add_query_arg('title',__('Online Viewer','ebook-store'),$src);
12961 if ($ebook_order) {
12962 return '<iframe style="float:left;" src="'.$src.'#' . $ebook_order['downloadlink'][0] . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
12963 } else {
12964 $meta = get_post_meta($ebook_post_id, 'ebook_wp_custom_attachment', true);
12965
12966 if (is_array($meta)) {
12967 $pdf_url = $meta['url'];
12968 } else {
12969 $pdf_url = '';
12970 }
12971
12972 //wp_die('URL ' . $pdf_url);
12973 if (is_ssl()) {
12974 $pdf_url = str_replace('http://', 'https://', $pdf_url);
12975 }
12976 return '<iframe style="float:left;" src="'.$src.'#' . $pdf_url . '" width=\'100%\' height=\'' . get_option('pdf_viewer_height',600) . '\' allowfullscreen webkitallowfullscreen></iframe>';
12977 }
12978
12979 }
12980 function pr_die($array) {
12981 wp_die('<pre>'.print_r($array,true).'</pre>');
12982 }
12983 function ebook_store_redirect_add_order() {
12984 if (endswith(esc_url_raw($_SERVER['REQUEST_URI']), 'post-new.php?post_type=ebook_order')) {
12985 header("Location: edit.php?post_type=ebook&page=ebook-store-add-order-page");
12986 }
12987 }
12988
12989 if (!function_exists('endswith')) {
12990 function endswith($string, $test) {
12991 $strlen = strlen($string);
12992 $testlen = strlen($test);
12993 if ($testlen > $strlen) return false;
12994 return substr_compare($string, $test, $strlen - $testlen, $testlen) === 0;
12995 }
12996 }
12997 add_action( 'woocommerce_after_account_downloads', 'ebook_store_woocommerce_after_account_downloads', 10, 1 );
12998 function ebook_store_woocommerce_after_account_downloads() {
12999
13000 echo '<h3>Ebook Downloads</h3>';
13001 //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>';
13002 echo '<table class="woocommerce-MyAccount-downloads shop_table shop_table_responsive">
13003 <thead>
13004 <tr>
13005 <th class="download-product"><span class="nobr">'.__('Product','ebook-store').'</span></th>
13006 <th class="download-remaining"><span class="nobr">'.__('Download Links','ebook-store').'</span></th>
13007 <!-- <th class="download-expires"><span class="nobr">'.__('Expires','ebook-store').'</span></th>
13008 <th class="download-file"><span class="nobr">'.__('File','ebook-store').'</span></th>-->
13009 </tr>
13010 </thead>
13011 <tbody>';
13012 $customer_user_id = get_current_user_id();
13013
13014 $customer_orders = wc_get_orders( array(
13015 'meta_key' => '_customer_user',
13016 'meta_value' => $customer_user_id,
13017 'limit' => -1,
13018 ));
13019 foreach ( $customer_orders as $order ) {
13020 ebook_store_woocommerce_order_details($order, true);
13021
13022 continue;
13023 }
13024 echo '
13025 </tbody></table>';
13026 }
13027
13028 function ebook_store_wp_super_cache_warning() {
13029 if (!file_exists(WP_PLUGIN_DIR . '/wp-super-cache/wp-cache.php')) {
13030 return true;
13031 }
13032
13033 if ( isset( $_GET['dismiss'] ) && $_GET['dismiss'] === 'wpsc' ) {
13034 update_option( 'ebookstorewpsc', 1 );
13035 return true;
13036 }
13037 if (get_option( 'ebookstorewpsc') < 1) {
13038 ?>
13039 <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
13040 ebook
13041 </textarea></p><a id="wpsc-dismiss" href="<?php echo esc_url($_SERVER['REQUEST_URI']); ?>&dismiss=wpsc">Dismiss</a></div>
13042 <?php
13043
13044 }
13045 }
13046
13047 function ebook_store_woocommerce_variation_fields( $loop, $variation_data, $variation ) {
13048 if ( get_option( 'ebook_store_woocommerce_integration' ) == 0 ) {
13049 return;
13050 }
13051
13052 $variation_id = isset( $variation->ID ) ? (int) $variation->ID : 0;
13053 $selected_ebook_id = $variation_id > 0 ? (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) : 0;
13054 $options = ebook_store_get_woocommerce_options();
13055 ?>
13056 <div class="form-row form-row-full">
13057 <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>
13058 <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">
13059 <?php foreach ( $options as $ebook_id => $ebook_title ) : ?>
13060 <option value="<?php echo esc_attr( $ebook_id ); ?>" <?php selected( $selected_ebook_id, (int) $ebook_id ); ?>>
13061 <?php echo esc_html( $ebook_title ); ?>
13062 </option>
13063 <?php endforeach; ?>
13064 </select>
13065 <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>
13066 </div>
13067 <?php
13068 }
13069
13070 function ebook_store_save_woocommerce_variation_data( $variation_id, $loop ) {
13071 $variation_id = (int) $variation_id;
13072 $old_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
13073 if ( $old_ebook_id > 0 ) {
13074 update_post_meta( $old_ebook_id, 'woocommerce_product_id', 0 );
13075 }
13076
13077 $new_ebook_id = 0;
13078 if ( isset( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) ) {
13079 $new_ebook_id = absint( wp_unslash( $_POST['ebook_store_book_id_variation'][ $variation_id ] ) );
13080 }
13081
13082 if ( $new_ebook_id > 0 ) {
13083 update_post_meta( $variation_id, 'ebook_store_book_id', $new_ebook_id );
13084 update_post_meta( $variation_id, '_enable_ebook_store_option', 'yes' );
13085 update_post_meta( $new_ebook_id, 'woocommerce_product_id', $variation_id );
13086 } else {
13087 delete_post_meta( $variation_id, 'ebook_store_book_id' );
13088 update_post_meta( $variation_id, '_enable_ebook_store_option', 'no' );
13089 }
13090 }
13091
13092 function ebook_store_get_wc_item_product_ids( $item ) {
13093 $product_id = 0;
13094 $variation_id = 0;
13095
13096 if ( is_object( $item ) ) {
13097 if ( method_exists( $item, 'get_product_id' ) ) {
13098 $product_id = (int) $item->get_product_id();
13099 }
13100 if ( method_exists( $item, 'get_variation_id' ) ) {
13101 $variation_id = (int) $item->get_variation_id();
13102 }
13103 } elseif ( is_array( $item ) ) {
13104 $product_id = isset( $item['product_id'] ) ? (int) $item['product_id'] : 0;
13105 $variation_id = isset( $item['variation_id'] ) ? (int) $item['variation_id'] : 0;
13106 }
13107
13108 return array(
13109 'product_id' => $product_id,
13110 'variation_id' => $variation_id,
13111 );
13112 }
13113
13114 function ebook_store_product_has_variation_ebook_mappings( $product_id ) {
13115 $product_id = (int) $product_id;
13116 if ( $product_id < 1 ) {
13117 return false;
13118 }
13119
13120 $variation_ids = get_posts(
13121 array(
13122 'post_parent' => $product_id,
13123 'post_type' => 'product_variation',
13124 'post_status' => array( 'publish', 'private' ),
13125 'posts_per_page' => -1,
13126 'fields' => 'ids',
13127 'no_found_rows' => true,
13128 )
13129 );
13130
13131 if ( ! is_array( $variation_ids ) || empty( $variation_ids ) ) {
13132 return false;
13133 }
13134
13135 foreach ( $variation_ids as $variation_id ) {
13136 if ( (int) get_post_meta( $variation_id, 'ebook_store_book_id', true ) > 0 ) {
13137 return true;
13138 }
13139 }
13140
13141 return false;
13142 }
13143
13144 function ebook_store_get_wc_product_ebook_mapping( $product_id, $variation_id = 0 ) {
13145 $product_id = (int) $product_id;
13146 $variation_id = (int) $variation_id;
13147
13148 if ( $variation_id > 0 ) {
13149 $variation_ebook_id = (int) get_post_meta( $variation_id, 'ebook_store_book_id', true );
13150 if ( $variation_ebook_id > 0 ) {
13151 return array(
13152 'ebook_id' => $variation_ebook_id,
13153 'product_id' => $product_id,
13154 'variation_id' => $variation_id,
13155 'mapped_post_id' => $variation_id,
13156 );
13157 }
13158
13159 if ( ebook_store_product_has_variation_ebook_mappings( $product_id ) ) {
13160 return array(
13161 'ebook_id' => 0,
13162 'product_id' => $product_id,
13163 'variation_id' => $variation_id,
13164 'mapped_post_id' => $variation_id,
13165 );
13166 }
13167
13168 return array(
13169 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
13170 'product_id' => $product_id,
13171 'variation_id' => $variation_id,
13172 'mapped_post_id' => $product_id,
13173 );
13174 }
13175
13176 return array(
13177 'ebook_id' => (int) get_post_meta( $product_id, 'ebook_store_book_id', true ),
13178 'product_id' => $product_id,
13179 'variation_id' => 0,
13180 'mapped_post_id' => $product_id,
13181 );
13182 }
13183
13184 function ebook_store_get_wc_item_ebook_mapping( $item ) {
13185 $ids = ebook_store_get_wc_item_product_ids( $item );
13186 return ebook_store_get_wc_product_ebook_mapping( $ids['product_id'], $ids['variation_id'] );
13187 }
13188
13189 function ebook_store_get_wc_product_reference_for_ebook( $ebook_id ) {
13190 global $wpdb;
13191
13192 $ebook_id = (int) $ebook_id;
13193 if ( $ebook_id < 1 || ! function_exists( 'wc_get_product' ) ) {
13194 return 0;
13195 }
13196
13197 $stored_reference_id = (int) get_post_meta( $ebook_id, 'woocommerce_product_id', true );
13198 $allowed_statuses = array( 'publish', 'private' );
13199 if ( $stored_reference_id > 0 && in_array( get_post_status( $stored_reference_id ), $allowed_statuses, true ) ) {
13200 $stored_mapping = (int) get_post_meta( $stored_reference_id, 'ebook_store_book_id', true );
13201 if ( $stored_mapping === $ebook_id ) {
13202 return $stored_reference_id;
13203 }
13204 }
13205
13206 $product_reference_id = 0;
13207 $status_placeholders = implode( ',', array_fill( 0, count( $allowed_statuses ), '%s' ) );
13208 $query_params = array_merge(
13209 array( 'ebook_store_book_id', (string) $ebook_id, 'product', 'product_variation' ),
13210 $allowed_statuses
13211 );
13212
13213 $product_reference_id = (int) $wpdb->get_var(
13214 $wpdb->prepare(
13215 "
13216 SELECT pm.post_id
13217 FROM {$wpdb->postmeta} pm
13218 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
13219 WHERE pm.meta_key = %s
13220 AND pm.meta_value = %s
13221 AND p.post_type IN (%s, %s)
13222 AND p.post_status IN ($status_placeholders)
13223 ORDER BY pm.post_id DESC
13224 LIMIT 1
13225 ",
13226 $query_params
13227 )
13228 );
13229
13230 if ( $product_reference_id > 0 && $product_reference_id !== $stored_reference_id && ! wp_doing_ajax() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
13231 update_post_meta( $ebook_id, 'woocommerce_product_id', $product_reference_id );
13232 }
13233
13234 return $product_reference_id;
13235 }
13236
13237 function ebook_store_get_wc_add_to_cart_config( $product_reference_id ) {
13238 $product_reference_id = (int) $product_reference_id;
13239 if ( $product_reference_id < 1 || ! function_exists( 'wc_get_product' ) ) {
13240 return false;
13241 }
13242
13243 $product = wc_get_product( $product_reference_id );
13244 if ( ! $product ) {
13245 return false;
13246 }
13247
13248 if ( $product->is_type( 'variation' ) ) {
13249 return array(
13250 'product_id' => (int) $product->get_parent_id(),
13251 'variation_id' => (int) $product->get_id(),
13252 'variation' => $product->get_variation_attributes(),
13253 'title' => $product->get_name(),
13254 );
13255 }
13256
13257 return array(
13258 'product_id' => (int) $product->get_id(),
13259 'variation_id' => 0,
13260 'variation' => array(),
13261 'title' => $product->get_name(),
13262 );
13263 }
13264
13265 function ebook_store_cart_contains_wc_product( $cart_config ) {
13266 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
13267 return false;
13268 }
13269
13270 foreach ( WC()->cart->get_cart() as $cart_item ) {
13271 $cart_product_id = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
13272 $cart_variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0;
13273 if ( $cart_product_id === (int) $cart_config['product_id'] && $cart_variation_id === (int) $cart_config['variation_id'] ) {
13274 return true;
13275 }
13276 }
13277
13278 return false;
13279 }
13280
13281 add_action('admin_post_ebook_store_test_encryption', 'ebook_store_test_encryption');
13282 add_action('admin_post_nopriv_ebook_store_test_encryption', 'ebook_store_test_encryption');
13283 function ebook_store_test_encryption() {
13284 if (!isset($_GET['ebook_id'])) {
13285 wp_die(esc_html__('Missing ebook ID.', 'ebook-store'));
13286 }
13287 error_log('Ebook Store Test Encryption: request=' . print_r($_REQUEST, true));
13288 $ebook_id = absint($_GET['ebook_id']);
13289 if (!$ebook_id || get_post_type($ebook_id) !== 'ebook') {
13290 wp_die(esc_html__('Invalid ebook.', 'ebook-store'));
13291 }
13292 if (!current_user_can('edit_post', $ebook_id)) {
13293 wp_die(esc_html__('You are not allowed to perform this test.', 'ebook-store'));
13294 }
13295 check_admin_referer('ebook_store_test_encryption_' . $ebook_id);
13296
13297 $test_format = isset($_GET['format']) ? sanitize_key( wp_unslash( $_GET['format'] ) ) : 'pdf';
13298 if ( $test_format === 'epub' ) {
13299 ebook_store_test_epub_watermark( $ebook_id );
13300 exit;
13301 }
13302
13303 $pdf_meta = get_post_meta($ebook_id, 'ebook_wp_custom_attachment', true);
13304 if (!is_array($pdf_meta) || empty($pdf_meta['file']) || !file_exists($pdf_meta['file'])) {
13305 error_log('Ebook Store Test Encryption: PDF missing for ebook ' . $ebook_id);
13306 wp_die(esc_html__('No PDF found for this ebook.', 'ebook-store'));
13307 }
13308 if (!ebook_store_encryptable($pdf_meta['file'])) {
13309 error_log('Ebook Store Test Encryption: PDF not encryptable');
13310 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'));
13311 }
13312
13313 global $attachment, $ebook_email_delivery;
13314 $original_attachment = isset($attachment) ? $attachment : null;
13315 $original_email_delivery = isset($ebook_email_delivery) ? $ebook_email_delivery : null;
13316
13317 $attachment = array(
13318 array('file' => $pdf_meta['file'])
13319 );
13320 $ebook_email_delivery = array(
13321 'order' => array(
13322 'order_id' => 0,
13323 'ebook' => $ebook_id,
13324 'payer_email' => 'test@example.com',
13325 )
13326 );
13327
13328 $data = array(
13329 'txn_id' => 'encryption-test',
13330 'payment_date' => current_time('mysql'),
13331 'payer_email' => 'test@example.com',
13332 'password' => 'test',
13333 'md5_nonce' => 'encryption_test_' . $ebook_id,
13334 'ebook_key' => ebook_store_test_verification_key( $ebook_id ),
13335 );
13336
13337 try {
13338 $encrypted_file = ebook_encrypt_pdf($data, null);
13339 } catch (Throwable $e) {
13340 error_log('Ebook Store Test Encryption: exception ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
13341 $attachment = $original_attachment;
13342 $ebook_email_delivery = $original_email_delivery;
13343 wp_die(sprintf(
13344 '%s %s',
13345 esc_html__('Encryption test failed with an exception.', 'ebook-store'),
13346 esc_html($e->getMessage())
13347 ));
13348 }
13349 if (!$encrypted_file || !file_exists($encrypted_file)) {
13350 error_log('Ebook Store Test Encryption: encryption failed, file missing.');
13351 $attachment = $original_attachment;
13352 $ebook_email_delivery = $original_email_delivery;
13353 wp_die(esc_html__('Encryption test failed. Please check your PHP error log for details.', 'ebook-store'));
13354 }
13355
13356 error_log('Ebook Store Test Encryption: success, streaming file ' . $encrypted_file);
13357 header('Content-Type: application/pdf');
13358 header('Content-Disposition: inline; filename="' . basename($pdf_meta['file']) . '"');
13359 header('Content-Length: ' . filesize($encrypted_file));
13360 ebook_readfile_chunked($encrypted_file);
13361 @unlink($encrypted_file);
13362
13363 $attachment = $original_attachment;
13364 $ebook_email_delivery = $original_email_delivery;
13365 exit;
13366 }
13367
13368 /**
13369 * Stream a watermarked copy of an ebook's ePub so the owner can preview exactly
13370 * what a buyer receives. Uses the configured watermark settings with sample buyer
13371 * values, and never touches the stored upload.
13372 *
13373 * @param int $ebook_id Ebook post ID (already permission- and nonce-checked).
13374 */
13375 function ebook_store_test_epub_watermark( $ebook_id ) {
13376 $epub_meta = get_post_meta( $ebook_id, 'ebook_wp_custom_attachment_epub', true );
13377 if ( ! is_array( $epub_meta ) || empty( $epub_meta['file'] ) || ! file_exists( $epub_meta['file'] ) ) {
13378 wp_die( esc_html__( 'No ePub found for this ebook.', 'ebook-store' ) );
13379 }
13380 if ( ! function_exists( 'ebook_store_watermark_epub' ) ) {
13381 wp_die( esc_html__( 'The ePub watermarking engine is not available on this server.', 'ebook-store' ) );
13382 }
13383
13384 $src = $epub_meta['file'];
13385
13386 // Build a representative watermark from the configured template, standing in
13387 // sample buyer values for a real order.
13388 $template = (string) get_option( 'ebook_store_epub_watermark_text', 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%. Please do not share.' );
13389 if ( trim( $template ) === '' ) {
13390 $template = 'Licensed to %%first_name%% %%last_name%% - %%payer_email%%';
13391 }
13392 $sample = str_replace(
13393 array( '%%first_name%%', '%%last_name%%', '%%payer_email%%', '%%residence_country%%', '%%payment_date%%', '%%txn_id%%' ),
13394 array( 'Test', 'Buyer', 'test@example.com', 'US', date_i18n( 'Y-m-d' ), 'watermark-test' ),
13395 $template
13396 );
13397
13398 $dir = untrailingslashit( get_temp_dir() );
13399 $dest = $dir . '/ebook-store-epub-test-' . $ebook_id . '-' . wp_generate_password( 8, false ) . '.epub';
13400
13401 $qr_text = ( get_option( 'ebook_store_epub_qr' ) && function_exists( 'ebook_store_get_order_verification_url' ) )
13402 ? ebook_store_get_order_verification_url( ebook_store_test_verification_key( $ebook_id ) )
13403 : '';
13404
13405 $out = ebook_store_watermark_epub(
13406 $src,
13407 $dest,
13408 $sample,
13409 array(
13410 'scope' => get_option( 'ebook_store_epub_watermark_scope', 'first' ),
13411 'position' => get_option( 'ebook_store_epub_watermark_position', 'bottom' ),
13412 'style' => get_option( 'ebook_store_epub_watermark_style', 'subtle' ),
13413 'stamp_metadata' => get_option( 'ebook_store_epub_stamp_metadata' ),
13414 'qr' => get_option( 'ebook_store_epub_qr' ),
13415 'qr_scope' => get_option( 'ebook_store_epub_qr_scope', 'first' ),
13416 'qr_position' => get_option( 'ebook_store_epub_qr_position', 'bottom' ),
13417 'qr_text' => $qr_text,
13418 'order_id' => 0,
13419 )
13420 );
13421
13422 if ( $out !== $dest || ! file_exists( $dest ) ) {
13423 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' ) );
13424 }
13425
13426 nocache_headers();
13427 header( 'Content-Type: application/epub+zip' );
13428 header( 'Content-Disposition: attachment; filename="watermark-test-' . basename( $src ) . '"' );
13429 header( 'Content-Length: ' . filesize( $dest ) );
13430 ebook_readfile_chunked( $dest );
13431 @unlink( $dest );
13432 exit;
13433 }
13434
13435 function ebook_store_deregister_embeds(){
13436 wp_dequeue_script( 'wp-embed' );
13437 }
13438
13439 /**
13440 * Log a adyen webhook event.
13441 *
13442 * Writes through ebook_store_log(), which keeps the file outside the web root,
13443 * caps its size and stays silent unless logging is switched on. The old version
13444 * appended to an uncapped .txt inside the plugin folder that any visitor could
13445 * fetch; one of those files reached 118 MB and contained live order tokens.
13446 *
13447 * @param string $message Message.
13448 * @param array $context Context.
13449 */
13450 function ebook_store_log_adyen_event( $message, $context = array() ) {
13451 ebook_store_log( 'adyen', $message, $context );
13452 }
13453
13454 function ebook_store_get_adyen_api_base_url() {
13455 $environment = (string) get_option( 'adyen_environment', 'test' );
13456 $version = 'v71';
13457
13458 if ( $environment === 'live' ) {
13459 $prefix = trim( (string) get_option( 'adyen_live_prefix', '' ) );
13460 if ( $prefix === '' ) {
13461 return new WP_Error( 'adyen_missing_live_prefix', 'Adyen live URL prefix not configured' );
13462 }
13463
13464 return 'https://' . rawurlencode( $prefix ) . '-checkout-live.adyenpayments.com/checkout/' . $version;
13465 }
13466
13467 return 'https://checkout-test.adyen.com/' . $version;
13468 }
13469
13470 function ebook_store_get_adyen_webhook_url() {
13471 return site_url( 'wp-json/ebook-store/v1/adyen-webhook' );
13472 }
13473
13474 function ebook_store_adyen_request( $method, $path, $body = null, $headers = array() ) {
13475 $api_key = trim( (string) get_option( 'adyen_api_key', '' ) );
13476 if ( $api_key === '' ) {
13477 return new WP_Error( 'adyen_missing_api_key', 'Adyen API key not configured' );
13478 }
13479
13480 $base_url = ebook_store_get_adyen_api_base_url();
13481 if ( is_wp_error( $base_url ) ) {
13482 return $base_url;
13483 }
13484
13485 $request_headers = array_merge(
13486 array(
13487 'Content-Type' => 'application/json',
13488 'X-API-Key' => $api_key,
13489 ),
13490 (array) $headers
13491 );
13492
13493 $args = array(
13494 'method' => strtoupper( (string) $method ),
13495 'timeout' => 20,
13496 'headers' => $request_headers,
13497 'user-agent' => 'Ebook-Store/' . ( defined( 'EBOOK_STORE_VERSION' ) ? EBOOK_STORE_VERSION : '1.0' ),
13498 );
13499
13500 if ( null !== $body ) {
13501 $args['body'] = wp_json_encode( $body );
13502 }
13503
13504 $response = wp_remote_request( rtrim( $base_url, '/' ) . '/' . ltrim( (string) $path, '/' ), $args );
13505 if ( is_wp_error( $response ) ) {
13506 return $response;
13507 }
13508
13509 $status_code = (int) wp_remote_retrieve_response_code( $response );
13510 $response_body = wp_remote_retrieve_body( $response );
13511 $data = $response_body !== '' ? json_decode( $response_body, true ) : array();
13512
13513 if ( $status_code < 200 || $status_code >= 300 ) {
13514 $message = is_array( $data ) && ! empty( $data['message'] ) ? (string) $data['message'] : 'Adyen request failed.';
13515 return new WP_Error(
13516 'adyen_request_failed',
13517 $message,
13518 array(
13519 'status_code' => $status_code,
13520 'body' => $data,
13521 )
13522 );
13523 }
13524
13525 return is_array( $data ) ? $data : array();
13526 }
13527
13528 function ebook_store_get_adyen_shopper_locale() {
13529 $locale = get_locale();
13530 if ( function_exists( 'ebook_store_get_selected_language_code' ) ) {
13531 $language_code = (string) ebook_store_get_selected_language_code();
13532 if ( $language_code !== '' && $language_code !== '0' ) {
13533 $locale = $language_code . '_' . strtoupper( $language_code );
13534 }
13535 }
13536
13537 $locale = str_replace( '-', '_', (string) $locale );
13538 if ( preg_match( '/^[a-z]{2}_[A-Z]{2}$/', $locale ) ) {
13539 return str_replace( '_', '-', $locale );
13540 }
13541
13542 $language = strtolower( substr( $locale, 0, 2 ) );
13543 if ( preg_match( '/^[a-z]{2}$/', $language ) ) {
13544 return $language . '-' . strtoupper( $language );
13545 }
13546
13547 return 'en-US';
13548 }
13549
13550 function ebook_store_get_adyen_country_code() {
13551 $locale = str_replace( '-', '_', (string) get_locale() );
13552 if ( preg_match( '/^[a-z]{2}_([A-Z]{2})$/', $locale, $matches ) ) {
13553 return strtoupper( $matches[1] );
13554 }
13555
13556 return '';
13557 }
13558
13559 function ebook_store_store_adyen_checkout_state( $state ) {
13560 if ( ! is_array( $state ) ) {
13561 return;
13562 }
13563
13564 $state['created_at'] = isset( $state['created_at'] ) ? (int) $state['created_at'] : time();
13565 $ttl = 7 * DAY_IN_SECONDS;
13566
13567 if ( ! empty( $state['payment_link_id'] ) ) {
13568 set_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ), $state, $ttl );
13569 }
13570
13571 if ( ! empty( $state['ebook_key'] ) ) {
13572 set_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ), $state, $ttl );
13573 }
13574 }
13575
13576 function ebook_store_get_adyen_checkout_state( $context = array() ) {
13577 $context = is_array( $context ) ? $context : array();
13578 $keys = array();
13579
13580 if ( ! empty( $context['payment_link_id'] ) ) {
13581 $keys[] = 'ebook_store_adyen_link_' . sanitize_key( (string) $context['payment_link_id'] );
13582 }
13583
13584 if ( ! empty( $context['ebook_key'] ) ) {
13585 $keys[] = 'ebook_store_adyen_ebook_' . sanitize_key( (string) $context['ebook_key'] );
13586 }
13587
13588 foreach ( $keys as $key ) {
13589 $state = get_transient( $key );
13590 if ( is_array( $state ) ) {
13591 return $state;
13592 }
13593 }
13594
13595 return false;
13596 }
13597
13598 function ebook_store_clear_adyen_checkout_state( $state ) {
13599 if ( ! is_array( $state ) ) {
13600 return;
13601 }
13602
13603 if ( ! empty( $state['payment_link_id'] ) ) {
13604 delete_transient( 'ebook_store_adyen_link_' . sanitize_key( (string) $state['payment_link_id'] ) );
13605 }
13606
13607 if ( ! empty( $state['ebook_key'] ) ) {
13608 delete_transient( 'ebook_store_adyen_ebook_' . sanitize_key( (string) $state['ebook_key'] ) );
13609 }
13610 }
13611
13612 function ebook_store_fetch_adyen_payment_link( $payment_link_id ) {
13613 $payment_link_id = trim( (string) $payment_link_id );
13614 if ( $payment_link_id === '' ) {
13615 return new WP_Error( 'adyen_missing_payment_link_id', 'Adyen payment link ID is missing' );
13616 }
13617
13618 return ebook_store_adyen_request( 'GET', 'paymentLinks/' . rawurlencode( $payment_link_id ) );
13619 }
13620
13621 function ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) {
13622 return 'ebook_store_adyen_lock_' . sanitize_key( (string) $psp_reference );
13623 }
13624
13625 function ebook_store_acquire_adyen_order_creation_lock( $psp_reference ) {
13626 $psp_reference = (string) $psp_reference;
13627 if ( $psp_reference === '' ) {
13628 return false;
13629 }
13630
13631 return add_option( ebook_store_get_adyen_order_creation_lock_key( $psp_reference ), time(), '', 'no' );
13632 }
13633
13634 function ebook_store_release_adyen_order_creation_lock( $psp_reference ) {
13635 $psp_reference = (string) $psp_reference;
13636 if ( $psp_reference === '' ) {
13637 return;
13638 }
13639
13640 delete_option( ebook_store_get_adyen_order_creation_lock_key( $psp_reference ) );
13641 }
13642
13643 function ebook_store_wait_for_adyen_order( $psp_reference, $ebook_key = '' ) {
13644 $psp_reference = (string) $psp_reference;
13645 $ebook_key = (string) $ebook_key;
13646
13647 for ( $attempt = 0; $attempt < 10; $attempt++ ) {
13648 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
13649 if ( $existing_order_id ) {
13650 return $existing_order_id;
13651 }
13652
13653 if ( $ebook_key !== '' ) {
13654 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
13655 if ( $existing_order_id ) {
13656 return $existing_order_id;
13657 }
13658 }
13659
13660 usleep( 200000 );
13661 }
13662
13663 return 0;
13664 }
13665
13666 function ebook_store_create_order_from_adyen_notification( $notification, $source = 'webhook' ) {
13667 $notification = is_array( $notification ) ? $notification : array();
13668 $psp_reference = isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '';
13669 $merchant_reference = isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '';
13670 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
13671 $payment_link_id = isset( $additional_data['paymentLinkId'] ) ? (string) $additional_data['paymentLinkId'] : '';
13672 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
13673 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
13674
13675 if ( $psp_reference === '' ) {
13676 return new WP_Error( 'adyen_missing_psp_reference', 'Adyen webhook is missing the PSP reference' );
13677 }
13678
13679 if ( $event_code !== 'AUTHORISATION' ) {
13680 return new WP_Error( 'adyen_invalid_event_code', 'Adyen notification is not an AUTHORISATION event' );
13681 }
13682
13683 if ( ! $success ) {
13684 return new WP_Error( 'adyen_payment_not_authorised', 'Adyen payment was not authorised successfully' );
13685 }
13686
13687 $existing_order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', $psp_reference );
13688 if ( $existing_order_id ) {
13689 return $existing_order_id;
13690 }
13691
13692 if ( $merchant_reference !== '' ) {
13693 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $merchant_reference );
13694 if ( $existing_order_id ) {
13695 return $existing_order_id;
13696 }
13697 }
13698
13699 $has_lock = ebook_store_acquire_adyen_order_creation_lock( $psp_reference );
13700 if ( ! $has_lock ) {
13701 $existing_order_id = ebook_store_wait_for_adyen_order( $psp_reference, $merchant_reference );
13702 if ( $existing_order_id ) {
13703 return $existing_order_id;
13704 }
13705
13706 return new WP_Error( 'adyen_order_creation_in_progress', 'Adyen order creation is already in progress' );
13707 }
13708
13709 try {
13710 $state = ebook_store_get_adyen_checkout_state(
13711 array(
13712 'payment_link_id' => $payment_link_id,
13713 'ebook_key' => $merchant_reference,
13714 )
13715 );
13716 $state = is_array( $state ) ? $state : array();
13717
13718 $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' );
13719 if ( is_wp_error( $payment_link ) ) {
13720 $payment_link = array();
13721 }
13722
13723 $ebook_id = ! empty( $state['ebook_id'] ) ? absint( $state['ebook_id'] ) : 0;
13724 if ( ! $ebook_id && ! empty( $payment_link['metadata']['ebook_id'] ) ) {
13725 $ebook_id = absint( $payment_link['metadata']['ebook_id'] );
13726 }
13727
13728 if ( ! $ebook_id ) {
13729 return new WP_Error( 'adyen_missing_ebook', 'Adyen payment could not be matched to an ebook' );
13730 }
13731
13732 $ebook_post = get_post( $ebook_id );
13733 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
13734 return new WP_Error( 'adyen_invalid_ebook', 'Adyen payment points to an invalid ebook' );
13735 }
13736
13737 $shopper_email = '';
13738 if ( ! empty( $payment_link['shopperEmail'] ) ) {
13739 $shopper_email = sanitize_email( (string) $payment_link['shopperEmail'] );
13740 }
13741 if ( $shopper_email === '' && ! empty( $additional_data['shopperEmail'] ) ) {
13742 $shopper_email = sanitize_email( (string) $additional_data['shopperEmail'] );
13743 }
13744 if ( $shopper_email === '' && ! empty( $state['buyer_email'] ) ) {
13745 $shopper_email = sanitize_email( (string) $state['buyer_email'] );
13746 }
13747 if ( $shopper_email === '' ) {
13748 $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 ) );
13749 }
13750
13751 $shopper_name = '';
13752 if ( ! empty( $payment_link['shopperName'] ) && is_array( $payment_link['shopperName'] ) ) {
13753 $shopper_name = trim(
13754 ( isset( $payment_link['shopperName']['firstName'] ) ? (string) $payment_link['shopperName']['firstName'] : '' ) . ' ' .
13755 ( isset( $payment_link['shopperName']['lastName'] ) ? (string) $payment_link['shopperName']['lastName'] : '' )
13756 );
13757 }
13758 $name_parts = ebook_store_parse_customer_name( $shopper_name );
13759 if ( $name_parts[0] === '' ) {
13760 $name_parts[0] = __( 'Adyen', 'ebook-store' );
13761 $name_parts[1] = __( 'Buyer', 'ebook-store' );
13762 }
13763
13764 $amount_value = isset( $notification['amount']['value'] ) ? ((float) $notification['amount']['value'] / 100) : 0.0;
13765 $currency = isset( $notification['amount']['currency'] ) ? strtoupper( (string) $notification['amount']['currency'] ) : ebook_store_get_store_currency();
13766 $country_code = ! empty( $payment_link['countryCode'] ) ? strtoupper( (string) $payment_link['countryCode'] ) : '';
13767
13768 $order_data = array(
13769 'first_name' => $name_parts[0],
13770 'last_name' => $name_parts[1],
13771 'payer_email' => $shopper_email,
13772 'payment_status' => 'Completed',
13773 'txn_id' => $psp_reference,
13774 'payment_type' => 'adyen',
13775 'mc_gross' => $amount_value,
13776 'mc_fee' => 0,
13777 'ebook_key' => $merchant_reference,
13778 'ebook' => $ebook_id,
13779 'post_id' => $ebook_id,
13780 'mc_currency' => $currency,
13781 'residence_country' => $country_code,
13782 'downloads' => 0,
13783 'adyen_psp_reference' => $psp_reference,
13784 'adyen_payment_link_id' => $payment_link_id,
13785 'adyen_event_code' => $event_code,
13786 'adyen_order_source' => $source,
13787 );
13788
13789 $order_id = ebook_store_add_order( $order_data, true, false, $merchant_reference );
13790 if ( ! $order_id ) {
13791 return new WP_Error( 'adyen_order_create_failed', 'Failed to create Ebook Store order from Adyen payment' );
13792 }
13793
13794 ebook_store_clear_adyen_checkout_state( $state );
13795 ebook_store_ga4_track_purchase_for_order( $order_id );
13796
13797 return (int) $order_id;
13798 } finally {
13799 ebook_store_release_adyen_order_creation_lock( $psp_reference );
13800 }
13801 }
13802
13803 function ebook_store_recover_adyen_order_by_ebook_key( $ebook_key ) {
13804 $ebook_key = (string) $ebook_key;
13805 if ( $ebook_key === '' ) {
13806 return new WP_Error( 'adyen_missing_ebook_key', 'Ebook key is missing for Adyen recovery' );
13807 }
13808
13809 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
13810 if ( $existing_order_id ) {
13811 return $existing_order_id;
13812 }
13813
13814 $state = ebook_store_get_adyen_checkout_state(
13815 array(
13816 'ebook_key' => $ebook_key,
13817 )
13818 );
13819 if ( ! is_array( $state ) || empty( $state['payment_link_id'] ) ) {
13820 return new WP_Error( 'adyen_checkout_state_missing', 'No Adyen checkout state found for this ebook key' );
13821 }
13822
13823 $payment_link = ebook_store_fetch_adyen_payment_link( (string) $state['payment_link_id'] );
13824 if ( is_wp_error( $payment_link ) ) {
13825 return $payment_link;
13826 }
13827
13828 if ( ! empty( $payment_link['status'] ) && strtolower( (string) $payment_link['status'] ) === 'completed' ) {
13829 return new WP_Error( 'adyen_waiting_for_webhook', 'Adyen payment is completed but Ebook Store is still waiting for the webhook confirmation.' );
13830 }
13831
13832 return 0;
13833 }
13834
13835 function ebook_store_create_adyen_payment_link( $ebook_id, $price, $title, $ebook_key ) {
13836 $ebook_id = absint( $ebook_id );
13837 if ( ! $ebook_id ) {
13838 $ebook_id = get_the_ID();
13839 }
13840
13841 $checkout_page = get_option( 'ebook_store_checkout_page' );
13842 if ( ! $checkout_page ) {
13843 $checkout_page = $ebook_id;
13844 }
13845
13846 $return_url = add_query_arg(
13847 array(
13848 'ebook_key' => $ebook_key,
13849 'action' => 'thank_you',
13850 'gateway' => 'adyen',
13851 ),
13852 get_permalink( $checkout_page )
13853 );
13854
13855 $country_code = ebook_store_get_adyen_country_code();
13856 $shopper_locale = ebook_store_get_adyen_shopper_locale();
13857 $merchant_account = trim( (string) get_option( 'adyen_merchant_account', '' ) );
13858
13859 if ( $merchant_account === '' ) {
13860 ebook_store_log_adyen_event(
13861 'Adyen payment link creation failed',
13862 array(
13863 'ebook_id' => $ebook_id,
13864 'error' => 'Adyen merchant account not configured',
13865 )
13866 );
13867 return false;
13868 }
13869
13870 $payload = array(
13871 'merchantAccount' => $merchant_account,
13872 'reference' => (string) $ebook_key,
13873 'amount' => array(
13874 'currency' => ebook_store_get_store_currency(),
13875 'value' => (int) round( (float) $price * 100 ),
13876 ),
13877 'description' => sprintf( 'Ebook Store order for %s', $title ),
13878 'shopperLocale' => $shopper_locale,
13879 'returnUrl' => $return_url,
13880 'requiredShopperFields'=> array( 'shopperEmail', 'shopperName' ),
13881 'expiresAt' => gmdate( 'Y-m-d\TH:i:s+00:00', time() + ( 30 * DAY_IN_SECONDS ) ),
13882 'metadata' => array(
13883 'ebook_id' => (string) $ebook_id,
13884 'ebook_key' => (string) $ebook_key,
13885 ),
13886 );
13887
13888 if ( $country_code !== '' ) {
13889 $payload['countryCode'] = $country_code;
13890 }
13891
13892 $response = ebook_store_adyen_request(
13893 'POST',
13894 'paymentLinks',
13895 $payload,
13896 array(
13897 'Idempotency-Key' => wp_generate_uuid4(),
13898 )
13899 );
13900
13901 if ( is_wp_error( $response ) ) {
13902 ebook_store_log_adyen_event(
13903 'Adyen payment link creation failed',
13904 array(
13905 'ebook_id' => $ebook_id,
13906 'error' => $response->get_error_message(),
13907 'data' => $response->get_error_data(),
13908 )
13909 );
13910 return false;
13911 }
13912
13913 if ( empty( $response['url'] ) || empty( $response['id'] ) ) {
13914 ebook_store_log_adyen_event(
13915 'Adyen payment link response missing data',
13916 array(
13917 'ebook_id' => $ebook_id,
13918 'response' => $response,
13919 )
13920 );
13921 return false;
13922 }
13923
13924 $state = array(
13925 'ebook_id' => $ebook_id,
13926 'ebook_key' => (string) $ebook_key,
13927 'title' => (string) $title,
13928 'payment_link_id' => (string) $response['id'],
13929 'store_amount' => (float) $price,
13930 'store_currency' => ebook_store_get_store_currency(),
13931 'country_code' => $country_code,
13932 );
13933 ebook_store_store_adyen_checkout_state( $state );
13934
13935 return (string) $response['url'];
13936 }
13937
13938 function ebook_store_verify_adyen_hmac( $notification ) {
13939 $notification = is_array( $notification ) ? $notification : array();
13940 $additional_data = isset( $notification['additionalData'] ) && is_array( $notification['additionalData'] ) ? $notification['additionalData'] : array();
13941 $provided_signature = isset( $additional_data['hmacSignature'] ) ? trim( (string) $additional_data['hmacSignature'] ) : '';
13942 $hmac_key = trim( (string) get_option( 'adyen_hmac_key', '' ) );
13943
13944 if ( $provided_signature === '' || $hmac_key === '' ) {
13945 return false;
13946 }
13947
13948 $signing_string = implode(
13949 ':',
13950 array(
13951 isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '',
13952 isset( $notification['originalReference'] ) ? (string) $notification['originalReference'] : '',
13953 isset( $notification['merchantAccountCode'] ) ? (string) $notification['merchantAccountCode'] : '',
13954 isset( $notification['merchantReference'] ) ? (string) $notification['merchantReference'] : '',
13955 isset( $notification['amount']['value'] ) ? (string) $notification['amount']['value'] : '',
13956 isset( $notification['amount']['currency'] ) ? (string) $notification['amount']['currency'] : '',
13957 isset( $notification['eventCode'] ) ? (string) $notification['eventCode'] : '',
13958 isset( $notification['success'] ) ? (string) $notification['success'] : '',
13959 )
13960 );
13961
13962 $decoded_key = base64_decode( $hmac_key, true );
13963 if ( $decoded_key === false ) {
13964 return false;
13965 }
13966
13967 $calculated_signature = base64_encode( hash_hmac( 'sha256', $signing_string, $decoded_key, true ) );
13968
13969 return hash_equals( $calculated_signature, $provided_signature );
13970 }
13971
13972 function ebook_store_register_adyen_webhook() {
13973 register_rest_route(
13974 'ebook-store/v1',
13975 '/adyen-webhook',
13976 array(
13977 'methods' => 'POST',
13978 'callback' => 'ebook_store_handle_adyen_webhook',
13979 'permission_callback' => '__return_true',
13980 )
13981 );
13982 }
13983 add_action( 'rest_api_init', 'ebook_store_register_adyen_webhook' );
13984
13985 function ebook_store_handle_adyen_webhook( WP_REST_Request $request ) {
13986 $payload = $request->get_body();
13987 $data = json_decode( $payload, true );
13988 $items = isset( $data['notificationItems'] ) && is_array( $data['notificationItems'] ) ? $data['notificationItems'] : array();
13989
13990 if ( empty( $items ) ) {
13991 ebook_store_log_adyen_event(
13992 'Adyen webhook ignored empty payload',
13993 array(
13994 'payload' => $payload,
13995 )
13996 );
13997 return new WP_REST_Response( '[accepted]', 200 );
13998 }
13999
14000 foreach ( $items as $entry ) {
14001 $notification = isset( $entry['NotificationRequestItem'] ) && is_array( $entry['NotificationRequestItem'] )
14002 ? $entry['NotificationRequestItem']
14003 : array();
14004 if ( empty( $notification ) ) {
14005 continue;
14006 }
14007
14008 if ( ! ebook_store_verify_adyen_hmac( $notification ) ) {
14009 ebook_store_log_adyen_event(
14010 'Adyen webhook HMAC verification failed',
14011 array(
14012 'notification' => $notification,
14013 )
14014 );
14015 return new WP_REST_Response( 'invalid hmac', 400 );
14016 }
14017
14018 $event_code = isset( $notification['eventCode'] ) ? strtoupper( (string) $notification['eventCode'] ) : '';
14019 $success = isset( $notification['success'] ) ? strtolower( (string) $notification['success'] ) === 'true' : false;
14020
14021 switch ( $event_code ) {
14022 case 'AUTHORISATION':
14023 $result = ebook_store_create_order_from_adyen_notification( $notification, 'webhook' );
14024 if ( is_wp_error( $result ) ) {
14025 ebook_store_log_adyen_event(
14026 'Adyen AUTHORISATION processing skipped',
14027 array(
14028 'error' => $result->get_error_message(),
14029 'notification' => $notification,
14030 )
14031 );
14032 } else {
14033 ebook_store_log_adyen_event(
14034 'Adyen order created from AUTHORISATION',
14035 array(
14036 'order_id' => $result,
14037 'pspReference' => isset( $notification['pspReference'] ) ? $notification['pspReference'] : '',
14038 )
14039 );
14040 }
14041 break;
14042
14043 case 'REFUND':
14044 if ( $success && ! empty( $notification['originalReference'] ) ) {
14045 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
14046 if ( $order_id ) {
14047 update_post_meta( $order_id, 'payment_status', 'Refunded' );
14048 update_post_meta( $order_id, 'refund_gateway', 'adyen' );
14049 update_post_meta( $order_id, 'refund_gateway_id', isset( $notification['pspReference'] ) ? (string) $notification['pspReference'] : '' );
14050 update_post_meta( $order_id, 'refund_gateway_status', 'received' );
14051 update_post_meta( $order_id, 'refund_date', current_time( 'mysql' ) );
14052 }
14053 }
14054 break;
14055
14056 case 'REFUND_FAILED':
14057 case 'REFUNDED_REVERSED':
14058 if ( ! empty( $notification['originalReference'] ) ) {
14059 $order_id = ebook_store_get_order_id_by_meta( 'adyen_psp_reference', (string) $notification['originalReference'] );
14060 if ( $order_id ) {
14061 update_post_meta( $order_id, 'payment_status', 'Completed' );
14062 update_post_meta( $order_id, 'refund_gateway_status', strtolower( $event_code ) );
14063 }
14064 }
14065 break;
14066 }
14067 }
14068
14069 return new WP_REST_Response( '[accepted]', 200 );
14070 }
14071
14072 /**
14073 * Log a stripe webhook event.
14074 *
14075 * Writes through ebook_store_log(), which keeps the file outside the web root,
14076 * caps its size and stays silent unless logging is switched on. The old version
14077 * appended to an uncapped .txt inside the plugin folder that any visitor could
14078 * fetch; one of those files reached 118 MB and contained live order tokens.
14079 *
14080 * @param string $message Message.
14081 * @param array $context Context.
14082 */
14083 function ebook_store_log_stripe_event( $message, $context = array() ) {
14084 ebook_store_log( 'stripe', $message, $context );
14085 }
14086
14087 function ebook_store_bootstrap_stripe_sdk() {
14088 require_once plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
14089
14090 $stripe_secret_key = (string) get_option( 'stripe_secret_key' );
14091 if ( $stripe_secret_key === '' ) {
14092 return new WP_Error( 'stripe_missing_secret_key', 'Stripe secret key not configured' );
14093 }
14094
14095 \Stripe\Stripe::setApiKey( $stripe_secret_key );
14096
14097 return true;
14098 }
14099
14100 function ebook_store_get_order_id_by_meta( $meta_key, $meta_value ) {
14101 if ( $meta_value === '' || $meta_value === null ) {
14102 return 0;
14103 }
14104
14105 $orders = get_posts(
14106 array(
14107 'post_type' => 'ebook_order',
14108 'post_status' => 'any',
14109 'posts_per_page' => 1,
14110 'fields' => 'ids',
14111 'meta_key' => $meta_key,
14112 'meta_value' => (string) $meta_value,
14113 )
14114 );
14115
14116 return empty( $orders ) ? 0 : (int) $orders[0];
14117 }
14118
14119 function ebook_store_parse_customer_name( $name ) {
14120 $name = trim( (string) $name );
14121 if ( $name === '' ) {
14122 return array( '', '' );
14123 }
14124
14125 $name_parts = preg_split( '/\s+/', $name, 2 );
14126
14127 return array(
14128 isset( $name_parts[0] ) ? $name_parts[0] : '',
14129 isset( $name_parts[1] ) ? $name_parts[1] : '',
14130 );
14131 }
14132
14133 function ebook_store_extract_stripe_billing_details( $session ) {
14134 $details = array(
14135 'name' => '',
14136 'email' => '',
14137 'address' => array(),
14138 'charge' => null,
14139 );
14140
14141 if ( ! empty( $session->customer_details ) ) {
14142 $details['name'] = isset( $session->customer_details->name ) ? (string) $session->customer_details->name : '';
14143 $details['email'] = isset( $session->customer_details->email ) ? (string) $session->customer_details->email : '';
14144 $details['address'] = ! empty( $session->customer_details->address ) ? (array) $session->customer_details->address : array();
14145 }
14146
14147 $payment_intent_id = '';
14148 if ( ! empty( $session->payment_intent ) ) {
14149 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
14150 ? (string) $session->payment_intent->id
14151 : (string) $session->payment_intent;
14152 }
14153
14154 if ( $payment_intent_id !== '' ) {
14155 try {
14156 $payment_intent = \Stripe\PaymentIntent::retrieve( $payment_intent_id );
14157 $charge = null;
14158
14159 if ( ! empty( $payment_intent->latest_charge ) ) {
14160 $charge = is_object( $payment_intent->latest_charge ) && isset( $payment_intent->latest_charge->id )
14161 ? $payment_intent->latest_charge
14162 : \Stripe\Charge::retrieve( (string) $payment_intent->latest_charge );
14163 } elseif ( ! empty( $payment_intent->charges ) && ! empty( $payment_intent->charges->data[0] ) ) {
14164 $charge = $payment_intent->charges->data[0];
14165 }
14166
14167 if ( $charge && ! empty( $charge->billing_details ) ) {
14168 $details['charge'] = $charge;
14169
14170 if ( $details['name'] === '' && ! empty( $charge->billing_details->name ) ) {
14171 $details['name'] = (string) $charge->billing_details->name;
14172 }
14173
14174 if ( $details['email'] === '' && ! empty( $charge->billing_details->email ) ) {
14175 $details['email'] = (string) $charge->billing_details->email;
14176 }
14177
14178 if ( empty( $details['address'] ) && ! empty( $charge->billing_details->address ) ) {
14179 $details['address'] = (array) $charge->billing_details->address;
14180 }
14181 }
14182 } catch ( Exception $e ) {
14183 ebook_store_log_stripe_event(
14184 'Stripe billing details lookup failed',
14185 array(
14186 'payment_intent_id' => $payment_intent_id,
14187 'error' => $e->getMessage(),
14188 )
14189 );
14190 }
14191 }
14192
14193 return $details;
14194 }
14195
14196 function ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id ) {
14197 if ( $payment_intent_id === '' ) {
14198 return null;
14199 }
14200
14201 try {
14202 $sessions = \Stripe\Checkout\Session::all(
14203 array(
14204 'payment_intent' => $payment_intent_id,
14205 'limit' => 1,
14206 )
14207 );
14208 } catch ( Exception $e ) {
14209 ebook_store_log_stripe_event(
14210 'Stripe session lookup by payment intent failed',
14211 array(
14212 'payment_intent_id' => $payment_intent_id,
14213 'error' => $e->getMessage(),
14214 )
14215 );
14216
14217 return null;
14218 }
14219
14220 return ! empty( $sessions->data[0] ) ? $sessions->data[0] : null;
14221 }
14222
14223 function ebook_store_find_stripe_session_by_ebook_key( $ebook_key ) {
14224 $ebook_key = (string) $ebook_key;
14225 if ( $ebook_key === '' ) {
14226 return null;
14227 }
14228
14229 try {
14230 $sessions = \Stripe\Checkout\Session::all(
14231 array(
14232 'limit' => 25,
14233 )
14234 );
14235 } catch ( Exception $e ) {
14236 ebook_store_log_stripe_event(
14237 'Stripe session lookup by ebook key failed',
14238 array(
14239 'ebook_key' => $ebook_key,
14240 'error' => $e->getMessage(),
14241 )
14242 );
14243
14244 return null;
14245 }
14246
14247 foreach ( $sessions->data as $session ) {
14248 if (
14249 ! empty( $session->metadata )
14250 && isset( $session->metadata->ebook_key )
14251 && (string) $session->metadata->ebook_key === $ebook_key
14252 ) {
14253 return $session;
14254 }
14255 }
14256
14257 return null;
14258 }
14259
14260 function ebook_store_create_order_from_stripe_session( $session, $source = 'webhook' ) {
14261 if ( empty( $session ) || empty( $session->id ) ) {
14262 return new WP_Error( 'stripe_missing_session', 'Stripe session data is missing' );
14263 }
14264
14265 $session_id = (string) $session->id;
14266 $payment_status = isset( $session->payment_status ) ? (string) $session->payment_status : '';
14267 $payment_intent_id = '';
14268 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_session_id', $session_id );
14269
14270 if ( $existing_order_id ) {
14271 return $existing_order_id;
14272 }
14273
14274 if ( ! empty( $session->payment_intent ) ) {
14275 $payment_intent_id = is_object( $session->payment_intent ) && isset( $session->payment_intent->id )
14276 ? (string) $session->payment_intent->id
14277 : (string) $session->payment_intent;
14278
14279 $existing_order_id = ebook_store_get_order_id_by_meta( 'stripe_payment_intent_id', $payment_intent_id );
14280 if ( $existing_order_id ) {
14281 return $existing_order_id;
14282 }
14283 }
14284
14285 if ( $payment_status !== 'paid' ) {
14286 return new WP_Error(
14287 'stripe_payment_not_paid',
14288 sprintf( 'Stripe session %s is not paid (%s)', $session_id, $payment_status )
14289 );
14290 }
14291
14292 $metadata = ! empty( $session->metadata ) ? $session->metadata : (object) array();
14293 $ebook_id = isset( $metadata->ebook_id ) ? absint( $metadata->ebook_id ) : 0;
14294 $ebook_key = isset( $metadata->ebook_key ) ? (string) $metadata->ebook_key : '';
14295
14296 if ( ! $ebook_id && ! empty( $session->client_reference_id ) ) {
14297 $ebook_id = absint( $session->client_reference_id );
14298 }
14299
14300 if ( $ebook_key !== '' ) {
14301 $existing_order_id = ebook_store_get_order_id_by_meta( 'ebook_key', $ebook_key );
14302 if ( $existing_order_id ) {
14303 return $existing_order_id;
14304 }
14305 }
14306
14307 if ( ! $ebook_id ) {
14308 return new WP_Error( 'stripe_missing_ebook', sprintf( 'Stripe session %s has no ebook_id metadata', $session_id ) );
14309 }
14310
14311 $ebook_post = get_post( $ebook_id );
14312 if ( ! $ebook_post || 'ebook' !== $ebook_post->post_type ) {
14313 return new WP_Error( 'stripe_invalid_ebook', sprintf( 'Stripe session %s points to invalid ebook %s', $session_id, $ebook_id ) );
14314 }
14315
14316 $billing_details = ebook_store_extract_stripe_billing_details( $session );
14317 $name_parts = ebook_store_parse_customer_name( $billing_details['name'] );
14318 $address = is_array( $billing_details['address'] ) ? $billing_details['address'] : array();
14319 $amount_total = isset( $session->amount_total ) ? ((float) $session->amount_total / 100) : 0;
14320 $amount_value = isset( $metadata->price ) && $metadata->price !== '' ? (float) $metadata->price : $amount_total;
14321 $txn_id = $payment_intent_id !== '' ? $payment_intent_id : $session_id;
14322
14323 $order_data = array(
14324 'first_name' => $name_parts[0],
14325 'last_name' => $name_parts[1],
14326 'payer_email' => (string) $billing_details['email'],
14327 'payment_status' => 'Completed',
14328 'txn_id' => $txn_id,
14329 'payment_type' => 'stripe',
14330 'mc_gross' => $amount_value,
14331 'ebook_key' => $ebook_key,
14332 'ebook' => $ebook_id,
14333 'post_id' => $ebook_id,
14334 'mc_currency' => isset( $session->currency ) && $session->currency ? strtoupper( (string) $session->currency ) : ebook_store_get_store_currency(),
14335 'residence_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
14336 'address_name' => (string) $billing_details['name'],
14337 'address_street' => isset( $address['line1'] ) ? (string) $address['line1'] : '',
14338 'address_city' => isset( $address['city'] ) ? (string) $address['city'] : '',
14339 'address_state' => isset( $address['state'] ) ? (string) $address['state'] : '',
14340 'address_zip' => isset( $address['postal_code'] ) ? (string) $address['postal_code'] : '',
14341 'address_country' => isset( $address['country'] ) ? (string) $address['country'] : '',
14342 'address_status' => 'confirmed',
14343 'downloads' => 0,
14344 'stripe_session_id' => $session_id,
14345 'stripe_payment_intent_id' => $payment_intent_id,
14346 'stripe_order_source' => $source,
14347 );
14348
14349 $order_id = ebook_store_add_order( $order_data, true, false, $ebook_key );
14350
14351 if ( ! $order_id ) {
14352 return new WP_Error( 'stripe_order_create_failed', sprintf( 'Failed to create order from Stripe session %s', $session_id ) );
14353 }
14354
14355 ebook_store_ga4_track_purchase_for_order( $order_id );
14356
14357 return (int) $order_id;
14358 }
14359
14360 function ebook_store_recover_stripe_order_by_ebook_key( $ebook_key ) {
14361 $bootstrap = ebook_store_bootstrap_stripe_sdk();
14362 if ( is_wp_error( $bootstrap ) ) {
14363 return $bootstrap;
14364 }
14365
14366 $session = ebook_store_find_stripe_session_by_ebook_key( $ebook_key );
14367 if ( ! $session ) {
14368 return new WP_Error( 'stripe_session_not_found', sprintf( 'No Stripe session found for ebook key %s', $ebook_key ) );
14369 }
14370
14371 return ebook_store_create_order_from_stripe_session( $session, 'thank_you' );
14372 }
14373
14374 function ebook_store_recover_stripe_order_by_session_id( $session_id ) {
14375 $session_id = (string) $session_id;
14376 if ( $session_id === '' ) {
14377 return new WP_Error( 'stripe_missing_session_id', 'Stripe session ID is missing' );
14378 }
14379
14380 $bootstrap = ebook_store_bootstrap_stripe_sdk();
14381 if ( is_wp_error( $bootstrap ) ) {
14382 return $bootstrap;
14383 }
14384
14385 try {
14386 $session = \Stripe\Checkout\Session::retrieve( $session_id );
14387 } catch ( Exception $e ) {
14388 return new WP_Error( 'stripe_session_lookup_failed', $e->getMessage() );
14389 }
14390
14391 return ebook_store_create_order_from_stripe_session( $session, 'thank_you_session' );
14392 }
14393
14394 function ebook_store_create_stripe_checkout($ebook_id, $price, $title, $ebook_key) {
14395 $ebook_id = absint($ebook_id);
14396 if (!$ebook_id) {
14397 $ebook_id = get_the_ID();
14398 }
14399
14400 try {
14401 $bootstrap = ebook_store_bootstrap_stripe_sdk();
14402 if (is_wp_error($bootstrap)) {
14403 throw new Exception($bootstrap->get_error_message());
14404 }
14405
14406 $checkout_page = get_option('ebook_store_checkout_page');
14407 $metadata = [
14408 'ebook_id' => $ebook_id,
14409 'ebook_key' => $ebook_key,
14410 'price' => $price,
14411 'title' => $title
14412 ];
14413
14414 $success_url = add_query_arg(
14415 array(
14416 'ebook_key' => $metadata['ebook_key'],
14417 'action' => 'thank_you',
14418 'stripe_session_id' => '{CHECKOUT_SESSION_ID}'
14419 ),
14420 get_permalink($checkout_page)
14421 );
14422
14423 $session_data = [
14424 'line_items' => [[
14425 'price_data' => [
14426 'currency' => ebook_store_get_store_currency(),
14427 'product_data' => [
14428 'name' => $title,
14429 ],
14430 'unit_amount' => $price * 100,
14431 ],
14432 'quantity' => 1,
14433 ]],
14434 'mode' => 'payment',
14435 'success_url' => $success_url,
14436 'cancel_url' => get_option('stripe_cancel_url', home_url()),
14437 'metadata' => $metadata,
14438 'client_reference_id' => (string) $ebook_id,
14439 'payment_intent_data' => [
14440 'metadata' => $metadata,
14441 ],
14442 'customer_email' => isset($_POST['email']) ? sanitize_email($_POST['email']) : null
14443 ];
14444
14445 $checkout_session = \Stripe\Checkout\Session::create($session_data);
14446
14447 return $checkout_session->url;
14448 } catch(Exception $e) {
14449 ebook_store_log_stripe_event(
14450 'Stripe checkout creation failed',
14451 [
14452 'ebook_id' => $ebook_id,
14453 'error' => $e->getMessage(),
14454 ]
14455 );
14456 return false;
14457 }
14458 }
14459
14460 // Add this near the other add_action calls
14461 add_action('rest_api_init', 'ebook_store_register_stripe_webhook');
14462
14463 function ebook_store_register_stripe_webhook() {
14464 register_rest_route('ebook-store/v1', '/stripe-webhook', array(
14465 'methods' => 'POST',
14466 'callback' => 'ebook_store_handle_stripe_webhook',
14467 'permission_callback' => '__return_true' // Public endpoint
14468 ));
14469 }
14470
14471 function ebook_store_handle_stripe_webhook(WP_REST_Request $request) {
14472 $payload = $request->get_body();
14473 $sig_header = $request->get_header( 'stripe-signature' );
14474
14475 try {
14476 $bootstrap = ebook_store_bootstrap_stripe_sdk();
14477 if ( is_wp_error( $bootstrap ) ) {
14478 throw new Exception( $bootstrap->get_error_message() );
14479 }
14480
14481 $webhook_secret = (string) get_option( 'stripe_webhook_secret' );
14482 if ( $webhook_secret === '' ) {
14483 throw new Exception( 'Stripe webhook secret not configured' );
14484 }
14485
14486 $event = \Stripe\Webhook::constructEvent( $payload, $sig_header, $webhook_secret );
14487
14488 ebook_store_log_stripe_event(
14489 'Stripe webhook verified',
14490 array(
14491 'type' => $event->type,
14492 'id' => isset( $event->id ) ? $event->id : '',
14493 )
14494 );
14495
14496 switch ( $event->type ) {
14497 case 'checkout.session.completed':
14498 case 'checkout.session.async_payment_succeeded':
14499 $order_id = ebook_store_create_order_from_stripe_session( $event->data->object, 'webhook' );
14500 if ( is_wp_error( $order_id ) ) {
14501 ebook_store_log_stripe_event(
14502 'Stripe checkout session order creation skipped',
14503 array(
14504 'type' => $event->type,
14505 'error' => $order_id->get_error_message(),
14506 )
14507 );
14508 } else {
14509 ebook_store_log_stripe_event(
14510 'Stripe order created from checkout session',
14511 array(
14512 'type' => $event->type,
14513 'order_id' => $order_id,
14514 'session' => isset( $event->data->object->id ) ? $event->data->object->id : '',
14515 )
14516 );
14517 }
14518 break;
14519
14520 case 'charge.updated':
14521 case 'charge.succeeded':
14522 $charge = $event->data->object;
14523
14524 if ( empty( $charge->paid ) || $charge->status !== 'succeeded' ) {
14525 break;
14526 }
14527
14528 $payment_intent_id = ! empty( $charge->payment_intent ) ? (string) $charge->payment_intent : '';
14529 $session = ebook_store_find_stripe_session_by_payment_intent( $payment_intent_id );
14530
14531 if ( ! $session ) {
14532 ebook_store_log_stripe_event(
14533 'Stripe charge event had no checkout session match',
14534 array(
14535 'type' => $event->type,
14536 'payment_intent_id' => $payment_intent_id,
14537 'charge_id' => isset( $charge->id ) ? $charge->id : '',
14538 )
14539 );
14540 break;
14541 }
14542
14543 $order_id = ebook_store_create_order_from_stripe_session( $session, 'charge_event' );
14544 if ( is_wp_error( $order_id ) ) {
14545 ebook_store_log_stripe_event(
14546 'Stripe charge event order creation skipped',
14547 array(
14548 'type' => $event->type,
14549 'payment_intent_id' => $payment_intent_id,
14550 'error' => $order_id->get_error_message(),
14551 )
14552 );
14553 } else {
14554 ebook_store_log_stripe_event(
14555 'Stripe order created from charge event',
14556 array(
14557 'type' => $event->type,
14558 'order_id' => $order_id,
14559 'payment_intent_id' => $payment_intent_id,
14560 )
14561 );
14562 }
14563 break;
14564
14565 default:
14566 ebook_store_log_stripe_event(
14567 'Stripe webhook ignored event',
14568 array(
14569 'type' => $event->type,
14570 )
14571 );
14572 break;
14573 }
14574
14575 return new WP_REST_Response( array( 'received' => true ), 200 );
14576 } catch ( Exception $e ) {
14577 ebook_store_log_stripe_event(
14578 'Stripe webhook error',
14579 array(
14580 'error_class' => get_class( $e ),
14581 'message' => $e->getMessage(),
14582 )
14583 );
14584
14585 return new WP_REST_Response(
14586 array( 'error' => $e->getMessage() ),
14587 $e instanceof \Stripe\Exception\SignatureVerificationException ? 400 : 500
14588 );
14589 }
14590 }
14591