• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 6987097851

25 Nov 2023 04:49AM UTC coverage: 49.206% (-0.1%) from 49.302%
6987097851

push

github

web-flow
Merge pull request #20878 from Yoast/JRF/ghactions-minor-tweak

GH Actions: update a few links in inline comments

15305 of 31104 relevant lines covered (49.21%)

4.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.41
/src/config/oauth-client.php
1
<?php
2

3
namespace Yoast\WP\SEO\Config;
4

5
use Exception;
6
use Yoast\WP\SEO\Exceptions\OAuth\Authentication_Failed_Exception;
7
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Empty_Property_Exception;
8
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Empty_Token_Exception;
9
use Yoast\WP\SEO\Exceptions\OAuth\Tokens\Failed_Storage_Exception;
10
use Yoast\WP\SEO\Helpers\Options_Helper;
11
use Yoast\WP\SEO\Values\OAuth\OAuth_Token;
12
use YoastSEO_Vendor\League\OAuth2\Client\Provider\Exception\IdentityProviderException;
13
use YoastSEO_Vendor\League\OAuth2\Client\Provider\GenericProvider;
14

15
/**
16
 * Class OAuth_Client
17
 */
18
abstract class OAuth_Client {
19

20
        /**
21
         * The option's key.
22
         *
23
         * @var string
24
         */
25
        protected $token_option = null;
26

27
        /**
28
         * The provider.
29
         *
30
         * @var Wincher_PKCE_Provider|GenericProvider
31
         */
32
        protected $provider;
33

34
        /**
35
         * The options helper.
36
         *
37
         * @var Options_Helper
38
         */
39
        protected $options_helper;
40

41
        /**
42
         * The token.
43
         *
44
         * @var OAuth_Token|null
45
         */
46
        protected $token = null;
47

48
        /**
49
         * OAuth_Client constructor.
50
         *
51
         * @param string                                $token_option   The option's name to save the token as.
52
         * @param Wincher_PKCE_Provider|GenericProvider $provider       The provider.
53
         * @param Options_Helper                        $options_helper The Options_Helper instance.
54
         *
55
         * @throws Empty_Property_Exception Exception thrown if a token property is empty.
56
         */
57
        public function __construct(
4✔
58
                $token_option,
59
                $provider,
60
                Options_Helper $options_helper
61
        ) {
2✔
62
                $this->provider       = $provider;
4✔
63
                $this->token_option   = $token_option;
4✔
64
                $this->options_helper = $options_helper;
4✔
65

66
                $tokens = $this->options_helper->get( $this->token_option );
4✔
67

68
                if ( ! empty( $tokens ) ) {
4✔
69
                        $this->token = new OAuth_Token(
2✔
70
                                $tokens['access_token'],
2✔
71
                                $tokens['refresh_token'],
2✔
72
                                $tokens['expires'],
2✔
73
                                $tokens['has_expired'],
2✔
74
                                $tokens['created_at'],
2✔
75
                                isset( $tokens['error_count'] ) ? $tokens['error_count'] : 0
2✔
76
                        );
1✔
77
                }
78
        }
2✔
79

80
        /**
81
         * Requests the access token and refresh token based on the passed code.
82
         *
83
         * @param string $code The code to send.
84
         *
85
         * @return OAuth_Token The requested tokens.
86
         *
87
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
88
         */
89
        public function request_tokens( $code ) {
4✔
90
                try {
91
                        $response = $this->provider
4✔
92
                                ->getAccessToken(
4✔
93
                                        'authorization_code',
4✔
94
                                        [
2✔
95
                                                'code' => $code,
4✔
96
                                        ]
2✔
97
                                );
2✔
98

99
                        $token = OAuth_Token::from_response( $response );
2✔
100

101
                        return $this->store_token( $token );
2✔
102
                } catch ( Exception $exception ) {
2✔
103
                        throw new Authentication_Failed_Exception( $exception );
2✔
104
                }
105
        }
106

107
        /**
108
         * Performs an authenticated GET request to the desired URL.
109
         *
110
         * @param string $url     The URL to send the request to.
111
         * @param array  $options The options to pass along to the request.
112
         *
113
         * @return mixed The parsed API response.
114
         *
115
         * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data.
116
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
117
         * @throws Empty_Token_Exception Exception thrown if the token is empty.
118
         */
119
        public function get( $url, $options = [] ) {
2✔
120
                return $this->do_request( 'GET', $url, $options );
2✔
121
        }
122

123
        /**
124
         * Performs an authenticated POST request to the desired URL.
125
         *
126
         * @param string $url     The URL to send the request to.
127
         * @param mixed  $body    The data to send along in the request's body.
128
         * @param array  $options The options to pass along to the request.
129
         *
130
         * @return mixed The parsed API response.
131
         *
132
         * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data.
133
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
134
         * @throws Empty_Token_Exception Exception thrown if the token is empty.
135
         */
136
        public function post( $url, $body, $options = [] ) {
2✔
137
                $options['body'] = $body;
2✔
138

139
                return $this->do_request( 'POST', $url, $options );
2✔
140
        }
141

142
        /**
143
         * Performs an authenticated DELETE request to the desired URL.
144
         *
145
         * @param string $url     The URL to send the request to.
146
         * @param array  $options The options to pass along to the request.
147
         *
148
         * @return mixed The parsed API response.
149
         *
150
         * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data.
151
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
152
         * @throws Empty_Token_Exception Exception thrown if the token is empty.
153
         */
154
        public function delete( $url, $options = [] ) {
2✔
155
                return $this->do_request( 'DELETE', $url, $options );
2✔
156
        }
157

158
        /**
159
         * Determines whether there are valid tokens available.
160
         *
161
         * @return bool Whether there are valid tokens.
162
         */
163
        public function has_valid_tokens() {
6✔
164
                return ! empty( $this->token ) && $this->token->has_expired() === false;
6✔
165
        }
166

167
        /**
168
         * Gets the stored tokens and refreshes them if they've expired.
169
         *
170
         * @return OAuth_Token The stored tokens.
171
         *
172
         * @throws Empty_Token_Exception Exception thrown if the token is empty.
173
         */
174
        public function get_tokens() {
6✔
175
                if ( empty( $this->token ) ) {
6✔
176
                        throw new Empty_Token_Exception();
2✔
177
                }
178

179
                if ( $this->token->has_expired() ) {
4✔
180
                        $this->token = $this->refresh_tokens( $this->token );
2✔
181
                }
182

183
                return $this->token;
4✔
184
        }
185

186
        /**
187
         * Stores the passed token.
188
         *
189
         * @param OAuth_Token $token The token to store.
190
         *
191
         * @return OAuth_Token The stored token.
192
         *
193
         * @throws Failed_Storage_Exception Exception thrown if storing of the token fails.
194
         */
195
        public function store_token( OAuth_Token $token ) {
4✔
196
                $saved = $this->options_helper->set( $this->token_option, $token->to_array() );
4✔
197

198
                if ( $saved === false ) {
4✔
199
                        throw new Failed_Storage_Exception();
2✔
200
                }
201

202
                return $token;
2✔
203
        }
204

205
        /**
206
         * Clears the stored token from storage.
207
         *
208
         * @return bool The stored token.
209
         *
210
         * @throws Failed_Storage_Exception Exception thrown if clearing of the token fails.
211
         */
212
        public function clear_token() {
×
213
                $saved = $this->options_helper->set( $this->token_option, [] );
×
214

215
                if ( $saved === false ) {
×
216
                        throw new Failed_Storage_Exception();
×
217
                }
218

219
                return true;
×
220
        }
221

222
        /**
223
         * Performs the specified request.
224
         *
225
         * @param string $method  The HTTP method to use.
226
         * @param string $url     The URL to send the request to.
227
         * @param array  $options The options to pass along to the request.
228
         *
229
         * @return mixed The parsed API response.
230
         *
231
         * @throws IdentityProviderException Exception thrown if there's something wrong with the identifying data.
232
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
233
         * @throws Empty_Token_Exception Exception thrown if the token is empty.
234
         */
235
        protected function do_request( $method, $url, array $options ) {
2✔
236
                $defaults = [
1✔
237
                        'headers' => $this->provider->getHeaders( $this->get_tokens()->access_token ),
2✔
238
                ];
1✔
239

240
                $options = \array_merge_recursive( $defaults, $options );
2✔
241

242
                if ( \array_key_exists( 'params', $options ) ) {
2✔
243
                        $url .= '?' . \http_build_query( $options['params'] );
×
244
                        unset( $options['params'] );
×
245
                }
246

247
                $request = $this->provider
2✔
248
                        ->getAuthenticatedRequest( $method, $url, null, $options );
2✔
249

250
                return $this->provider->getParsedResponse( $request );
2✔
251
        }
252

253
        /**
254
         * Refreshes the outdated tokens.
255
         *
256
         * @param OAuth_Token $tokens The outdated tokens.
257
         *
258
         * @return OAuth_Token The refreshed tokens.
259
         *
260
         * @throws Authentication_Failed_Exception Exception thrown if authentication has failed.
261
         */
262
        protected function refresh_tokens( OAuth_Token $tokens ) {
6✔
263
                // We do this dance with transients since we need to make sure we don't
264
                // delete valid tokens because of a race condition when two calls are
265
                // made simultaneously to this function and refresh token rotation is
266
                // turned on in the OAuth server. This is not 100% safe, but should at
267
                // least be much better than not having any lock at all.
268
                $lock_name = \sprintf( 'lock:%s', $this->token_option );
6✔
269
                $can_lock  = \get_transient( $lock_name ) === false;
6✔
270
                $has_lock  = $can_lock && \set_transient( $lock_name, true, 30 );
6✔
271

272
                try {
273
                        $new_tokens = $this->provider->getAccessToken(
6✔
274
                                'refresh_token',
6✔
275
                                [
3✔
276
                                        'refresh_token' => $tokens->refresh_token,
6✔
277
                                ]
3✔
278
                        );
3✔
279

280
                        $token_obj = OAuth_Token::from_response( $new_tokens );
2✔
281

282
                        return $this->store_token( $token_obj );
2✔
283
                } catch ( Exception $exception ) {
4✔
284
                        // If we tried to refresh but the refresh token is invalid, delete
285
                        // the tokens so that we don't try again. Only do this if we got the
286
                        // lock at the beginning of the call.
287
                        if ( $has_lock && $exception->getMessage() === 'invalid_grant' ) {
4✔
288
                                try {
289
                                        // To protect from race conditions, only do this if we've
290
                                        // seen an error before with the same token.
291
                                        if ( $tokens->error_count >= 1 ) {
2✔
292
                                                $this->clear_token();
2✔
293
                                        }
294
                                        else {
295
                                                $tokens->error_count += 1;
×
296
                                                $this->store_token( $tokens );
2✔
297
                                        }
298
                                } catch ( Exception $e ) {  // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
×
299
                                        // Pass through.
300
                                }
301
                        }
302

303
                        throw new Authentication_Failed_Exception( $exception );
4✔
304
                } finally {
305
                        \delete_transient( $lock_name );
6✔
306
                }
307
        }
308
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc