PluginProbe ʕ •ᴥ•ʔ
Code Manager / 1.0.0
Code Manager v1.0.0
1.0.49 1.0.48 1.0.47 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 1.0.25 1.0.26 1.0.27 1.0.28 1.0.3 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 1.0.39 1.0.4 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
code-manager / freemius / includes / sdk / FreemiusWordPress.php
code-manager / freemius / includes / sdk Last commit date
Exceptions 5 years ago FreemiusBase.php 5 years ago FreemiusWordPress.php 5 years ago LICENSE.txt 5 years ago index.php 5 years ago
FreemiusWordPress.php
704 lines
1 <?php
2 /**
3 * Copyright 2016 Freemius, Inc.
4 *
5 * Licensed under the GPL v2 (the "License"); you may
6 * not use this file except in compliance with the License. You may obtain
7 * a copy of the License at
8 *
9 * http://choosealicense.com/licenses/gpl-v2/
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations
15 * under the License.
16 */
17
18 require_once dirname( __FILE__ ) . '/FreemiusBase.php';
19
20 if ( ! defined( 'FS_SDK__USER_AGENT' ) ) {
21 define( 'FS_SDK__USER_AGENT', 'fs-php-' . Freemius_Api_Base::VERSION );
22 }
23
24 if ( ! defined( 'FS_SDK__SIMULATE_NO_CURL' ) ) {
25 define( 'FS_SDK__SIMULATE_NO_CURL', false );
26 }
27
28 if ( ! defined( 'FS_SDK__SIMULATE_NO_API_CONNECTIVITY_CLOUDFLARE' ) ) {
29 define( 'FS_SDK__SIMULATE_NO_API_CONNECTIVITY_CLOUDFLARE', false );
30 }
31
32 if ( ! defined( 'FS_SDK__SIMULATE_NO_API_CONNECTIVITY_SQUID_ACL' ) ) {
33 define( 'FS_SDK__SIMULATE_NO_API_CONNECTIVITY_SQUID_ACL', false );
34 }
35
36 if ( ! defined( 'FS_SDK__HAS_CURL' ) ) {
37 if ( FS_SDK__SIMULATE_NO_CURL ) {
38 define( 'FS_SDK__HAS_CURL', false );
39 } else {
40 $curl_required_methods = array(
41 'curl_version',
42 'curl_exec',
43 'curl_init',
44 'curl_close',
45 'curl_setopt',
46 'curl_setopt_array',
47 'curl_error',
48 );
49
50 $has_curl = true;
51 foreach ( $curl_required_methods as $m ) {
52 if ( ! function_exists( $m ) ) {
53 $has_curl = false;
54 break;
55 }
56 }
57
58 define( 'FS_SDK__HAS_CURL', $has_curl );
59 }
60 }
61
62 $curl_version = FS_SDK__HAS_CURL ?
63 curl_version() :
64 array( 'version' => '7.37' );
65
66 if ( ! defined( 'FS_API__PROTOCOL' ) ) {
67 define( 'FS_API__PROTOCOL', version_compare( $curl_version['version'], '7.37', '>=' ) ? 'https' : 'http' );
68 }
69
70 if ( ! defined( 'FS_API__LOGGER_ON' ) ) {
71 define( 'FS_API__LOGGER_ON', false );
72 }
73
74 if ( ! defined( 'FS_API__ADDRESS' ) ) {
75 define( 'FS_API__ADDRESS', '://api.freemius.com' );
76 }
77 if ( ! defined( 'FS_API__SANDBOX_ADDRESS' ) ) {
78 define( 'FS_API__SANDBOX_ADDRESS', '://sandbox-api.freemius.com' );
79 }
80
81 if ( class_exists( 'Freemius_Api_WordPress' ) ) {
82 return;
83 }
84
85 class Freemius_Api_WordPress extends Freemius_Api_Base {
86 private static $_logger = array();
87
88 /**
89 * @param string $pScope 'app', 'developer', 'user' or 'install'.
90 * @param number $pID Element's id.
91 * @param string $pPublic Public key.
92 * @param string|bool $pSecret Element's secret key.
93 * @param bool $pSandbox Whether or not to run API in sandbox mode.
94 */
95 public function __construct( $pScope, $pID, $pPublic, $pSecret = false, $pSandbox = false ) {
96 // If secret key not provided, use public key encryption.
97 if ( is_bool( $pSecret ) ) {
98 $pSecret = $pPublic;
99 }
100
101 parent::Init( $pScope, $pID, $pPublic, $pSecret, $pSandbox );
102 }
103
104 public static function GetUrl( $pCanonizedPath = '', $pIsSandbox = false ) {
105 $address = ( $pIsSandbox ? FS_API__SANDBOX_ADDRESS : FS_API__ADDRESS );
106
107 if ( ':' === $address[0] ) {
108 $address = self::$_protocol . $address;
109 }
110
111 return $address . $pCanonizedPath;
112 }
113
114 #----------------------------------------------------------------------------------
115 #region Servers Clock Diff
116 #----------------------------------------------------------------------------------
117
118 /**
119 * @var int Clock diff in seconds between current server to API server.
120 */
121 private static $_clock_diff = 0;
122
123 /**
124 * Set clock diff for all API calls.
125 *
126 * @since 1.0.3
127 *
128 * @param $pSeconds
129 */
130 public static function SetClockDiff( $pSeconds ) {
131 self::$_clock_diff = $pSeconds;
132 }
133
134 /**
135 * Find clock diff between current server to API server.
136 *
137 * @since 1.0.2
138 * @return int Clock diff in seconds.
139 */
140 public static function FindClockDiff() {
141 $time = time();
142 $pong = self::Ping();
143
144 return ( $time - strtotime( $pong->timestamp ) );
145 }
146
147 #endregion
148
149 /**
150 * @var string http or https
151 */
152 private static $_protocol = FS_API__PROTOCOL;
153
154 /**
155 * Set API connection protocol.
156 *
157 * @since 1.0.4
158 */
159 public static function SetHttp() {
160 self::$_protocol = 'http';
161 }
162
163 /**
164 * @since 1.0.4
165 *
166 * @return bool
167 */
168 public static function IsHttps() {
169 return ( 'https' === self::$_protocol );
170 }
171
172 /**
173 * Sign request with the following HTTP headers:
174 * Content-MD5: MD5(HTTP Request body)
175 * Date: Current date (i.e Sat, 14 Feb 2016 20:24:46 +0000)
176 * Authorization: FS {scope_entity_id}:{scope_entity_public_key}:base64encode(sha256(string_to_sign,
177 * {scope_entity_secret_key}))
178 *
179 * @param string $pResourceUrl
180 * @param array $pWPRemoteArgs
181 *
182 * @return array
183 */
184 function SignRequest( $pResourceUrl, $pWPRemoteArgs ) {
185 $auth = $this->GenerateAuthorizationParams(
186 $pResourceUrl,
187 $pWPRemoteArgs['method'],
188 ! empty( $pWPRemoteArgs['body'] ) ? $pWPRemoteArgs['body'] : ''
189 );
190
191 $pWPRemoteArgs['headers']['Date'] = $auth['date'];
192 $pWPRemoteArgs['headers']['Authorization'] = $auth['authorization'];
193
194 if ( ! empty( $auth['content_md5'] ) ) {
195 $pWPRemoteArgs['headers']['Content-MD5'] = $auth['content_md5'];
196 }
197
198 return $pWPRemoteArgs;
199 }
200
201 /**
202 * Generate Authorization request headers:
203 *
204 * Content-MD5: MD5(HTTP Request body)
205 * Date: Current date (i.e Sat, 14 Feb 2016 20:24:46 +0000)
206 * Authorization: FS {scope_entity_id}:{scope_entity_public_key}:base64encode(sha256(string_to_sign,
207 * {scope_entity_secret_key}))
208 *
209 * @author Vova Feldman
210 *
211 * @param string $pResourceUrl
212 * @param string $pMethod
213 * @param string $pPostParams
214 *
215 * @return array
216 * @throws Freemius_Exception
217 */
218 function GenerateAuthorizationParams(
219 $pResourceUrl,
220 $pMethod = 'GET',
221 $pPostParams = ''
222 ) {
223 $pMethod = strtoupper( $pMethod );
224
225 $eol = "\n";
226 $content_md5 = '';
227 $content_type = '';
228 $now = ( time() - self::$_clock_diff );
229 $date = date( 'r', $now );
230
231 if ( in_array( $pMethod, array( 'POST', 'PUT' ) ) && ! empty( $pPostParams ) ) {
232 $content_md5 = md5( $pPostParams );
233 $content_type = 'application/json';
234 }
235
236 $string_to_sign = implode( $eol, array(
237 $pMethod,
238 $content_md5,
239 $content_type,
240 $date,
241 $pResourceUrl
242 ) );
243
244 // If secret and public keys are identical, it means that
245 // the signature uses public key hash encoding.
246 $auth_type = ( $this->_secret !== $this->_public ) ? 'FS' : 'FSP';
247
248 $auth = array(
249 'date' => $date,
250 'authorization' => $auth_type . ' ' . $this->_id . ':' .
251 $this->_public . ':' .
252 self::Base64UrlEncode( hash_hmac(
253 'sha256', $string_to_sign, $this->_secret
254 ) )
255 );
256
257 if ( ! empty( $content_md5 ) ) {
258 $auth['content_md5'] = $content_md5;
259 }
260
261 return $auth;
262 }
263
264 /**
265 * Get API request URL signed via query string.
266 *
267 * @since 1.2.3 Stopped using http_build_query(). Instead, use urlencode(). In some environments the encoding of http_build_query() can generate a URL that once used with a redirect, the `&` querystring separator is escaped to `&amp;` which breaks the URL (Added by @svovaf).
268 *
269 * @param string $pPath
270 *
271 * @throws Freemius_Exception
272 *
273 * @return string
274 */
275 function GetSignedUrl( $pPath ) {
276 $resource = explode( '?', $this->CanonizePath( $pPath ) );
277 $pResourceUrl = $resource[0];
278
279 $auth = $this->GenerateAuthorizationParams( $pResourceUrl );
280
281 return Freemius_Api_WordPress::GetUrl(
282 $pResourceUrl . '?' .
283 ( 1 < count( $resource ) && ! empty( $resource[1] ) ? $resource[1] . '&' : '' ) .
284 'authorization=' . urlencode( $auth['authorization'] ) .
285 '&auth_date=' . urlencode( $auth['date'] )
286 , $this->_isSandbox );
287 }
288
289 /**
290 * @author Vova Feldman
291 *
292 * @param string $pUrl
293 * @param array $pWPRemoteArgs
294 *
295 * @return mixed
296 */
297 private static function ExecuteRequest( $pUrl, &$pWPRemoteArgs ) {
298 $start = microtime( true );
299
300 $response = wp_remote_request( $pUrl, $pWPRemoteArgs );
301
302 if ( FS_API__LOGGER_ON ) {
303 $end = microtime( true );
304
305 $has_body = ( isset( $pWPRemoteArgs['body'] ) && ! empty( $pWPRemoteArgs['body'] ) );
306 $is_http_error = is_wp_error( $response );
307
308 self::$_logger[] = array(
309 'id' => count( self::$_logger ),
310 'start' => $start,
311 'end' => $end,
312 'total' => ( $end - $start ),
313 'method' => $pWPRemoteArgs['method'],
314 'path' => $pUrl,
315 'body' => $has_body ? $pWPRemoteArgs['body'] : null,
316 'result' => ! $is_http_error ?
317 $response['body'] :
318 json_encode( $response->get_error_messages() ),
319 'code' => ! $is_http_error ? $response['response']['code'] : null,
320 'backtrace' => debug_backtrace(),
321 );
322 }
323
324 return $response;
325 }
326
327 /**
328 * @return array
329 */
330 static function GetLogger() {
331 return self::$_logger;
332 }
333
334 /**
335 * @param string $pCanonizedPath
336 * @param string $pMethod
337 * @param array $pParams
338 * @param null|array $pWPRemoteArgs
339 * @param bool $pIsSandbox
340 * @param null|callable $pBeforeExecutionFunction
341 *
342 * @return object[]|object|null
343 *
344 * @throws \Freemius_Exception
345 */
346 private static function MakeStaticRequest(
347 $pCanonizedPath,
348 $pMethod = 'GET',
349 $pParams = array(),
350 $pWPRemoteArgs = null,
351 $pIsSandbox = false,
352 $pBeforeExecutionFunction = null
353 ) {
354 // Connectivity errors simulation.
355 if ( FS_SDK__SIMULATE_NO_API_CONNECTIVITY_CLOUDFLARE ) {
356 self::ThrowCloudFlareDDoSException();
357 } else if ( FS_SDK__SIMULATE_NO_API_CONNECTIVITY_SQUID_ACL ) {
358 self::ThrowSquidAclException();
359 }
360
361 if ( empty( $pWPRemoteArgs ) ) {
362 $user_agent = 'Freemius/WordPress-SDK/' . Freemius_Api_Base::VERSION . '; ' .
363 home_url();
364
365 $pWPRemoteArgs = array(
366 'method' => strtoupper( $pMethod ),
367 'connect_timeout' => 10,
368 'timeout' => 60,
369 'follow_redirects' => true,
370 'redirection' => 5,
371 'user-agent' => $user_agent,
372 'blocking' => true,
373 );
374 }
375
376 if ( ! isset( $pWPRemoteArgs['headers'] ) ||
377 ! is_array( $pWPRemoteArgs['headers'] )
378 ) {
379 $pWPRemoteArgs['headers'] = array();
380 }
381
382 if ( in_array( $pMethod, array( 'POST', 'PUT' ) ) ) {
383 if ( is_array( $pParams ) && 0 < count( $pParams ) ) {
384 $pWPRemoteArgs['headers']['Content-type'] = 'application/json';
385 $pWPRemoteArgs['body'] = json_encode( $pParams );
386 }
387 }
388
389 $request_url = self::GetUrl( $pCanonizedPath, $pIsSandbox );
390
391 $resource = explode( '?', $pCanonizedPath );
392
393 if ( FS_SDK__HAS_CURL ) {
394 // Disable the 'Expect: 100-continue' behaviour. This causes cURL to wait
395 // for 2 seconds if the server does not support this header.
396 $pWPRemoteArgs['headers']['Expect'] = '';
397 }
398
399 if ( 'https' === substr( strtolower( $request_url ), 0, 5 ) ) {
400 $pWPRemoteArgs['sslverify'] = false;
401 }
402
403 if ( false !== $pBeforeExecutionFunction &&
404 is_callable( $pBeforeExecutionFunction )
405 ) {
406 $pWPRemoteArgs = call_user_func( $pBeforeExecutionFunction, $resource[0], $pWPRemoteArgs );
407 }
408
409 $result = self::ExecuteRequest( $request_url, $pWPRemoteArgs );
410
411 if ( is_wp_error( $result ) ) {
412 /**
413 * @var WP_Error $result
414 */
415 if ( self::IsCurlError( $result ) ) {
416 /**
417 * With dual stacked DNS responses, it's possible for a server to
418 * have IPv6 enabled but not have IPv6 connectivity. If this is
419 * the case, cURL will try IPv4 first and if that fails, then it will
420 * fall back to IPv6 and the error EHOSTUNREACH is returned by the
421 * operating system.
422 */
423 $matches = array();
424 $regex = '/Failed to connect to ([^:].*): Network is unreachable/';
425 if ( preg_match( $regex, $result->get_error_message( 'http_request_failed' ), $matches ) ) {
426 /**
427 * Validate IP before calling `inet_pton()` to avoid PHP un-catchable warning.
428 * @author Vova Feldman (@svovaf)
429 */
430 if ( filter_var( $matches[1], FILTER_VALIDATE_IP ) ) {
431 if ( strlen( inet_pton( $matches[1] ) ) === 16 ) {
432 // error_log('Invalid IPv6 configuration on server, Please disable or get native IPv6 on your server.');
433 // Hook to an action triggered just before cURL is executed to resolve the IP version to v4.
434 add_action( 'http_api_curl', 'Freemius_Api_WordPress::CurlResolveToIPv4', 10, 1 );
435
436 // Re-run request.
437 $result = self::ExecuteRequest( $request_url, $pWPRemoteArgs );
438 }
439 }
440 }
441 }
442
443 if ( is_wp_error( $result ) ) {
444 self::ThrowWPRemoteException( $result );
445 }
446 }
447
448 $response_body = $result['body'];
449
450 if ( empty( $response_body ) ) {
451 return null;
452 }
453
454 $decoded = json_decode( $response_body );
455
456 if ( is_null( $decoded ) ) {
457 if ( preg_match( '/Please turn JavaScript on/i', $response_body ) &&
458 preg_match( '/text\/javascript/', $response_body )
459 ) {
460 self::ThrowCloudFlareDDoSException( $response_body );
461 } else if ( preg_match( '/Access control configuration prevents your request from being allowed at this time. Please contact your service provider if you feel this is incorrect./', $response_body ) &&
462 preg_match( '/squid/', $response_body )
463 ) {
464 self::ThrowSquidAclException( $response_body );
465 } else {
466 $decoded = (object) array(
467 'error' => (object) array(
468 'type' => 'Unknown',
469 'message' => $response_body,
470 'code' => 'unknown',
471 'http' => 402
472 )
473 );
474 }
475 }
476
477 return $decoded;
478 }
479
480
481 /**
482 * Makes an HTTP request. This method can be overridden by subclasses if
483 * developers want to do fancier things or use something other than wp_remote_request()
484 * to make the request.
485 *
486 * @param string $pCanonizedPath The URL to make the request to
487 * @param string $pMethod HTTP method
488 * @param array $pParams The parameters to use for the POST body
489 * @param null|array $pWPRemoteArgs wp_remote_request options.
490 *
491 * @return object[]|object|null
492 *
493 * @throws Freemius_Exception
494 */
495 public function MakeRequest(
496 $pCanonizedPath,
497 $pMethod = 'GET',
498 $pParams = array(),
499 $pWPRemoteArgs = null
500 ) {
501 $resource = explode( '?', $pCanonizedPath );
502
503 // Only sign request if not ping.json connectivity test.
504 $sign_request = ( '/v1/ping.json' !== strtolower( substr( $resource[0], - strlen( '/v1/ping.json' ) ) ) );
505
506 return self::MakeStaticRequest(
507 $pCanonizedPath,
508 $pMethod,
509 $pParams,
510 $pWPRemoteArgs,
511 $this->_isSandbox,
512 $sign_request ? array( &$this, 'SignRequest' ) : null
513 );
514 }
515
516 /**
517 * Sets CURLOPT_IPRESOLVE to CURL_IPRESOLVE_V4 for cURL-Handle provided as parameter
518 *
519 * @param resource $handle A cURL handle returned by curl_init()
520 *
521 * @return resource $handle A cURL handle returned by curl_init() with CURLOPT_IPRESOLVE set to
522 * CURL_IPRESOLVE_V4
523 *
524 * @link https://gist.github.com/golderweb/3a2aaec2d56125cc004e
525 */
526 static function CurlResolveToIPv4( $handle ) {
527 curl_setopt( $handle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4 );
528
529 return $handle;
530 }
531
532 #----------------------------------------------------------------------------------
533 #region Connectivity Test
534 #----------------------------------------------------------------------------------
535
536 /**
537 * If successful connectivity to the API endpoint using ping.json endpoint.
538 *
539 * - OR -
540 *
541 * Validate if ping result object is valid.
542 *
543 * @param mixed $pPong
544 *
545 * @return bool
546 */
547 public static function Test( $pPong = null ) {
548 $pong = is_null( $pPong ) ?
549 self::Ping() :
550 $pPong;
551
552 return (
553 is_object( $pong ) &&
554 isset( $pong->api ) &&
555 'pong' === $pong->api
556 );
557 }
558
559 /**
560 * Ping API to test connectivity.
561 *
562 * @return object
563 */
564 public static function Ping() {
565 try {
566 $result = self::MakeStaticRequest( '/v' . FS_API__VERSION . '/ping.json' );
567 } catch ( Freemius_Exception $e ) {
568 // Map to error object.
569 $result = (object) $e->getResult();
570 } catch ( Exception $e ) {
571 // Map to error object.
572 $result = (object) array(
573 'error' => array(
574 'type' => 'Unknown',
575 'message' => $e->getMessage() . ' (' . $e->getFile() . ': ' . $e->getLine() . ')',
576 'code' => 'unknown',
577 'http' => 402
578 )
579 );
580 }
581
582 return $result;
583 }
584
585 #endregion
586
587 #----------------------------------------------------------------------------------
588 #region Connectivity Exceptions
589 #----------------------------------------------------------------------------------
590
591 /**
592 * @param \WP_Error $pError
593 *
594 * @return bool
595 */
596 private static function IsCurlError( WP_Error $pError ) {
597 $message = $pError->get_error_message( 'http_request_failed' );
598
599 return ( 0 === strpos( $message, 'cURL' ) );
600 }
601
602 /**
603 * @param WP_Error $pError
604 *
605 * @throws Freemius_Exception
606 */
607 private static function ThrowWPRemoteException( WP_Error $pError ) {
608 if ( self::IsCurlError( $pError ) ) {
609 $message = $pError->get_error_message( 'http_request_failed' );
610
611 #region Check if there are any missing cURL methods.
612
613 $curl_required_methods = array(
614 'curl_version',
615 'curl_exec',
616 'curl_init',
617 'curl_close',
618 'curl_setopt',
619 'curl_setopt_array',
620 'curl_error',
621 );
622
623 // Find all missing methods.
624 $missing_methods = array();
625 foreach ( $curl_required_methods as $m ) {
626 if ( ! function_exists( $m ) ) {
627 $missing_methods[] = $m;
628 }
629 }
630
631 if ( ! empty( $missing_methods ) ) {
632 throw new Freemius_Exception( array(
633 'error' => (object) array(
634 'type' => 'cUrlMissing',
635 'message' => $message,
636 'code' => 'curl_missing',
637 'http' => 402
638 ),
639 'missing_methods' => $missing_methods,
640 ) );
641 }
642
643 #endregion
644
645 // cURL error - "cURL error {{errno}}: {{error}}".
646 $parts = explode( ':', substr( $message, strlen( 'cURL error ' ) ), 2 );
647
648 $code = ( 0 < count( $parts ) ) ? $parts[0] : 'http_request_failed';
649 $message = ( 1 < count( $parts ) ) ? $parts[1] : $message;
650
651 $e = new Freemius_Exception( array(
652 'error' => array(
653 'code' => $code,
654 'message' => $message,
655 'type' => 'CurlException',
656 ),
657 ) );
658 } else {
659 $e = new Freemius_Exception( array(
660 'error' => array(
661 'code' => $pError->get_error_code(),
662 'message' => $pError->get_error_message(),
663 'type' => 'WPRemoteException',
664 ),
665 ) );
666 }
667
668 throw $e;
669 }
670
671 /**
672 * @param string $pResult
673 *
674 * @throws Freemius_Exception
675 */
676 private static function ThrowCloudFlareDDoSException( $pResult = '' ) {
677 throw new Freemius_Exception( array(
678 'error' => (object) array(
679 'type' => 'CloudFlareDDoSProtection',
680 'message' => $pResult,
681 'code' => 'cloudflare_ddos_protection',
682 'http' => 402
683 )
684 ) );
685 }
686
687 /**
688 * @param string $pResult
689 *
690 * @throws Freemius_Exception
691 */
692 private static function ThrowSquidAclException( $pResult = '' ) {
693 throw new Freemius_Exception( array(
694 'error' => (object) array(
695 'type' => 'SquidCacheBlock',
696 'message' => $pResult,
697 'code' => 'squid_cache_block',
698 'http' => 402
699 )
700 ) );
701 }
702
703 #endregion
704 }