updates.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. <?php
  2. if ( ! defined( 'ABSPATH' ) ) {
  3. exit; // Exit if accessed directly
  4. }
  5. if ( ! class_exists( 'ACF_Updates' ) ) :
  6. class ACF_Updates {
  7. /** @var string The ACF_Updates version */
  8. var $version = '2.4';
  9. /** @var array The array of registered plugins */
  10. var $plugins = array();
  11. /** @var int Counts the number of plugin update checks */
  12. var $checked = 0;
  13. /*
  14. * __construct
  15. *
  16. * Sets up the class functionality.
  17. *
  18. * @date 5/03/2014
  19. * @since 5.0.0
  20. *
  21. * @param void
  22. * @return void
  23. */
  24. function __construct() {
  25. // append update information to transient
  26. add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_plugins_transient' ), 10, 1 );
  27. // modify plugin data visible in the 'View details' popup
  28. add_filter( 'plugins_api', array( $this, 'modify_plugin_details' ), 10, 3 );
  29. }
  30. /*
  31. * add_plugin
  32. *
  33. * Registeres a plugin for updates.
  34. *
  35. * @date 8/4/17
  36. * @since 5.5.10
  37. *
  38. * @param array $plugin The plugin array.
  39. * @return void
  40. */
  41. function add_plugin( $plugin ) {
  42. // validate
  43. $plugin = wp_parse_args(
  44. $plugin,
  45. array(
  46. 'id' => '',
  47. 'key' => '',
  48. 'slug' => '',
  49. 'basename' => '',
  50. 'version' => '',
  51. )
  52. );
  53. // Check if is_plugin_active() function exists. This is required on the front end of the
  54. // site, since it is in a file that is normally only loaded in the admin.
  55. if ( ! function_exists( 'is_plugin_active' ) ) {
  56. require_once ABSPATH . 'wp-admin/includes/plugin.php';
  57. }
  58. // add if is active plugin (not included in theme)
  59. if ( is_plugin_active( $plugin['basename'] ) ) {
  60. $this->plugins[ $plugin['basename'] ] = $plugin;
  61. }
  62. }
  63. /**
  64. * get_plugin_by
  65. *
  66. * Returns a registered plugin for the give key and value.
  67. *
  68. * @date 3/8/18
  69. * @since 5.7.2
  70. *
  71. * @param string $key The array key to compare
  72. * @param string $value The value to compare against
  73. * @return array|false
  74. */
  75. function get_plugin_by( $key = '', $value = null ) {
  76. foreach ( $this->plugins as $plugin ) {
  77. if ( $plugin[ $key ] === $value ) {
  78. return $plugin;
  79. }
  80. }
  81. return false;
  82. }
  83. /*
  84. * request
  85. *
  86. * Makes a request to the ACF connect server.
  87. *
  88. * @date 8/4/17
  89. * @since 5.5.10
  90. *
  91. * @param string $endpoint The API endpoint.
  92. * @param array $body The body to post.
  93. * @return (array|string|WP_Error)
  94. */
  95. function request( $endpoint = '', $body = null ) {
  96. // Determine URL.
  97. $url = "https://connect.advancedcustomfields.com/$endpoint";
  98. // Staging environment.
  99. if ( defined( 'ACF_DEV_API' ) && ACF_DEV_API ) {
  100. $url = trailingslashit( ACF_DEV_API ) . $endpoint;
  101. acf_log( $url, $body );
  102. }
  103. // Make request.
  104. $raw_response = wp_remote_post(
  105. $url,
  106. array(
  107. 'timeout' => 10,
  108. 'body' => $body,
  109. )
  110. );
  111. // Handle response error.
  112. if ( is_wp_error( $raw_response ) ) {
  113. return $raw_response;
  114. // Handle http error.
  115. } elseif ( wp_remote_retrieve_response_code( $raw_response ) != 200 ) {
  116. return new WP_Error( 'server_error', wp_remote_retrieve_response_message( $raw_response ) );
  117. }
  118. // Decode JSON response.
  119. $json = json_decode( wp_remote_retrieve_body( $raw_response ), true );
  120. // Allow non json value.
  121. if ( $json === null ) {
  122. return wp_remote_retrieve_body( $raw_response );
  123. }
  124. // return
  125. return $json;
  126. }
  127. /**
  128. * Returns update information for the given plugin id.
  129. *
  130. * @since 5.5.10
  131. *
  132. * @param string $id The plugin id such as 'pro'.
  133. * @param boolean $force_check Bypasses cached result. Defaults to false.
  134. * @return array|WP_Error
  135. */
  136. public function get_plugin_info( $id = '', $force_check = false ) {
  137. $transient_name = 'acf_plugin_info_' . $id;
  138. // check cache but allow for $force_check override.
  139. if ( ! $force_check ) {
  140. $transient = get_transient( $transient_name );
  141. if ( $transient !== false ) {
  142. return $transient;
  143. }
  144. }
  145. $response = $this->request( 'v2/plugins/get-info?p=' . $id );
  146. // convert string (misc error) to WP_Error object.
  147. if ( is_string( $response ) ) {
  148. $response = new WP_Error( 'server_error', esc_html( $response ) );
  149. }
  150. // allow json to include expiration but force minimum and max for safety.
  151. $expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS );
  152. // update transient.
  153. set_transient( $transient_name, $response, $expiration );
  154. return $response;
  155. }
  156. /**
  157. * get_plugin_update
  158. *
  159. * Returns specific data from the 'update-check' response.
  160. *
  161. * @date 3/8/18
  162. * @since 5.7.2
  163. *
  164. * @param string $basename The plugin basename.
  165. * @param boolean $force_check Bypasses cached result. Defaults to false
  166. * @return array
  167. */
  168. function get_plugin_update( $basename = '', $force_check = false ) {
  169. // get updates
  170. $updates = $this->get_plugin_updates( $force_check );
  171. // check for and return update
  172. if ( is_array( $updates ) && isset( $updates['plugins'][ $basename ] ) ) {
  173. return $updates['plugins'][ $basename ];
  174. }
  175. return false;
  176. }
  177. /**
  178. * get_plugin_updates
  179. *
  180. * Checks for plugin updates.
  181. *
  182. * @date 8/7/18
  183. * @since 5.6.9
  184. * @since 5.7.2 Added 'checked' comparison
  185. *
  186. * @param boolean $force_check Bypasses cached result. Defaults to false.
  187. * @return array|WP_Error.
  188. */
  189. function get_plugin_updates( $force_check = false ) {
  190. // var
  191. $transient_name = 'acf_plugin_updates';
  192. // construct array of 'checked' plugins
  193. // sort by key to avoid detecting change due to "include order"
  194. $checked = array();
  195. foreach ( $this->plugins as $basename => $plugin ) {
  196. $checked[ $basename ] = $plugin['version'];
  197. }
  198. ksort( $checked );
  199. // $force_check prevents transient lookup
  200. if ( ! $force_check ) {
  201. $transient = get_transient( $transient_name );
  202. // if cached response was found, compare $transient['checked'] against $checked and ignore if they don't match (plugins/versions have changed)
  203. if ( is_array( $transient ) ) {
  204. $transient_checked = isset( $transient['checked'] ) ? $transient['checked'] : array();
  205. if ( wp_json_encode( $checked ) !== wp_json_encode( $transient_checked ) ) {
  206. $transient = false;
  207. }
  208. }
  209. if ( $transient !== false ) {
  210. return $transient;
  211. }
  212. }
  213. // vars
  214. $post = array(
  215. 'plugins' => wp_json_encode( $this->plugins ),
  216. 'wp' => wp_json_encode(
  217. array(
  218. 'wp_name' => get_bloginfo( 'name' ),
  219. 'wp_url' => acf_get_home_url(),
  220. 'wp_version' => get_bloginfo( 'version' ),
  221. 'wp_language' => get_bloginfo( 'language' ),
  222. 'wp_timezone' => get_option( 'timezone_string' ),
  223. 'php_version' => PHP_VERSION,
  224. )
  225. ),
  226. 'acf' => wp_json_encode(
  227. array(
  228. 'acf_version' => get_option( 'acf_version' ),
  229. 'acf_pro' => ( defined( 'ACF_PRO' ) && ACF_PRO ),
  230. 'block_count' => acf_pro_get_registered_block_count(),
  231. )
  232. ),
  233. );
  234. // request
  235. $response = $this->request( 'v2/plugins/update-check', $post );
  236. // append checked reference
  237. if ( is_array( $response ) ) {
  238. $response['checked'] = $checked;
  239. }
  240. // allow json to include expiration but force minimum and max for safety
  241. $expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS );
  242. // update transient
  243. set_transient( $transient_name, $response, $expiration );
  244. // return
  245. return $response;
  246. }
  247. /**
  248. * This function safely gets the expiration value from a response.
  249. *
  250. * @since 5.6.9
  251. *
  252. * @param mixed $response The response from the server. Default false.
  253. * @param int $min The minimum expiration limit. Default 0.
  254. * @param int $max The maximum expiration limit. Default 0.
  255. * @return int
  256. */
  257. public function get_expiration( $response = false, $min = 0, $max = 0 ) {
  258. $expiration = 0;
  259. // check possible error conditions.
  260. if ( is_wp_error( $response ) || is_string( $response ) ) {
  261. return 5 * MINUTE_IN_SECONDS;
  262. }
  263. // use the server requested expiration if present.
  264. if ( is_array( $response ) && isset( $response['expiration'] ) ) {
  265. $expiration = (int) $response['expiration'];
  266. }
  267. // use the minimum if neither check matches, or ensure the server expiration isn't lower than our minimum.
  268. if ( $expiration < $min ) {
  269. return $min;
  270. }
  271. // ensure the server expiration isn't higher than our max.
  272. if ( $expiration > $max ) {
  273. return $max;
  274. }
  275. return $expiration;
  276. }
  277. /*
  278. * refresh_plugins_transient
  279. *
  280. * Deletes transients and allows a fresh lookup.
  281. *
  282. * @date 11/4/17
  283. * @since 5.5.10
  284. *
  285. * @param void
  286. * @return void
  287. */
  288. function refresh_plugins_transient() {
  289. delete_site_transient( 'update_plugins' );
  290. delete_transient( 'acf_plugin_updates' );
  291. }
  292. /*
  293. * modify_plugins_transient
  294. *
  295. * Called when WP updates the 'update_plugins' site transient. Used to inject ACF plugin update info.
  296. *
  297. * @date 16/01/2014
  298. * @since 5.0.0
  299. *
  300. * @param object $transient
  301. * @return $transient
  302. */
  303. function modify_plugins_transient( $transient ) {
  304. // bail early if no response (error)
  305. if ( ! isset( $transient->response ) ) {
  306. return $transient;
  307. }
  308. // force-check (only once)
  309. $force_check = ( $this->checked == 0 ) ? ! empty( $_GET['force-check'] ) : false; // phpcs:ignore -- False positive, value not used.
  310. // fetch updates (this filter is called multiple times during a single page load)
  311. $updates = $this->get_plugin_updates( $force_check );
  312. // append
  313. if ( is_array( $updates ) ) {
  314. foreach ( $updates['plugins'] as $basename => $update ) {
  315. $transient->response[ $basename ] = (object) $update;
  316. }
  317. }
  318. // increase
  319. $this->checked++;
  320. // return
  321. return $transient;
  322. }
  323. /*
  324. * modify_plugin_details
  325. *
  326. * Returns the plugin data visible in the 'View details' popup
  327. *
  328. * @date 17/01/2014
  329. * @since 5.0.0
  330. *
  331. * @param object $result
  332. * @param string $action
  333. * @param object $args
  334. * @return $result
  335. */
  336. function modify_plugin_details( $result, $action = null, $args = null ) {
  337. // vars
  338. $plugin = false;
  339. // only for 'plugin_information' action
  340. if ( $action !== 'plugin_information' ) {
  341. return $result;
  342. }
  343. // find plugin via slug
  344. $plugin = $this->get_plugin_by( 'slug', $args->slug );
  345. if ( ! $plugin ) {
  346. return $result;
  347. }
  348. // connect
  349. $response = $this->get_plugin_info( $plugin['id'] );
  350. // bail early if no response
  351. if ( ! is_array( $response ) ) {
  352. return $result;
  353. }
  354. // remove tags (different context)
  355. unset( $response['tags'] );
  356. // convert to object
  357. $response = (object) $response;
  358. // sections
  359. $sections = array(
  360. 'description' => '',
  361. 'installation' => '',
  362. 'changelog' => '',
  363. 'upgrade_notice' => '',
  364. );
  365. foreach ( $sections as $k => $v ) {
  366. $sections[ $k ] = $response->$k;
  367. }
  368. $response->sections = $sections;
  369. // return
  370. return $response;
  371. }
  372. }
  373. /*
  374. * acf_updates
  375. *
  376. * The main function responsible for returning the one true acf_updates instance to functions everywhere.
  377. * Use this function like you would a global variable, except without needing to declare the global.
  378. *
  379. * Example: <?php $acf_updates = acf_updates(); ?>
  380. *
  381. * @date 9/4/17
  382. * @since 5.5.12
  383. *
  384. * @param void
  385. * @return object
  386. */
  387. function acf_updates() {
  388. global $acf_updates;
  389. if ( ! isset( $acf_updates ) ) {
  390. $acf_updates = new ACF_Updates();
  391. }
  392. return $acf_updates;
  393. }
  394. /*
  395. * acf_register_plugin_update
  396. *
  397. * Alias of acf_updates()->add_plugin().
  398. *
  399. * @type function
  400. * @date 12/4/17
  401. * @since 5.5.10
  402. *
  403. * @param array $plugin
  404. * @return void
  405. */
  406. function acf_register_plugin_update( $plugin ) {
  407. acf_updates()->add_plugin( $plugin );
  408. }
  409. endif; // class_exists check