PluginProbe
Leaky Paywall / trunk
Leaky Paywall vtrunk
5.1.9 5.1.8 5.1.7 5.1.6 5.1.5 5.1.4 5.1.3 5.1.2 5.1.1 5.1.0 5.0.9 4.16.17 4.16.2 4.16.3 4.16.4 4.16.5 4.16.6 4.16.7 4.16.8 4.16.9 4.17.0 4.17.1 4.17.2 4.18.0 4.18.1 All 180 releases
leaky-paywall / functions.php

functions.php in Leaky Paywall trunk, at functions.php

5,754 lines 169.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * All helper functions used with Leaky Paywall
5 *
6 * @package Leaky Paywall
7 * @since 1.0.0
8 */
9
10 if (!function_exists('get_leaky_paywall_settings')) {
11
12 /**
13 * Helper function to get Leaky Paywall settings for current site
14 *
15 * @since 1.0.0
16 *
17 * @return mixed Value set for the Leaky paywall settings.
18 */
19 function get_leaky_paywall_settings()
20 {
21 $settings = new Leaky_Paywall_Settings();
22 return $settings->get_settings();
23 }
24 }
25
26 if (!function_exists('update_leaky_paywall_settings')) {
27
28 /**
29 * Helper function to save zeen101's Leaky Paywall settings for current site
30 *
31 * @since 1.0.0
32 *
33 * @param array $settings The settings array.
34 * @return mixed Value set for the issuem options.
35 */
36 function update_leaky_paywall_settings($settings)
37 {
38
39 $lp_settings = new Leaky_Paywall_Settings();
40 $lp_settings->update_settings($settings);
41 }
42 }
43
44 if (!function_exists('is_multisite_premium')) {
45 /**
46 * Check if multisite
47 */
48 function is_multisite_premium()
49 {
50 if (is_multisite()) {
51 return true;
52 }
53 return false;
54 }
55 }
56
57
58
59 if (!function_exists('is_level_deleted')) {
60 /**
61 * Check if a level is deleted
62 *
63 * @param integer $level_id The level id.
64 * @return bool
65 */
66 function is_level_deleted($level_id)
67 {
68
69 $level = get_leaky_paywall_subscription_level($level_id);
70
71 if (isset($level['deleted']) && $level['deleted'] > 0) {
72 return true;
73 }
74
75 return false;
76 }
77 }
78
79 function is_level_hidden($level)
80 {
81 if (isset($level['hide_registration_form']) && $level['hide_registration_form'] == 'on') {
82 return true;
83 }
84 return false;
85 }
86
87 if (!function_exists('get_leaky_paywall_subscribers_site_id_by_subscriber_id')) {
88 /**
89 * Get subscriber's site id by their subscriber id
90 *
91 * @param string $subscriber_id The subscriber's subscriber id.
92 * @param string $mode The payment mode.
93 * @return string The site id
94 */
95 function get_leaky_paywall_subscribers_site_id_by_subscriber_id($subscriber_id, $mode = false)
96 {
97 $site_id = '';
98 if (empty($mode)) {
99 $settings = get_leaky_paywall_settings();
100 $mode = leaky_paywall_get_current_mode();
101 }
102
103 if (is_multisite_premium()) {
104 global $wpdb;
105 $results = $wpdb->get_col(
106 $wpdb->prepare(
107 "
108 SELECT $wpdb->usermeta.meta_key
109 FROM $wpdb->usermeta
110 WHERE $wpdb->usermeta.meta_key LIKE %s
111 AND $wpdb->usermeta.meta_value = %s
112 ",
113 '_issuem_leaky_paywall_' . $mode . '_subscriber_id%',
114 $subscriber_id
115 )
116 );
117 if (!empty($results)) {
118 foreach ($results as $result) {
119 if (preg_match('/_issuem_leaky_paywall_' . $mode . '_subscriber_id(_(.+))/', $result, $matches)) {
120 return $matches[2]; // should be the site ID that matches for this subscriber_id.
121 }
122 }
123 }
124 }
125
126 return $site_id;
127 }
128 }
129
130 if (!function_exists('leaky_paywall_get_subscriber_blog_id_by_subscriber_id')) {
131 /**
132 * Get the blog id of the site that owns a gateway customer.
133 *
134 * Unlike get_leaky_paywall_subscribers_site_id_by_subscriber_id(), this can
135 * report the main site. The main site stores its subscriber meta without a
136 * site suffix, so the suffixed scan cannot see it and returns an empty string
137 * both for "main site" and for "not found".
138 *
139 * Used by inbound handlers (gateway webhooks and anything else without an
140 * ambient site) that need to switch to the owning site before doing work.
141 *
142 * @since 5.1.x
143 *
144 * @param string $subscriber_id The gateway customer id (e.g. a Stripe cus_ id).
145 * @param string $mode The payment mode. Defaults to the current mode.
146 * @return int Blog id, or 0 if no site in the network has this customer.
147 */
148 function leaky_paywall_get_subscriber_blog_id_by_subscriber_id($subscriber_id, $mode = '')
149 {
150 if (!is_multisite_premium() || empty($subscriber_id)) {
151 return 0;
152 }
153
154 if (empty($mode)) {
155 $mode = leaky_paywall_get_current_mode();
156 }
157
158 $site_id = get_leaky_paywall_subscribers_site_id_by_subscriber_id($subscriber_id, $mode);
159
160 if ($site_id) {
161 return (int) $site_id;
162 }
163
164 // Fall back to the unsuffixed key, which only the main site uses. Query
165 // usermeta directly rather than via get_users(): on multisite WP_User_Query
166 // is scoped to the current blog's members, so it would miss a subscriber
167 // who belongs to a different site.
168 global $wpdb;
169
170 $found = $wpdb->get_var(
171 $wpdb->prepare(
172 "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value = %s LIMIT 1",
173 '_issuem_leaky_paywall_' . $mode . '_subscriber_id',
174 $subscriber_id
175 )
176 );
177
178 return $found ? (int) get_main_site_id() : 0;
179 }
180 }
181
182 if (!function_exists('get_leaky_paywall_subscribers_site_id_by_subscriber_email')) {
183 /**
184 * Get subscriber's site id by their email
185 *
186 * @param string $subscriber_email The subscriber's email.
187 * @param string $mode The payment mode.
188 * @return string The site id
189 */
190 function get_leaky_paywall_subscribers_site_id_by_subscriber_email($subscriber_email, $mode = false)
191 {
192 $site_id = '';
193 if (empty($mode)) {
194 $settings = get_leaky_paywall_settings();
195 $mode = 'off' === $settings['test_mode'] ? 'live' : 'test';
196 }
197
198 if (is_multisite_premium()) {
199 global $wpdb;
200 $results = $wpdb->get_col(
201 $wpdb->prepare(
202 "
203 SELECT $wpdb->usermeta.meta_key
204 FROM $wpdb->usermeta
205 WHERE $wpdb->usermeta.meta_key LIKE %s
206 AND $wpdb->usermeta.meta_value = %s
207 ",
208 '_issuem_leaky_paywall_' . $mode . '_subscriber_email%',
209 $subscriber_email
210 )
211 );
212 if (!empty($results)) {
213 foreach ($results as $result) {
214 if (preg_match('/_issuem_leaky_paywall_' . $mode . '_subscriber_email(_(.+))/', $result, $matches)) {
215 return $matches[2]; // should be the site ID that matches for this subscriber_id.
216 }
217 }
218 }
219 }
220
221 return $site_id;
222 }
223 }
224
225 if (!function_exists('get_leaky_paywall_subscriber_by_subscriber_id')) {
226
227 /**
228 * Get a subscriber by subscriber id
229 *
230 * @param string $subscriber_id The subscriber id.
231 * @param string $mode The payment mode.
232 * @param integer $blog_id The blog id.
233 * @return object The subscriber
234 */
235 function get_leaky_paywall_subscriber_by_subscriber_id($subscriber_id, $mode = false, $blog_id = false)
236 {
237 $site = '';
238
239 if (empty($mode)) {
240 $settings = get_leaky_paywall_settings();
241 $mode = 'off' === $settings['test_mode'] ? 'live' : 'test';
242 }
243
244 if (is_multisite_premium()) {
245 if (empty($blog_id)) {
246 $blog_id = get_leaky_paywall_subscribers_site_id_by_subscriber_id($subscriber_id);
247 if ($blog_id) {
248 $site = '_' . $blog_id;
249 }
250 } else {
251 $site = '_' . $blog_id;
252 }
253 }
254
255 $args = array(
256 'meta_key' => '_issuem_leaky_paywall_' . $mode . '_subscriber_id' . $site,
257 'meta_value' => $subscriber_id,
258 );
259 $users = get_users($args);
260
261 if (!empty($users)) {
262 foreach ($users as $user) {
263 return $user;
264 }
265 }
266
267 return false;
268 }
269 }
270
271 if (!function_exists('get_leaky_paywall_subscriber_by_subscriber_email')) {
272
273 function get_leaky_paywall_subscriber_by_subscriber_email($subscriber_email, $mode = false, $blog_id = false)
274 {
275 $site = '';
276
277 if (is_email($subscriber_email)) {
278 if (empty($mode)) {
279 $settings = get_leaky_paywall_settings();
280 $mode = 'off' === $settings['test_mode'] ? 'live' : 'test';
281 }
282
283 if (is_multisite_premium()) {
284 if (empty($blog_id)) {
285 $blog_id = get_leaky_paywall_subscribers_site_id_by_subscriber_email($subscriber_email);
286 if ($blog_id) {
287 $site = '_' . $blog_id;
288 }
289 } else {
290 $site = '_' . $blog_id;
291 }
292 }
293
294 $args = array(
295 'meta_key' => '_issuem_leaky_paywall_' . $mode . '_subscriber_email' . $site,
296 'meta_value' => $subscriber_email,
297 );
298 $users = get_users($args);
299
300 if (!empty($users)) {
301 foreach ($users as $user) {
302 return $user;
303 }
304 }
305 }
306
307 return false;
308 }
309 }
310
311 if (!function_exists('add_leaky_paywall_login_hash')) {
312
313 /**
314 * Adds unique hash to login table for user's login link
315 *
316 * @since 1.0.0
317 *
318 * @param string $email address of user "logging" in.
319 * @param string $hash of user "logging" in.
320 * @return mixed $wpdb insert ID or false
321 */
322 function add_leaky_paywall_login_hash($email, $hash)
323 {
324
325 $expiration = apply_filters('leaky_paywall_login_link_expiration', 60 * 60); // 1 hour.
326 set_transient('_lpl_' . $hash, $email, $expiration);
327 }
328 }
329
330 if (!function_exists('is_leaky_paywall_login_hash_unique')) {
331
332 /**
333 * Verifies hash is valid for login link
334 *
335 * @since 1.0.0
336 *
337 * @param string $hash of user "logging" in.
338 * @return mixed $wpdb var or false
339 */
340 function is_leaky_paywall_login_hash_unique($hash)
341 {
342
343 if (preg_match('#^[0-9a-f]{32}$#i', $hash)) { // verify we get a valid 32 character md5 hash.
344
345 return !(false !== get_transient('_lpl_' . $hash));
346 }
347
348 return false;
349 }
350 }
351
352 if (!function_exists('verify_leaky_paywall_login_hash')) {
353
354 /**
355 * Verifies hash is valid length and hasn't expired
356 *
357 * @since 1.0.0
358 *
359 * @param string $hash of user "logging" in.
360 * @return mixed $wpdb var or false
361 */
362 function verify_leaky_paywall_login_hash($hash)
363 {
364
365 if (preg_match('#^[0-9a-f]{32}$#i', $hash)) { // verify we get a valid 32 character md5 hash.
366
367 return (bool) get_transient('_lpl_' . $hash);
368 }
369
370 return false;
371 }
372 }
373
374 if (!function_exists('get_leaky_paywall_email_from_login_hash')) {
375
376 /**
377 * Gets logging in user's email address from login link's hash
378 *
379 * @since 1.0.0
380 *
381 * @param string $hash of user "logging" in.
382 * @return string email from $wpdb or false if invalid hash or expired link
383 */
384 function get_leaky_paywall_email_from_login_hash($hash)
385 {
386
387 if (preg_match('#^[0-9a-f]{32}$#i', $hash)) { // verify we get a valid 32 character md5 hash.
388 return get_transient('_lpl_' . $hash);
389 }
390
391 return false;
392 }
393 }
394
395
396 /**
397 * Returns the list of payment statuses that grant content access.
398 *
399 * @since 4.23.0
400 * @return array
401 */
402 function leaky_paywall_access_statuses() {
403 return apply_filters( 'leaky_paywall_access_statuses', array( 'active', 'pending_cancel', 'trial', 'past_due' ) );
404 }
405
406 /**
407 * Flush the per-post restriction and exemption transients when a post is saved
408 * or its terms change, so editing a post's categories/tags (or its exemption
409 * eligibility) takes effect immediately instead of waiting out the 15-minute
410 * transient window used by Leaky_Paywall_Restrictions.
411 *
412 * @since 5.1.7
413 *
414 * @param int $post_id The post (object) id being changed.
415 */
416 function leaky_paywall_flush_restriction_caches( $post_id ) {
417 $post_id = absint( $post_id );
418
419 if ( ! $post_id || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
420 return;
421 }
422
423 delete_transient( 'lp_restriction_exception_' . $post_id );
424 delete_transient( 'lp_restricted_' . $post_id );
425 }
426 add_action( 'save_post', 'leaky_paywall_flush_restriction_caches' );
427 add_action( 'set_object_terms', 'leaky_paywall_flush_restriction_caches' );
428
429 /**
430 * Set a subscriber's payment status and fire transition hooks.
431 *
432 * All status changes should go through this function so that
433 * plugins and integrations can react via the action hooks.
434 *
435 * Fires:
436 * - leaky_paywall_status_transition( $new_status, $old_status, $user_id ) — on every change
437 * - leaky_paywall_status_{$old_status}_to_{$new_status}( $user_id ) — specific transition
438 * - leaky_paywall_status_gained_access( $new_status, $user_id ) — when moving to an access status
439 * - leaky_paywall_status_lost_access( $new_status, $old_status, $user_id ) — when losing access
440 *
441 * @since 4.23.0
442 *
443 * @param int $user_id WordPress user ID.
444 * @param string $new_status The new payment status.
445 * @param string $source Optional. What triggered the change (e.g. 'stripe_webhook', 'admin', 'cron').
446 * @return bool True if status was changed, false if it was already the same.
447 */
448 function leaky_paywall_set_subscriber_status( $user_id, $new_status, $source = '' ) {
449
450 $mode = leaky_paywall_get_current_mode();
451 $site = leaky_paywall_get_current_site();
452 $meta_key = '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site;
453
454 $old_status = get_user_meta( $user_id, $meta_key, true );
455
456 /**
457 * Filter the target status before it is written.
458 *
459 * Lets extensions veto or remap a transition before it happens — e.g. the
460 * Double Opt In extension forces access-granting transitions to "pending"
461 * until the user has verified their email. Returning the same value as
462 * $old_status will result in a no-op.
463 *
464 * @since 5.0.x
465 *
466 * @param string $new_status The status the caller is trying to set.
467 * @param string $old_status The current status on the user.
468 * @param int $user_id WordPress user ID.
469 * @param string $source What triggered the change (e.g. 'stripe_webhook').
470 */
471 $new_status = apply_filters( 'leaky_paywall_target_subscriber_status', $new_status, $old_status, $user_id, $source );
472
473 // No change — skip hooks and DB write.
474 if ( $old_status === $new_status ) {
475 return false;
476 }
477
478 update_user_meta( $user_id, $meta_key, $new_status );
479
480 /**
481 * Fires on every status change.
482 *
483 * @param string $new_status The new payment status.
484 * @param string $old_status The previous payment status.
485 * @param int $user_id WordPress user ID.
486 * @param string $source What triggered the change.
487 */
488 do_action( 'leaky_paywall_status_transition', $new_status, $old_status, $user_id, $source );
489
490 /**
491 * Fires for a specific transition, e.g. leaky_paywall_status_active_to_expired.
492 *
493 * @param int $user_id WordPress user ID.
494 * @param string $source What triggered the change.
495 */
496 if ( $old_status ) {
497 do_action( 'leaky_paywall_status_' . $old_status . '_to_' . $new_status, $user_id, $source );
498 }
499
500 $access_statuses = leaky_paywall_access_statuses();
501 $had_access = in_array( $old_status, $access_statuses, true );
502 $has_access = in_array( $new_status, $access_statuses, true );
503
504 if ( ! $had_access && $has_access ) {
505 /**
506 * Fires when a subscriber gains access.
507 *
508 * @param string $new_status The new payment status.
509 * @param int $user_id WordPress user ID.
510 * @param string $source What triggered the change.
511 */
512 do_action( 'leaky_paywall_status_gained_access', $new_status, $user_id, $source );
513 }
514
515 if ( $had_access && ! $has_access ) {
516 /**
517 * Fires when a subscriber loses access.
518 *
519 * @param string $new_status The new payment status.
520 * @param string $old_status The previous payment status.
521 * @param int $user_id WordPress user ID.
522 * @param string $source What triggered the change.
523 */
524 do_action( 'leaky_paywall_status_lost_access', $new_status, $old_status, $user_id, $source );
525 }
526
527 return true;
528 }
529
530 /**
531 * Set a subscriber's level ID through a central function that fires transition hooks.
532 *
533 * @since 4.23.0
534 *
535 * @param int $user_id WordPress user ID.
536 * @param string $new_level_id The new level ID.
537 * @param string $source Optional. What triggered the change (e.g. 'stripe_webhook', 'admin', 'registration').
538 * @return bool True if level was changed, false if it was already the same.
539 */
540 function leaky_paywall_set_subscriber_level( $user_id, $new_level_id, $source = '' ) {
541
542 $mode = leaky_paywall_get_current_mode();
543 $site = leaky_paywall_get_current_site();
544 $meta_key = '_issuem_leaky_paywall_' . $mode . '_level_id' . $site;
545
546 $old_level_id = get_user_meta( $user_id, $meta_key, true );
547
548 // No change — skip hooks and DB write.
549 if ( (string) $old_level_id === (string) $new_level_id ) {
550 return false;
551 }
552
553 update_user_meta( $user_id, $meta_key, $new_level_id );
554
555 /**
556 * Fires on every level change.
557 *
558 * @since 4.23.0
559 *
560 * @param string $new_level_id The new level ID.
561 * @param string $old_level_id The previous level ID (empty string for new subscribers).
562 * @param int $user_id WordPress user ID.
563 * @param string $source What triggered the change.
564 */
565 do_action( 'leaky_paywall_level_transition', $new_level_id, $old_level_id, $user_id, $source );
566
567 /**
568 * Fires for a specific level transition, e.g. leaky_paywall_level_0_to_1.
569 *
570 * @since 4.23.0
571 *
572 * @param int $user_id WordPress user ID.
573 * @param string $source What triggered the change.
574 */
575 if ( '' !== $old_level_id ) {
576 do_action( 'leaky_paywall_level_' . $old_level_id . '_to_' . $new_level_id, $user_id, $source );
577 }
578
579 return true;
580 }
581
582 if (!function_exists('leaky_paywall_user_has_access')) {
583
584 /**
585 * Determine if a user has access based on their payment status.
586 *
587 * Status is the single source of truth for access. The daily expiration
588 * cron and gateway webhooks are responsible for transitioning statuses
589 * (e.g. active → expired) when a subscription period ends.
590 *
591 * @since 4.9.3
592 *
593 * @param object $user object from WordPress database.
594 * @return bool true if the user has access based on their payment status.
595 */
596 function leaky_paywall_user_has_access($user = null)
597 {
598 $explicit_user = null !== $user;
599
600 if (null === $user) {
601 $user = wp_get_current_user();
602 }
603
604 if (!is_object($user)) {
605 return false;
606 }
607
608 $mode = leaky_paywall_get_current_mode();
609 $site = leaky_paywall_get_current_site();
610 $payment_status = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true);
611
612 $has_access = in_array( $payment_status, leaky_paywall_access_statuses(), true );
613
614 // Enforce the expires date even for statuses like pending_cancel
615 // that would otherwise grant access indefinitely. A subscriber
616 // canceled at Stripe's period-end keeps `pending_cancel` locally
617 // until the gateway's subscription.deleted webhook flips them to
618 // expired; if that webhook fails to arrive (Stripe outage,
619 // receiver blocked, wp-cron down) they historically retained
620 // content access for months. The daily cron also skipped
621 // pending_cancel by design, so this runtime check is the safety
622 // net that ends access on time regardless of webhook or cron
623 // health. Empty or 0000-00-00 means "no expiration".
624 if ( $has_access ) {
625 $expires = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, true );
626 $has_expiration = ! empty( $expires )
627 && '0000-00-00 00:00:00' !== $expires
628 && '0' !== $expires;
629
630 if ( $has_expiration && strtotime( $expires ) < time() ) {
631 $has_access = false;
632
633 // Self-heal the stale status so the admin dashboard
634 // reflects reality (no more "Expires Soon" on
635 // subscribers whose access has actually ended) and
636 // transition hooks fire for downstream integrations
637 // like mailing-list sync. set_subscriber_status is a
638 // no-op when target matches current, so this runs
639 // exactly once per subscriber.
640 if ( function_exists( 'leaky_paywall_set_subscriber_status' ) ) {
641 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'access_check_expired' );
642 }
643 }
644 }
645
646 // Only apply the logged-in check when checking the current visitor.
647 // When a user object is explicitly provided (e.g. REST API lookup),
648 // skip this — the caller already identified the user.
649 if ( ! $explicit_user && ! is_user_logged_in() ) {
650 $has_access = false;
651 }
652
653 if (leaky_paywall_user_can_bypass_paywall_by_role($user)) {
654 $has_access = true;
655 }
656
657 return apply_filters('leaky_paywall_user_has_access', $has_access, $user);
658 }
659 }
660
661 /**
662 * Determine if a user already has an active subscription at the given level.
663 *
664 * Used to prevent duplicate subscription creation when a logged-in subscriber
665 * tries to check out for a level they already have an active subscription for.
666 *
667 * @since 5.0.7
668 *
669 * @param WP_User|int|null $user User object or ID. Defaults to current user.
670 * @param int $level_id The level ID to check against.
671 * @return bool True if the user has an active subscription at this level.
672 */
673 function leaky_paywall_user_has_active_subscription_at_level( $user, $level_id ) {
674 if ( null === $user ) {
675 $user = wp_get_current_user();
676 } elseif ( is_numeric( $user ) ) {
677 $user = get_userdata( $user );
678 }
679
680 if ( ! ( $user instanceof WP_User ) || ! $user->ID ) {
681 return false;
682 }
683
684 $level_id = intval( $level_id );
685
686 if ( $level_id < 0 ) {
687 return false;
688 }
689
690 $mode = leaky_paywall_get_current_mode();
691 $site = leaky_paywall_get_current_site();
692
693 $current_level_id = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_level_id' . $site, true );
694
695 if ( '' === $current_level_id || intval( $current_level_id ) !== $level_id ) {
696 return false;
697 }
698
699 // Non-recurring levels require manual re-registration to renew, so an active
700 // subscription at this level is not a duplicate — it just means they are
701 // within their current paid term and may want to extend it.
702 $level = get_leaky_paywall_subscription_level( $level_id );
703
704 if ( empty( $level['recurring'] ) || 'on' !== $level['recurring'] ) {
705 return false;
706 }
707
708 $payment_status = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true );
709
710 $has_active = in_array( $payment_status, leaky_paywall_access_statuses(), true );
711
712 /**
713 * Filter whether a user has an active subscription at a given level.
714 *
715 * Allows publishers to override the duplicate-subscription check, e.g. to
716 * allow certain users to create multiple subscriptions at the same level.
717 *
718 * @since 5.0.7
719 *
720 * @param bool $has_active Whether the user has an active subscription at this level.
721 * @param WP_User $user The user being checked.
722 * @param int $level_id The level ID being checked.
723 */
724 return apply_filters( 'leaky_paywall_user_has_active_subscription_at_level', $has_active, $user, $level_id );
725 }
726
727 /**
728 * Determine if a user has access based on their user role.
729 *
730 * @since 4.14.5
731 *
732 * @param object $user User object from WordPress database.
733 * @return bool true if the user has access based on their role or false they do not
734 */
735 function leaky_paywall_user_can_bypass_paywall_by_role($user)
736 {
737
738 $settings = get_leaky_paywall_settings();
739 $roles = (array) $user->roles;
740 $can_bypass = false;
741
742 foreach ($roles as $role) {
743
744 if (in_array($role, $settings['bypass_paywall_restrictions'], true)) {
745 $can_bypass = true;
746 }
747 }
748
749 return $can_bypass;
750 }
751
752 /**
753 * Get the current Leaky Paywall mode setting. Lives in the Payments tab
754 *
755 * @return string live or test
756 */
757 function leaky_paywall_get_current_mode()
758 {
759 $settings = get_leaky_paywall_settings();
760 $mode = 'off' === $settings['test_mode'] ? 'live' : 'test';
761
762 return apply_filters('leaky_paywall_current_mode', $mode);
763 }
764
765 /**
766 * Get the current Leaky Paywall site id, if multisite
767 *
768 * @return string the id of the site
769 */
770 function leaky_paywall_get_current_site()
771 {
772 if (is_multisite_premium() && !is_main_site()) {
773 $site = '_' . get_current_blog_id();
774 } else {
775 $site = '';
776 }
777
778 return apply_filters('leaky_paywall_current_site', $site);
779 }
780
781 /**
782 * Best-effort resolution of the customer's IP address for the current request.
783 *
784 * Used at transaction creation time so we can:
785 * - Store the IP on each LP transaction for support/audit/tax evidence
786 * - Pass it to payment gateways (Stripe Radar, Authorize.Net fraud filters)
787 * that use it as a signal in their fraud scoring
788 *
789 * Header precedence:
790 * 1. CF-Connecting-IP — Cloudflare-injected real client IP
791 * 2. X-Forwarded-For — standard proxy header; leftmost entry is originator
792 * 3. X-Real-IP — some reverse proxies (nginx, HAProxy)
793 * 4. REMOTE_ADDR — direct connection fallback
794 *
795 * Each candidate is validated with FILTER_VALIDATE_IP before being returned,
796 * so a spoofed or malformed header can't put junk into transaction meta or
797 * downstream gateway calls.
798 *
799 * Publishers who need to disable IP capture entirely (privacy stance) or
800 * override for unusual proxy chains can hook the filter and return their
801 * own value (or empty string to disable).
802 *
803 * @since 5.x
804 *
805 * @return string Validated IP address, or empty string if none detected.
806 */
807 function leaky_paywall_get_customer_ip()
808 {
809 $candidates = array(
810 'HTTP_CF_CONNECTING_IP',
811 'HTTP_X_FORWARDED_FOR',
812 'HTTP_X_REAL_IP',
813 'REMOTE_ADDR',
814 );
815
816 $ip = '';
817
818 foreach ($candidates as $key) {
819 if (empty($_SERVER[$key])) {
820 continue;
821 }
822
823 // X-Forwarded-For may be "client, proxy1, proxy2". The originating
824 // client is always the leftmost entry in the standard implementation.
825 $raw = trim(explode(',', (string) $_SERVER[$key])[0]);
826
827 if (filter_var($raw, FILTER_VALIDATE_IP)) {
828 $ip = $raw;
829 break;
830 }
831 }
832
833 return apply_filters('leaky_paywall_customer_ip', $ip);
834 }
835
836
837 if (!function_exists('leaky_paywall_get_currency')) {
838
839 /**
840 * Get the currency value set in the Leaky Paywall settings
841 *
842 * @since 4.9.3
843 *
844 * @return string Currency code (i.e USD)
845 */
846 function leaky_paywall_get_currency()
847 {
848 $settings = get_leaky_paywall_settings();
849 $currency = $settings['leaky_paywall_currency'];
850
851 return apply_filters('leaky_paywall_currency', $currency);
852 }
853 }
854
855
856 /**
857 * Verified if user has paid through Stripe
858 *
859 * @since 1.0.0
860 *
861 * @param string $email address of user "logged" in.
862 * @param integer $blog_id The blog id.
863 * @return mixed Expiration date or subscriptions status or false if not paid
864 */
865 function leaky_paywall_has_user_paid($email = false, $blog_id = null)
866 {
867
868 $settings = get_leaky_paywall_settings();
869 $paid = false;
870 $canceled = false;
871 $expired = false;
872 $sites = array(''); // Empty String for non-Multisite, so we cycle through "sites" one time with no $site set.
873 $mode = leaky_paywall_get_current_mode();
874 if (empty($email)) {
875 $user = wp_get_current_user();
876 if (0 === $user->ID) { // no user.
877 return false;
878 }
879 } else {
880 if (is_email($email)) {
881 $user = get_user_by('email', $email);
882
883 if (!$user) { // no user found with that email address.
884 return false;
885 }
886 } else {
887 return false;
888 }
889 }
890
891 if (is_multisite_premium()) {
892 if (is_null($blog_id)) {
893 global $blog_id;
894 if (!is_main_site($blog_id)) {
895 $sites = array('_all', '_' . $blog_id);
896 } else {
897 $sites = array('_all', '_' . $blog_id, '');
898 }
899 } elseif (is_int($blog_id)) {
900 $sites = array('_' . $blog_id);
901 } elseif (empty($blog_id)) {
902 $sites = array('');
903 } else {
904 $sites = array($blog_id);
905 }
906 }
907
908 foreach ($sites as $site) {
909
910 $subscriber_id = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_subscriber_id' . $site, true);
911 $expires = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, true);
912 $payment_gateway = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_gateway' . $site, true);
913 $payment_status = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true);
914 $plan = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_plan' . $site, true);
915
916 if ('stripe' !== $payment_gateway) {
917
918 if ('paypal_standard' === $payment_gateway || 'paypal-standard' === $payment_gateway) {
919 if (!empty($plan) && in_array($payment_status, array('active', 'pending_cancel'), true)) {
920 return 'subscription';
921 }
922 }
923
924 switch ($payment_status) {
925
926 case 'Active':
927 case 'active':
928 case 'pending_cancel':
929 case 'trial':
930 $expires = apply_filters('leaky_paywall_has_user_paid_expires', $expires, $payment_gateway, $payment_status, $subscriber_id, $plan, $expires, $user, $mode, $site);
931 if (empty($expires) || '0000-00-00 00:00:00' === $expires) {
932 return 'unlimited';
933 }
934
935 // Runtime safety net for non-Stripe gateways (Manual,
936 // PayPal Standard, Authorize.Net, etc.). If the expires
937 // date is in the past, the daily cron *should* have
938 // already flipped this user to 'expired' — but if cron
939 // isn't running on this site, they'd stay 'active'
940 // forever. Do the transition here on-the-fly so access
941 // is correct on the very next page load regardless of
942 // cron health. The Stripe branch below does its own
943 // version of this check via strtotime($expires) < time().
944 if ( strtotime( $expires ) < time() && 'pending_cancel' !== $payment_status ) {
945 if ( function_exists( 'leaky_paywall_set_subscriber_status' ) ) {
946 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'runtime_check' );
947 }
948 $expired = $expires;
949 break;
950 }
951
952 $paid = true;
953 break;
954 case 'refunded':
955 case 'refund':
956 case 'cancelled':
957 case 'canceled':
958 case 'reversed':
959 case 'buyer_complaint':
960 case 'denied':
961 case 'expired':
962 case 'failed':
963 case 'voided':
964 case 'deactivated':
965 case 'suspended':
966 break;
967 }
968 } else {
969
970 // check with Stripe to make sure the user has an active subscription.
971
972 $stripe = leaky_paywall_initialize_stripe_api();
973
974 try {
975 if (empty($subscriber_id)) {
976 switch ($payment_status) {
977 case 'Active':
978 case 'active':
979 case 'pending_cancel':
980 case 'trial':
981 if (empty($expires) || '0000-00-00 00:00:00' === $expires) {
982 return 'unlimited';
983 }
984 $paid = true;
985 break;
986 case 'refunded':
987 case 'refund':
988 case 'cancelled':
989 case 'canceled':
990 case 'reversed':
991 case 'buyer_complaint':
992 case 'denied':
993 case 'expired':
994 case 'failed':
995 case 'voided':
996 case 'deactivated':
997 case 'suspended':
998 break;
999 }
1000 } else {
1001 $cu = $stripe->customers->retrieve($subscriber_id, [], leaky_paywall_get_stripe_connect_params());
1002
1003 if (!empty($cu)) {
1004 if (!empty($cu->deleted) && true === $cu->deleted) {
1005 $canceled = true;
1006 }
1007 }
1008
1009 if (!empty($plan)) {
1010 if (isset($cu->subscriptions)) {
1011 $subscriptions = $cu->subscriptions->all(array('limit' => '1'));
1012 foreach ($subscriptions->data as $subscription) {
1013 if (leaky_paywall_is_valid_stripe_subscription($subscription)) {
1014 return 'subscription';
1015 }
1016 }
1017 }
1018 }
1019
1020 $ch = $stripe->charges->all(
1021 array(
1022 'count' => 1,
1023 'customer' => $subscriber_id,
1024 )
1025 );
1026
1027 if (empty($expires) || '0000-00-00 00:00:00' === $expires) {
1028 return 'unlimited';
1029 } else {
1030 if (strtotime($expires) < time()) {
1031 if (true === $ch->data[0]->paid && false === $ch->data[0]->refunded) {
1032 $expired = $expires;
1033 }
1034 } else {
1035 $paid = true;
1036 }
1037 }
1038 }
1039 } catch (\Throwable $th) {
1040 /* Translators: %s - error message */
1041 $results = '<h1>' . sprintf(__('Error processing request: %s', 'leaky-paywall'), $th->getMessage()) . '</h1>';
1042 }
1043 }
1044 } // end foreach
1045
1046 if (is_bool($canceled) && $canceled) {
1047 $paid = false;
1048 }
1049
1050 if (is_bool($expired) && $expired) {
1051 $paid = false;
1052 }
1053
1054 return apply_filters('leaky_paywall_has_user_paid', $paid, $payment_gateway, $payment_status, $subscriber_id, $plan, $expires, $user, $mode, $site);
1055 }
1056
1057
1058 if (!function_exists('leaky_paywall_set_expiration_date')) {
1059
1060 /**
1061 * Set a user's expiration data
1062 *
1063 * @param int $user_id the user id.
1064 * @param array $data information about the subscription.
1065 */
1066 function leaky_paywall_set_expiration_date($user_id, $data)
1067 {
1068
1069 if (empty($user_id)) {
1070 return;
1071 }
1072
1073 $expires = '0000-00-00 00:00:00'; // default to never expire.
1074 $settings = get_leaky_paywall_settings();
1075 $mode = leaky_paywall_get_current_mode();
1076 $site = leaky_paywall_get_current_site();
1077
1078 // A level set to "Forever" (subscription_length_type "unlimited") keeps its
1079 // last interval/interval_count in the level config - the admin UI only hides
1080 // those inputs when Forever is chosen, it never clears them, and the level
1081 // sanitizer does not reset them on save. Without this check the interval math
1082 // below hands an unlimited subscriber a real expiration date (the default
1083 // Free Registration level ships as unlimited + "1 month", so REST and
1084 // front-end free signups were expiring a month out). Treat unlimited as
1085 // never expire unless the caller passed an explicit expiration.
1086 $is_unlimited = false;
1087
1088 if (isset($data['subscription_length_type'])) {
1089 $is_unlimited = 'unlimited' === $data['subscription_length_type'];
1090 } elseif (isset($data['level_id']) && '' !== $data['level_id']) {
1091 $level = get_leaky_paywall_subscription_level($data['level_id']);
1092 $is_unlimited = is_array($level) && isset($level['subscription_length_type']) && 'unlimited' === $level['subscription_length_type'];
1093 }
1094
1095 if (isset($data['expires']) && $data['expires']) {
1096 $expires = $data['expires'];
1097 } elseif ($is_unlimited) {
1098 $expires = '0000-00-00 00:00:00';
1099 } elseif (!empty($data['interval']) && isset($data['interval_count']) && 1 <= $data['interval_count']) {
1100 $expires = date_i18n('Y-m-d 23:59:59', strtotime('+' . $data['interval_count'] . ' ' . $data['interval'])); // we're generous, give them the whole day!
1101 }
1102
1103 if (!$is_unlimited && 'on' === $settings['add_expiration_dates']) {
1104
1105 $current_expires = get_user_meta($user_id, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, true);
1106
1107 if ($current_expires) {
1108 // if they already have an expiration date and aren't expired, add on to their current expiration date.
1109 if (strtotime($current_expires) > time()) {
1110 $expires = date_i18n('Y-m-d 23:59:59', strtotime($current_expires . ' +' . $data['interval_count'] . ' ' . $data['interval']));
1111 }
1112 }
1113 }
1114
1115 update_user_meta($user_id, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, apply_filters('leaky_paywall_set_expiration_date', $expires, $data, $user_id));
1116 }
1117 }
1118
1119 if (!function_exists('leaky_paywall_ensure_blog_membership')) {
1120 /**
1121 * Make sure a subscriber is a member of the site they just subscribed to.
1122 *
1123 * On multisite, "membership" is the {$wpdb->prefix}capabilities role entry, and
1124 * WP_User_Query is scoped to it. leaky_paywall_new_subscriber() reuses an
1125 * existing network account when one matches the email and only writes
1126 * subscription meta, so a reader who already had an account on one magazine and
1127 * then subscribed to another ended up with a perfectly good subscription on a
1128 * site they were not a member of.
1129 *
1130 * Anything that looks subscribers up by query then cannot see them:
1131 * get_leaky_paywall_subscriber_by_subscriber_id() is how the gateway webhooks
1132 * find people, so those subscribers were charged and silently ignored.
1133 *
1134 * Never touches an existing member, so it cannot downgrade a role.
1135 *
1136 * @since 5.1.x
1137 *
1138 * @param int $user_id WordPress user ID.
1139 * @return void
1140 */
1141 function leaky_paywall_ensure_blog_membership($user_id)
1142 {
1143 if (!is_multisite_premium() || empty($user_id) || is_wp_error($user_id)) {
1144 return;
1145 }
1146
1147 $blog_id = get_current_blog_id();
1148
1149 if (is_user_member_of_blog($user_id, $blog_id)) {
1150 return;
1151 }
1152
1153 $role = apply_filters('leaky_paywall_subscriber_blog_role', 'subscriber', $user_id, $blog_id);
1154
1155 add_user_to_blog($blog_id, $user_id, $role);
1156 }
1157 }
1158
1159 if (!function_exists('leaky_paywall_new_subscriber')) {
1160
1161 /**
1162 * Adds new subscriber to subscriber table
1163 *
1164 * @since 1.0.0
1165 *
1166 * @param deprecated $hash No longer used.
1167 * @param string $email address of user "logged" in.
1168 * @param int $customer_id The customer id.
1169 * @param array $meta_args Arguments passed from type of subscriber.
1170 * @param string $login optional login name to use instead of email address.
1171 * @return mixed $wpdb insert ID or false
1172 */
1173 function leaky_paywall_new_subscriber(string $hash = null, $email, $customer_id, $meta_args, $login = '')
1174 {
1175
1176 if (!is_email($email)) {
1177 return false;
1178 }
1179
1180 if (apply_filters('leaky_paywall_new_subscriber_abort', false, $email, $meta_args)) {
1181 return false;
1182 }
1183
1184 $settings = get_leaky_paywall_settings();
1185 $mode = leaky_paywall_get_current_mode();
1186 $site = leaky_paywall_get_current_site();
1187 $user = get_user_by('email', $email);
1188
1189 if ($user) {
1190 // the user already exists.
1191 // grab the ID for later.
1192 $user_id = $user->ID;
1193 $userdata = get_user_by('id', $user_id);
1194
1195 $user_data = array(
1196 'user_login' => $userdata->user_login,
1197 'user_email' => $userdata->user_email,
1198 'first_name' => $userdata->first_name,
1199 'last_name' => $userdata->last_name,
1200 'display_name' => $userdata->display_name,
1201 'user_registered' => $userdata->user_registered,
1202 );
1203
1204 } else {
1205
1206 // the user doesn't already exist.
1207
1208 // if they submitted a custom login name, use that.
1209 if (isset($meta_args['login'])) {
1210 $login = $meta_args['login'];
1211 }
1212
1213 // create a new user with their email address as their username.
1214 // grab the ID for later.
1215 if (empty($login)) {
1216 $parts = explode('@', $email);
1217 $login = $parts[0];
1218 }
1219
1220 // Avoid collisions.
1221 $user = get_user_by('login', $login);
1222 while ($user) {
1223 $login = $user->user_login . '_' . substr(uniqid(), 5);
1224 $user = get_user_by('login', $login);
1225 }
1226
1227 if (isset($meta_args['password'])) {
1228 $password = $meta_args['password'];
1229 } else {
1230 $password = wp_generate_password();
1231 }
1232
1233 $user_data = array(
1234 'user_login' => $login,
1235 'user_email' => $email,
1236 'user_pass' => $password,
1237 'first_name' => isset($meta_args['first_name']) ? $meta_args['first_name'] : '',
1238 'last_name' => isset($meta_args['last_name']) ? $meta_args['last_name'] : '',
1239 'display_name' => isset($meta_args['first_name']) ? $meta_args['first_name'] . ' ' . $meta_args['last_name'] : '',
1240 'user_registered' => date_i18n('Y-m-d H:i:s'),
1241 );
1242
1243 $user_data = apply_filters('leaky_paywall_userdata_before_user_create', $user_data);
1244 $user_id = wp_insert_user($user_data);
1245 }
1246
1247 // wp_insert_user() returns WP_Error on failure (duplicate email, blocked
1248 // username, security-plugin reject, etc.). empty() returns false for a
1249 // WP_Error object, so the empty() guard below misses it — execution
1250 // would continue with $user_id = WP_Error and crash downstream code that
1251 // uses it as an int (e.g., as an array key in level-transition tracking).
1252 if ( is_wp_error( $user_id ) ) {
1253 leaky_paywall_log_error(
1254 array(
1255 'error_code' => $user_id->get_error_code(),
1256 'error_message' => $user_id->get_error_message(),
1257 'attempted_login' => $user_data['user_login'] ?? '',
1258 'attempted_email' => $user_data['user_email'] ?? '',
1259 ),
1260 'wp_insert_user returned WP_Error'
1261 );
1262 return $user_id;
1263 }
1264
1265 if (empty($user_id)) {
1266 leaky_paywall_log_error($meta_args, 'could not create user');
1267 return false;
1268 } else {
1269 $logged_userdata = $user_data;
1270
1271 if (is_array($logged_userdata)) {
1272 unset($logged_userdata['user_pass']);
1273 }
1274
1275 leaky_paywall_log($logged_userdata, 'leaky paywall - new subscriber created');
1276 }
1277
1278 leaky_paywall_ensure_blog_membership($user_id);
1279
1280 leaky_paywall_set_expiration_date($user_id, $meta_args);
1281 unset($meta_args['site']);
1282
1283 if (isset($meta_args['created']) && $meta_args['created']) {
1284 $created_date = strtotime($meta_args['created']);
1285 $meta_args['created'] = gmdate('Y-m-d H:i:s', $created_date);
1286 } else {
1287 $meta_args['created'] = gmdate('Y-m-d H:i:s');
1288 }
1289
1290 // set free level subscribers to active.
1291 if ('0' === $meta_args['price']) {
1292 $meta_args['payment_status'] = 'active';
1293 }
1294
1295 $meta = apply_filters('leaky_paywall_new_subscriber_meta', $meta_args, $email, $customer_id, $meta_args);
1296
1297 // remove any extra underscores from site variable.
1298 $site = str_replace('__', '_', $site);
1299
1300 foreach ($meta as $key => $value) {
1301
1302 // do not want to store their password as plain text.
1303 if ('confirm_password' === $key || 'password' === $key) {
1304 continue;
1305 }
1306
1307 // Level ID is handled separately via leaky_paywall_set_subscriber_level().
1308 if ( 'level_id' === $key ) {
1309 continue;
1310 }
1311
1312 // Payment status routed through leaky_paywall_set_subscriber_status() so the change is recorded in status history.
1313 if ( 'payment_status' === $key ) {
1314 leaky_paywall_set_subscriber_status( $user_id, $value, 'registration' );
1315 continue;
1316 }
1317
1318 update_user_meta($user_id, '_issuem_leaky_paywall_' . $mode . '_' . $key . $site, $value);
1319 }
1320
1321 if ( isset( $meta['level_id'] ) ) {
1322 leaky_paywall_set_subscriber_level( $user_id, $meta['level_id'], 'registration' );
1323 }
1324
1325 do_action('leaky_paywall_new_subscriber', $user_id, $email, $meta, $customer_id, $meta_args, $user_data);
1326
1327 return $user_id;
1328 }
1329 }
1330
1331 if (!function_exists('leaky_paywall_update_subscriber')) {
1332
1333 /**
1334 * Updates an existing subscriber to subscriber table
1335 *
1336 * @since 1.0.0
1337 *
1338 * @param deprecated $hash No longer used.
1339 * @param string $email address of user "logged" in.
1340 * @param int $customer_id Customer ID.
1341 * @param array $meta_args Arguments passed from type of subscriber.
1342 * @return mixed $wpdb insert ID or false
1343 */
1344 function leaky_paywall_update_subscriber(string $hash = null, $email, $customer_id, $meta_args)
1345 {
1346
1347 if (!is_email($email)) {
1348 return false;
1349 }
1350
1351 $settings = get_leaky_paywall_settings();
1352 $mode = leaky_paywall_get_current_mode();
1353 $site = leaky_paywall_get_current_site();
1354
1355 $expires = '0000-00-00 00:00:00';
1356 $user = get_user_by('email', $email);
1357
1358 if (is_user_logged_in() && !is_admin()) {
1359 // Update the existing user.
1360 $user_id = get_current_user_id();
1361 } elseif ($user) {
1362 // the user already exists.
1363 // grab the ID for later.
1364 $user_id = $user->ID;
1365 } else {
1366 return false; // User does not exist, cannot update.
1367 }
1368
1369 $level = get_leaky_paywall_subscription_level($meta_args['level_id']);
1370
1371 // do not update levels if it is a pay per post purchase.
1372 if (isset($level['pay_per_post'])) {
1373 return $user_id;
1374 }
1375
1376 $current_level_id = get_user_meta($user_id, '_issuem_leaky_paywall_' . $mode . '_level_id' . $site, true);
1377
1378 leaky_paywall_ensure_blog_membership($user_id);
1379
1380 leaky_paywall_set_expiration_date($user_id, $meta_args);
1381 unset($meta_args['site']);
1382
1383 $meta = array(
1384 'level_id' => $meta_args['level_id'],
1385 'subscriber_id' => $customer_id,
1386 'price' => $meta_args['price'],
1387 'description' => $meta_args['description'],
1388 'plan' => $meta_args['plan'],
1389 'payment_gateway' => $meta_args['payment_gateway'],
1390 'payment_status' => $meta_args['payment_status'],
1391 );
1392
1393 $meta = apply_filters('leaky_paywall_update_subscriber_meta', $meta, $email, $customer_id, $meta_args);
1394
1395 do_action('leaky_paywall_before_update_subscriber', $user_id, $current_level_id, $meta);
1396
1397 foreach ($meta as $key => $value) {
1398
1399 // Level ID is handled separately via leaky_paywall_set_subscriber_level().
1400 if ( 'level_id' === $key ) {
1401 continue;
1402 }
1403
1404 // Payment status routed through leaky_paywall_set_subscriber_status() so the change is recorded in status history.
1405 if ( 'payment_status' === $key ) {
1406 leaky_paywall_set_subscriber_status( $user_id, $value, 'subscriber_update' );
1407 continue;
1408 }
1409
1410 update_user_meta($user_id, '_issuem_leaky_paywall_' . $mode . '_' . $key . $site, $value);
1411 }
1412
1413 if ( isset( $meta['level_id'] ) ) {
1414 leaky_paywall_set_subscriber_level( $user_id, $meta['level_id'], 'subscriber_update' );
1415 }
1416
1417 $user_id = wp_update_user(
1418 array(
1419 'ID' => $user_id,
1420 'user_email' => $email,
1421 'first_name' => $meta_args['first_name'],
1422 'last_name' => $meta_args['last_name']
1423 )
1424 );
1425
1426 do_action('leaky_paywall_update_subscriber', $user_id, $email, $meta, $customer_id, $meta_args);
1427
1428 return $user_id;
1429 }
1430 }
1431
1432
1433 /**
1434 * Check if any Leaky Paywall add-on has a valid, active license.
1435 *
1436 * @since 5.0
1437 *
1438 * @return bool
1439 */
1440 function leaky_paywall_is_pro() {
1441
1442 // Single all-access Pro license (5.2.0+).
1443 if ( function_exists( 'leaky_paywall_pro_is_active' ) && leaky_paywall_pro_is_active() ) {
1444 return true;
1445 }
1446
1447 // Backwards compatibility: any valid per-extension license also counts.
1448 if ( ! class_exists( 'Leaky_Paywall_License_Key' ) ) {
1449 return false;
1450 }
1451
1452 foreach ( Leaky_Paywall_License_Key::get_registered_slugs() as $slug ) {
1453 $settings = get_option( $slug );
1454
1455 if ( ! empty( $settings['license_status'] ) && 'valid' === $settings['license_status'] ) {
1456 return true;
1457 }
1458 }
1459
1460 return false;
1461 }
1462
1463 /**
1464 * Check if the current visitor is a search engine bot
1465 *
1466 * @since 5.0
1467 *
1468 * @return bool
1469 */
1470 function leaky_paywall_is_search_engine_bot() {
1471 $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? strtolower( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) ) : '';
1472
1473 if ( empty( $user_agent ) ) {
1474 return false;
1475 }
1476
1477 $bots = apply_filters( 'leaky_paywall_search_engine_bots', array(
1478 'googlebot',
1479 'bingbot',
1480 'slurp',
1481 'duckduckbot',
1482 'baiduspider',
1483 'yandexbot',
1484 ) );
1485
1486 foreach ( $bots as $bot ) {
1487 if ( strpos( $user_agent, $bot ) !== false ) {
1488 return true;
1489 }
1490 }
1491
1492 return false;
1493 }
1494
1495 /**
1496 * Get all valid and active Leaky Paywall levels
1497 *
1498 * @since 4.9.0
1499 *
1500 * @return array List of active levels
1501 */
1502 function leaky_paywall_get_levels()
1503 {
1504 $settings = get_leaky_paywall_settings();
1505 $blog_id = get_current_blog_id();
1506
1507 $level_list = array();
1508
1509 foreach ($settings['levels'] as $key => $level) {
1510
1511 if (!empty($level['deleted'])) {
1512 continue;
1513 }
1514
1515 // if (is_multisite_premium() && !empty($level['site']) && 'all' !== $level['site'] && $blog_id !== $level['site']) {
1516 // continue;
1517 // }
1518
1519 if (!is_numeric($key)) {
1520 continue;
1521 }
1522
1523 $level_list[$key] = $level;
1524 $level_list[$key]['id'] = $key;
1525 }
1526
1527 return $level_list;
1528 }
1529
1530
1531 if (!function_exists('leaky_paywall_translate_payment_gateway_slug_to_name')) {
1532
1533 /**
1534 * Translate a payment gateway slug to a name
1535 *
1536 * @param string $slug The slug.
1537 * @return string The name of the gateway
1538 */
1539 function leaky_paywall_translate_payment_gateway_slug_to_name($slug)
1540 {
1541
1542 $return = 'Unknown';
1543
1544 switch ($slug) {
1545
1546 case 'stripe':
1547 $return = 'Stripe';
1548 break;
1549
1550 case 'paypal_standard':
1551 case 'paypal-standard':
1552 $return = 'PayPal';
1553 break;
1554
1555 case 'free_registration':
1556 $return = __('Free Registration', 'leaky-paywall');
1557 break;
1558
1559 case 'manual':
1560 $return = __('Manually Added', 'leaky-paywall');
1561 break;
1562 default:
1563 $return = $slug;
1564 break;
1565 }
1566
1567 return apply_filters('leaky_paywall_translate_payment_gateway_slug_to_name', $return, $slug);
1568 }
1569 }
1570
1571 if (!function_exists('create_leaky_paywall_login_hash')) {
1572
1573 /**
1574 * Creates a 32-character hash string
1575 *
1576 * Generally used to create a unique hash for each subscriber, stored in the database
1577 * and used for campaign links.
1578 *
1579 * @since 1.0.0
1580 *
1581 * @param string $str String you want to hash.
1582 */
1583 function create_leaky_paywall_login_hash($str)
1584 {
1585
1586 if (defined('SECURE_AUTH_SALT')) {
1587 $salt[] = SECURE_AUTH_SALT;
1588 }
1589
1590 if (defined('AUTH_SALT')) {
1591 $salt[] = AUTH_SALT;
1592 }
1593
1594 $salt[] = get_bloginfo('name');
1595 $salt[] = time();
1596
1597 $hash = md5(md5(implode($salt)) . md5($str));
1598
1599 while (!is_leaky_paywall_login_hash_unique($hash)) {
1600 $hash = create_leaky_paywall_login_hash($hash); // I did this on purpose...
1601 }
1602
1603 return $hash; // doesn't have to be too secure, just want a pretty random and very unique string.
1604
1605 }
1606 }
1607
1608 if (!function_exists('leaky_paywall_attempt_login')) {
1609 /**
1610 * Attempt a login
1611 *
1612 * @param string $login_hash The login hash.
1613 */
1614 function leaky_paywall_attempt_login($login_hash)
1615 {
1616 $email = get_leaky_paywall_email_from_login_hash($login_hash);
1617 if (false !== $email) {
1618 $user = get_user_by('email', $email);
1619
1620 if ($user) {
1621 delete_transient('_lpl_' . $login_hash); // one time use.
1622 wp_set_current_user($user->ID);
1623 wp_set_auth_cookie($user->ID, true);
1624 }
1625 }
1626 }
1627 }
1628
1629 if (!function_exists('leaky_paywall_subscriber_restrictions')) {
1630
1631 /**
1632 * Returns current user's subscription restrictions
1633 *
1634 * @since 2.0.0
1635 *
1636 * @return array subscriber's subscription restrictions
1637 */
1638 function leaky_paywall_subscriber_restrictions()
1639 {
1640 $settings = get_leaky_paywall_settings();
1641
1642 if (isset($settings['restrictions']['post_types'])) {
1643 $restrictions = $settings['restrictions']['post_types']; // defaults.
1644 } else {
1645 $restrictions = '';
1646 }
1647
1648 if (is_multisite_premium()) {
1649 $restriction_levels = leaky_paywall_subscriber_current_level_ids();
1650 if (!empty($restriction_levels)) {
1651
1652 $restrictions = array();
1653 $merged_restrictions = array();
1654 foreach ($restriction_levels as $restriction_level) {
1655 if (!empty($settings['levels'][$restriction_level]['post_types'])) {
1656 $restrictions = array_merge($restrictions, $settings['levels'][$restriction_level]['post_types']);
1657 }
1658 }
1659 $merged_restrictions = array();
1660 foreach ($restrictions as $key => $restriction) {
1661 if (empty($merged_restrictions)) {
1662 $merged_restrictions[$key] = $restriction;
1663 continue;
1664 } else {
1665 $post_type_found = false;
1666 foreach ($merged_restrictions as $tmp_key => $tmp_restriction) {
1667 if ($restriction['post_type'] === $tmp_restriction['post_type']) {
1668 $post_type_found = true;
1669 $post_type_found_key = $tmp_key;
1670 break;
1671 }
1672 }
1673 if (!$post_type_found) {
1674 $merged_restrictions[$key] = $restriction;
1675 } else {
1676 if (-1 === $restriction['allowed_value'] || 'unlimited' === $restriction['allowed']) { // unlimited, just use it.
1677 $merged_restrictions[$post_type_found_key] = $restriction;
1678 } elseif ($merged_restrictions[$post_type_found_key]['allowed_value'] < $restriction['allowed_value']) {
1679 $merged_restrictions[$post_type_found_key] = $restriction;
1680 }
1681 }
1682 }
1683 }
1684 $restrictions = $merged_restrictions;
1685 }
1686 } else {
1687 $restriction_level = leaky_paywall_subscriber_current_level_id();
1688 if (false !== $restriction_level) {
1689
1690 if (!empty($settings['levels'][$restriction_level]['post_types'])) {
1691 $restrictions = $settings['levels'][$restriction_level]['post_types'];
1692 }
1693 }
1694 }
1695 return apply_filters('leaky_paywall_subscriber_restrictions', $restrictions);
1696 }
1697 }
1698
1699 if (!function_exists('leaky_paywall_subscriber_current_level_id')) {
1700
1701 /**
1702 * Returns current user's subscription restrictions
1703 *
1704 * @since 2.0.0
1705 *
1706 * @return array subscriber's subscription restrictions
1707 */
1708 function leaky_paywall_subscriber_current_level_id($user = null)
1709 {
1710
1711 if (null === $user) {
1712 $user = wp_get_current_user();
1713 }
1714
1715 if (leaky_paywall_user_has_access($user)) {
1716
1717 $sites = array('');
1718 if (is_multisite_premium()) {
1719 global $blog_id;
1720 if (!is_main_site($blog_id)) {
1721 $sites = array('_all', '_' . $blog_id);
1722 } else {
1723 $sites = array('_all', '_' . $blog_id, '');
1724 }
1725 }
1726
1727 $mode = leaky_paywall_get_current_mode();
1728
1729 foreach ($sites as $site) {
1730 $level_id = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_level_id' . $site, true);
1731 $level_id = apply_filters('get_leaky_paywall_users_level_id', $level_id, $user, $mode, $site);
1732 $level_id = apply_filters('get_leaky_paywall_subscription_level_level_id', $level_id);
1733 if (is_numeric($level_id)) {
1734 return $level_id;
1735 }
1736 }
1737 }
1738
1739 return false;
1740 }
1741 }
1742
1743 if (!function_exists('leaky_paywall_subscriber_current_level_ids')) {
1744
1745 /**
1746 * Returns current user's subscription restrictions
1747 *
1748 * @since 3.0.0
1749 *
1750 * @return array subscriber's subscription restrictions
1751 */
1752 function leaky_paywall_subscriber_current_level_ids()
1753 {
1754 $level_ids = array();
1755 $settings = get_leaky_paywall_settings();
1756
1757 $sites = array('');
1758 if (is_multisite_premium()) {
1759 global $blog_id;
1760 if (!is_main_site($blog_id)) {
1761 $sites = array('_all', '_' . $blog_id);
1762 } else {
1763 $sites = array('_all', '_' . $blog_id, '');
1764 }
1765 }
1766
1767 $user = wp_get_current_user();
1768 $mode = leaky_paywall_get_current_mode();
1769
1770 foreach ($sites as $site) {
1771 $level_id = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_level_id' . $site, true);
1772 $level_id = apply_filters('get_leaky_paywall_users_level_id', $level_id, $user, $mode, $site);
1773 $level_id = apply_filters('get_leaky_paywall_subscription_level_level_id', $level_id);
1774 $status = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true);
1775
1776 if ( is_numeric( $level_id ) && in_array( $status, leaky_paywall_access_statuses(), true ) ) {
1777 $level_ids[] = $level_id;
1778 }
1779 }
1780
1781 return apply_filters('leaky_paywall_subscriber_current_level_ids', $level_ids);
1782 }
1783 }
1784
1785 if (!function_exists('leaky_paywall_server_pdf_download')) {
1786 /**
1787 * Download PDF
1788 *
1789 * @param integer $download_id The download id of the pdf.
1790 */
1791 function leaky_paywall_server_pdf_download($download_id)
1792 {
1793 // Grab the download info.
1794 $url = wp_get_attachment_url($download_id);
1795
1796 wp_safe_redirect($url);
1797 die();
1798 }
1799 }
1800
1801 function build_leaky_paywall_subscription_levels_row_summary($level, $row_key)
1802 {
1803 $nonce = wp_create_nonce('leaky-paywall-level-row-nonce');
1804 $settings = get_leaky_paywall_settings();
1805 $duration = $level['subscription_length_type'] == 'unlimited' ? 'Forever' : $level['interval_count'] . ' ' . $level['interval'];
1806 $delete_link = admin_url() . 'admin.php?page=leaky-paywall-settings&tab=subscriptions&delete_level_id=' . $row_key . '&_wpnonce=' . esc_attr($nonce);
1807 $page_for_register = $settings['page_for_register'] > 0 ? esc_url(get_page_link($settings['page_for_register']) . '?level_id=' . esc_attr($row_key)) : '';
1808
1809 ?>
1810 <tr>
1811 <td><?php echo esc_html($row_key); ?></td>
1812 <td><?php echo esc_html($level['label']); ?><br>
1813 <div class="row-actions"><a href="<?php echo esc_url(admin_url()); ?>admin.php?page=leaky-paywall-settings&tab=subscriptions&level_id=<?php echo absint($row_key); ?>">Edit</a> | <span class="delete"><a class="leaky-paywall-level-delete" data-level-id="<?php echo esc_attr($row_key); ?>" href="<?php echo esc_url($delete_link); ?>">Delete</a></span></div>
1814 </td>
1815 <td><?php echo esc_html($level['price']); ?></td>
1816 <td><?php echo esc_html($duration); ?></td>
1817 <td><?php echo ! empty( $level['recurring'] ) && 'on' === $level['recurring'] ? 'recurring' : 'one time'; ?></td>
1818 <td><?php echo $page_for_register; ?></td>
1819 </tr>
1820
1821 <?php
1822 }
1823
1824 if (!function_exists('build_leaky_paywall_subscription_levels_row')) {
1825
1826 /**
1827 * Build subscription level row
1828 *
1829 * @since 1.0.0
1830 *
1831 * @param array $level The level.
1832 * @param integer $row_key The row key.
1833 * @return string The HTML for the level row
1834 */
1835 function build_leaky_paywall_subscription_levels_row($level = array(), $row_key = '')
1836 {
1837
1838 global $leaky_paywall;
1839 $settings = get_leaky_paywall_settings();
1840
1841 $default = array(
1842 'label' => '',
1843 'description' => '',
1844 'registration_form_description' => '',
1845 'price' => '',
1846 'subscription_length_type' => 'limited',
1847 'interval_count' => 1,
1848 'interval' => 'month',
1849 'recurring' => 'off',
1850 'hide_subscribe_card' => 'off',
1851 'hide_registration_form' => 'off',
1852 'plan_id' => array(),
1853 'deleted' => 0,
1854 'site' => 'all',
1855 );
1856
1857 // Only a brand new level gets a starter access rule. Merging one into a
1858 // saved level would draw a rule the access resolver does not honour.
1859 $is_new_level = empty($level);
1860
1861 $level = wp_parse_args($level, $default);
1862
1863 if ($is_new_level) {
1864 $level['post_types'] = array(
1865 array(
1866 'post_type' => ACTIVE_ISSUEM ? 'article' : 'post',
1867 'allowed' => 'unlimited',
1868 'allowed_value' => 0,
1869 ),
1870 );
1871 }
1872
1873 if (empty($level['recurring'])) {
1874 $level['recurring'] = 'off';
1875 }
1876
1877 if (!empty($level['deleted'])) {
1878 $deleted = 'hidden';
1879 } else {
1880 $deleted = '';
1881 }
1882
1883 ob_start();
1884 ?>
1885
1886 <table class="issuem-leaky-paywall-subscription-level-row-table leaky-paywall-table <?php echo esc_attr($deleted); ?>">
1887 <?php
1888 if (isset($settings['page_for_register']) && $settings['page_for_register']) {
1889 ?>
1890 <tr>
1891 <th>
1892 <label for="level-name-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Direct Sign Up Link', 'leaky-paywall'); ?></label>
1893 </th>
1894 <td>
1895 <p><?php echo esc_url(get_page_link($settings['page_for_register'])) . '?level_id=' . esc_attr($row_key); ?></p>
1896 </td>
1897 </tr>
1898 <?php
1899 } ?>
1900
1901
1902 <tr>
1903 <th>
1904 <label for="level-name-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Subscription Level Name', 'leaky-paywall'); ?></label>
1905 </th>
1906 <td>
1907 <input id="level-name-<?php echo esc_attr($row_key); ?>" type="text" class="regular-text" name="levels[<?php echo esc_attr($row_key); ?>][label]" value="<?php echo esc_attr($level['label']); ?>" />
1908 </td>
1909 </tr>
1910
1911 <tr>
1912 <th>
1913 <label for="level-description-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Subscribe Card Description', 'leaky-paywall'); ?></label>
1914 </th>
1915 <td>
1916 <textarea id="level-description-<?php echo esc_attr($row_key); ?>" name="levels[<?php echo esc_attr($row_key); ?>][description]" class="large-text"><?php echo wp_kses_post(stripslashes($level['description'] ?? '')); ?></textarea>
1917 <p class="description"><?php esc_html_e('If entered, this will replace the auto-generated access description on the subscribe cards. HTML allowed.', 'leaky-paywall'); ?></p>
1918 </td>
1919 </tr>
1920
1921 <tr>
1922 <th>
1923 <label for="level-registration-form-description-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Registration Form Description', 'leaky-paywall'); ?></label>
1924 </th>
1925 <td>
1926 <textarea id="level-registration-form-description-<?php echo esc_attr($row_key); ?>" name="levels[<?php echo esc_attr($row_key); ?>][registration_form_description]" class="large-text"><?php echo wp_kses_post(stripslashes($level['registration_form_description'] ?? '')); ?></textarea>
1927 <p class="description"><?php esc_html_e('If entered, this will replace the auto-generated content access description on the registration form. HTML allowed.', 'leaky-paywall'); ?></p>
1928 </td>
1929 </tr>
1930
1931 <?php
1932 if (is_plugin_active('leaky-paywall-recurring-payments/leaky-paywall-recurring-payments.php')) {
1933 ?>
1934 <tr>
1935 <th>
1936 <label for="level-recurring-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Recurring', 'leaky-paywall'); ?></label>
1937 </th>
1938 <td>
1939 <input id="level-recurring-<?php echo esc_attr($row_key); ?>" class="stripe-recurring" type="checkbox" name="levels[<?php echo esc_attr($row_key); ?>][recurring]" value="on" <?php echo checked('on', $level['recurring'] ?? '', false); ?> /> Enable recurring payments<br>
1940 <span style="color: #999; font-size: 11px;" class="recurring-help <?php echo checked('on', $level['recurring'] ?? '', false) ? '' : 'hidden'; ?>">Webhooks must be setup in your payment gateway account for recurring payments to work properly. <a target="_blank" href="https://docs.leakypaywall.com/article/120-leaky-paywall-recurring-payments">See documentation here.</a></span>
1941
1942 <?php
1943
1944 if (is_array($level['plan_id'])) {
1945 foreach ($level['plan_id'] as $plan_id) {
1946 if (!$plan_id) {
1947 continue;
1948 }
1949 ?>
1950 <input type="hidden" class="level-plan_id-<?php echo esc_attr($row_key); ?>" name="levels[<?php echo esc_attr($row_key); ?>][plan_id][]" value="<?php echo esc_attr($plan_id); ?>">
1951 <?php
1952 }
1953 } else {
1954 ?>
1955 <input type="hidden" id="level-plan_id-<?php echo esc_attr($row_key); ?>" name="levels[<?php echo esc_attr($row_key); ?>][plan_id]" value="<?php echo esc_attr($level['plan_id']); ?>">
1956 <?php
1957 }
1958
1959 ?>
1960
1961 </td>
1962 </tr>
1963 <?php
1964 } else {
1965 ?>
1966 <tr>
1967 <th>
1968 <label><?php esc_html_e('Recurring', 'leaky-paywall'); ?></label>
1969 </th>
1970 <td>
1971 <label class="lp-pro-feature-toggle">
1972 <input type="checkbox" disabled />
1973 <?php esc_html_e( 'Enable recurring payments', 'leaky-paywall' ); ?>
1974 <span class="lp-pro-badge"><?php esc_html_e( 'Pro', 'leaky-paywall' ); ?></span>
1975 </label>
1976 <p class="description">
1977 <a href="#" class="lp-pro-feature-link"><?php esc_html_e( 'Upgrade to Pro to enable recurring subscription payments.', 'leaky-paywall' ); ?></a>
1978 </p>
1979 </td>
1980 </tr>
1981 <?php
1982 }
1983 ?>
1984
1985 <tr>
1986 <th>
1987 <label for="level-price-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Subscription Price', 'leaky-paywall'); ?></label>
1988 </th>
1989 <td>
1990 <input id="level-price-<?php echo esc_attr($row_key); ?>" type="text" style="width: 100px;" name="levels[<?php echo esc_attr($row_key); ?>][price]" value="<?php echo esc_attr($level['price']); ?>" />
1991 <p class="description"><?php esc_html_e('0 for Free Subscriptions', 'leaky-paywall'); ?></p>
1992 </td>
1993 </tr>
1994
1995 <tr>
1996 <th>
1997 <label for="level-interval-count-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Subscription Length', 'leaky-paywall'); ?></label>
1998 </th>
1999 <td>
2000 <select class="subscription_length_type" name="levels[<?php echo esc_attr($row_key); ?>][subscription_length_type]">
2001 <option value="unlimited" <?php echo selected('unlimited', $level['subscription_length_type'], false); ?>><?php esc_html_e('Forever', 'leaky-paywall'); ?></option>
2002 <option value="limited" <?php echo selected('limited', $level['subscription_length_type'], false); ?>> <?php esc_html_e('Limited for...', 'leaky-paywall'); ?></option>
2003 </select>
2004
2005 <?php
2006 if ('unlimited' === $level['subscription_length_type']) {
2007 $subscription_length_input_style = 'display: none;';
2008 } else {
2009 $subscription_length_input_style = '';
2010 }
2011 ?>
2012
2013 <div class="interval_div" style="<?php echo esc_attr($subscription_length_input_style); ?>">
2014 <input id="level-interval-count-<?php echo esc_attr($row_key); ?>" type="text" class="interval_count small-text" name="levels[<?php echo esc_attr($row_key); ?>][interval_count]" value="<?php echo esc_attr($level['interval_count']); ?>" />
2015 <select id="interval" name="levels[<?php echo esc_attr($row_key); ?>][interval]">
2016 <option value="day" <?php echo selected('day' === $level['interval'], true, false); ?>><?php esc_html_e('Day(s)', 'leaky-paywall'); ?></option>
2017 <option value="week" <?php echo selected('week' === $level['interval'], true, false); ?>><?php esc_html_e('Week(s)', 'leaky-paywall'); ?></option>
2018 <option value="month" <?php echo selected('month' === $level['interval'], true, false); ?>><?php esc_html_e('Month(s)', 'leaky-paywall'); ?></option>
2019 <option value="year" <?php echo selected('year' === $level['interval'], true, false); ?>><?php esc_html_e('Year(s)', 'leaky-paywall'); ?></option>
2020 </select>
2021 </div>
2022 </td>
2023 </tr>
2024
2025 <tr>
2026 <th><?php esc_html_e('Access Options', 'leaky-paywall'); ?></th>
2027 <td id="issuem-leaky-paywall-subsciption-row-<?php echo esc_attr($row_key); ?>-post-types">
2028
2029 <table class="leaky-paywall-interal-setting-table">
2030 <tr>
2031 <th>Number Allowed</th>
2032 <th>Post Type</th>
2033 <th>Taxonomy <span style="font-weight: normal; font-size: 11px; color: #999;"> Category,tag,etc.</span></th>
2034 <th class="narrow-cell">&nbsp;</th>
2035 </tr>
2036
2037 <?php
2038 $last_key = -1;
2039 if (!empty($level['post_types'])) {
2040 foreach ($level['post_types'] as $select_post_key => $select_post_type) {
2041
2042 build_leaky_paywall_subscription_row_post_type($select_post_type, $select_post_key, $row_key);
2043
2044 $last_key = $select_post_key;
2045 }
2046 }
2047 ?>
2048 </table>
2049 </td>
2050 </tr>
2051
2052 <tr>
2053 <th>&nbsp;</th>
2054 <td>
2055 <script>
2056 var leaky_paywall_subscription_row_<?php echo esc_attr($row_key); ?>_last_post_type_key = <?php echo intval($last_key); ?>;
2057 </script>
2058 <p><input data-row-key="<?php echo esc_attr($row_key); ?>" class="button-secondary" id="add-subscription-row-post-type" class="add-new-issuem-leaky-paywall-row-post-type" type="submit" name="add_leaky_paywall_subscription_row_post_type" value="<?php esc_attr_e('+ Add Access Option', 'leaky-paywall'); ?>" /></p>
2059 <?php
2060 if ($leaky_paywall->is_site_wide_enabled()) {
2061 echo '<p class="description">';
2062 esc_attr_e('Post Types that are not native the to the site currently being viewed are marked with an asterisk.', 'leaky-paywall');
2063 echo '</p>';
2064 }
2065 ?>
2066
2067 <p class="description"><?php esc_html_e('Access processed from top to bottom. Set limit to 0 to block access.', 'leaky-paywall'); ?></p>
2068 </td>
2069 </tr>
2070
2071 <tr>
2072 <th>
2073 <label for="level-hide-subscribe-card-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Hide Subscribe Card', 'leaky-paywall'); ?></label>
2074 </th>
2075 <td>
2076 <input id="level-hide-subscribe-card-<?php echo esc_attr($row_key); ?>" class="hide-subscribe- card" type="checkbox" name="levels[<?php echo esc_attr($row_key); ?>][hide_subscribe_card]" value="on" <?php echo checked('on', $level['hide_subscribe_card'], false); ?> /> <?php esc_html_e('Do not display subscribe card on subscribe page', 'leaky-paywall'); ?>
2077 </td>
2078 </tr>
2079
2080 <tr>
2081 <th>
2082 <label for="level-hide-registration-form-<?php echo esc_attr($row_key); ?>"><?php esc_html_e('Hide Registration Form', 'leaky-paywall'); ?></label>
2083 </th>
2084 <td>
2085 <input id="level-hide-registration-form-<?php echo esc_attr($row_key); ?>" class="hide-registration-form" type="checkbox" name="levels[<?php echo esc_attr($row_key); ?>][hide_registration_form]" value="on" <?php echo checked('on', $level['hide_registration_form'], false); ?> /> <?php esc_html_e('Disable the direct sign up link for this level, but still allow manual assignment.', 'leaky-paywall'); ?>
2086 </td>
2087 </tr>
2088
2089 <?php
2090 if (is_multisite_premium()) {
2091 ?>
2092 <tr>
2093 <th><?php esc_html_e('Site', 'leaky-paywall'); ?></th>
2094 <td id="issuem-leaky-paywall-subsciption-row-<?php echo esc_attr($row_key); ?>-site">
2095 <select id="site" name="levels[<?php echo esc_attr($row_key); ?>][site]">
2096 <?php
2097 if (is_super_admin()) {
2098 ?>
2099 <option value="all" <?php echo selected('all', $level['site'], false); ?>><?php esc_html_e('All Sites', 'leaky-paywall'); ?></option>
2100 <?php
2101 $sites = get_sites();
2102 foreach ($sites as $site) {
2103 $site_details = get_blog_details($site->id);
2104 ?>
2105 <option value="<?php echo esc_attr($site->id); ?>" <?php echo selected($site->id, $level['site'], false); ?>><?php echo esc_html($site_details->blogname); ?></option>
2106 <?php
2107 }
2108 } else {
2109 $site_details = get_blog_details(get_current_blog_id());
2110 ?>
2111 <option value="<?php echo get_current_blog_id(); ?>" <?php echo selected(get_current_blog_id(), $level['site'], false); ?>><?php echo esc_html($site_details->blogname); ?></option>
2112 <?php
2113 }
2114 ?>
2115
2116
2117 </select>
2118 </td>
2119 </tr>
2120
2121 <?php
2122 }
2123
2124 // leaving for backwards compatibility, but it will deprecated.
2125 echo wp_kses_post(apply_filters('build_leaky_paywall_subscription_levels_row_addon_filter', '', $level, $row_key));
2126
2127 do_action('leaky_paywall_after_subscription_levels_row', $level, $row_key);
2128
2129 echo '</table>';
2130
2131 $content = ob_get_contents();
2132 ob_end_clean();
2133
2134 return $content;
2135 }
2136 }
2137
2138 if (!function_exists('build_leaky_paywall_subscription_row_ajax')) {
2139
2140 /**
2141 * AJAX Wrapper
2142 *
2143 * @since 1.0.0
2144 */
2145 function build_leaky_paywall_subscription_row_ajax()
2146 {
2147 if ( ! current_user_can( 'manage_options' ) ) {
2148 die();
2149 }
2150
2151 check_ajax_referer( 'leaky-paywall-js-nonce', 'nonce' );
2152
2153 if (isset($_REQUEST['row-key'])) {
2154 // phpcs:ignore
2155 die(build_leaky_paywall_subscription_levels_row(array(), sanitize_text_field(wp_unslash($_REQUEST['row-key']))));
2156 } else {
2157 die();
2158 }
2159 }
2160 add_action('wp_ajax_issuem-leaky-paywall-add-new-subscription-row', 'build_leaky_paywall_subscription_row_ajax');
2161 }
2162
2163 if (!function_exists('build_leaky_paywall_subscription_row_post_type')) {
2164
2165 /**
2166 * Build Leaky Paywall subscription row
2167 *
2168 * @since 1.0.0
2169 *
2170 * @param array $select_post_type Data for post type.
2171 * @param integer $select_post_key The post key.
2172 * @param integer $row_key The row key.
2173 * @return mixed Value set for the issuem options.
2174 */
2175 function build_leaky_paywall_subscription_row_post_type($select_post_type = array(), $select_post_key = '', $row_key = '')
2176 {
2177
2178 $default_select_post_type = array(
2179 'post_type' => ACTIVE_ISSUEM ? 'article' : 'post',
2180 'allowed' => 'unlimited',
2181 'allowed_value' => 0,
2182 'site' => 0,
2183 'taxonomy' => '',
2184 );
2185 $select_post_type = wp_parse_args($select_post_type, $default_select_post_type);
2186
2187 echo '<tr class="issuem-leaky-paywall-row-post-type">';
2188 echo '<td><select class="allowed_type" name="levels[' . esc_attr($row_key) . '][post_types][' . esc_attr($select_post_key) . '][allowed]">';
2189 echo '<option value="unlimited" ' . selected('unlimited', $select_post_type['allowed'], false) . '>' . esc_html__('Unlimited', 'leaky-paywall') . '</option>';
2190 echo '<option value="limited" ' . selected('limited', $select_post_type['allowed'], false) . '>' . esc_html__('Limit to...', 'leaky-paywall') . '</option>';
2191 echo '</select>';
2192
2193 if ('unlimited' === $select_post_type['allowed']) {
2194 $allowed_value_input_style = 'display: none;';
2195 } else {
2196 $allowed_value_input_style = '';
2197 }
2198
2199 echo '<div class="allowed_value_div" style="' . esc_attr($allowed_value_input_style) . '">';
2200 $allowed_value_output = max( 0, intval( $select_post_type['allowed_value'] ) );
2201 echo '<input type="number" min="0" class="allowed_value small-text" name="levels[' . esc_attr($row_key) . '][post_types][' . esc_attr($select_post_key) . '][allowed_value]" value="' . esc_attr($allowed_value_output) . '" placeholder="' . esc_attr__('#', 'leaky-paywall') . '" />';
2202 echo '</div></td>';
2203
2204 echo '<td><select class="select_level_post_type" name="levels[' . esc_attr($row_key) . '][post_types][' . esc_attr($select_post_key) . '][post_type]">';
2205 $post_types = get_post_types(array('public' => true), 'objects');
2206 $post_types_names = get_post_types(array(), 'names');
2207 $hidden_post_types = array('attachment', 'revision', 'nav_menu_item');
2208 if (in_array($select_post_type['post_type'], $post_types_names, true)) {
2209 foreach ($post_types as $post_type) {
2210 if (in_array($post_type->name, $hidden_post_types, true)) {
2211 continue;
2212 }
2213 echo '<option value="' . esc_attr($post_type->name) . '" ' . selected($post_type->name, $select_post_type['post_type'], false) . '>' . esc_html($post_type->labels->name) . '</option>';
2214 }
2215 } else {
2216 echo '<option value="' . esc_attr($select_post_type['post_type']) . '">' . esc_html($select_post_type['post_type']) . ' &#42;</option>';
2217 }
2218 echo '</select></td>';
2219
2220 // get taxonomies for this post type.
2221 echo '<td><select style="width: 100%;" name="levels[' . esc_attr($row_key) . '][post_types][' . esc_attr($select_post_key) . '][taxonomy]">';
2222 $tax_post_type = $select_post_type['post_type'] ? $select_post_type['post_type'] : 'post';
2223 $taxes = get_object_taxonomies($tax_post_type, 'objects');
2224 $hidden_taxes = apply_filters('leaky_paywall_settings_hidden_taxonomies', array('post_format'));
2225
2226 echo '<option value="all" ' . selected('all', $select_post_type['taxonomy'], false) . '>All</option>';
2227
2228 foreach ($taxes as $tax) {
2229
2230 if (in_array($tax->name, $hidden_taxes, true)) {
2231 continue;
2232 }
2233
2234 // create option group for this taxonomy.
2235 echo '<optgroup label="' . esc_attr($tax->label) . '">';
2236
2237 // create options for this taxonomy.
2238 $terms = get_terms(
2239 array(
2240 'taxonomy' => $tax->name,
2241 'hide_empty' => false,
2242 )
2243 );
2244
2245 foreach ($terms as $term) {
2246 echo '<option value="' . esc_attr($term->term_id) . '" ' . selected($term->term_id, $select_post_type['taxonomy'], false) . '>' . esc_html($term->name) . '</option>';
2247 }
2248
2249 echo '</optgroup>';
2250 }
2251 echo '</select></td>';
2252
2253 echo '<td class="narrow-cell"><span class="delete-x delete-post-type-row">&times;</span></td>';
2254
2255 echo '</tr>';
2256 }
2257 }
2258
2259 if (!function_exists('build_leaky_paywall_subscription_row_post_type_ajax')) {
2260
2261 /**
2262 * AJAX Wrapper
2263 *
2264 * @since 1.0.0
2265 */
2266 function build_leaky_paywall_subscription_row_post_type_ajax()
2267 {
2268 if ( ! current_user_can( 'manage_options' ) ) {
2269 die();
2270 }
2271
2272 check_ajax_referer( 'leaky-paywall-js-nonce', 'nonce' );
2273
2274 if (isset($_REQUEST['select-post-key']) && isset($_REQUEST['row-key'])) {
2275 $settings = get_leaky_paywall_settings();
2276
2277 if (is_multisite_premium() && isset($_SERVER['HTTP_REFERER']) && preg_match('#^' . network_admin_url() . '#i', sanitize_text_field(wp_unslash($_SERVER['HTTP_REFERER'])))) {
2278 if (!defined('WP_NETWORK_ADMIN')) {
2279 define('WP_NETWORK_ADMIN', true);
2280 }
2281 }
2282
2283 // phpcs:ignore
2284 die(build_leaky_paywall_subscription_row_post_type(array(), sanitize_text_field(wp_unslash($_REQUEST['select-post-key'])), sanitize_text_field(wp_unslash($_REQUEST['row-key']))));
2285 }
2286 die();
2287 }
2288 add_action('wp_ajax_issuem-leaky-paywall-add-new-subscription-row-post-type', 'build_leaky_paywall_subscription_row_post_type_ajax');
2289 }
2290
2291
2292 /**
2293 * Build a default restriction row
2294 *
2295 * @since 1.0.0
2296 *
2297 * @param array $restriction The restriction.
2298 * @param integer $row_key The row key.
2299 * @return mixed Value set for the issuem options.
2300 */
2301 function build_leaky_paywall_default_restriction_row($restriction = array(), $row_key = '')
2302 {
2303
2304 $settings = get_leaky_paywall_settings();
2305
2306 if (empty($restriction)) {
2307 $restriction = array(
2308 'post_type' => '',
2309 'taxonomy' => '',
2310 'allowed_value' => '0',
2311 );
2312 }
2313
2314 if (!isset($restriction['taxonomy'])) {
2315 $restriction['taxonomy'] = 'all';
2316 }
2317
2318 echo '<tr class="issuem-leaky-paywall-restriction-row">';
2319 $hidden_post_types = array('attachment', 'revision', 'nav_menu_item', 'lp_transaction', 'custom_css');
2320 $post_types = get_post_types(array('public' => true), 'objects');
2321
2322 echo '<td><select class="leaky-paywall-restriction-post-type" id="restriction-post-type-' . esc_attr($row_key) . '" name="restrictions[post_types][' . esc_attr($row_key) . '][post_type]">';
2323 foreach ($post_types as $post_type) {
2324
2325 if (in_array($post_type->name, $hidden_post_types, true)) {
2326 continue;
2327 }
2328
2329 echo '<option value="' . esc_attr($post_type->name) . '" ' . selected($post_type->name, $restriction['post_type'], false) . '>' . esc_html($post_type->labels->name) . '</option>';
2330 }
2331
2332 echo '</select></td>';
2333
2334 // get taxonomies for this post type.
2335 echo '<td><select style="width: 100%;" name="restrictions[post_types][' . esc_attr($row_key) . '][taxonomy]">';
2336 $tax_post_type = $restriction['post_type'] ? $restriction['post_type'] : 'post';
2337 $taxes = get_object_taxonomies($tax_post_type, 'objects');
2338 $hidden_taxes = apply_filters('leaky_paywall_settings_hidden_taxonomies', array('post_format', 'yst_prominent_words'));
2339
2340 echo '<option value="all" ' . selected('all', $restriction['taxonomy'], false) . '>All</option>';
2341
2342 foreach ($taxes as $tax) {
2343
2344 if (in_array($tax->name, $hidden_taxes, true)) {
2345 continue;
2346 }
2347
2348 // create option group for this taxonomy.
2349 echo '<optgroup label="' . esc_attr($tax->label) . '">';
2350
2351 // create options for this taxonomy.
2352 $terms = get_terms(
2353 array(
2354 'taxonomy' => $tax->name,
2355 'hide_empty' => false,
2356 )
2357 );
2358
2359 foreach ($terms as $term) {
2360 echo '<option value="' . esc_attr($term->term_id) . '" ' . selected($term->term_id, $restriction['taxonomy'], false) . '>' . esc_html($term->name) . '</option>';
2361 }
2362
2363 echo '</optgroup>';
2364 }
2365 echo '</select></td>';
2366
2367 echo '<td>';
2368
2369 $restriction_allowed_output = max( 0, intval( $restriction['allowed_value'] ) );
2370
2371 if ('on' === $settings['enable_combined_restrictions']) {
2372 echo '<p class="allowed-number-helper-text" style="color: #555; font-size: 12px;">Using combined restrictions.</p>';
2373 echo '<input style="display: none;" id="restriction-allowed-' . esc_attr($row_key) . '" type="number" min="0" class="small-text restriction-allowed-number-setting" name="restrictions[post_types][' . esc_attr($row_key) . '][allowed_value]" value="' . esc_attr($restriction_allowed_output) . '" />';
2374 } else {
2375 echo '<p class="allowed-number-helper-text" style="color: #555; font-size: 12px; display: none;">Using combined restrictions.</p>';
2376 echo '<input id="restriction-allowed-' . esc_attr($row_key) . '" type="number" min="0" class="small-text restriction-allowed-number-setting" name="restrictions[post_types][' . esc_attr($row_key) . '][allowed_value]" value="' . esc_attr($restriction_allowed_output) . '" />';
2377 }
2378
2379 echo '</td>';
2380
2381 echo '<td class="narrow-cell"><span class="delete-x delete-restriction-row">&times;</span></td>';
2382
2383 echo '</tr>';
2384 }
2385
2386 /**
2387 * Get the taxonomies for the selected post type in a restriction setting row
2388 *
2389 * @since 4.7.5
2390 */
2391 function leaky_paywall_get_restriction_row_post_type_taxonomies()
2392 {
2393
2394 if (!wp_verify_nonce(sanitize_key($_POST['nonce']), 'leaky-paywall-js-nonce')) {
2395 die(esc_html__('Failed Security Check', 'leaky-paywall'));
2396 }
2397
2398 $post_type = isset($_REQUEST['post_type']) ? sanitize_text_field(wp_unslash($_REQUEST['post_type'])) : '';
2399 $taxes = get_object_taxonomies($post_type, 'objects');
2400 $hidden_taxes = apply_filters('leaky_paywall_settings_hidden_taxonomies', array('post_format', 'yst_prominent_words'));
2401
2402 ob_start();
2403 ?>
2404
2405 <select style="width: 100%;">
2406 <option value="all">All</option>
2407
2408 <?php
2409 foreach ($taxes as $tax) {
2410
2411 if (in_array($tax->name, $hidden_taxes, true)) {
2412 continue;
2413 }
2414
2415 // create option group for this taxonomy.
2416 echo '<optgroup label="' . esc_attr($tax->label) . '">';
2417
2418 // create options for this taxonomy.
2419 $terms = get_terms(
2420 array(
2421 'taxonomy' => $tax->name,
2422 'hide_empty' => false,
2423 )
2424 );
2425
2426 foreach ($terms as $term) {
2427 echo '<option value="' . esc_attr($term->term_id) . '">' . esc_html($term->name) . '</option>';
2428 }
2429
2430 echo '</optgroup>';
2431 }
2432
2433 ?>
2434
2435 </select>
2436
2437 <?php
2438 $content = ob_get_contents();
2439 ob_end_clean();
2440
2441 wp_send_json($content);
2442 }
2443 add_action('wp_ajax_leaky-paywall-get-restriction-row-post-type-taxonomies', 'leaky_paywall_get_restriction_row_post_type_taxonomies');
2444
2445
2446 if (!function_exists('build_leaky_paywall_default_restriction_row_ajax')) {
2447
2448 /**
2449 * AJAX Wrapper
2450 *
2451 * @since 1.0.0
2452 */
2453 function build_leaky_paywall_default_restriction_row_ajax()
2454 {
2455
2456 if (!wp_verify_nonce(sanitize_key($_POST['nonce']), 'leaky-paywall-js-nonce')) {
2457 die(esc_html__('Failed Security Check', 'leaky-paywall'));
2458 }
2459
2460 if (isset($_REQUEST['row-key'])) {
2461 // phpcs:ignore
2462 die(build_leaky_paywall_default_restriction_row(array(), sanitize_text_field(wp_unslash($_REQUEST['row-key']))));
2463 } else {
2464 die();
2465 }
2466 }
2467 add_action('wp_ajax_issuem-leaky-paywall-add-new-restriction-row', 'build_leaky_paywall_default_restriction_row_ajax');
2468 }
2469
2470 /**
2471 * AJAX handler to toggle an email's enabled/disabled state.
2472 */
2473 function leaky_paywall_ajax_toggle_email() {
2474 check_ajax_referer( 'leaky-paywall-js-nonce', 'nonce' );
2475
2476 if ( ! current_user_can( 'manage_options' ) ) {
2477 wp_send_json_error( 'Unauthorized', 403 );
2478 }
2479
2480 $email_id = isset( $_POST['email_id'] ) ? sanitize_text_field( wp_unslash( $_POST['email_id'] ) ) : '';
2481 $email = LP_Emails::instance()->get_email( $email_id );
2482
2483 if ( ! $email ) {
2484 wp_send_json_error( 'Email not found', 404 );
2485 }
2486
2487 $option_key = 'leaky_paywall_email_' . $email->id . '_settings';
2488 $settings = get_option( $option_key, array() );
2489 $currently_enabled = isset( $settings['enabled'] ) && 'yes' === $settings['enabled'];
2490 $settings['enabled'] = $currently_enabled ? 'no' : 'yes';
2491
2492 update_option( $option_key, $settings );
2493
2494 wp_send_json_success( array( 'enabled' => $settings['enabled'] ) );
2495 }
2496 add_action( 'wp_ajax_lp_toggle_email', 'leaky_paywall_ajax_toggle_email' );
2497
2498 if (!function_exists('wp_print_r')) {
2499
2500 /**
2501 * Helper function used for printing out debug information
2502 *
2503 * HT: Glenn Ansley @ iThemes.com
2504 *
2505 * @since 1.0.0
2506 *
2507 * @param int $args Arguments to pass to print_r.
2508 * @param bool $die TRUE to die else FALSE (default TRUE).
2509 */
2510 function wp_print_r($args, $die = true)
2511 {
2512
2513 $echo = '<pre>' . print_r($args, true) . '</pre>';
2514
2515 if ($die) {
2516 die(esc_attr($echo));
2517 } else {
2518 echo esc_attr($echo);
2519 }
2520 }
2521 }
2522
2523 if (!function_exists('get_leaky_paywall_subscription_level')) {
2524 /**
2525 * Get the Leaky Paywall level
2526 *
2527 * @param int $level_id The level id.
2528 */
2529 function get_leaky_paywall_subscription_level($level_id)
2530 {
2531
2532 $settings = get_leaky_paywall_settings();
2533
2534 $level_id = apply_filters('get_leaky_paywall_subscription_level_level_id', $level_id);
2535 if (isset($settings['levels'][$level_id])) {
2536 $level = $settings['levels'][$level_id];
2537 $level['id'] = $level_id;
2538 } else {
2539 $level = false;
2540 }
2541
2542 return apply_filters('get_leaky_paywall_subscription_level', $level, $level_id);
2543 }
2544 }
2545
2546 if (!function_exists('leaky_paywall_subscription_options')) {
2547
2548 /**
2549 * Display the subscription card options
2550 */
2551 function leaky_paywall_subscription_options()
2552 {
2553
2554 global $blog_id;
2555
2556 $settings = get_leaky_paywall_settings();
2557 $mode = leaky_paywall_get_current_mode();
2558 $site = leaky_paywall_get_current_site();
2559 $current_level_ids = leaky_paywall_subscriber_current_level_ids();
2560
2561 $results = apply_filters('leaky_paywall_subscription_options', '');
2562 // If someone wants to completely override this, they can with the above filter.
2563 if (empty($results)) {
2564
2565 $has_allowed_value = false;
2566 $results .= '<h2 class="subscription-options-title">' . __('Subscription Options', 'leaky-paywall') . '</h2>';
2567
2568 $results .= apply_filters('leaky_paywall_subscription_options_header', '');
2569
2570 if (!empty($settings['levels'])) {
2571
2572 $results .= apply_filters('leaky_paywall_before_subscription_options', '');
2573
2574 $results .= '<div class="leaky_paywall_subscription_options">';
2575 foreach (apply_filters('leaky_paywall_subscription_levels', $settings['levels']) as $level_id => $level) {
2576
2577 if (!empty($level['deleted'])) {
2578 continue;
2579 }
2580
2581 if (isset($level['hide_subscribe_card']) && 'on' === $level['hide_subscribe_card']) {
2582 continue;
2583 }
2584
2585 if (is_multisite_premium() && !empty($level['site']) && 'all' !== $level['site'] && $blog_id !== $level['site']) {
2586 continue;
2587 }
2588
2589 $level = apply_filters('leaky_paywall_subscription_options_level', $level, $level_id);
2590
2591 if (isset($level['recurring']) && 'on' === $level['recurring']) {
2592 $is_recurring = true;
2593 } else {
2594 $is_recurring = false;
2595 }
2596
2597 $payment_options = '';
2598 $allowed_content = '';
2599
2600 if (in_array($level_id, $current_level_ids)) {
2601 $current_level = 'current-level';
2602 } else {
2603 $current_level = '';
2604 }
2605
2606 $results .= '<div id="option-' . $level_id . '" class="leaky_paywall_subscription_option ' . $current_level . '">';
2607 if ($current_level) {
2608 $results .= '<p class="leaky-paywall-subscription-current-level">' . __('Your Current Level', 'leaky-paywall') . '</p>';
2609 }
2610 $results .= '<h3 class="leaky_paywall_subscription_option_title">' . apply_filters('leaky_paywall_subscription_option_title', stripslashes($level['label'])) . '</h3>';
2611
2612 $results .= '<div class="leaky_paywall_subscription_allowed_content">';
2613
2614 if (!empty($level['post_types']) && empty($level['description'])) {
2615 foreach ($level['post_types'] as $post_type) {
2616
2617 if (isset($post_type['taxonomy'])) {
2618
2619 $term = get_term_by('term_taxonomy_id', $post_type['taxonomy']);
2620
2621 if (is_object($term)) {
2622 $name = $term->name;
2623 } else {
2624 $name = '';
2625 }
2626
2627 $post_type_obj = get_post_type_object($post_type['post_type']);
2628 if (!empty($post_type_obj)) {
2629 // 'unlimited' is the admin dropdown value; when chosen
2630 // the numeric allowed_value input is hidden and its
2631 // stored value stays at the default 0 — gating the
2632 // unlimited branch on the numeric value alone would
2633 // then render "Access 0 …" by mistake.
2634 $is_unlimited = isset($post_type['allowed']) && 'unlimited' === $post_type['allowed'];
2635
2636 if ($is_unlimited) {
2637 /* Translators: %1$s - name, %2$s - post type name */
2638 $allowed_content .= '<p>' . sprintf(__('Unlimited %1$s %2$s', 'leaky-paywall'), $name, $post_type_obj->labels->name) . '</p>';
2639 } elseif (0 <= $post_type['allowed_value']) {
2640 $has_allowed_value = true;
2641 $plural = (1 === (int) $post_type['allowed_value']) ? '' : 's';
2642
2643 /* Translators: %1$s - allowed value, %2$s - name, %3$s - post type name */
2644 $allowed_content .= '<p>' . sprintf(__('Access %1$s %2$s %3$s*', 'leaky-paywall'), $post_type['allowed_value'], $name, $post_type_obj->labels->singular_name . $plural) . '</p>';
2645 }
2646 }
2647 } else {
2648
2649 /* @todo: We may need to change the site ID during this process, some sites may have different post types enabled */
2650 $post_type_obj = get_post_type_object($post_type['post_type']);
2651 if (!empty($post_type_obj)) {
2652 $is_unlimited = isset($post_type['allowed']) && 'unlimited' === $post_type['allowed'];
2653
2654 if ($is_unlimited) {
2655 /* Translators: %s - type of post object */
2656 $allowed_content .= '<p>' . sprintf(__('Unlimited %s', 'leaky-paywall'), $post_type_obj->labels->name) . '</p>';
2657 } elseif (0 <= $post_type['allowed_value']) {
2658 $has_allowed_value = true;
2659 $plural = (1 === (int) $post_type['allowed_value']) ? '' : 's';
2660
2661 /* Translators: %1$s - allowed value, %2$s - post type name */
2662 $allowed_content .= '<p>' . sprintf(__('Access %1$s %2$s*', 'leaky-paywall'), $post_type['allowed_value'], $post_type_obj->labels->singular_name . $plural) . '</p>';
2663 }
2664 }
2665 }
2666 }
2667 } else {
2668 $allowed_content = stripslashes($level['description']);
2669 }
2670 $results .= apply_filters('leaky_paywall_subscription_options_allowed_content', $allowed_content, $level_id, $level);
2671 $results .= '</div>';
2672
2673 $subscription_price = '';
2674
2675 $subscription_price .= '<div class="leaky_paywall_subscription_price">';
2676 $subscription_price .= '<p>';
2677 if (!empty($level['price'])) {
2678 if (!empty($level['recurring']) && 'on' === $level['recurring'] && apply_filters('leaky_paywall_subscription_options_price_recurring_on', true, $current_level)) {
2679 $subscription_price .= '<strong>' . leaky_paywall_get_level_display_price($level) . ' ' . leaky_paywall_human_readable_interval($level['interval_count'], $level['interval']) . ' ' . __('(recurring)', 'leaky-paywall') . '</strong>';
2680 $subscription_price .= apply_filters('leaky_paywall_before_subscription_options_recurring_price', '');
2681 } else {
2682 /* Translators: %1$s - display price, %2$s - interval */
2683 $subscription_price .= '<strong>' . sprintf(__('%1$s %2$s', 'leaky-paywall'), leaky_paywall_get_level_display_price($level), leaky_paywall_human_readable_interval($level['interval_count'], $level['interval'])) . '</strong>';
2684 $subscription_price .= apply_filters('leaky_paywall_before_subscription_options_non_recurring_price', '');
2685 }
2686
2687 if (!empty($level['trial_period'])) {
2688 /* Translators: %s - trial period days */
2689 $subscription_price .= '<span class="leaky-paywall-trial-period">' . sprintf(__('Free for the first %s day(s)', 'leaky-paywall'), $level['trial_period']) . '</span>';
2690 }
2691 } else {
2692 $subscription_price .= '<strong>' . __('Free', 'leaky-paywall') . '</strong>';
2693 }
2694
2695 $subscription_price .= '</p>';
2696 $subscription_price .= '</div>';
2697
2698 $results .= apply_filters('leaky_paywall_subscription_options_subscription_price', $subscription_price, $level_id, $level);
2699
2700 $subscription_action = '';
2701 $subscription_action .= '<div class="leaky_paywall_subscription_payment_options">';
2702
2703 // Don't show payment options if the users is currently subscribed to this level and it is a recurring level.
2704 if (in_array($level_id, $current_level_ids, true)) {
2705
2706 $subscription_action .= '<div class="leaky_paywall_subscription_current_level"><span>';
2707 $subscription_action .= __('Your Current Subscription', 'leaky-paywall');
2708 $subscription_action .= '</span></div>';
2709 }
2710
2711 if (in_array($level_id, $current_level_ids, true) && leaky_paywall_user_has_access() && $is_recurring) {
2712 $subscription_action .= ''; // they already have an active recurring subscription to this level.
2713 } else {
2714
2715 $subscription_action .= apply_filters('leaky_paywall_subscription_options_payment_options', $payment_options, $level, $level_id);
2716 }
2717
2718 $subscription_action .= '</div>';
2719
2720 $results .= apply_filters('leaky_paywall_subscription_options_subscription_action', $subscription_action, $level_id, $current_level_ids, $payment_options);
2721
2722 $results .= '</div>';
2723 }
2724
2725 $results .= apply_filters('leaky_paywall_subscription_options_after_last_subscription_option', '');
2726
2727 $results .= '</div>';
2728
2729 $results .= apply_filters('leaky_paywall_subscription_options_after_subscription_options', '');
2730
2731 if ($has_allowed_value) {
2732
2733 $results .= '<div class="leaky_paywall_subscription_limit_details">';
2734 $results .= '*' . ucfirst(leaky_paywall_human_readable_interval($settings['cookie_expiration'], $settings['cookie_expiration_interval']));
2735 $results .= '</div>';
2736 }
2737 }
2738
2739 $results .= apply_filters('leaky_paywall_subscription_options_footer', '');
2740 }
2741
2742 return $results;
2743 }
2744 }
2745
2746
2747 /**
2748 * Pass a PHP date format string to this function to return its jQuery datepicker equivalent
2749 *
2750 * @since 1.1.0
2751 * @param string $date_format PHP Date Format.
2752 * @return string jQuery datePicker Format.
2753 */
2754 function leaky_paywall_jquery_datepicker_format($date_format)
2755 {
2756
2757 // http://us2.php.net/manual/en/function.date.php .
2758 // http://api.jqueryui.com/datepicker/#utility-formatDate .
2759 $php_format = array(
2760 // day.
2761 '/d/', // Day of the month, 2 digits with leading zeros.
2762 '/D/', // A textual representation of a day, three letters.
2763 '/j/', // Day of the month without leading zeros.
2764 '/l/', // A full textual representation of the day of the week.
2765 // '/N/', //ISO-8601 numeric representation of the day of the week (added in PHP 5.1.0).
2766 // '/S/', //English ordinal suffix for the day of the month, 2 characters.
2767 // '/w/', //Numeric representation of the day of the week.
2768 '/z/', // The day of the year (starting from 0).
2769
2770 // week.
2771 // '/W/', //ISO-8601 week number of year, weeks starting on Monday (added in PHP 4.1.0).
2772
2773 // month.
2774 '/F/', // A full textual representation of a month, such as January or March.
2775 '/m/', // Numeric representation of a month, with leading zeros.
2776 '/M/', // A short textual representation of a month, three letters.
2777 '/n/', // numeric month no leading zeros.
2778 // 't/', //Number of days in the given month.
2779
2780 // year.
2781 // '/L/', //Whether it's a leap year.
2782 // '/o/', //ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead. (added in PHP 5.1.0).
2783 '/Y/', // A full numeric representation of a year, 4 digits.
2784 '/y/', // A two digit representation of a year.
2785 );
2786
2787 $datepicker_format = array(
2788 // day.
2789 'dd', // day of month (two digit).
2790 'D', // day name short.
2791 'd', // day of month (no leading zero).
2792 'DD', // day name long.
2793 // '', //N - Equivalent does not exist in datePicker.
2794 // '', //S - Equivalent does not exist in datePicker.
2795 // '', //w - Equivalent does not exist in datePicker.
2796 'z' => 'o', // The day of the year (starting from 0).
2797
2798 // week.
2799 // '', //W - Equivalent does not exist in datePicker.
2800
2801 // month.
2802 'MM', // month name long.
2803 'mm', // month of year (two digit).
2804 'M', // month name short.
2805 'm', // month of year (no leading zero).
2806 // '', //t - Equivalent does not exist in datePicker.
2807
2808 // year.
2809 // '', //L - Equivalent does not exist in datePicker.
2810 // '', //o - Equivalent does not exist in datePicker.
2811 'yy', // year (four digit).
2812 'y', // month name long.
2813 );
2814
2815 return preg_replace($php_format, $datepicker_format, preg_quote($date_format, '/'));
2816 }
2817
2818
2819 /**
2820 * Add lost password link to login form
2821 */
2822 function leaky_paywall_add_lost_password_link()
2823 {
2824 return '<a id="leaky-paywall-lost-password-link" href="' . wp_lostpassword_url() . '">' . __('Lost Password?', 'leaky-paywall') . '</a>';
2825 }
2826
2827
2828 /**
2829 * Get the payment gateways
2830 *
2831 * @return array $gateways The gateways.
2832 */
2833 function leaky_paywall_payment_gateways()
2834 {
2835 $gateways = array(
2836 'manual' => __('Manual', 'leaky-paywall'),
2837 'stripe' => __('Stripe', 'leaky-paywall'),
2838 'paypal_standard' => __('PayPal Standard', 'leaky-paywall'),
2839 'free_registration' => __('Free Registration', 'leaky-paywall'),
2840 );
2841 return apply_filters('leaky_paywall_subscriber_payment_gateways', $gateways);
2842 }
2843
2844
2845 /**
2846 * Create a human readable interval
2847 *
2848 * @param string $interval_count The interval count.
2849 * @param string $interval The interval.
2850 */
2851 function leaky_paywall_human_readable_interval($interval_count, $interval)
2852 {
2853
2854 if (0 >= $interval_count) {
2855 return __('for life', 'leaky-paywall');
2856 }
2857
2858 if (1 < $interval_count) {
2859 $interval .= 's';
2860 }
2861
2862 switch ($interval) {
2863 case 'day':
2864 $interval_str = __('day', 'leaky-paywall');
2865 break;
2866 case 'days':
2867 $interval_str = __('days', 'leaky-paywall');
2868 break;
2869 case 'week':
2870 $interval_str = __('week', 'leaky-paywall');
2871 break;
2872 case 'weeks':
2873 $interval_str = __('weeks', 'leaky-paywall');
2874 break;
2875 case 'month':
2876 $interval_str = __('month', 'leaky-paywall');
2877 break;
2878 case 'months':
2879 $interval_str = __('months', 'leaky-paywall');
2880 break;
2881 case 'year':
2882 $interval_str = __('year', 'leaky-paywall');
2883 break;
2884 case 'years':
2885 $interval_str = __('years', 'leaky-paywall');
2886 break;
2887 default:
2888 $interval_str = $interval;
2889 break;
2890 }
2891
2892 if (1 === $interval_count) {
2893 return __('every', 'leaky-paywall') . ' ' . $interval_str;
2894 } else {
2895 return __('every', 'leaky-paywall') . ' ' . $interval_count . ' ' . $interval_str;
2896 }
2897 }
2898
2899
2900 /**
2901 * Send email based on subscription status
2902 *
2903 * @param integer $user_id The user id.
2904 * @param string $status The status of the notification.
2905 * @param array $args The details of the subscriber.
2906 */
2907 function leaky_paywall_email_subscription_status( $user_id, $status = 'new', $args = '' ) {
2908
2909 if ( empty( $user_id ) ) {
2910 return;
2911 }
2912
2913 $password = '';
2914 if ( ! empty( $args ) && is_array( $args ) ) {
2915 $password = isset( $args['password'] ) ? $args['password'] : '';
2916 }
2917
2918 do_action( 'leaky_paywall_before_email_status', $user_id, $status );
2919
2920 $emails = LP_Emails::instance();
2921 $email_args = array( 'password' => $password, 'status' => $status );
2922
2923 switch ( $status ) {
2924 case 'new':
2925 case 'update':
2926 $new_sub = $emails->get_email( 'new_subscriber' );
2927 if ( $new_sub ) {
2928 $new_sub->trigger( $user_id, $email_args );
2929 }
2930
2931 $admin = $emails->get_email( 'admin_new_subscriber' );
2932 if ( $admin ) {
2933 $admin->trigger( $user_id, $email_args );
2934 }
2935 break;
2936
2937 case 'renewal_reminder':
2938 $renewal = $emails->get_email( 'renewal_reminder' );
2939 if ( $renewal ) {
2940 $renewal->trigger( $user_id, $email_args );
2941 }
2942 break;
2943 }
2944 }
2945
2946
2947 /**
2948 * Register cron job on plugin activation.
2949 */
2950 function leaky_paywall_process_renewal_reminder_schedule()
2951 {
2952
2953 if (!wp_next_scheduled('leaky_paywall_process_renewal_reminder')) {
2954 wp_schedule_event(time(), 'daily', 'leaky_paywall_process_renewal_reminder');
2955 }
2956 }
2957 add_action('admin_init', 'leaky_paywall_process_renewal_reminder_schedule');
2958
2959 /**
2960 * Remove our renewal reminder scheduled event if Leaky Paywall is deactivated
2961 */
2962 function leaky_paywall_process_renewal_reminder_deactivation()
2963 {
2964 wp_clear_scheduled_hook('leaky_paywall_process_renewal_reminder');
2965 }
2966 register_deactivation_hook(__FILE__, 'leaky_paywall_process_renewal_reminder_deactivation');
2967
2968
2969 /**
2970 * Process renewal reminder email for each Leaky Paywall subscriber
2971 *
2972 * @since 4.9.3
2973 */
2974 function leaky_paywall_maybe_send_renewal_reminder()
2975 {
2976
2977 $renewal_email = LP_Emails::instance()->get_email( 'renewal_reminder' );
2978
2979 if ( ! $renewal_email || ! $renewal_email->is_enabled() ) {
2980 return;
2981 }
2982
2983 if ( empty( $renewal_email->body ) ) {
2984 return;
2985 }
2986
2987 $mode = leaky_paywall_get_current_mode();
2988 $site = leaky_paywall_get_current_site();
2989
2990 leaky_paywall_log(current_time('Y-m-d'), 'process renewal reminder');
2991
2992 $start_date = time(); // now
2993 $end_date = strtotime('+' . absint( $renewal_email->days_before ) . ' day'); // x days in the future
2994
2995 $args = array(
2996 'number' => 99,
2997 'meta_query' => array(
2998 'relation' => 'AND',
2999 array(
3000 'key' => '_issuem_leaky_paywall_' . $mode . '_level_id' . $site,
3001 'compare' => 'EXISTS',
3002 ),
3003 array(
3004 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3005 'value' => array(gmdate('Y-m-d', $start_date), gmdate('Y-m-d', $end_date)),
3006 'compare' => 'BETWEEN',
3007 'type' => 'DATE',
3008 ),
3009 array(
3010 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3011 'value' => '0000-00-00 00:00:00',
3012 'compare' => '!=',
3013 ),
3014
3015 ),
3016 );
3017
3018 $users = get_users($args);
3019
3020 if (empty($users)) {
3021 return;
3022 }
3023
3024 foreach ($users as $user) {
3025
3026 $user_id = $user->ID;
3027 $expiration = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, true);
3028 $plan = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_plan' . $site, true);
3029
3030 // do not send renewal reminders to users with recurring plans.
3031 if (!empty($plan)) {
3032 continue;
3033 }
3034
3035 // user does not have an expiration date sent, so we can't do the calculations needed.
3036 if (empty($expiration) || '0000-00-00 00:00:00' === $expiration) {
3037 continue;
3038 }
3039
3040 // if expiration is the past, continue.
3041 if (strtotime($expiration) < current_time('timestamp')) {
3042 continue;
3043 }
3044
3045 $already_emailed = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_renewal_emailed' . $site, true);
3046
3047 if ($already_emailed) {
3048 continue;
3049 }
3050
3051 leaky_paywall_email_subscription_status($user_id, 'renewal_reminder');
3052 update_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_renewal_emailed' . $site, current_time('timestamp'));
3053 }
3054 }
3055 add_action('leaky_paywall_process_renewal_reminder', 'leaky_paywall_maybe_send_renewal_reminder');
3056
3057
3058 /**
3059 * Register the expiration check cron job.
3060 *
3061 * Hooked to `init` (not `admin_init`) so scheduling happens on any page
3062 * load, frontend or admin. The earlier admin_init-only version silently
3063 * failed to schedule when no one visited wp-admin, which — combined with
3064 * a non-firing WP-Cron — left users' expiration status untouched
3065 * indefinitely.
3066 *
3067 * @since 4.23.0
3068 */
3069 function leaky_paywall_expiration_check_schedule() {
3070 if ( ! wp_next_scheduled( 'leaky_paywall_process_expiration_check' ) ) {
3071 wp_schedule_event( time(), 'daily', 'leaky_paywall_process_expiration_check' );
3072 }
3073 }
3074 add_action( 'init', 'leaky_paywall_expiration_check_schedule' );
3075
3076 /**
3077 * Clear the expiration check cron on plugin deactivation.
3078 *
3079 * @since 4.23.0
3080 */
3081 function leaky_paywall_expiration_check_deactivation() {
3082 wp_clear_scheduled_hook( 'leaky_paywall_process_expiration_check' );
3083 }
3084 register_deactivation_hook( __FILE__, 'leaky_paywall_expiration_check_deactivation' );
3085
3086 /**
3087 * Safety net: transition subscribers with access-granting statuses
3088 * whose expiration dates have passed to 'expired'.
3089 *
3090 * Processes in batches to avoid memory issues on large sites.
3091 *
3092 * @since 4.23.0
3093 */
3094 function leaky_paywall_process_expiration_check() {
3095 $mode = leaky_paywall_get_current_mode();
3096 $site = leaky_paywall_get_current_site();
3097
3098 // Include pending_cancel. Historically we excluded it here and
3099 // deferred to the gateway's subscription.deleted webhook — that
3100 // works when the webhook actually arrives, but a missed delivery
3101 // (Stripe outage, WAF rule, receiver blocked, wp-cron dead) left
3102 // canceled subscribers with content access for months. The
3103 // 24-hour grace period below still gives the webhook first
3104 // shot; this is the safety net for when it doesn't fire.
3105 $cron_statuses = leaky_paywall_access_statuses();
3106 $cron_statuses = array_values( $cron_statuses );
3107 $expires_key = '_issuem_leaky_paywall_' . $mode . '_expires' . $site;
3108
3109 // Grace period: don't expire until 24 hours after expiration.
3110 // This gives payment gateways time to process renewals and send
3111 // webhooks before the cron steps in (prevents the end-of-February
3112 // race condition where Stripe invoices are pending but not yet paid).
3113 $grace_cutoff = gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) );
3114
3115 $args = array(
3116 'number' => 200,
3117 'meta_query' => array(
3118 'relation' => 'AND',
3119 array(
3120 'key' => '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site,
3121 'value' => $cron_statuses,
3122 'compare' => 'IN',
3123 ),
3124 array(
3125 'key' => $expires_key,
3126 'value' => $grace_cutoff,
3127 'compare' => '<',
3128 'type' => 'DATETIME',
3129 ),
3130 array(
3131 'key' => $expires_key,
3132 'value' => '0000-00-00 00:00:00',
3133 'compare' => '!=',
3134 ),
3135 array(
3136 'key' => $expires_key,
3137 'value' => '0',
3138 'compare' => '!=',
3139 ),
3140 array(
3141 'key' => $expires_key,
3142 'value' => '',
3143 'compare' => '!=',
3144 ),
3145 ),
3146 );
3147
3148 $users = get_users( $args );
3149
3150 if ( empty( $users ) ) {
3151 return;
3152 }
3153
3154 foreach ( $users as $user ) {
3155 $status = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true );
3156
3157 // For past_due subscribers, verify with Stripe before expiring.
3158 if ( 'past_due' === $status ) {
3159 leaky_paywall_sync_past_due_subscriber( $user );
3160 continue;
3161 }
3162
3163 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'cron' );
3164 do_action( 'leaky_paywall_cron_expired_subscriber', $user );
3165 }
3166 }
3167 add_action( 'leaky_paywall_process_expiration_check', 'leaky_paywall_process_expiration_check' );
3168
3169 /**
3170 * Sync a past_due subscriber with Stripe to determine if their subscription
3171 * has been canceled. If no active subscription is found, expire them.
3172 *
3173 * @since 5.0.6
3174 *
3175 * @param WP_User $user The subscriber.
3176 */
3177 function leaky_paywall_sync_past_due_subscriber( $user ) {
3178 $mode = leaky_paywall_get_current_mode();
3179 $site = leaky_paywall_get_current_site();
3180 $subscriber_id = lp_get_subscriber_meta( 'subscriber_id', $user );
3181
3182 if ( ! $subscriber_id ) {
3183 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'cron' );
3184 do_action( 'leaky_paywall_cron_expired_subscriber', $user );
3185 return;
3186 }
3187
3188 $gateway = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_gateway' . $site, true );
3189
3190 if ( 'stripe' !== $gateway && 'stripe_checkout' !== $gateway ) {
3191 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'cron' );
3192 do_action( 'leaky_paywall_cron_expired_subscriber', $user );
3193 return;
3194 }
3195
3196 try {
3197 $stripe = leaky_paywall_initialize_stripe_api();
3198 $subscriptions = $stripe->subscriptions->all( array(
3199 'customer' => $subscriber_id,
3200 'status' => 'past_due',
3201 'limit' => 1,
3202 ), leaky_paywall_get_stripe_connect_params() );
3203
3204 if ( ! empty( $subscriptions->data ) ) {
3205 // Stripe still says past_due — leave them alone, retries may still be in progress.
3206 return;
3207 }
3208
3209 // No past_due subscription found — check if there's an active one.
3210 $active_subs = $stripe->subscriptions->all( array(
3211 'customer' => $subscriber_id,
3212 'status' => 'active',
3213 'limit' => 1,
3214 ), leaky_paywall_get_stripe_connect_params() );
3215
3216 if ( ! empty( $active_subs->data ) ) {
3217 leaky_paywall_set_subscriber_status( $user->ID, 'active', 'cron' );
3218 return;
3219 }
3220
3221 // No active or past_due subscription — expire them.
3222 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'cron' );
3223 do_action( 'leaky_paywall_cron_expired_subscriber', $user );
3224
3225 } catch ( \Throwable $th ) {
3226 leaky_paywall_log_error( $th->getMessage(), 'leaky paywall - past_due sync error for user ' . $user->ID );
3227 }
3228 }
3229
3230
3231 /**
3232 * One-time migration to move email settings from the monolithic LP settings
3233 * array to per-email wp_option keys.
3234 *
3235 * Converts the inverted checkbox logic ('on' = disabled) to standard
3236 * 'yes'/'no' values.
3237 *
3238 * @since 4.23.0
3239 */
3240 function lp_maybe_migrate_email_settings() {
3241
3242 if ( get_option( 'lp_email_settings_migrated' ) ) {
3243 return;
3244 }
3245
3246 $settings = get_option( 'issuem-leaky-paywall' );
3247
3248 if ( ! $settings ) {
3249 update_option( 'lp_email_settings_migrated', '1' );
3250 return;
3251 }
3252
3253 // New Subscriber Email.
3254 // Old inverted logic: 'on' = disabled, 'off' = enabled.
3255 $new_sub = array(
3256 'enabled' => ( isset( $settings['new_subscriber_email'] ) && 'on' === $settings['new_subscriber_email'] ) ? 'no' : 'yes',
3257 'subject' => isset( $settings['new_email_subject'] ) ? $settings['new_email_subject'] : '',
3258 'body' => isset( $settings['new_email_body'] ) ? $settings['new_email_body'] : '',
3259 );
3260 update_option( 'leaky_paywall_email_new_subscriber_settings', $new_sub );
3261
3262 // Admin New Subscriber Email.
3263 $admin_new = array(
3264 'enabled' => ( isset( $settings['new_subscriber_admin_email'] ) && 'on' === $settings['new_subscriber_admin_email'] ) ? 'no' : 'yes',
3265 'subject' => isset( $settings['admin_new_subscriber_email_subject'] ) ? $settings['admin_new_subscriber_email_subject'] : '',
3266 'body' => '',
3267 'recipients' => isset( $settings['admin_new_subscriber_email_recipients'] ) ? $settings['admin_new_subscriber_email_recipients'] : get_option( 'admin_email' ),
3268 );
3269 update_option( 'leaky_paywall_email_admin_new_subscriber_settings', $admin_new );
3270
3271 // Renewal Reminder Email.
3272 $renewal = array(
3273 'enabled' => ( isset( $settings['renewal_reminder_email'] ) && 'on' === $settings['renewal_reminder_email'] ) ? 'no' : 'yes',
3274 'subject' => isset( $settings['renewal_reminder_email_subject'] ) ? $settings['renewal_reminder_email_subject'] : '',
3275 'body' => isset( $settings['renewal_reminder_email_body'] ) ? $settings['renewal_reminder_email_body'] : '',
3276 'days_before' => isset( $settings['renewal_reminder_days_before'] ) ? $settings['renewal_reminder_days_before'] : '7',
3277 );
3278 update_option( 'leaky_paywall_email_renewal_reminder_settings', $renewal );
3279
3280 update_option( 'lp_email_settings_migrated', '1' );
3281
3282 leaky_paywall_log( 'Email settings migrated to per-email options', 'migration' );
3283 }
3284 add_action( 'admin_init', 'lp_maybe_migrate_email_settings', 5 );
3285
3286
3287 /**
3288 * Schedule the status migration to run in the background via Action Scheduler.
3289 *
3290 * @since 4.23.0
3291 * @since 4.24.0 Moved from admin_init to Action Scheduler for performance.
3292 */
3293 function leaky_paywall_maybe_schedule_status_migration() {
3294 if ( get_option( 'leaky_paywall_status_migration_v2' ) ) {
3295 return;
3296 }
3297
3298 if ( as_has_scheduled_action( 'leaky_paywall_run_status_migration_batch' ) ) {
3299 return;
3300 }
3301
3302 as_enqueue_async_action( 'leaky_paywall_run_status_migration_batch' );
3303 }
3304 add_action( 'admin_init', 'leaky_paywall_maybe_schedule_status_migration' );
3305
3306 /**
3307 * One-time migration to align existing subscriber statuses with the new
3308 * status-as-source-of-truth system. Runs in the background via Action Scheduler.
3309 *
3310 * 1. canceled Stripe subscribers → sync with Stripe for authoritative status
3311 * 2. canceled non-Stripe subscribers + future expiration → pending_cancel
3312 * 3. active/trial + past expiration + no recurring plan → expired
3313 * 4. remaining canceled non-Stripe subscribers + past/no expiration → expired
3314 *
3315 * @since 4.23.0
3316 * @since 4.24.0 Runs as an async Action Scheduler batch instead of on admin_init.
3317 * @since 4.25.0 Sync canceled Stripe subscribers with Stripe before migrating.
3318 */
3319 function leaky_paywall_run_status_migration_batch() {
3320
3321 $mode = leaky_paywall_get_current_mode();
3322 $site = leaky_paywall_get_current_site();
3323 $now = gmdate( 'Y-m-d H:i:s' );
3324 $batch = 100;
3325 $processed = 0;
3326
3327 // 1. Canceled Stripe subscribers → sync with Stripe for authoritative status.
3328 // The LP expires meta may not reflect the actual Stripe subscription period end,
3329 // so we check Stripe directly instead of relying on the local expires date.
3330 // This step is API-heavy — up to 4 Stripe requests per user — so we
3331 // process fewer per batch than the other steps and pace the calls, to
3332 // keep well under conntrack/egress caps on managed hosts.
3333 $stripe_batch = 25;
3334 $canceled_stripe_users = get_users( array(
3335 'number' => $stripe_batch,
3336 'meta_query' => array(
3337 'relation' => 'AND',
3338 array(
3339 'key' => '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site,
3340 'value' => 'canceled',
3341 'compare' => '=',
3342 ),
3343 array(
3344 'key' => '_issuem_leaky_paywall_' . $mode . '_subscriber_id' . $site,
3345 'value' => 'cus_',
3346 'compare' => 'LIKE',
3347 ),
3348 ),
3349 ) );
3350
3351 foreach ( $canceled_stripe_users as $user ) {
3352 if ( function_exists( 'leaky_paywall_sync_stripe_subscription' ) ) {
3353 leaky_paywall_sync_stripe_subscription( $user );
3354 // Pace outbound API calls at ~5/user/sec so the migration
3355 // doesn't saturate outbound connections on hosts with tight
3356 // conntrack or egress limits.
3357 usleep( 200000 );
3358 } else {
3359 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'migration' );
3360 }
3361 $processed++;
3362 }
3363
3364 // 2. Canceled non-Stripe subscribers with future expiration → pending_cancel.
3365 $users_to_pending = get_users( array(
3366 'number' => $batch,
3367 'meta_query' => array(
3368 'relation' => 'AND',
3369 array(
3370 'key' => '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site,
3371 'value' => 'canceled',
3372 'compare' => '=',
3373 ),
3374 array(
3375 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3376 'value' => $now,
3377 'compare' => '>',
3378 'type' => 'DATETIME',
3379 ),
3380 array(
3381 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3382 'value' => '0000-00-00 00:00:00',
3383 'compare' => '!=',
3384 ),
3385 ),
3386 ) );
3387
3388 foreach ( $users_to_pending as $user ) {
3389 leaky_paywall_set_subscriber_status( $user->ID, 'pending_cancel', 'migration' );
3390 $processed++;
3391 }
3392
3393 // 3. Active/trial subscribers with past expiration + no recurring plan → expired.
3394 // Note: Active subscribers with a recurring plan are skipped — their subscription may still be valid.
3395 $users_to_expire = get_users( array(
3396 'number' => $batch,
3397 'meta_query' => array(
3398 'relation' => 'AND',
3399 array(
3400 'key' => '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site,
3401 'value' => array( 'active', 'trial' ),
3402 'compare' => 'IN',
3403 ),
3404 array(
3405 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3406 'value' => $now,
3407 'compare' => '<',
3408 'type' => 'DATETIME',
3409 ),
3410 array(
3411 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3412 'value' => '0000-00-00 00:00:00',
3413 'compare' => '!=',
3414 ),
3415 array(
3416 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3417 'value' => '0',
3418 'compare' => '!=',
3419 ),
3420 array(
3421 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3422 'value' => '',
3423 'compare' => '!=',
3424 ),
3425 ),
3426 ) );
3427
3428 foreach ( $users_to_expire as $user ) {
3429 $old_status = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true );
3430 $plan = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_plan' . $site, true );
3431
3432 // Skip active recurring subscribers — their subscription may still be valid.
3433 if ( 'active' === $old_status && ! empty( $plan ) ) {
3434 continue;
3435 }
3436
3437 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'migration' );
3438 $processed++;
3439 }
3440
3441 // 4. Remaining canceled non-Stripe subscribers with past or no expiration → expired.
3442 $users_canceled_expired = get_users( array(
3443 'number' => $batch,
3444 'meta_query' => array(
3445 'relation' => 'AND',
3446 array(
3447 'key' => '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site,
3448 'value' => 'canceled',
3449 'compare' => '=',
3450 ),
3451 array(
3452 'relation' => 'OR',
3453 array(
3454 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3455 'value' => $now,
3456 'compare' => '<',
3457 'type' => 'DATETIME',
3458 ),
3459 array(
3460 'key' => '_issuem_leaky_paywall_' . $mode . '_expires' . $site,
3461 'value' => '0000-00-00 00:00:00',
3462 'compare' => '=',
3463 ),
3464 ),
3465 ),
3466 ) );
3467
3468 foreach ( $users_canceled_expired as $user ) {
3469 leaky_paywall_set_subscriber_status( $user->ID, 'expired', 'migration' );
3470 $processed++;
3471 }
3472
3473 if ( 0 === $processed ) {
3474 update_option( 'leaky_paywall_status_migration_v2', true );
3475 leaky_paywall_log( 'Status migration v2 complete', 'migration' );
3476 } else {
3477 leaky_paywall_log( 'Migrated ' . $processed . ' subscribers, scheduling next batch', 'migration' );
3478 as_enqueue_async_action( 'leaky_paywall_run_status_migration_batch' );
3479 }
3480 }
3481 add_action( 'leaky_paywall_run_status_migration_batch', 'leaky_paywall_run_status_migration_batch' );
3482
3483
3484 /**
3485 * Calculate the differce between two date values
3486 *
3487 * @since 4.9.3
3488 * @param string $date_1 The first date.
3489 * @param string $date_2 The second date.
3490 * @param string $difference_format The difference format.
3491 * @return string
3492 */
3493 function leaky_paywall_date_difference($date_1, $date_2, $difference_format = '%a')
3494 {
3495
3496 $datetime1 = date_create($date_1);
3497 $datetime2 = date_create($date_2);
3498
3499 $interval = date_diff($datetime1, $datetime2);
3500
3501 return $interval->format($difference_format);
3502 }
3503
3504 /**
3505 * Set email content type
3506 *
3507 * @param string $content_type The content type.
3508 * @return string
3509 */
3510 function leaky_paywall_set_email_content_type($content_type)
3511 {
3512 return 'text/html';
3513 }
3514
3515
3516 /**
3517 * Filter email tags
3518 *
3519 * @param string $message The email message.
3520 * @param int $user_id The user id.
3521 * @param string $display_name The display name of the user.
3522 * @param string $password The password of the user.
3523 * @return string
3524 */
3525 function leaky_paywall_filter_email_tags($message, $user_id, $display_name, $password)
3526 {
3527
3528 $user = get_userdata($user_id);
3529
3530 if ( ! $user ) {
3531 return $message;
3532 }
3533
3534 $site_name = stripslashes_deep(html_entity_decode(get_bloginfo('name'), ENT_COMPAT, 'UTF-8'));
3535
3536 $values = array(
3537 'blogname' => $site_name,
3538 'sitename' => $site_name,
3539 'username' => $user->user_login,
3540 'useremail' => $user->user_email,
3541 'firstname' => $user->user_firstname,
3542 'lastname' => $user->user_lastname,
3543 'displayname' => $display_name,
3544 'password' => $password,
3545 );
3546
3547 // Match %token% or %token|fallback%. The fallback supplies a default when
3548 // the token resolves to an empty string (e.g. %firstname|there% renders
3549 // "there" for subscribers with no first name on file).
3550 $message = preg_replace_callback(
3551 '/%([a-z_][a-z0-9_]*)(?:\|([^%]*))?%/',
3552 function ( $m ) use ( $values ) {
3553 if ( ! array_key_exists( $m[1], $values ) ) {
3554 return $m[0];
3555 }
3556
3557 $value = (string) $values[ $m[1] ];
3558
3559 if ( '' !== $value ) {
3560 return $value;
3561 }
3562
3563 return isset( $m[2] ) ? trim( $m[2] ) : '';
3564 },
3565 $message
3566 );
3567
3568 return $message;
3569 }
3570
3571
3572 /**
3573 * Get currencies supported by Leaky Paywall
3574 *
3575 * @return array
3576 */
3577 function leaky_paywall_supported_currencies()
3578 {
3579 $currencies = array(
3580 'AED' => array(
3581 'symbol' => '&#1583;.&#1573;',
3582 'label' => __('UAE dirham', 'leaky-paywall'),
3583 'country' => __('UAE', 'leaky-paywall'),
3584 ),
3585 'AFN' => array(
3586 'symbol' => 'Afs',
3587 'label' => __('Afghan afghani', 'leaky-paywall'),
3588 'country' => __('Afghanistan', 'leaky-paywall'),
3589 ),
3590 'ALL' => array(
3591 'symbol' => 'L',
3592 'label' => __('Albanian lek', 'leaky-paywall'),
3593 'country' => __('Albania', 'leaky-paywall'),
3594 ),
3595 'AMD' => array(
3596 'symbol' => 'AMD',
3597 'label' => __('Armenian dram', 'leaky-paywall'),
3598 'country' => __('Armenia', 'leaky-paywall'),
3599 ),
3600 'ANG' => array(
3601 'symbol' => 'NA&#402;',
3602 'label' => __('Netherlands Antillean gulden', 'leaky-paywall'),
3603 'country' => __('Netherlands', 'leaky-paywall'),
3604 ),
3605 'AOA' => array(
3606 'symbol' => 'Kz',
3607 'label' => __('Angolan kwanza', 'leaky-paywall'),
3608 'country' => __('Angolia', 'leaky-paywall'),
3609 ),
3610 'ARS' => array(
3611 'symbol' => '$',
3612 'label' => __('Argentine peso', 'leaky-paywall'),
3613 'country' => __('Argentina', 'leaky-paywall'),
3614 ),
3615 'AUD' => array(
3616 'symbol' => '$',
3617 'label' => __('Australian dollar', 'leaky-paywall'),
3618 'country' => __('Australia', 'leaky-paywall'),
3619 ),
3620 'AWG' => array(
3621 'symbol' => '&#402;',
3622 'label' => __('Aruban florin', 'leaky-paywall'),
3623 'country' => __('Aruba', 'leaky-paywall'),
3624 ),
3625 'AZN' => array(
3626 'symbol' => 'AZN',
3627 'label' => __('Azerbaijani manat', 'leaky-paywall'),
3628 'country' => __('Azerbaij', 'leaky-paywall'),
3629 ),
3630 'BAM' => array(
3631 'symbol' => 'KM',
3632 'label' => __('Bosnia and Herzegovina konvertibilna marka', 'leaky-paywall'),
3633 'country' => __('Bosnia', 'leaky-paywall'),
3634 ),
3635 'BBD' => array(
3636 'symbol' => 'Bds$',
3637 'label' => __('Barbadian dollar', 'leaky-paywall'),
3638 'country' => __('Barbadian', 'leaky-paywall'),
3639 ),
3640 'BDT' => array(
3641 'symbol' => '&#2547;',
3642 'label' => __('Bangladeshi taka', 'leaky-paywall'),
3643 'country' => __('Bangladesh', 'leaky-paywall'),
3644 ),
3645 'BGN' => array(
3646 'symbol' => 'BGN',
3647 'label' => __('Bulgarian lev', 'leaky-paywall'),
3648 'country' => __('Bulgaria', 'leaky-paywall'),
3649 ),
3650 'BIF' => array(
3651 'symbol' => 'FBu',
3652 'label' => __('Burundi franc', 'leaky-paywall'),
3653 'country' => __('Burundi', 'leaky-paywall'),
3654 ),
3655 'BMD' => array(
3656 'symbol' => 'BD$',
3657 'label' => __('Bermudian dollar', 'leaky-paywall'),
3658 'country' => __('Bermuda', 'leaky-paywall'),
3659 ),
3660 'BND' => array(
3661 'symbol' => 'B$',
3662 'label' => __('Brunei dollar', 'leaky-paywall'),
3663 'country' => __('Brunei', 'leaky-paywall'),
3664 ),
3665 'BOB' => array(
3666 'symbol' => 'Bs.',
3667 'label' => __('Bolivian boliviano', 'leaky-paywall'),
3668 'country' => __('Bolivia', 'leaky-paywall'),
3669 ),
3670 'BRL' => array(
3671 'symbol' => 'R$',
3672 'label' => __('Brazilian real', 'leaky-paywall'),
3673 'country' => __('Brazil', 'leaky-paywall'),
3674 ),
3675 'BSD' => array(
3676 'symbol' => 'B$',
3677 'label' => __('Bahamian dollar', 'leaky-paywall'),
3678 'country' => __('Bahamas', 'leaky-paywall'),
3679 ),
3680 'BWP' => array(
3681 'symbol' => 'P',
3682 'label' => __('Botswana pula', 'leaky-paywall'),
3683 'country' => __('Botswana', 'leaky-paywall'),
3684 ),
3685 'BZD' => array(
3686 'symbol' => 'BZ$',
3687 'label' => __('Belize dollar', 'leaky-paywall'),
3688 'country' => __('Belize', 'leaky-paywall'),
3689 ),
3690 'CAD' => array(
3691 'symbol' => '$',
3692 'label' => __('Canadian dollar', 'leaky-paywall'),
3693 'country' => __('Canada', 'leaky-paywall'),
3694 ),
3695 'CDF' => array(
3696 'symbol' => 'F',
3697 'label' => __('Congolese franc', 'leaky-paywall'),
3698 'country' => __('Congo', 'leaky-paywall'),
3699 ),
3700 'CHF' => array(
3701 'symbol' => 'CHF',
3702 'label' => __('Swiss franc', 'leaky-paywall'),
3703 'country' => __('Switzerland', 'leaky-paywall'),
3704 ),
3705 'CLP' => array(
3706 'symbol' => '$',
3707 'label' => __('Chilean peso', 'leaky-paywall'),
3708 'country' => __('Chili', 'leaky-paywall'),
3709 ),
3710 'CNY' => array(
3711 'symbol' => '&#165;',
3712 'label' => __('Chinese Yuan Renminbi', 'leaky-paywall'),
3713 'country' => __('Chinese Yuan', 'leaky-paywall'),
3714 ),
3715 'COP' => array(
3716 'symbol' => 'Col$',
3717 'label' => __('Colombian peso', 'leaky-paywall'),
3718 'country' => __('Colombia', 'leaky-paywall'),
3719 ),
3720 'CRC' => array(
3721 'symbol' => '&#8353;',
3722 'label' => __('Costa Rican colon', 'leaky-paywall'),
3723 'country' => __('Costa Rica', 'leaky-paywall'),
3724 ),
3725 'CVE' => array(
3726 'symbol' => 'Esc',
3727 'label' => __('Cape Verdean escudo', 'leaky-paywall'),
3728 'country' => __('Cape Verde', 'leaky-paywall'),
3729 ),
3730 'CZK' => array(
3731 'symbol' => 'K&#269;',
3732 'label' => __('Czech koruna', 'leaky-paywall'),
3733 'country' => __('Czech', 'leaky-paywall'),
3734 ),
3735 'DJF' => array(
3736 'symbol' => 'Fdj',
3737 'label' => __('Djiboutian franc', 'leaky-paywall'),
3738 'country' => __('Djibouti', 'leaky-paywall'),
3739 ),
3740 'DKK' => array(
3741 'symbol' => 'kr',
3742 'label' => __('Danish krone', 'leaky-paywall'),
3743 'country' => __('Danish', 'leaky-paywall'),
3744 ),
3745 'DOP' => array(
3746 'symbol' => 'RD$',
3747 'label' => __('Dominican peso', 'leaky-paywall'),
3748 'country' => __('Dominican Republic', 'leaky-paywall'),
3749 ),
3750 'DZD' => array(
3751 'symbol' => '&#1583;.&#1580;',
3752 'label' => __('Algerian dinar', 'leaky-paywall'),
3753 'country' => __('Algeria', 'leaky-paywall'),
3754 ),
3755 'EEK' => array(
3756 'symbol' => 'KR',
3757 'label' => __('Estonian kroon', 'leaky-paywall'),
3758 'country' => __('Estonia', 'leaky-paywall'),
3759 ),
3760 'EGP' => array(
3761 'symbol' => '&#163;',
3762 'label' => __('Egyptian pound', 'leaky-paywall'),
3763 'country' => __('Egypt', 'leaky-paywall'),
3764 ),
3765 'ETB' => array(
3766 'symbol' => 'Br',
3767 'label' => __('Ethiopian birr', 'leaky-paywall'),
3768 'country' => __('Ethiopia', 'leaky-paywall'),
3769 ),
3770 'EUR' => array(
3771 'symbol' => '&#8364;',
3772 'label' => __('European Euro', 'leaky-paywall'),
3773 'country' => __('Euro', 'leaky-paywall'),
3774 ),
3775 'FJD' => array(
3776 'symbol' => 'FJ$',
3777 'label' => __('Fijian dollar', 'leaky-paywall'),
3778 'country' => __('Fiji', 'leaky-paywall'),
3779 ),
3780 'FKP' => array(
3781 'symbol' => '&#163;',
3782 'label' => __('Falkland Islands pound', 'leaky-paywall'),
3783 'country' => __('Falkland Islands', 'leaky-paywall'),
3784 ),
3785 'GBP' => array(
3786 'symbol' => '&#163;',
3787 'label' => __('British pound', 'leaky-paywall'),
3788 'country' => __('Great Britian', 'leaky-paywall'),
3789 ),
3790 'GEL' => array(
3791 'symbol' => 'GEL',
3792 'label' => __('Georgian lari', 'leaky-paywall'),
3793 'country' => __('Georgia', 'leaky-paywall'),
3794 ),
3795 'GIP' => array(
3796 'symbol' => '&#163;',
3797 'label' => __('Gibraltar pound', 'leaky-paywall'),
3798 'country' => __('Gibraltar', 'leaky-paywall'),
3799 ),
3800 'GMD' => array(
3801 'symbol' => 'D',
3802 'label' => __('Gambian dalasi', 'leaky-paywall'),
3803 'country' => __('Gambia', 'leaky-paywall'),
3804 ),
3805 'GNF' => array(
3806 'symbol' => 'FG',
3807 'label' => __('Guinean franc', 'leaky-paywall'),
3808 'country' => __('Guinea', 'leaky-paywall'),
3809 ),
3810 'GTQ' => array(
3811 'symbol' => 'Q',
3812 'label' => __('Guatemalan quetzal', 'leaky-paywall'),
3813 'country' => __('Guatemala', 'leaky-paywall'),
3814 ),
3815 'GYD' => array(
3816 'symbol' => 'GY$',
3817 'label' => __('Guyanese dollar', 'leaky-paywall'),
3818 'country' => __('Guyanese', 'leaky-paywall'),
3819 ),
3820 'HKD' => array(
3821 'symbol' => 'HK$',
3822 'label' => __('Hong Kong dollar', 'leaky-paywall'),
3823 'country' => __('Hong Kong', 'leaky-paywall'),
3824 ),
3825 'HNL' => array(
3826 'symbol' => 'L',
3827 'label' => __('Honduran lempira', 'leaky-paywall'),
3828 'country' => __('Honduras', 'leaky-paywall'),
3829 ),
3830 'HRK' => array(
3831 'symbol' => 'kn',
3832 'label' => __('Croatian kuna', 'leaky-paywall'),
3833 'country' => __('Croatia', 'leaky-paywall'),
3834 ),
3835 'HTG' => array(
3836 'symbol' => 'G',
3837 'label' => __('Haitian gourde', 'leaky-paywall'),
3838 'country' => __('Haiti', 'leaky-paywall'),
3839 ),
3840 'HUF' => array(
3841 'symbol' => 'Ft',
3842 'label' => __('Hungarian forint', 'leaky-paywall'),
3843 'country' => __('Hungary', 'leaky-paywall'),
3844 ),
3845 'IDR' => array(
3846 'symbol' => 'Rp',
3847 'label' => __('Indonesian rupiah', 'leaky-paywall'),
3848 'country' => __('Idonesia', 'leaky-paywall'),
3849 ),
3850 'ILS' => array(
3851 'symbol' => '&#8362;',
3852 'label' => __('Israeli new sheqel', 'leaky-paywall'),
3853 'country' => __('Israel', 'leaky-paywall'),
3854 ),
3855 'INR' => array(
3856 'symbol' => '&#8377;',
3857 'label' => __('Indian rupee', 'leaky-paywall'),
3858 'country' => __('India', 'leaky-paywall'),
3859 ),
3860 'ISK' => array(
3861 'symbol' => 'kr',
3862 'label' => __('Icelandic króna', 'leaky-paywall'),
3863 'country' => __('Iceland', 'leaky-paywall'),
3864 ),
3865 'JMD' => array(
3866 'symbol' => 'J$',
3867 'label' => __('Jamaican dollar', 'leaky-paywall'),
3868 'country' => __('Jamaica', 'leaky-paywall'),
3869 ),
3870 'JPY' => array(
3871 'symbol' => '&#165;',
3872 'label' => __('Japanese yen', 'leaky-paywall'),
3873 'country' => __('Japan', 'leaky-paywall'),
3874 ),
3875 'KES' => array(
3876 'symbol' => 'KSh',
3877 'label' => __('Kenyan shilling', 'leaky-paywall'),
3878 'country' => __('Kenya', 'leaky-paywall'),
3879 ),
3880 'KGS' => array(
3881 'symbol' => '&#1089;&#1086;&#1084;',
3882 'label' => __('Kyrgyzstani som', 'leaky-paywall'),
3883 'country' => __('Kyrgyzstan', 'leaky-paywall'),
3884 ),
3885 'KHR' => array(
3886 'symbol' => '&#6107;',
3887 'label' => __('Cambodian riel', 'leaky-paywall'),
3888 'country' => __('Cambodia', 'leaky-paywall'),
3889 ),
3890 'KMF' => array(
3891 'symbol' => 'KMF',
3892 'label' => __('Comorian franc', 'leaky-paywall'),
3893 'country' => __('Comorian', 'leaky-paywall'),
3894 ),
3895 'KRW' => array(
3896 'symbol' => 'W',
3897 'label' => __('South Korean won', 'leaky-paywall'),
3898 'country' => __('South Korea', 'leaky-paywall'),
3899 ),
3900 'KYD' => array(
3901 'symbol' => 'KY$',
3902 'label' => __('Cayman Islands dollar', 'leaky-paywall'),
3903 'country' => __('Cayman Islands', 'leaky-paywall'),
3904 ),
3905 'KZT' => array(
3906 'symbol' => 'T',
3907 'label' => __('Kazakhstani tenge', 'leaky-paywall'),
3908 'country' => __('Kazakhstan', 'leaky-paywall'),
3909 ),
3910 'LAK' => array(
3911 'symbol' => 'KN',
3912 'label' => __('Lao kip', 'leaky-paywall'),
3913 'country' => __('Loa', 'leaky-paywall'),
3914 ),
3915 'LBP' => array(
3916 'symbol' => '&#163;',
3917 'label' => __('Lebanese lira', 'leaky-paywall'),
3918 'country' => __('Lebanese', 'leaky-paywall'),
3919 ),
3920 'LKR' => array(
3921 'symbol' => 'Rs',
3922 'label' => __('Sri Lankan rupee', 'leaky-paywall'),
3923 'country' => __('Sri Lanka', 'leaky-paywall'),
3924 ),
3925 'LRD' => array(
3926 'symbol' => 'L$',
3927 'label' => __('Liberian dollar', 'leaky-paywall'),
3928 'country' => __('Liberia', 'leaky-paywall'),
3929 ),
3930 'LSL' => array(
3931 'symbol' => 'M',
3932 'label' => __('Lesotho loti', 'leaky-paywall'),
3933 'country' => __('Lesotho', 'leaky-paywall'),
3934 ),
3935 'LTL' => array(
3936 'symbol' => 'Lt',
3937 'label' => __('Lithuanian litas', 'leaky-paywall'),
3938 'country' => __('Lithuania', 'leaky-paywall'),
3939 ),
3940 'LVL' => array(
3941 'symbol' => 'Ls',
3942 'label' => __('Latvian lats', 'leaky-paywall'),
3943 'country' => __('Latvia', 'leaky-paywall'),
3944 ),
3945 'MAD' => array(
3946 'symbol' => 'MAD',
3947 'label' => __('Moroccan dirham', 'leaky-paywall'),
3948 'country' => __('Morocco', 'leaky-paywall'),
3949 ),
3950 'MDL' => array(
3951 'symbol' => 'MDL',
3952 'label' => __('Moldovan leu', 'leaky-paywall'),
3953 'country' => __('Moldova', 'leaky-paywall'),
3954 ),
3955 'MGA' => array(
3956 'symbol' => 'FMG',
3957 'label' => __('Malagasy ariary', 'leaky-paywall'),
3958 'country' => __('Malagasy', 'leaky-paywall'),
3959 ),
3960 'MKD' => array(
3961 'symbol' => 'MKD',
3962 'label' => __('Macedonian denar', 'leaky-paywall'),
3963 'country' => __('Macedonia', 'leaky-paywall'),
3964 ),
3965 'MNT' => array(
3966 'symbol' => '&#8366;',
3967 'label' => __('Mongolian tugrik', 'leaky-paywall'),
3968 'country' => __('Mongolia', 'leaky-paywall'),
3969 ),
3970 'MOP' => array(
3971 'symbol' => 'P',
3972 'label' => __('Macanese pataca', 'leaky-paywall'),
3973 'country' => __('Macanese', 'leaky-paywall'),
3974 ),
3975 'MRO' => array(
3976 'symbol' => 'UM',
3977 'label' => __('Mauritanian ouguiya', 'leaky-paywall'),
3978 'country' => '',
3979 ),
3980 'MUR' => array(
3981 'symbol' => 'Rs',
3982 'label' => __('Mauritian rupee', 'leaky-paywall'),
3983 'country' => '',
3984 ),
3985 'MVR' => array(
3986 'symbol' => 'Rf',
3987 'label' => __('Maldivian rufiyaa', 'leaky-paywall'),
3988 'country' => '',
3989 ),
3990 'MWK' => array(
3991 'symbol' => 'MK',
3992 'label' => __('Malawian kwacha', 'leaky-paywall'),
3993 'country' => '',
3994 ),
3995 'MXN' => array(
3996 'symbol' => '$',
3997 'label' => __('Mexican peso', 'leaky-paywall'),
3998 'country' => '',
3999 ),
4000 'MYR' => array(
4001 'symbol' => 'RM',
4002 'label' => __('Malaysian ringgit', 'leaky-paywall'),
4003 'country' => '',
4004 ),
4005 'MZN' => array(
4006 'symbol' => 'MT',
4007 'label' => __('Mozambique Metical', 'leaky-paywall'),
4008 'country' => '',
4009 ),
4010 'NAD' => array(
4011 'symbol' => 'N$',
4012 'label' => __('Namibian dollar', 'leaky-paywall'),
4013 'country' => '',
4014 ),
4015 'NGN' => array(
4016 'symbol' => '&#8358;',
4017 'label' => __('Nigerian naira', 'leaky-paywall'),
4018 'country' => '',
4019 ),
4020 'NIO' => array(
4021 'symbol' => 'C$',
4022 'label' => __('Nicaraguan Córdoba', 'leaky-paywall'),
4023 'country' => '',
4024 ),
4025 'NOK' => array(
4026 'symbol' => 'kr',
4027 'label' => __('Norwegian krone', 'leaky-paywall'),
4028 'country' => '',
4029 ),
4030 'NPR' => array(
4031 'symbol' => 'NRs',
4032 'label' => __('Nepalese rupee', 'leaky-paywall'),
4033 'country' => '',
4034 ),
4035 'NZD' => array(
4036 'symbol' => 'NZ$',
4037 'label' => __('New Zealand dollar', 'leaky-paywall'),
4038 'country' => '',
4039 ),
4040 'PAB' => array(
4041 'symbol' => 'B./',
4042 'label' => __('Panamanian balboa', 'leaky-paywall'),
4043 'country' => '',
4044 ),
4045 'PEN' => array(
4046 'symbol' => 'S/.',
4047 'label' => __('Peruvian nuevo sol', 'leaky-paywall'),
4048 'country' => '',
4049 ),
4050 'PGK' => array(
4051 'symbol' => 'K',
4052 'label' => __('Papua New Guinean kina', 'leaky-paywall'),
4053 'country' => '',
4054 ),
4055 'PHP' => array(
4056 'symbol' => '&#8369;',
4057 'label' => __('Philippine peso', 'leaky-paywall'),
4058 'country' => '',
4059 ),
4060 'PKR' => array(
4061 'symbol' => 'Rs.',
4062 'label' => __('Pakistani rupee', 'leaky-paywall'),
4063 'country' => '',
4064 ),
4065 'PLN' => array(
4066 'symbol' => 'z&#322;',
4067 'label' => __('Polish zloty', 'leaky-paywall'),
4068 'country' => '',
4069 ),
4070 'PYG' => array(
4071 'symbol' => '&#8370;',
4072 'label' => __('Paraguayan guarani', 'leaky-paywall'),
4073 'country' => '',
4074 ),
4075 'QAR' => array(
4076 'symbol' => 'QR',
4077 'label' => __('Qatari riyal', 'leaky-paywall'),
4078 'country' => '',
4079 ),
4080 'RON' => array(
4081 'symbol' => 'L',
4082 'label' => __('Romanian leu', 'leaky-paywall'),
4083 'country' => '',
4084 ),
4085 'RSD' => array(
4086 'symbol' => 'din.',
4087 'label' => __('Serbian dinar', 'leaky-paywall'),
4088 'country' => '',
4089 ),
4090 'RUB' => array(
4091 'symbol' => 'R',
4092 'label' => __('Russian ruble', 'leaky-paywall'),
4093 'country' => '',
4094 ),
4095 'RWF' => array(
4096 'symbol' => 'R&#8355;',
4097 'label' => __('Rwandan Franc'),
4098 'country' => '',
4099 ),
4100 'SAR' => array(
4101 'symbol' => 'SR',
4102 'label' => __('Saudi riyal', 'leaky-paywall'),
4103 ),
4104 'SBD' => array(
4105 'symbol' => 'SI$',
4106 'label' => __('Solomon Islands dollar', 'leaky-paywall'),
4107 'country' => '',
4108 ),
4109 'SCR' => array(
4110 'symbol' => 'SR',
4111 'label' => __('Seychellois rupee', 'leaky-paywall'),
4112 'country' => '',
4113 ),
4114 'SEK' => array(
4115 'symbol' => 'kr',
4116 'label' => __('Swedish krona', 'leaky-paywall'),
4117 'country' => '',
4118 ),
4119 'SGD' => array(
4120 'symbol' => 'S$',
4121 'label' => __('Singapore dollar', 'leaky-paywall'),
4122 'country' => '',
4123 ),
4124 'SHP' => array(
4125 'symbol' => '&#163;',
4126 'label' => __('Saint Helena pound', 'leaky-paywall'),
4127 'country' => '',
4128 ),
4129 'SLL' => array(
4130 'symbol' => 'Le',
4131 'label' => __('Sierra Leonean leone', 'leaky-paywall'),
4132 'country' => '',
4133 ),
4134 'SOS' => array(
4135 'symbol' => 'Sh.',
4136 'label' => __('Somali shilling', 'leaky-paywall'),
4137 'country' => '',
4138 ),
4139 'SRD' => array(
4140 'symbol' => '$',
4141 'label' => __('Surinamese dollar', 'leaky-paywall'),
4142 'country' => '',
4143 ),
4144 'STD' => array(
4145 'symbol' => 'STD',
4146 'label' => __('São Tomé and Príncipe Dobra', 'leaky-paywall'),
4147 'country' => '',
4148 ),
4149 'SVC' => array(
4150 'symbol' => '$',
4151 'label' => __('El Salvador Colon', 'leaky-paywall'),
4152 'country' => '',
4153 ),
4154 'SZL' => array(
4155 'symbol' => 'E',
4156 'label' => __('Swazi lilangeni', 'leaky-paywall'),
4157 'country' => '',
4158 ),
4159 'THB' => array(
4160 'symbol' => '&#3647;',
4161 'label' => __('Thai baht', 'leaky-paywall'),
4162 'country' => '',
4163 ),
4164 'TJS' => array(
4165 'symbol' => 'TJS',
4166 'label' => __('Tajikistani somoni', 'leaky-paywall'),
4167 'country' => '',
4168 ),
4169 'TOP' => array(
4170 'symbol' => 'T$',
4171 'label' => __("Tonga Pa'anga", 'leaky-paywall'),
4172 'country' => '',
4173 ),
4174 'TRY' => array(
4175 'symbol' => 'TRY',
4176 'label' => __('Turkish new lira', 'leaky-paywall'),
4177 'country' => '',
4178 ),
4179 'TTD' => array(
4180 'symbol' => 'TT$',
4181 'label' => __('Trinidad and Tobago dollar', 'leaky-paywall'),
4182 'country' => '',
4183 ),
4184 'TWD' => array(
4185 'symbol' => 'NT$',
4186 'label' => __('New Taiwan dollar', 'leaky-paywall'),
4187 'country' => '',
4188 ),
4189 'TZS' => array(
4190 'symbol' => 'TZS',
4191 'label' => __('Tanzanian shilling', 'leaky-paywall'),
4192 'country' => '',
4193 ),
4194 'UAH' => array(
4195 'symbol' => 'UAH',
4196 'label' => __('Ukrainian hryvnia', 'leaky-paywall'),
4197 'country' => '',
4198 ),
4199 'UGX' => array(
4200 'symbol' => 'USh',
4201 'label' => __('Ugandan shilling', 'leaky-paywall'),
4202 'country' => '',
4203 ),
4204 'USD' => array(
4205 'symbol' => '$',
4206 'label' => __('United States dollar', 'leaky-paywall'),
4207 'country' => __('United States', 'leaky-paywall'),
4208 ),
4209 'UYU' => array(
4210 'symbol' => '$U',
4211 'label' => __('Uruguayan peso', 'leaky-paywall'),
4212 'country' => '',
4213 ),
4214 'UZS' => array(
4215 'symbol' => 'UZS',
4216 'label' => __('Uzbekistani som', 'leaky-paywall'),
4217 'country' => '',
4218 ),
4219 'VND' => array(
4220 'symbol' => '&#8363;',
4221 'label' => __('Vietnamese dong', 'leaky-paywall'),
4222 'country' => '',
4223 ),
4224 'VUV' => array(
4225 'symbol' => 'VT',
4226 'label' => __('Vanuatu vatu', 'leaky-paywall'),
4227 'country' => '',
4228 ),
4229 'WST' => array(
4230 'symbol' => 'WS$',
4231 'label' => __('Samoan tala', 'leaky-paywall'),
4232 'country' => '',
4233 ),
4234 'XAF' => array(
4235 'symbol' => 'CFA',
4236 'label' => __('Central African CFA franc', 'leaky-paywall'),
4237 'country' => '',
4238 ),
4239 'XCD' => array(
4240 'symbol' => 'EC$',
4241 'label' => __('East Caribbean dollar', 'leaky-paywall'),
4242 'country' => '',
4243 ),
4244 'XOF' => array(
4245 'symbol' => 'CFA',
4246 'label' => __('West African CFA franc', 'leaky-paywall'),
4247 'country' => '',
4248 ),
4249 'XPF' => array(
4250 'symbol' => 'F',
4251 'label' => __('CFP franc', 'leaky-paywall'),
4252 'country' => '',
4253 ),
4254 'YER' => array(
4255 'symbol' => 'YER',
4256 'label' => __('Yemeni rial', 'leaky-paywall'),
4257 'country' => '',
4258 ),
4259 'ZAR' => array(
4260 'symbol' => 'R',
4261 'label' => __('South African rand', 'leaky-paywall'),
4262 'country' => '',
4263 ),
4264 'ZMW' => array(
4265 'symbol' => 'ZK',
4266 'label' => __('Zambian kwacha', 'leaky-paywall'),
4267 'country' => '',
4268 ),
4269 );
4270
4271 return apply_filters('leaky_paywall_supported_currencies', $currencies);
4272 }
4273
4274 if (!function_exists('zeen101_dot_com_leaky_rss_feed_check')) {
4275
4276 /**
4277 * Check leakypaywall.com for new RSS items in the leaky blast feed, to update users of latest Leaky Paywall news
4278 *
4279 * @since 1.1.1
4280 */
4281 function zeen101_dot_com_leaky_rss_feed_check()
4282 {
4283
4284 include_once ABSPATH . WPINC . '/feed.php';
4285
4286 $output = '';
4287 $feedurl = 'https://leakypaywall.com/feed/?post_type=blast&target=leaky-paywall';
4288
4289 $rss = fetch_feed($feedurl);
4290
4291 if ($rss && !is_wp_error($rss)) {
4292
4293 $rss_items = $rss->get_items(0, 1);
4294
4295 foreach ($rss_items as $item) {
4296
4297 $last_rss_item = get_option('last_zeen101_dot_com_leaky_rss_item');
4298
4299 $latest_rss_item = $item->get_content();
4300
4301 if ($last_rss_item !== $latest_rss_item) {
4302
4303 $current_user = wp_get_current_user();
4304
4305 update_option('last_zeen101_dot_com_leaky_rss_item', $latest_rss_item);
4306
4307 update_user_meta($current_user->ID, 'leaky_paywall_rss_item_notice_link', 0);
4308 }
4309 }
4310 }
4311 }
4312 add_action('zeen101_dot_com_leaky_rss_feed_check', 'zeen101_dot_com_leaky_rss_feed_check');
4313
4314 if (!wp_next_scheduled('zeen101_dot_com_leaky_rss_feed_check')) {
4315 wp_schedule_event(time(), 'daily', 'zeen101_dot_com_leaky_rss_feed_check');
4316 }
4317 }
4318
4319
4320 if (!function_exists('object_to_array')) {
4321 /**
4322 * Helper function to convert object to array
4323 *
4324 * @since 3.7.0
4325 * @param object $object An object.
4326 * @return array
4327 */
4328 function object_to_array($object)
4329 {
4330
4331 if (!is_object($object) && !is_array($object)) {
4332 return $object;
4333 }
4334
4335 return array_map('objectToArray', (array) $object);
4336 }
4337 }
4338
4339 /**
4340 * Allow csv files to be uploaded via the media uploader
4341 *
4342 * @param array $existing_mimes Existing mimes.
4343 * @since 3.7.1
4344 */
4345 function leaky_paywall_upload_mimes($existing_mimes = array())
4346 {
4347 $existing_mimes['csv'] = 'text/csv';
4348 return $existing_mimes;
4349 }
4350 add_filter('upload_mimes', 'leaky_paywall_upload_mimes');
4351
4352
4353 /**
4354 * Convert csv file to array
4355 *
4356 * @param string $filename csv file name.
4357 * @param string $delimiter separator for data fields.
4358 * @return array array of data from csv
4359 */
4360 function leaky_paywall_csv_to_array($filename = '', $delimiter = ',')
4361 {
4362
4363 if (!file_exists($filename) || !is_readable($filename)) {
4364 return;
4365 }
4366
4367 $header = null;
4368 $data = array();
4369 $handle = fopen($filename, 'r');
4370
4371 if (false !== $handle) {
4372 $row = fgetcsv($handle, 1000, $delimiter);
4373
4374 while (false !== $row) {
4375
4376 if (!$header) {
4377 $header = $row;
4378 } else {
4379 $data[] = array_combine($header, $row);
4380 }
4381 }
4382 fclose($handle);
4383 }
4384 return $data;
4385 }
4386
4387 /**
4388 * Get the old form input value
4389 *
4390 * @param string $input name of input.
4391 * @param bool $echo Whether to echo value.
4392 * @return string
4393 */
4394 function leaky_paywall_old_form_value($input, $echo = true)
4395 {
4396
4397 $value = '';
4398
4399 if (isset($_POST[$input]) && sanitize_text_field(wp_unslash($_POST[$input]))) {
4400 $value = sanitize_text_field(wp_unslash($_POST[$input]));
4401 }
4402
4403 if ($echo) {
4404 echo esc_attr($value);
4405 } else {
4406 return $value;
4407 }
4408 }
4409
4410 /**
4411 * Get the current site's selected currency symbol
4412 *
4413 * @since 4.5.2
4414 * @return string
4415 */
4416 function leaky_paywall_get_current_currency_symbol()
4417 {
4418
4419 $currency = leaky_paywall_get_currency();
4420 $currencies = leaky_paywall_supported_currencies();
4421
4422 return $currencies[$currency]['symbol'];
4423 }
4424
4425 /**
4426 * Check if the current registration has an amount equal to zero (and thus free)
4427 *
4428 * @since 4.7.1
4429 * @param array $meta Registration data.
4430 * @return bool
4431 */
4432 function leaky_paywall_is_free_registration($meta)
4433 {
4434
4435 if ($meta['price'] > 0) {
4436 $is_free = false;
4437 } else {
4438 $is_free = true;
4439 }
4440
4441 return apply_filters('leaky_paywall_is_free_registration', $is_free, $meta);
4442 }
4443
4444 /**
4445 * Determine if the current subscriber can view the content
4446 *
4447 * @since 4.7.1
4448 * @return bool
4449 */
4450 function leaky_paywall_subscriber_can_view()
4451 {
4452
4453 $restricted = new Leaky_Paywall_Restrictions();
4454 return $restricted->subscriber_can_view();
4455 }
4456
4457 /**
4458 * Log Leaky Paywall events and data to a file
4459 *
4460 * @param array|object $data the data to store.
4461 * @param string $event name of event.
4462 *
4463 * @since 4.7.1
4464 * @return void
4465 */
4466 function leaky_paywall_log($data, $event)
4467 {
4468
4469 leaky_paywall_debug_log($event . ' | ' . wp_json_encode(leaky_paywall_scrub_log_data($data)));
4470 }
4471
4472 /**
4473 * Log a Leaky Paywall error to a file
4474 *
4475 * Errors are written whether or not debug mode is on, so a publisher who has
4476 * never enabled logging still has something to send us when checkout or a
4477 * webhook fails. Use leaky_paywall_log() for anything on a success path.
4478 *
4479 * @param array|object $data the data to store.
4480 * @param string $event name of event.
4481 *
4482 * @since 5.1.8
4483 * @return void
4484 */
4485 function leaky_paywall_log_error($data, $event)
4486 {
4487
4488 leaky_paywall_debug_log('ERROR | ' . $event . ' | ' . wp_json_encode(leaky_paywall_scrub_log_data($data)), true);
4489 }
4490
4491 /**
4492 * Remove credentials from data on its way to the log
4493 *
4494 * Registration payloads carry the subscriber's chosen password, and the
4495 * arrays and serialized form strings logged around checkout failures carry
4496 * it with them. Nothing in a log needs it.
4497 *
4498 * @param mixed $data the data about to be logged.
4499 *
4500 * @since 5.1.8
4501 * @return mixed
4502 */
4503 function leaky_paywall_scrub_log_data($data)
4504 {
4505
4506 $keys = apply_filters(
4507 'leaky_paywall_log_scrubbed_keys',
4508 array('password', 'confirm_password', 'user_pass', 'pass', 'pwd')
4509 );
4510
4511 if (is_object($data)) {
4512 $data = clone $data;
4513
4514 foreach ($keys as $key) {
4515 if (isset($data->$key)) {
4516 $data->$key = '[redacted]';
4517 }
4518 }
4519
4520 return $data;
4521 }
4522
4523 if (is_array($data)) {
4524 foreach ($keys as $key) {
4525 if (isset($data[$key])) {
4526 $data[$key] = '[redacted]';
4527 }
4528 }
4529
4530 return $data;
4531 }
4532
4533 // Serialized form strings, e.g. the checkout form posted as formData.
4534 if (is_string($data) && false !== strpos($data, '=')) {
4535 foreach ($keys as $key) {
4536 $data = preg_replace('/(^|&)' . preg_quote($key, '/') . '=[^&]*/', '$1' . $key . '=[redacted]', $data);
4537 }
4538 }
4539
4540 return $data;
4541 }
4542
4543 /**
4544 * Show Leaky Paywall profile fields on user
4545 *
4546 * @param object $user The user object.
4547 */
4548 function leaky_paywall_show_extra_profile_fields($user)
4549 {
4550
4551 $mode = leaky_paywall_get_current_mode();
4552 $site = leaky_paywall_get_current_site();
4553
4554 $level_id = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_level_id' . $site, true);
4555
4556 if (!is_numeric($level_id)) {
4557 return;
4558 }
4559
4560 $level = get_leaky_paywall_subscription_level($level_id);
4561
4562 // A stored level_id can outlive the level itself: a deleted level, or an
4563 // id that arrived with an import. get_leaky_paywall_subscription_level()
4564 // returns false in that case. Keep rendering the rest of the record,
4565 // because the admin needs to see it in order to correct the level.
4566 if (is_array($level) && isset($level['label'])) {
4567 $description = $level['label'];
4568 } else {
4569 $description = sprintf(
4570 /* translators: %s: the subscription level id stored on the subscriber */
4571 __('Level %s no longer exists', 'leaky-paywall'),
4572 $level_id
4573 );
4574 }
4575
4576 $gateway = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_gateway' . $site, true);
4577 $status = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_payment_status' . $site, true);
4578 $expires = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_expires' . $site, true);
4579 $plan = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_plan' . $site, true);
4580 $subscriber_id = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_subscriber_id' . $site, true);
4581 $subscriber_notes = get_user_meta($user->ID, '_leaky_paywall_subscriber_notes', true);
4582 $renewal_emailed = get_user_meta($user->ID, '_issuem_leaky_paywall_' . $mode . '_renewal_emailed' . $site, true);
4583
4584 leaky_paywall_sync_stripe_subscription($user);
4585
4586 ?>
4587
4588 <h3>Leaky Paywall</h3>
4589
4590 <table class="form-table">
4591
4592 <tr>
4593 <th><label for="level_id">Level ID</label></th>
4594
4595 <td>
4596 <?php echo esc_html($level_id); ?>
4597
4598 </td>
4599 </tr>
4600
4601 <tr>
4602 <th><label for="level_description">Level Description</label></th>
4603
4604 <td>
4605 <?php echo esc_html($description); ?>
4606
4607 </td>
4608 </tr>
4609
4610 <tr>
4611 <th><label for="payment_gateway">Payment Gateway</label></th>
4612
4613 <td>
4614 <?php echo esc_html($gateway); ?>
4615
4616 </td>
4617 </tr>
4618
4619 <tr>
4620 <th><label for="payment_status">Payment Status</label></th>
4621
4622 <td>
4623 <?php echo esc_html($status); ?>
4624
4625 </td>
4626 </tr>
4627
4628 <tr>
4629 <th><label for="expires">Expires</label></th>
4630
4631 <td>
4632 <?php echo esc_html($expires); ?>
4633
4634 </td>
4635 </tr>
4636
4637 <?php
4638 if ($plan) {
4639 ?>
4640 <tr>
4641 <th><label for="plan">Plan</label></th>
4642
4643 <td>
4644 <?php echo esc_html($plan); ?>
4645
4646 </td>
4647 </tr>
4648 <?php
4649 }
4650 ?>
4651
4652
4653 <?php
4654 if ($subscriber_id) {
4655 ?>
4656 <tr>
4657 <th><label for="subscriber_id">Subscriber ID</label></th>
4658
4659 <td>
4660 <?php echo esc_html($subscriber_id); ?>
4661
4662 </td>
4663 </tr>
4664 <?php
4665 }
4666 ?>
4667
4668 <?php
4669 if ($subscriber_notes) {
4670 ?>
4671 <tr>
4672 <th><label for="subscriber_notes">Subscriber Notes</label></th>
4673
4674 <td>
4675 <?php echo esc_html($subscriber_notes); ?>
4676
4677 </td>
4678 </tr>
4679 <?php
4680 }
4681 ?>
4682
4683 <?php
4684 if ($renewal_emailed) {
4685 ?>
4686 <tr>
4687 <th><label for="subscriber_notes">Renewal Reminder Email Sent</label></th>
4688
4689 <td>
4690 <?php echo esc_html($renewal_emailed); ?>
4691
4692 </td>
4693 </tr>
4694 <?php
4695 } ?>
4696
4697 <?php do_action('leaky_paywall_after_wp_user_profile_fields', $user); ?>
4698
4699 </table>
4700 <?php
4701 }
4702 add_action('show_user_profile', 'leaky_paywall_show_extra_profile_fields');
4703 add_action('edit_user_profile', 'leaky_paywall_show_extra_profile_fields');
4704
4705 /**
4706 * Add settings link to plugin table for Leaky Paywall
4707 *
4708 * @since 4.10.4
4709 * @param array $links default plugin links.
4710 * @return array $links
4711 */
4712 function leaky_paywall_plugin_add_settings_link($links)
4713 {
4714 $action_link = '';
4715
4716 if ( ! leaky_paywall_is_pro() ) {
4717 $action_link .= '<span class="lp-pro-upgrade"><a target="_blank" href="https://leakypaywall.com/upgrade-to-leaky-paywall-pro/?utm_source=WordPress&utm_medium=all-plugins&utm_content=upgrade-to-pro&utm_campaign=lp">' . __('Upgrade to Pro') . '</a></span> | ';
4718 }
4719
4720 $action_link .= '<a href="admin.php?page=leaky-paywall-settings">' . __('Settings') . '</a>';
4721 $action_link .= ' | <a href="admin.php?page=leaky-paywall-license">' . __('License', 'leaky-paywall') . '</a>';
4722 array_unshift($links, $action_link);
4723 return $links;
4724 }
4725 add_filter('plugin_action_links_' . LEAKY_PAYWALL_BASENAME, 'leaky_paywall_plugin_add_settings_link');
4726
4727 /**
4728 * Plugin row meta links for add ons
4729 *
4730 * @since 4.10.4
4731 * @param array $input already defined meta links.
4732 * @param string $file plugin file path and name being processed.
4733 * @return array $input
4734 */
4735 function leaky_paywall_plugin_row_meta($input, $file)
4736 {
4737
4738 if ('leaky-paywall/leaky-paywall.php' !== $file) {
4739 return $input;
4740 }
4741
4742 $lp_link = esc_url(
4743 add_query_arg(
4744 array(
4745 'utm_source' => 'plugins-page',
4746 'utm_medium' => 'plugin-row',
4747 'utm_campaign' => 'admin',
4748 ),
4749 'https://leakypaywall.com/downloads/category/leaky-paywall-addons/'
4750 )
4751 );
4752
4753 $links = array(
4754 '<a href="' . $lp_link . '">' . esc_html__('Extensions', 'leaky-paywall') . '</a>',
4755 );
4756
4757 $input = array_merge($input, $links);
4758
4759 return $input;
4760 }
4761 add_filter('plugin_row_meta', 'leaky_paywall_plugin_row_meta', 10, 2);
4762
4763 /**
4764 * Maybe delete user
4765 */
4766 function leaky_paywall_maybe_delete_user()
4767 {
4768
4769 if (!isset($_POST['leaky-paywall-delete-account-nonce'])) {
4770 return;
4771 }
4772
4773 if (!wp_verify_nonce(sanitize_key(wp_unslash($_POST['leaky-paywall-delete-account-nonce'])), 'leaky-paywall-delete-account')) {
4774 return;
4775 }
4776
4777 $settings = get_leaky_paywall_settings();
4778
4779 require_once ABSPATH . 'wp-admin/includes/user.php';
4780
4781 $user = wp_get_current_user();
4782
4783 if (in_array('subscriber', $user->roles, true)) {
4784
4785 $stripe_cancel_note = '';
4786 $cancelled_subs = array();
4787 $checked_stripe = false;
4788 $mode = leaky_paywall_get_current_mode();
4789 $site = leaky_paywall_get_current_site();
4790 $subscriber_id = get_user_meta( $user->ID, '_issuem_leaky_paywall_' . $mode . '_subscriber_id' . $site, true );
4791
4792 if ( $subscriber_id && strpos( $subscriber_id, 'cus_' ) === 0 ) {
4793 $checked_stripe = true;
4794 $stripe = leaky_paywall_initialize_stripe_api();
4795 try {
4796 $subs = $stripe->subscriptions->all(
4797 array(
4798 'customer' => $subscriber_id,
4799 'status' => 'all',
4800 ),
4801 leaky_paywall_get_stripe_connect_params()
4802 );
4803 $cancelable = array( 'active', 'trialing', 'past_due', 'incomplete' );
4804 foreach ( $subs->data as $sub ) {
4805 if ( ! in_array( $sub->status, $cancelable, true ) ) {
4806 continue;
4807 }
4808 $stripe->subscriptions->cancel( $sub->id, array(), leaky_paywall_get_stripe_connect_params() );
4809 $cancelled_subs[] = $sub->id;
4810 leaky_paywall_log( $user->user_email, 'Stripe subscription ' . $sub->id . ' cancelled on account deletion' );
4811 }
4812 } catch ( \Stripe\Exception\ApiErrorException $e ) {
4813 $stripe_cancel_note .= '<p><strong>Note:</strong> An error occurred while cancelling the Stripe subscription: ' . esc_html( $e->getMessage() ) . '</p>';
4814 leaky_paywall_log_error( $user->user_email . ' (user ' . $user->ID . ')', 'Stripe cancellation error on account deletion: ' . $e->getMessage() );
4815 }
4816 }
4817
4818 wp_delete_user($user->ID);
4819
4820 do_action('leaky_paywall_after_user_deleted', $user);
4821
4822 $admin_message = '';
4823 $headers = array();
4824
4825 $admin_emails = array();
4826 $admin_emails = get_option('admin_email');
4827
4828 $site_name = stripslashes_deep(html_entity_decode(get_bloginfo('name'), ENT_COMPAT, 'UTF-8'));
4829 $from_name = isset($settings['from_name']) ? $settings['from_name'] : $site_name;
4830 $from_email = isset($settings['from_email']) ? $settings['from_email'] : get_option('admin_email');
4831
4832 $headers[] = 'From: ' . stripslashes_deep(html_entity_decode($from_name, ENT_COMPAT, 'UTF-8')) . " <$from_email>";
4833 $headers[] = 'Reply-To: ' . $from_email;
4834 $headers[] = 'Content-Type: text/html; charset=UTF-8';
4835
4836 if ( $cancelled_subs ) {
4837 $stripe_cancel_note = '<p>' . esc_html(
4838 sprintf(
4839 /* translators: %s: comma-separated list of Stripe subscription IDs */
4840 _n(
4841 'Stripe subscription %s was cancelled.',
4842 'Stripe subscriptions %s were cancelled.',
4843 count( $cancelled_subs ),
4844 'leaky-paywall'
4845 ),
4846 implode( ', ', $cancelled_subs )
4847 )
4848 ) . '</p>' . $stripe_cancel_note;
4849 } elseif ( $checked_stripe && ! $stripe_cancel_note ) {
4850 $stripe_cancel_note = '<p>' . esc_html__( 'They had no active Stripe subscription to cancel.', 'leaky-paywall' ) . '</p>';
4851 }
4852
4853 $admin_message = '<p>The user ' . $user->user_email . ' has deleted their account.</p>' . $stripe_cancel_note;
4854
4855 /* Translators: %s - site name */
4856 wp_mail($admin_emails, sprintf(esc_attr__('User Account Deleted on %s', 'leaky-paywall'), $site_name), $admin_message, $headers);
4857
4858 wp_die(wp_kses_post('<p>Your account has been deleted. Your access and information has been removed.</p><p><a href="' . home_url() . '">Continue</a></p>'), 'Account Deleted');
4859 }
4860
4861 wp_die(wp_kses_post('<p>Your user role cannot be deleted from the My Account page. Please contact a site administrator.</p><p><a href="' . home_url() . '">Continue</a></p>'), 'Account Deleted');
4862 }
4863 add_action('init', 'leaky_paywall_maybe_delete_user');
4864
4865
4866 /**
4867 * Get level display price
4868 *
4869 * @param array $level Leaky Paywall level.
4870 */
4871 function leaky_paywall_get_level_display_price($level)
4872 {
4873
4874 $price = $level['price'];
4875 $display_price = leaky_paywall_format_display_price($price);
4876
4877 return apply_filters('leaky_paywall_display_price', $display_price, $level);
4878 }
4879
4880 function leaky_paywall_format_display_price($price)
4881 {
4882
4883 $settings = get_leaky_paywall_settings();
4884
4885 $currency_position = $settings['leaky_paywall_currency_position'];
4886 $thousand_separator = $settings['leaky_paywall_thousand_separator'];
4887 $decimal_separator = $settings['leaky_paywall_decimal_separator'];
4888 $decimal_number = empty($settings['leaky_paywall_decimal_number']) ? '0' : $settings['leaky_paywall_decimal_number'];
4889 $currency_symbol = leaky_paywall_get_current_currency_symbol();
4890
4891 $broken_price = explode('.', $price);
4892
4893 $before_decimal = $broken_price[0];
4894 $after_decimal = substr(isset($broken_price[1]) ? $broken_price[1] : '', 0, $decimal_number);
4895
4896 if (!$after_decimal && 2 === $decimal_number) {
4897 $after_decimal = '00';
4898 }
4899
4900 if ($price > 0) {
4901
4902 $decimal = $after_decimal ? $decimal_separator : '';
4903
4904 if ($before_decimal) {
4905 $formatted_number = number_format($before_decimal, 0, '', $thousand_separator) . $decimal . $after_decimal;
4906 } else {
4907 $formatted_number = '0' . $decimal . $after_decimal; // less than $1
4908 }
4909
4910 switch ($currency_position) {
4911 case 'left':
4912 $display_price = $currency_symbol . $formatted_number;
4913 break;
4914 case 'right':
4915 $display_price = $formatted_number . $currency_symbol;
4916 break;
4917 case 'left_space':
4918 $display_price = $currency_symbol . ' ' . $formatted_number;
4919 break;
4920 case 'right_space':
4921 $display_price = $formatted_number . ' ' . $currency_symbol;
4922 break;
4923 default:
4924 $display_price = $currency_symbol . $formatted_number;
4925 break;
4926 }
4927 } else {
4928 $display_price = __('Free', 'leaky-paywall');
4929 }
4930
4931 return $display_price;
4932 }
4933
4934
4935 /**
4936 * Replace language-specific characters by ASCII-equivalents.
4937 *
4938 * @param string $s Initial string.
4939 * @return string
4940 */
4941 function leaky_paywall_normalize_chars($s)
4942 {
4943 $replace = array(
4944 'ъ' => '-',
4945 'Ь' => '-',
4946 'Ъ' => '-',
4947 'ь' => '-',
4948 'Ă' => 'A',
4949 'Ą' => 'A',
4950 'À' => 'A',
4951 'Ã' => 'A',
4952 'Á' => 'A',
4953 'Æ' => 'A',
4954 'Â' => 'A',
4955 '�
4956 ' => 'A',
4957 'Ä' => 'Ae',
4958 'Þ' => 'B',
4959 'Ć' => 'C',
4960 'ץ' => 'C',
4961 'Ç' => 'C',
4962 'È' => 'E',
4963 'Ę' => 'E',
4964 'É' => 'E',
4965 'Ë' => 'E',
4966 'Ê' => 'E',
4967 'Ğ' => 'G',
4968 'İ' => 'I',
4969 'Ï' => 'I',
4970 'Î' => 'I',
4971 'Í' => 'I',
4972 'Ì' => 'I',
4973 'Ł' => 'L',
4974 'Ñ' => 'N',
4975 'Ń' => 'N',
4976 'Ø' => 'O',
4977 'Ó' => 'O',
4978 'Ò' => 'O',
4979 'Ô' => 'O',
4980 'Õ' => 'O',
4981 'Ö' => 'Oe',
4982 'Ş' => 'S',
4983 'Ś' => 'S',
4984 'Ș' => 'S',
4985 'Š' => 'S',
4986 'Ț' => 'T',
4987 'Ù' => 'U',
4988 'Û' => 'U',
4989 'Ú' => 'U',
4990 'Ü' => 'Ue',
4991 'Ý' => 'Y',
4992 'Ź' => 'Z',
4993 'Ž' => 'Z',
4994 'Ż' => 'Z',
4995 'â' => 'a',
4996 'ǎ' => 'a',
4997 '�
4998 ' => 'a',
4999 'á' => 'a',
5000 'ă' => 'a',
5001 'ã' => 'a',
5002 'Ǎ' => 'a',
5003 'а' => 'a',
5004 'А' => 'a',
5005 'å' => 'a',
5006 'à' => 'a',
5007 'א' => 'a',
5008 'Ǻ' => 'a',
5009 'Ā' => 'a',
5010 'ǻ' => 'a',
5011 'ā' => 'a',
5012 'ä' => 'ae',
5013 'æ' => 'ae',
5014 'Ǽ' => 'ae',
5015 'ǽ' => 'ae',
5016 'б' => 'b',
5017 'ב' => 'b',
5018 'Б' => 'b',
5019 'þ' => 'b',
5020 'ĉ' => 'c',
5021 'Ĉ' => 'c',
5022 'Ċ' => 'c',
5023 'ć' => 'c',
5024 'ç' => 'c',
5025 'ц' => 'c',
5026 'צ' => 'c',
5027 'ċ' => 'c',
5028 'Ц' => 'c',
5029 'Č' => 'c',
5030 'č' => 'c',
5031 'Ч' => 'ch',
5032 'ч' => 'ch',
5033 'ד' => 'd',
5034 'ď' => 'd',
5035 'Đ' => 'd',
5036 'Ď' => 'd',
5037 'đ' => 'd',
5038 'д' => 'd',
5039 'Д' => 'D',
5040 'ð' => 'd',
5041 'є' => 'e',
5042 'ע' => 'e',
5043 'е' => 'e',
5044 'Е' => 'e',
5045 'Ə' => 'e',
5046 'ę' => 'e',
5047 'ĕ' => 'e',
5048 'ē' => 'e',
5049 'Ē' => 'e',
5050 'Ė' => 'e',
5051 'ė' => 'e',
5052 'ě' => 'e',
5053 'Ě' => 'e',
5054 'Є' => 'e',
5055 'Ĕ' => 'e',
5056 'ê' => 'e',
5057 'ə' => 'e',
5058 'è' => 'e',
5059 'ë' => 'e',
5060 'é' => 'e',
5061 'ф' => 'f',
5062 'ƒ' => 'f',
5063 'Ф' => 'f',
5064 'ġ' => 'g',
5065 'Ģ' => 'g',
5066 'Ġ' => 'g',
5067 'Ĝ' => 'g',
5068 'Г' => 'g',
5069 'г' => 'g',
5070 'ĝ' => 'g',
5071 'ğ' => 'g',
5072 'ג' => 'g',
5073 'Ґ' => 'g',
5074 'ґ' => 'g',
5075 'ģ' => 'g',
5076 'ח' => 'h',
5077 'ħ' => 'h',
5078 'Х' => 'h',
5079 'Ħ' => 'h',
5080 'Ĥ' => 'h',
5081 'ĥ' => 'h',
5082 '�
5083 ' => 'h',
5084 'ה' => 'h',
5085 'î' => 'i',
5086 'ï' => 'i',
5087 'í' => 'i',
5088 'ì' => 'i',
5089 'į' => 'i',
5090 'ĭ' => 'i',
5091 'ı' => 'i',
5092 'Ĭ' => 'i',
5093 'И' => 'i',
5094 'ĩ' => 'i',
5095 'ǐ' => 'i',
5096 'Ĩ' => 'i',
5097 'Ǐ' => 'i',
5098 'и' => 'i',
5099 'Į' => 'i',
5100 'י' => 'i',
5101 'Ї' => 'i',
5102 'Ī' => 'i',
5103 'І' => 'i',
5104 'ї' => 'i',
5105 'і' => 'i',
5106 'ī' => 'i',
5107 'ij' => 'ij',
5108 'IJ' => 'ij',
5109 'й' => 'j',
5110 'Й' => 'j',
5111 'Ĵ' => 'j',
5112 'ĵ' => 'j',
5113 'я' => 'ja',
5114 'Я' => 'ja',
5115 'Э' => 'je',
5116 'э' => 'je',
5117 'ё' => 'jo',
5118 'Ё' => 'jo',
5119 'ю' => 'ju',
5120 'Ю' => 'ju',
5121 'ĸ' => 'k',
5122 'כ' => 'k',
5123 'Ķ' => 'k',
5124 'К' => 'k',
5125 'к' => 'k',
5126 'ķ' => 'k',
5127 'ך' => 'k',
5128 'Ŀ' => 'l',
5129 'ŀ' => 'l',
5130 'Л' => 'l',
5131 'ł' => 'l',
5132 'ļ' => 'l',
5133 'ĺ' => 'l',
5134 'Ĺ' => 'l',
5135 'Ļ' => 'l',
5136 'л' => 'l',
5137 'Ľ' => 'l',
5138 'ľ' => 'l',
5139 'ל' => 'l',
5140 'מ' => 'm',
5141 'М' => 'm',
5142 'ם' => 'm',
5143 'м' => 'm',
5144 'ñ' => 'n',
5145 'н' => 'n',
5146 '�
5147 ' => 'n',
5148 'ן' => 'n',
5149 'ŋ' => 'n',
5150 'נ' => 'n',
5151 'Н' => 'n',
5152 'ń' => 'n',
5153 'Ŋ' => 'n',
5154 'ņ' => 'n',
5155 'ʼn' => 'n',
5156 'Ň' => 'n',
5157 'ň' => 'n',
5158 'о' => 'o',
5159 'О' => 'o',
5160 'ő' => 'o',
5161 'õ' => 'o',
5162 'ô' => 'o',
5163 'Ő' => 'o',
5164 'ŏ' => 'o',
5165 'Ŏ' => 'o',
5166 'Ō' => 'o',
5167 'ō' => 'o',
5168 'ø' => 'o',
5169 'ǿ' => 'o',
5170 'ǒ' => 'o',
5171 'ò' => 'o',
5172 'Ǿ' => 'o',
5173 'Ǒ' => 'o',
5174 'ơ' => 'o',
5175 'ó' => 'o',
5176 'Ơ' => 'o',
5177 'œ' => 'oe',
5178 'Œ' => 'oe',
5179 'ö' => 'oe',
5180 'פ' => 'p',
5181 'ף' => 'p',
5182 'п' => 'p',
5183 'П' => 'p',
5184 'ק' => 'q',
5185 'ŕ' => 'r',
5186 'ř' => 'r',
5187 'Ř' => 'r',
5188 'ŗ' => 'r',
5189 'Ŗ' => 'r',
5190 'ר' => 'r',
5191 'Ŕ' => 'r',
5192 'Р' => 'r',
5193 'р' => 'r',
5194 'ș' => 's',
5195 'с' => 's',
5196 'Ŝ' => 's',
5197 'š' => 's',
5198 'ś' => 's',
5199 'ס' => 's',
5200 'ş' => 's',
5201 'С' => 's',
5202 'ŝ' => 's',
5203 'Щ' => 'sch',
5204 'щ' => 'sch',
5205 'ш' => 'sh',
5206 'Ш' => 'sh',
5207 'ß' => 'ss',
5208 'т' => 't',
5209 'ט' => 't',
5210 'ŧ' => 't',
5211 'ת' => 't',
5212 'ť' => 't',
5213 'ţ' => 't',
5214 'Ţ' => 't',
5215 'Т' => 't',
5216 'ț' => 't',
5217 'Ŧ' => 't',
5218 'Ť' => 't',
5219 '' => 'tm',
5220 'ū' => 'u',
5221 'у' => 'u',
5222 'Ũ' => 'u',
5223 'ũ' => 'u',
5224 'Ư' => 'u',
5225 'ư' => 'u',
5226 'Ū' => 'u',
5227 'Ǔ' => 'u',
5228 'ų' => 'u',
5229 'Ų' => 'u',
5230 'ŭ' => 'u',
5231 'Ŭ' => 'u',
5232 'Ů' => 'u',
5233 'ů' => 'u',
5234 'ű' => 'u',
5235 'Ű' => 'u',
5236 'Ǖ' => 'u',
5237 'ǔ' => 'u',
5238 'Ǜ' => 'u',
5239 'ù' => 'u',
5240 'ú' => 'u',
5241 'û' => 'u',
5242 'У' => 'u',
5243 'ǚ' => 'u',
5244 'ǜ' => 'u',
5245 'Ǚ' => 'u',
5246 'Ǘ' => 'u',
5247 'ǖ' => 'u',
5248 'ǘ' => 'u',
5249 'ü' => 'ue',
5250 'в' => 'v',
5251 'ו' => 'v',
5252 'В' => 'v',
5253 'ש' => 'w',
5254 'ŵ' => 'w',
5255 'Ŵ' => 'w',
5256 'ы' => 'y',
5257 'ŷ' => 'y',
5258 'ý' => 'y',
5259 'ÿ' => 'y',
5260 'Ÿ' => 'y',
5261 'Ŷ' => 'y',
5262 'Ы' => 'y',
5263 'ž' => 'z',
5264 'З' => 'z',
5265 'з' => 'z',
5266 'ź' => 'z',
5267 'ז' => 'z',
5268 'ż' => 'z',
5269 'ſ' => 'z',
5270 'Ж' => 'zh',
5271 'ж' => 'zh',
5272 );
5273 return strtr($s, $replace);
5274 }
5275
5276 /**
5277 * Add leaky paywall links to admin toolbar
5278 *
5279 * @param object $admin_bar The admin bar object.
5280 */
5281 function leaky_paywall_add_toolbar_items($admin_bar)
5282 {
5283
5284 if (!current_user_can('edit_user')) {
5285 return;
5286 }
5287
5288 $admin_bar->add_menu(
5289 array(
5290 'id' => 'leaky-paywall-toolbar',
5291 'title' => 'Leaky Paywall',
5292 'href' => admin_url() . 'admin.php?page=issuem-leaky-paywall',
5293 'meta' => array(
5294 'title' => __('Leaky Paywall'),
5295 ),
5296 )
5297 );
5298 $admin_bar->add_menu(
5299 array(
5300 'id' => 'leaky-paywall-toolbar-settings',
5301 'parent' => 'leaky-paywall-toolbar',
5302 'title' => 'Settings',
5303 'href' => admin_url() . 'admin.php?page=leaky-paywall-settings',
5304 'meta' => array(
5305 'title' => __('Settings'),
5306 'target' => '',
5307 'class' => 'my_menu_item_class',
5308 ),
5309 )
5310 );
5311 $admin_bar->add_menu(
5312 array(
5313 'id' => 'leaky-paywall-toolbar-subscribers',
5314 'parent' => 'leaky-paywall-toolbar',
5315 'title' => 'Subscribers',
5316 'href' => admin_url() . 'admin.php?page=leaky-paywall-subscribers',
5317 'meta' => array(
5318 'title' => __('Subscribers'),
5319 'target' => '',
5320 'class' => 'my_menu_item_class',
5321 ),
5322 )
5323 );
5324
5325 $admin_bar->add_menu(
5326 array(
5327 'id' => 'leaky-paywall-toolbar-transactions',
5328 'parent' => 'leaky-paywall-toolbar',
5329 'title' => 'Transactions',
5330 'href' => admin_url() . 'edit.php?post_type=lp_transaction',
5331 'meta' => array(
5332 'title' => __('Transactions'),
5333 'target' => '',
5334 'class' => 'my_menu_item_class',
5335 ),
5336 )
5337 );
5338
5339 $admin_bar->add_menu(
5340 array(
5341 'id' => 'leaky-paywall-toolbar-tools',
5342 'parent' => 'leaky-paywall-toolbar',
5343 'title' => 'Tools',
5344 'href' => admin_url( 'admin.php?page=leaky-paywall-tools' ),
5345 'meta' => array(
5346 'title' => __('Tools'),
5347 'target' => '',
5348 'class' => 'my_menu_item_class',
5349 ),
5350 )
5351 );
5352
5353 $admin_bar->add_menu(
5354 array(
5355 'id' => 'leaky-paywall-toolbar-extensions',
5356 'parent' => 'leaky-paywall-toolbar',
5357 'title' => 'Extensions',
5358 'href' => admin_url( 'admin.php?page=leaky-paywall-extensions' ),
5359 'meta' => array(
5360 'title' => __('Extensions'),
5361 'target' => '',
5362 'class' => 'my_menu_item_class',
5363 ),
5364 )
5365 );
5366
5367 }
5368 add_action('admin_bar_menu', 'leaky_paywall_add_toolbar_items', 100);
5369
5370 /**
5371 * Display Leaky Paywall rate us notice
5372 */
5373 function leaky_paywall_display_rate_us_notice()
5374 {
5375
5376 $notice_id = 'lp_rate_us_feedback';
5377
5378 if (!current_user_can('manage_options')) {
5379 return;
5380 }
5381
5382 $current_user_has_viewed = get_user_meta(get_current_user_id(), $notice_id, true);
5383
5384 if ('dashboard' !== get_current_screen()->id || $current_user_has_viewed) {
5385 return;
5386 }
5387
5388 $site = leaky_paywall_get_current_site();
5389
5390 $args = array(
5391 'number' => 11,
5392 'meta_query' => array(
5393 array(
5394 'key' => '_issuem_leaky_paywall_live_level_id' . $site,
5395 'compare' => 'EXISTS',
5396 ),
5397 ),
5398 );
5399
5400 $wp_user_search = new WP_User_Query($args);
5401 $total_live_subscribers = count($wp_user_search->get_results());
5402
5403 if (100 >= $total_live_subscribers) {
5404 return;
5405 }
5406
5407 $dismiss_url = add_query_arg(
5408 array(
5409 'action' => 'leaky_paywall_set_admin_notice_viewed',
5410 'nonce' => wp_create_nonce('leaky-paywall-admin-notice-nonce'),
5411 'notice_id' => esc_attr($notice_id),
5412 ),
5413 admin_url()
5414 );
5415
5416 ?>
5417 <div class="notice updated is-dismissible leaky-paywall-message leaky-paywall-message-dismissed" data-notice_id="<?php echo esc_attr($notice_id); ?>">
5418 <div class="leaky-paywall-message-inner">
5419
5420 <div class="leaky-paywall-message-content">
5421 <p><strong><?php echo esc_html__('Congrats!', 'leaky-paywall'); ?></strong> 🥳<?php esc_html_e('You have more than 100 subscribers with Leaky Paywall. Please help us by leaving a review on WordPress.org. We read every review and use your feedback to make Leaky Paywall better for everyone!', 'leaky-paywall'); ?></p>
5422 <p class="leaky-paywall-message-actions">
5423 <a href="https://wordpress.org/support/plugin/leaky-paywall/reviews/?filter=5/#new-post" target="_blank" class="button button-primary"><?php esc_html_e('Leave a Review', 'leaky-paywall'); ?></a>
5424 <a href="<?php echo esc_url_raw($dismiss_url); ?>" class="button leaky-paywall-button-notice-dismiss"><?php esc_html_e('Hide', 'leaky-paywall'); ?></a>
5425 </p>
5426 </div>
5427 <div class="leaky-paywall-message-logo">
5428 <img src="<?php echo esc_url(LEAKY_PAYWALL_URL); ?>/images/zeen101-logo.png" alt="ZEEN101" width="100">
5429 </div>
5430 </div>
5431 </div>
5432
5433 <style>
5434 .leaky-paywall-message-inner {
5435 overflow: hidden;
5436 width: 100%;
5437 }
5438
5439 .leaky-paywall-message-inner .leaky-paywall-message-content {
5440 width: 60%;
5441 float: left;
5442 }
5443
5444 .leaky-paywall-message-inner .leaky-paywall-message-logo {
5445 width: 20%;
5446 float: right;
5447 text-align: right;
5448 padding-top: 7px;
5449 padding-bottom: 7px;
5450 }
5451 </style>
5452 <?php
5453 }
5454 add_action('admin_notices', 'leaky_paywall_display_rate_us_notice', 20);
5455
5456 /**
5457 * Update admin notice viewed
5458 */
5459 function leaky_paywall_update_admin_notice_viewed()
5460 {
5461
5462 if (!isset($_GET['nonce'])) {
5463 return;
5464 }
5465
5466 if (!wp_verify_nonce(sanitize_key($_GET['nonce']), 'leaky-paywall-admin-notice-nonce')) {
5467 return;
5468 }
5469
5470 if (!isset($_GET['action'])) {
5471 return;
5472 }
5473
5474 if ('leaky_paywall_set_admin_notice_viewed' !== $_GET['action']) {
5475 return;
5476 }
5477
5478 if (!isset($_GET['notice_id'])) {
5479 return;
5480 }
5481
5482 update_user_meta(get_current_user_id(), sanitize_text_field(wp_unslash($_GET['notice_id'])), true);
5483 }
5484 add_action('admin_init', 'leaky_paywall_update_admin_notice_viewed');
5485
5486 /**
5487 * Get a Transaction ID from an email address
5488 *
5489 * @param string $email The email address.
5490 */
5491 function leaky_paywall_get_transaction_id_from_email($email)
5492 {
5493
5494 $transaction_id = '';
5495
5496 $args = array(
5497 'post_type' => 'lp_transaction',
5498 'number_of_posts' => 1,
5499 'meta_query' => array(
5500 array(
5501 'key' => '_email',
5502 'value' => $email,
5503 'compare' => '=',
5504 ),
5505 ),
5506 );
5507
5508 $transactions = get_posts($args);
5509
5510 if (!empty($transactions)) {
5511 $transaction = $transactions[0];
5512 $transaction_id = $transaction->ID;
5513 }
5514
5515 return $transaction_id;
5516 }
5517
5518 /**
5519 * Sets a Gateway Transaction ID in post meta for the given Transaction ID
5520 *
5521 * @since 4.14.5
5522 * @param int $transaction_id Transaction ID.
5523 * @param string $gateway_transaction_id The transaction ID from the gateway.
5524 */
5525 function leaky_paywall_set_payment_transaction_id($transaction_id, $gateway_transaction_id)
5526 {
5527
5528 update_post_meta($transaction_id, '_gateway_txn_id', $gateway_transaction_id);
5529 }
5530
5531 /**
5532 * Find a transaction by its gateway transaction ID.
5533 *
5534 * @since 4.21.0
5535 *
5536 * @param string $gateway_id The gateway transaction ID (charge ID or payment intent ID).
5537 * @return int Transaction post ID, or 0 if not found.
5538 */
5539 function leaky_paywall_find_transaction_by_gateway_id( $gateway_id ) {
5540 $args = array(
5541 'post_type' => 'lp_transaction',
5542 'posts_per_page' => 1,
5543 'post_status' => 'publish',
5544 'meta_query' => array(
5545 array(
5546 'key' => '_gateway_txn_id',
5547 'value' => $gateway_id,
5548 'compare' => '=',
5549 ),
5550 ),
5551 'fields' => 'ids',
5552 );
5553
5554 $posts = get_posts( $args );
5555
5556 return ! empty( $posts ) ? $posts[0] : 0;
5557 }
5558
5559 /**
5560 * Get the total amount already refunded for a given charge ID.
5561 *
5562 * Looks up existing refund transactions linked to the charge to prevent
5563 * duplicate refund records when Stripe retries webhooks.
5564 *
5565 * @since 4.21.0
5566 *
5567 * @param string $charge_id The Stripe charge ID.
5568 * @return float Total refunded amount (positive number).
5569 */
5570 function leaky_paywall_get_existing_refund_total( $charge_id ) {
5571 global $wpdb;
5572
5573 $total = $wpdb->get_var(
5574 $wpdb->prepare(
5575 "SELECT COALESCE( SUM( ABS( CAST( pm_price.meta_value AS DECIMAL(10,2) ) ) ), 0 )
5576 FROM {$wpdb->posts} p
5577 INNER JOIN {$wpdb->postmeta} pm_price
5578 ON p.ID = pm_price.post_id AND pm_price.meta_key = '_price'
5579 INNER JOIN {$wpdb->postmeta} pm_ref
5580 ON p.ID = pm_ref.post_id AND pm_ref.meta_key = '_refund_charge_id'
5581 WHERE p.post_type = 'lp_transaction'
5582 AND pm_ref.meta_value = %s",
5583 $charge_id
5584 )
5585 );
5586
5587 return (float) $total;
5588 }
5589
5590 /**
5591 * Create a refund transaction with a negative price.
5592 *
5593 * @since 4.21.0
5594 *
5595 * @param int $original_txn_id The original transaction post ID (0 if not found).
5596 * @param object $user The WordPress user object.
5597 * @param string $charge_id The Stripe charge ID.
5598 * @param float $refund_amount The refund amount (positive number).
5599 * @return int The refund transaction post ID.
5600 */
5601 function leaky_paywall_create_refund_transaction( $original_txn_id, $user, $charge_id, $refund_amount ) {
5602 $level_id = '';
5603 $currency = '';
5604
5605 if ( $original_txn_id ) {
5606 $level_id = get_post_meta( $original_txn_id, '_level_id', true );
5607 $currency = get_post_meta( $original_txn_id, '_currency', true );
5608 }
5609
5610 $transaction = array(
5611 'post_title' => 'Refund for ' . $user->user_email,
5612 'post_content' => '',
5613 'post_status' => 'publish',
5614 'post_author' => 1,
5615 'post_type' => 'lp_transaction',
5616 );
5617
5618 $transaction_id = wp_insert_post( $transaction );
5619
5620 update_post_meta( $transaction_id, '_email', $user->user_email );
5621 update_post_meta( $transaction_id, '_first_name', $user->first_name );
5622 update_post_meta( $transaction_id, '_last_name', $user->last_name );
5623 update_post_meta( $transaction_id, '_level_id', $level_id );
5624 update_post_meta( $transaction_id, '_gateway', 'stripe' );
5625 update_post_meta( $transaction_id, '_gateway_txn_id', $charge_id );
5626 update_post_meta( $transaction_id, '_price', '-' . number_format( $refund_amount, 2, '.', '' ) );
5627 update_post_meta( $transaction_id, '_currency', $currency );
5628 update_post_meta( $transaction_id, '_status', 'refund' );
5629 update_post_meta( $transaction_id, '_transaction_status', 'complete' );
5630 update_post_meta( $transaction_id, '_is_recurring', false );
5631 update_post_meta( $transaction_id, '_refund_charge_id', $charge_id );
5632
5633 if ( $original_txn_id ) {
5634 update_post_meta( $transaction_id, '_refund_for', $original_txn_id );
5635 }
5636
5637 do_action( 'leaky_paywall_after_create_refund_transaction', $transaction_id, $original_txn_id, $user );
5638
5639 return $transaction_id;
5640 }
5641
5642
5643 /**
5644 * Check login fail
5645 *
5646 * @param string $username The username.
5647 */
5648 function leaky_paywall_login_fail($username)
5649 {
5650
5651 $settings = get_leaky_paywall_settings();
5652 $referrer = wp_get_referer();
5653 $clean_referrer = str_replace(array('http://', 'https://'), '', $referrer);
5654 $login_link = str_replace(array('http://', 'https://'), '', get_page_link($settings['page_for_login']));
5655 $profile_link = str_replace(array('http://', 'https://'), '', get_page_link($settings['page_for_profile']));
5656
5657 // Only run this check if the user was on the leaky paywall login page or profile page. This keeps it from breaking other login plugins.
5658 if ($clean_referrer !== $login_link && $clean_referrer !== $profile_link) {
5659 return;
5660 }
5661
5662 if (!empty($referrer) && !strstr($referrer, 'wp-login') && !strstr($referrer, 'wp-admin')) {
5663 wp_safe_redirect($referrer . '/?login=failed');
5664 exit();
5665 }
5666 }
5667 add_action('wp_login_failed', 'leaky_paywall_login_fail');
5668
5669 /**
5670 * Gets the IP address of the user.
5671 *
5672 * @return string The IP address of the user.
5673 */
5674 function leaky_paywall_get_ip()
5675 {
5676
5677 if (!empty($_SERVER['HTTP_CLIENT_IP'])) //check ip from share internet
5678 {
5679 $ip = sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
5680 } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) //to check ip is pass from proxy
5681 {
5682
5683 $ip_array = array_values(array_filter(explode(',', sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']))));
5684
5685 if (is_array($ip_array)) {
5686 $ip = $ip_array[0];
5687 } else {
5688 $ip = sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
5689 }
5690 } else {
5691 $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';
5692 }
5693
5694 if (!filter_var($ip, FILTER_VALIDATE_IP)) {
5695 $ip = '';
5696 }
5697
5698 return apply_filters('leaky_paywall_ip_address', $ip);
5699 }
5700
5701
5702
5703 /**
5704 * Load any CSS we need for the plugins list table.
5705 */
5706 function leaky_paywall_plugin_list_styles()
5707 {
5708 echo '<style>span.lp-pro-upgrade a, span.lp-pro-upgrade a:hover{color: #759542; font-weight: 600;}</style>';
5709 }
5710 add_action('admin_print_styles-plugins.php', 'leaky_paywall_plugin_list_styles');
5711
5712 /**
5713 * Allow interval text to be translatable
5714 */
5715 function leaky_paywall_get_interval_text($interval, $interval_count)
5716 {
5717
5718 $interval_text = '';
5719
5720 if ($interval == 'day') {
5721 if ($interval_count > 1) {
5722 $interval_text = esc_html__('days', 'leaky-paywall');
5723 } else {
5724 $interval_text = esc_html__('day', 'leaky-paywall');
5725 }
5726 }
5727
5728 if ($interval == 'week') {
5729 if ($interval_count > 1) {
5730 $interval_text = esc_html__('weeks', 'leaky-paywall');
5731 } else {
5732 $interval_text = esc_html__('week', 'leaky-paywall');
5733 }
5734 }
5735
5736 if ($interval == 'month') {
5737 if ($interval_count > 1) {
5738 $interval_text = esc_html__('months', 'leaky-paywall');
5739 } else {
5740 $interval_text = esc_html__('month', 'leaky-paywall');
5741 }
5742 }
5743
5744 if ($interval == 'year') {
5745 if ($interval_count > 1) {
5746 $interval_text = esc_html__('years', 'leaky-paywall');
5747 } else {
5748 $interval_text = esc_html__('year', 'leaky-paywall');
5749 }
5750 }
5751
5752 return $interval_text;
5753 }
5754