PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / template-details / REST / Checkout.php

Checkout.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/template-details/REST/Checkout.php

145 lines 4.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\TemplateDetails\REST;
4
5 use Templately\API\API;
6
7 /**
8 * Plugin-initiated checkout.
9 *
10 * Asks the backend for a frontend checkout URL carrying a short-lived login
11 * token bound to the connected api_key account (no front-end re-login), and
12 * hands it back to the React app, which redirects the buyer to it. After
13 * payment the frontend returns the buyer to `return_url` with
14 * `templately_purchase`.
15 */
16 class Checkout extends API {
17
18 public function register_routes() {
19 $this->post( 'checkout', [ $this, 'checkout' ] );
20 }
21
22 public function checkout() {
23 $purchase_type = $this->get_param( 'purchase_type', 'template' );
24
25 /**
26 * Buying a template is a content decision any contributor may make, but a
27 * subscription is bought *for the account* and changes what everyone on it
28 * pays. The base gate is `delete_posts`, so without this a Contributor
29 * could start a plan purchase against the site owner's account.
30 *
31 * Gate by what the action TOUCHES, not by which endpoint it lives on —
32 * this one route serves both, so the branch is where the check belongs.
33 * The client mirrors it with `window.templately.can_manage_account`, which
34 * hides the Subscription entry rather than offering a button that 403s.
35 */
36 if ( 'subscription' === $purchase_type && ! current_user_can( 'manage_options' ) ) {
37 return $this->error(
38 'insufficient_permission',
39 __( 'Only administrators can change the subscription.', 'templately' ),
40 'checkout',
41 rest_authorization_required_code()
42 );
43 }
44
45 $id = $this->get_param( 'id', 0, 'intval' );
46 $return_url = $this->get_param( 'return_url', admin_url( 'admin.php?page=templately' ), 'esc_url_raw' );
47
48 if ( empty( $id ) ) {
49 return $this->error( 'invalid_checkout_item', __( 'No item selected for purchase.', 'templately' ), 'checkout', 422 );
50 }
51
52 $funcArgs = [
53 'api_key' => $this->api_key,
54 'purchase_type' => $purchase_type,
55 'id' => $id,
56 'return_url' => $return_url,
57 ];
58
59 foreach ( [ 'item_type', 'billing_interval', 'coupon' ] as $optional ) {
60 $value = $this->get_param( $optional, '' );
61 if ( ! empty( $value ) ) {
62 $funcArgs[ $optional ] = $value;
63 }
64 }
65
66 $response = $this->http()->mutation( 'pluginCheckout', 'status, message, data', $funcArgs )->post();
67
68 if ( is_wp_error( $response ) ) {
69 return $this->error( 'invalid_checkout_response', $response->get_error_message(), 'checkout' );
70 }
71
72 $data = ! empty( $response['data'] ) ? json_decode( $response['data'], true ) : [];
73
74 // Frontend checkout URL created — the app redirects the buyer to it.
75 if ( ! empty( $data['url'] ) ) {
76 /**
77 * The client assigns this straight to `window.location.href`, so verify
78 * it is an https URL on a Templately host before handing it over. The
79 * response is our own API's, but a redirect target that arrives over the
80 * wire and is followed unchecked is an open redirect waiting to happen.
81 */
82 $url = esc_url_raw( $data['url'] );
83
84 if ( ! self::is_templately_checkout_url( $url ) ) {
85 return $this->error( 'invalid_checkout_url', __( 'The checkout could not be verified. Please try again.', 'templately' ), 'checkout' );
86 }
87
88 return $this->success( [ 'url' => $url ] );
89 }
90
91 $message = ! empty( $response['message'] ) ? $response['message'] : __( 'Could not start the checkout. Please try again.', 'templately' );
92
93 return $this->error( 'checkout_failed', $message, 'checkout' );
94 }
95
96 /**
97 * Is `$url` an https URL on a Templately host?
98 *
99 * Both halves are load-bearing. The scheme test stops an `http://` (or
100 * `javascript:`) target reaching `window.location.href`; the host test is an
101 * EXACT match on the apex or a true dot-anchored suffix, so a look-alike host
102 * such as `templately.com.evil.tld` or `nottemplately.com` is refused.
103 *
104 * The suffix comparison is written with `substr()` rather than
105 * `str_ends_with()` on purpose: the plugin advertises WordPress 5.0 / PHP 7.2,
106 * and core only polyfills `str_ends_with()` from WP 5.9. A fatal here would be
107 * a fatal in a security control.
108 *
109 * @param string $url Candidate redirect target, already run through esc_url_raw().
110 *
111 * @return bool
112 */
113 private static function is_templately_checkout_url( $url ) {
114 if ( ! is_string( $url ) || '' === $url ) {
115 return false;
116 }
117
118 if ( 'https' !== wp_parse_url( $url, PHP_URL_SCHEME ) ) {
119 return false;
120 }
121
122 $host = wp_parse_url( $url, PHP_URL_HOST );
123
124 if ( empty( $host ) || ! is_string( $host ) ) {
125 return false;
126 }
127
128 $host = strtolower( $host );
129
130 foreach ( [ 'templately.com', 'templately.dev' ] as $domain ) {
131 if ( $host === $domain ) {
132 return true;
133 }
134
135 $suffix = '.' . $domain;
136
137 if ( substr( $host, - strlen( $suffix ) ) === $suffix ) {
138 return true;
139 }
140 }
141
142 return false;
143 }
144 }
145