class-my-yoast-api-request.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO\Inc
  6. */
  7. /**
  8. * Handles requests to MyYoast.
  9. */
  10. class WPSEO_MyYoast_Api_Request {
  11. /**
  12. * The Request URL.
  13. *
  14. * @var string
  15. */
  16. protected $url;
  17. /**
  18. * The request parameters.
  19. *
  20. * @var array
  21. */
  22. protected $args = [
  23. 'method' => 'GET',
  24. 'timeout' => 5,
  25. 'headers' => [
  26. 'Accept-Encoding' => '*',
  27. ],
  28. ];
  29. /**
  30. * Contains the fetched response.
  31. *
  32. * @var stdClass
  33. */
  34. protected $response;
  35. /**
  36. * Contains the error message when request went wrong.
  37. *
  38. * @var string
  39. */
  40. protected $error_message = '';
  41. /**
  42. * The MyYoast client object.
  43. *
  44. * @var WPSEO_MyYoast_Client
  45. */
  46. protected $client;
  47. /**
  48. * Constructor.
  49. *
  50. * @codeCoverageIgnore
  51. *
  52. * @param string $url The request url.
  53. * @param array $args The request arguments.
  54. */
  55. public function __construct( $url, array $args = [] ) {
  56. $this->url = 'https://my.yoast.com/api/' . $url;
  57. $this->args = wp_parse_args( $args, $this->args );
  58. }
  59. /**
  60. * Fires the request.
  61. *
  62. * @return bool True when request is successful.
  63. */
  64. public function fire() {
  65. try {
  66. $response = $this->do_request( $this->url, $this->args );
  67. $this->response = $this->decode_response( $response );
  68. return true;
  69. }
  70. /**
  71. * The Authentication exception only occurs when using Access Tokens (>= PHP 5.6).
  72. * In other case this exception won't be thrown.
  73. *
  74. * When authentication failed just try to get a new access token based
  75. * on the refresh token. If that request also has an authentication issue
  76. * we just invalidate the access token by removing it.
  77. */
  78. catch ( WPSEO_MyYoast_Authentication_Exception $authentication_exception ) {
  79. try {
  80. $access_token = $this->get_access_token();
  81. if ( $access_token !== false ) {
  82. $response = $this->do_request( $this->url, $this->args );
  83. $this->response = $this->decode_response( $response );
  84. }
  85. return true;
  86. }
  87. catch ( WPSEO_MyYoast_Authentication_Exception $authentication_exception ) {
  88. $this->error_message = $authentication_exception->getMessage();
  89. $this->remove_access_token( $this->get_current_user_id() );
  90. return false;
  91. }
  92. catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) {
  93. $this->error_message = $bad_request_exception->getMessage();
  94. return false;
  95. }
  96. }
  97. catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request_exception ) {
  98. $this->error_message = $bad_request_exception->getMessage();
  99. return false;
  100. }
  101. }
  102. /**
  103. * Retrieves the error message.
  104. *
  105. * @return string The set error message.
  106. */
  107. public function get_error_message() {
  108. return $this->error_message;
  109. }
  110. /**
  111. * Retrieves the response.
  112. *
  113. * @return stdClass The response object.
  114. */
  115. public function get_response() {
  116. return $this->response;
  117. }
  118. /**
  119. * Performs the request using WordPress internals.
  120. *
  121. * @codeCoverageIgnore
  122. *
  123. * @param string $url The request URL.
  124. * @param array $request_arguments The request arguments.
  125. *
  126. * @return string The retrieved body.
  127. * @throws WPSEO_MyYoast_Authentication_Exception When authentication has failed.
  128. * @throws WPSEO_MyYoast_Bad_Request_Exception When request is invalid.
  129. */
  130. protected function do_request( $url, $request_arguments ) {
  131. $request_arguments = $this->enrich_request_arguments( $request_arguments );
  132. $response = wp_remote_request( $url, $request_arguments );
  133. if ( is_wp_error( $response ) ) {
  134. throw new WPSEO_MyYoast_Bad_Request_Exception( $response->get_error_message() );
  135. }
  136. $response_code = wp_remote_retrieve_response_code( $response );
  137. $response_message = wp_remote_retrieve_response_message( $response );
  138. // Do nothing, response code is okay.
  139. if ( $response_code === 200 || strpos( $response_code, '200' ) !== false ) {
  140. return wp_remote_retrieve_body( $response );
  141. }
  142. // Authentication failed, throw an exception.
  143. if ( strpos( $response_code, '401' ) && $this->has_oauth_support() ) {
  144. throw new WPSEO_MyYoast_Authentication_Exception( esc_html( $response_message ), 401 );
  145. }
  146. throw new WPSEO_MyYoast_Bad_Request_Exception( esc_html( $response_message ), (int) $response_code );
  147. }
  148. /**
  149. * Decodes the JSON encoded response.
  150. *
  151. * @param string $response The response to decode.
  152. *
  153. * @return stdClass The json decoded response.
  154. * @throws WPSEO_MyYoast_Invalid_JSON_Exception When decoded string is not a JSON object.
  155. */
  156. protected function decode_response( $response ) {
  157. $response = json_decode( $response );
  158. if ( ! is_object( $response ) ) {
  159. throw new WPSEO_MyYoast_Invalid_JSON_Exception(
  160. esc_html__( 'No JSON object was returned.', 'wordpress-seo' )
  161. );
  162. }
  163. return $response;
  164. }
  165. /**
  166. * Checks if MyYoast tokens are allowed and adds the token to the request body.
  167. *
  168. * When tokens are disallowed it will add the url to the request body.
  169. *
  170. * @param array $request_arguments The arguments to enrich.
  171. *
  172. * @return array The enriched arguments.
  173. */
  174. protected function enrich_request_arguments( array $request_arguments ) {
  175. $request_arguments = wp_parse_args( $request_arguments, [ 'headers' => [] ] );
  176. $addon_version_headers = $this->get_installed_addon_versions();
  177. foreach ( $addon_version_headers as $addon => $version ) {
  178. $request_arguments['headers'][ $addon . '-version' ] = $version;
  179. }
  180. $request_body = $this->get_request_body();
  181. if ( $request_body !== [] ) {
  182. $request_arguments['body'] = $request_body;
  183. }
  184. return $request_arguments;
  185. }
  186. /**
  187. * Retrieves the request body based on URL or access token support.
  188. *
  189. * @codeCoverageIgnore
  190. *
  191. * @return array The request body.
  192. */
  193. public function get_request_body() {
  194. if ( ! $this->has_oauth_support() ) {
  195. return [ 'url' => WPSEO_Utils::get_home_url() ];
  196. }
  197. try {
  198. $access_token = $this->get_access_token();
  199. if ( $access_token ) {
  200. return [ 'token' => $access_token->getToken() ];
  201. }
  202. }
  203. // @codingStandardsIgnoreLine Generic.CodeAnalysis.EmptyStatement.DetectedCATCH -- There is nothing to do.
  204. catch ( WPSEO_MyYoast_Bad_Request_Exception $bad_request ) {
  205. // Do nothing.
  206. }
  207. return [];
  208. }
  209. /**
  210. * Retrieves the access token.
  211. *
  212. * @codeCoverageIgnore
  213. *
  214. * @return bool|WPSEO_MyYoast_AccessToken_Interface The AccessToken when valid.
  215. * @throws WPSEO_MyYoast_Bad_Request_Exception When something went wrong in getting the access token.
  216. */
  217. protected function get_access_token() {
  218. $client = $this->get_client();
  219. if ( ! $client ) {
  220. return false;
  221. }
  222. $access_token = $client->get_access_token();
  223. if ( ! $access_token ) {
  224. return false;
  225. }
  226. if ( ! $access_token->hasExpired() ) {
  227. return $access_token;
  228. }
  229. try {
  230. $access_token = $client
  231. ->get_provider()
  232. ->getAccessToken(
  233. 'refresh_token',
  234. [
  235. 'refresh_token' => $access_token->getRefreshToken(),
  236. ]
  237. );
  238. $client->save_access_token( $this->get_current_user_id(), $access_token );
  239. return $access_token;
  240. }
  241. catch ( Exception $e ) {
  242. $error_code = $e->getCode();
  243. if ( $error_code >= 400 && $error_code < 500 ) {
  244. $this->remove_access_token( $this->get_current_user_id() );
  245. }
  246. throw new WPSEO_MyYoast_Bad_Request_Exception( $e->getMessage() );
  247. }
  248. }
  249. /**
  250. * Retrieves an instance of the MyYoast client.
  251. *
  252. * @codeCoverageIgnore
  253. *
  254. * @return WPSEO_MyYoast_Client Instance of the client.
  255. */
  256. protected function get_client() {
  257. if ( $this->client === null ) {
  258. $this->client = new WPSEO_MyYoast_Client();
  259. }
  260. return $this->client;
  261. }
  262. /**
  263. * Wraps the get current user id function.
  264. *
  265. * @codeCoverageIgnore
  266. *
  267. * @return int The user id.
  268. */
  269. protected function get_current_user_id() {
  270. return get_current_user_id();
  271. }
  272. /**
  273. * Removes the access token for given user id.
  274. *
  275. * @codeCoverageIgnore
  276. *
  277. * @param int $user_id The user id.
  278. *
  279. * @return void
  280. */
  281. protected function remove_access_token( $user_id ) {
  282. if ( ! $this->has_oauth_support() ) {
  283. return;
  284. }
  285. // Remove the access token entirely.
  286. $this->get_client()->remove_access_token( $user_id );
  287. }
  288. /**
  289. * Retrieves the installed addons as http headers.
  290. *
  291. * @codeCoverageIgnore
  292. *
  293. * @return array The installed addon versions.
  294. */
  295. protected function get_installed_addon_versions() {
  296. $addon_manager = new WPSEO_Addon_Manager();
  297. return $addon_manager->get_installed_addons_versions();
  298. }
  299. /**
  300. * Wraps the has_access_token support method.
  301. *
  302. * @codeCoverageIgnore
  303. *
  304. * @return bool False to disable the support.
  305. */
  306. protected function has_oauth_support() {
  307. return false;
  308. // @todo: Uncomment the following statement when we are implementing the oAuth flow.
  309. // return WPSEO_Utils::has_access_token_support();
  310. }
  311. }