Completed
Push — add/product-ratings-to-search ( 91683c...b05261 )
by
unknown
344:08 queued 334:42
created

Token_Subscription_Service::get_site_id()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
nc 1
dl 0
loc 1
c 0
b 0
f 0
1
<?php
2
/**
3
 * A paywall that exchanges JWT tokens from WordPress.com to allow
4
 * a current visitor to view content that has been deemed "Premium content".
5
 *
6
 * @package Automattic\Jetpack\Extensions\Premium_Content
7
 */
8
9
namespace Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service;
10
11
use Automattic\Jetpack\Extensions\Premium_Content\JWT;
12
13
/**
14
 * Class Token_Subscription_Service
15
 *
16
 * @package Automattic\Jetpack\Extensions\Premium_Content\Subscription_Service
17
 */
18
abstract class Token_Subscription_Service implements Subscription_Service {
19
20
	const JWT_AUTH_TOKEN_COOKIE_NAME = 'jp-premium-content-session';
21
	const DECODE_EXCEPTION_FEATURE   = 'memberships';
22
	const DECODE_EXCEPTION_MESSAGE   = 'Problem decoding provided token';
23
	const REST_URL_ORIGIN            = 'https://subscribe.wordpress.com/';
24
25
	/**
26
	 * Initialize the token subscription service.
27
	 *
28
	 * @inheritDoc
29
	 */
30
	public function initialize() {
31
		$token = $this->token_from_request();
32
		if ( null !== $token ) {
33
			$this->set_token_cookie( $token );
0 ignored issues
show
Security Bug introduced by
It seems like $token defined by $this->token_from_request() on line 31 can also be of type false; however, Automattic\Jetpack\Exten...ice::set_token_cookie() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
34
		}
35
	}
36
37
	/**
38
	 * The user is visiting with a subscriber token cookie.
39
	 *
40
	 * This is theoretically where the cookie JWT signature verification
41
	 * thing will happen.
42
	 *
43
	 * How to obtain one of these (or what exactly it is) is
44
	 * still a WIP (see api/auth branch)
45
	 *
46
	 * @inheritDoc
47
	 *
48
	 * @param array $valid_plan_ids List of valid plan IDs.
49
	 */
50
	public function visitor_can_view_content( $valid_plan_ids ) {
51
52
		// URL token always has a precedence, so it can overwrite the cookie when new data available.
53
		$token = $this->token_from_request();
54
		if ( $token ) {
55
			$this->set_token_cookie( $token );
56
		} else {
57
			$token = $this->token_from_cookie();
58
		}
59
60
		$is_valid_token = true;
61
62
		if ( empty( $token ) ) {
63
			// no token, no access.
64
			$is_valid_token = false;
65
		} else {
66
			$payload = $this->decode_token( $token );
67
			if ( empty( $payload ) ) {
68
				$is_valid_token = false;
69
			}
70
		}
71
72
		if ( $is_valid_token ) {
73
			$subscriptions = (array) $payload['subscriptions'];
0 ignored issues
show
Bug introduced by
The variable $payload does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
74
		} elseif ( is_user_logged_in() ) {
75
			/*
76
			 * If there is no token, but the user is logged in,
77
			 * get current subscriptions and determine if the user has
78
			 * a valid subscription to match the plan ID.
79
			 */
80
81
			/**
82
			 * Filter the subscriptions attached to a specific user on a given site.
83
			 *
84
			 * @since 9.4.0
85
			 *
86
			 * @param array $subscriptions Array of subscriptions.
87
			 * @param int   $user_id The user's ID.
88
			 * @param int   $site_id ID of the current site.
89
			 */
90
			$subscriptions = apply_filters(
91
				'earn_get_user_subscriptions_for_site_id',
92
				array(),
93
				wp_get_current_user()->ID,
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with wp_get_current_user()->ID.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
94
				$this->get_site_id()
95
			);
96
97
			if ( empty( $subscriptions ) ) {
98
				return false;
99
			}
100
			// format the subscriptions so that they can be validated.
101
			$subscriptions = self::abbreviate_subscriptions( $subscriptions );
102
		} else {
103
			return false;
104
		}
105
106
		return $this->validate_subscriptions( $valid_plan_ids, $subscriptions );
107
	}
108
109
	/**
110
	 * Decode the given token.
111
	 *
112
	 * @param string $token Token to decode.
113
	 *
114
	 * @return array|false
115
	 */
116
	public function decode_token( $token ) {
117
		try {
118
			$key = $this->get_key();
119
			return $key ? (array) JWT::decode( $token, $key, array( 'HS256' ) ) : false;
120
		} catch ( \Exception $exception ) {
121
			return false;
122
		}
123
	}
124
125
	/**
126
	 * Get the key for decoding the auth token.
127
	 *
128
	 * @return string|false
129
	 */
130
	abstract public function get_key();
131
132
	/**
133
	 * Get the ID of the current site.
134
	 *
135
	 * @return int
136
	 */
137
	abstract public function get_site_id();
138
139
	// phpcs:disable
140
	/**
141
	 * Get the URL to access the protected content.
142
	 *
143
	 * @param string $mode Access mode (either "subscribe" or "login").
144
	 */
145
	public function access_url( $mode = 'subscribe' ) {
146
		global $wp;
147
		$permalink = get_permalink();
148
		if ( empty( $permalink ) ) {
149
			$permalink = add_query_arg( $wp->query_vars, home_url( $wp->request ) );
150
		}
151
152
		$login_url = $this->get_rest_api_token_url( $this->get_site_id(), $permalink );
153
		return $login_url;
154
	}
155
	// phpcs:enable
156
157
	/**
158
	 * Get the token stored in the auth cookie.
159
	 *
160
	 * @return ?string
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
161
	 */
162
	private function token_from_cookie() {
163
		if ( isset( $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ] ) ) {
164
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
165
			return $_COOKIE[ self::JWT_AUTH_TOKEN_COOKIE_NAME ];
166
		}
167
	}
168
169
	/**
170
	 * Store the auth cookie.
171
	 *
172
	 * @param  string $token Auth token.
173
	 * @return void
174
	 */
175
	private function set_token_cookie( $token ) {
176
		if ( ! empty( $token ) ) {
177
			setcookie( self::JWT_AUTH_TOKEN_COOKIE_NAME, $token, 0, '/' );
178
		}
179
	}
180
181
	/**
182
	 * Get the token if present in the current request.
183
	 *
184
	 * @return ?string
0 ignored issues
show
Documentation introduced by
The doc-type ?string could not be parsed: Unknown type name "?string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
185
	 */
186
	private function token_from_request() {
187
		$token = null;
188
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
189
		if ( isset( $_GET['token'] ) ) {
190
			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended
191
			if ( preg_match( '/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/', $_GET['token'], $matches ) ) {
192
				// token matches a valid JWT token pattern.
193
				$token = reset( $matches );
194
			}
195
		}
196
		return $token;
197
	}
198
199
	/**
200
	 * Return true if any ID/date pairs are valid. Otherwise false.
201
	 *
202
	 * @param int[]                          $valid_plan_ids List of valid plan IDs.
203
	 * @param array<int, Token_Subscription> $token_subscriptions : ID must exist in the provided <code>$valid_subscriptions</code> parameter.
204
	 *                                                            The provided end date needs to be greater than <code>now()</code>.
205
	 *
206
	 * @return bool
207
	 */
208
	protected function validate_subscriptions( $valid_plan_ids, $token_subscriptions ) {
209
		// Create a list of product_ids to compare against.
210
		$product_ids = array();
211
		foreach ( $valid_plan_ids as $plan_id ) {
212
			$product_id = (int) get_post_meta( $plan_id, 'jetpack_memberships_product_id', true );
213
			if ( isset( $product_id ) ) {
214
				$product_ids[] = $product_id;
215
			}
216
		}
217
218
		foreach ( $token_subscriptions as $product_id => $token_subscription ) {
219
			if ( in_array( $product_id, $product_ids, true ) ) {
220
				$end = is_int( $token_subscription->end_date ) ? $token_subscription->end_date : strtotime( $token_subscription->end_date );
221
				if ( $end > time() ) {
222
					return true;
223
				}
224
			}
225
		}
226
		return false;
227
	}
228
229
	/**
230
	 * Get the URL of the JWT endpoint.
231
	 *
232
	 * @param  int    $site_id Site ID.
233
	 * @param  string $redirect_url URL to redirect after checking the token validity.
234
	 * @return string URL of the JWT endpoint.
235
	 */
236
	private function get_rest_api_token_url( $site_id, $redirect_url ) {
237
		return sprintf( '%smemberships/jwt?site_id=%d&redirect_url=%s', self::REST_URL_ORIGIN, $site_id, rawurlencode( $redirect_url ) );
238
	}
239
240
	/**
241
	 * Report the subscriptions as an ID => [ 'end_date' => ]. mapping
242
	 *
243
	 * @param array $subscriptions_from_bd List of subscriptions from BD.
244
	 *
245
	 * @return array<int, array>
0 ignored issues
show
Documentation introduced by
The doc-type array<int, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
246
	 */
247 View Code Duplication
	public static function abbreviate_subscriptions( $subscriptions_from_bd ) {
248
		$subscriptions = array();
249
		foreach ( $subscriptions_from_bd as $subscription ) {
250
			// We are picking the expiry date that is the most in the future.
251
			if (
252
				'active' === $subscription['status'] && (
253
					! isset( $subscriptions[ $subscription['product_id'] ] ) ||
254
					empty( $subscription['end_date'] ) || // Special condition when subscription has no expiry date - we will default to a year from now for the purposes of the token.
255
					strtotime( $subscription['end_date'] ) > strtotime( (string) $subscriptions[ $subscription['product_id'] ]['end_date'] )
256
				)
257
			) {
258
				$subscriptions[ $subscription['product_id'] ]           = new \stdClass();
259
				$subscriptions[ $subscription['product_id'] ]->end_date = empty( $subscription['end_date'] ) ? ( time() + 365 * 24 * 3600 ) : $subscription['end_date'];
260
			}
261
		}
262
		return $subscriptions;
263
	}
264
}
265