PLL_Plugin_Updater

Allows plugins to use their own update API.

Defined (1)

The class is defined in the following location(s).

/install/plugin-updater.php  
  1. class PLL_Plugin_Updater { 
  2.  
  3. private $api_url = ''; 
  4. private $api_data = array(); 
  5. private $name = ''; 
  6. private $slug = ''; 
  7. private $version = ''; 
  8. private $wp_override = false; 
  9. private $cache_key = ''; 
  10.  
  11. /** 
  12. * Class constructor. 
  13. * @uses plugin_basename() 
  14. * @uses hook() 
  15. * @param string $_api_url The URL pointing to the custom API endpoint. 
  16. * @param string $_plugin_file Path to the plugin file. 
  17. * @param array $_api_data Optional data to send with API calls. 
  18. */ 
  19. public function __construct( $_api_url, $_plugin_file, $_api_data = null ) { 
  20.  
  21. global $edd_plugin_data; 
  22.  
  23. $this->api_url = trailingslashit( $_api_url ); 
  24. $this->api_data = $_api_data; 
  25. $this->name = plugin_basename( $_plugin_file ); 
  26. $this->slug = basename( $_plugin_file, '.php' ); 
  27. $this->version = $_api_data['version']; 
  28. $this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false; 
  29. $this->beta = ! empty( $this->api_data['beta'] ) ? true : false; 
  30. $this->cache_key = md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ); 
  31.  
  32. $edd_plugin_data[ $this->slug ] = $this->api_data; 
  33.  
  34. // Set up hooks. 
  35. $this->init(); 
  36.  
  37.  
  38. /** 
  39. * Set up WordPress filters to hook into WP's update process. 
  40. * @uses add_filter() 
  41. * @return void 
  42. */ 
  43. public function init() { 
  44.  
  45. add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) ); 
  46. add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 ); 
  47. remove_action( 'after_plugin_row_' . $this->name, 'wp_plugin_update_row', 10 ); 
  48. add_action( 'after_plugin_row_' . $this->name, array( $this, 'show_update_notification' ), 10, 2 ); 
  49. add_action( 'admin_init', array( $this, 'show_changelog' ) ); 
  50.  
  51.  
  52. /** 
  53. * Check for Updates at the defined API endpoint and modify the update array. 
  54. * This function dives into the update API just when WordPress creates its update array,  
  55. * then adds a custom API call and injects the custom plugin data retrieved from the API. 
  56. * It is reassembled from parts of the native WordPress plugin update code. 
  57. * See wp-includes/update.php line 121 for the original wp_update_plugins() function. 
  58. * @uses api_request() 
  59. * @param array $_transient_data Update array build by WordPress. 
  60. * @return array Modified update array with custom plugin data. 
  61. */ 
  62. public function check_update( $_transient_data ) { 
  63.  
  64. global $pagenow; 
  65.  
  66. if ( ! is_object( $_transient_data ) ) { 
  67. $_transient_data = new stdClass; 
  68.  
  69. if ( 'plugins.php' == $pagenow && is_multisite() ) { 
  70. return $_transient_data; 
  71.  
  72. if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) { 
  73. return $_transient_data; 
  74.  
  75. $version_info = $this->get_cached_version_info(); 
  76.  
  77. if ( false === $version_info ) { 
  78. $version_info = $this->api_request( 'plugin_latest_version', array( 'slug' => $this->slug, 'beta' => $this->beta ) ); 
  79.  
  80. $this->set_version_info_cache( $version_info ); 
  81.  
  82.  
  83. if ( false !== $version_info && is_object( $version_info ) && isset( $version_info->new_version ) ) { 
  84.  
  85. if ( version_compare( $this->version, $version_info->new_version, '<' ) ) { 
  86.  
  87. $_transient_data->response[ $this->name ] = $version_info; 
  88.  
  89.  
  90. $_transient_data->last_checked = current_time( 'timestamp' ); 
  91. $_transient_data->checked[ $this->name ] = $this->version; 
  92.  
  93.  
  94. return $_transient_data; 
  95.  
  96. /** 
  97. * show update nofication row -- needed for multisite subsites, because WP won't tell you otherwise! 
  98. * @param string $file 
  99. * @param array $plugin 
  100. */ 
  101. public function show_update_notification( $file, $plugin ) { 
  102.  
  103. if ( is_network_admin() ) { 
  104. return; 
  105.  
  106. if( ! current_user_can( 'update_plugins' ) ) { 
  107. return; 
  108.  
  109. if( ! is_multisite() ) { 
  110. return; 
  111.  
  112. if ( $this->name != $file ) { 
  113. return; 
  114.  
  115. // Remove our filter on the site transient 
  116. remove_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ), 10 ); 
  117.  
  118. $update_cache = get_site_transient( 'update_plugins' ); 
  119.  
  120. $update_cache = is_object( $update_cache ) ? $update_cache : new stdClass(); 
  121.  
  122. if ( empty( $update_cache->response ) || empty( $update_cache->response[ $this->name ] ) ) { 
  123.  
  124. $version_info = $this->get_cached_version_info(); 
  125.  
  126. if ( false === $version_info ) { 
  127. $version_info = $this->api_request( 'plugin_latest_version', array( 'slug' => $this->slug, 'beta' => $this->beta ) ); 
  128.  
  129. $this->set_version_info_cache( $version_info ); 
  130.  
  131. if ( ! is_object( $version_info ) ) { 
  132. return; 
  133.  
  134. if ( version_compare( $this->version, $version_info->new_version, '<' ) ) { 
  135.  
  136. $update_cache->response[ $this->name ] = $version_info; 
  137.  
  138.  
  139. $update_cache->last_checked = current_time( 'timestamp' ); 
  140. $update_cache->checked[ $this->name ] = $this->version; 
  141.  
  142. set_site_transient( 'update_plugins', $update_cache ); 
  143.  
  144. } else { 
  145.  
  146. $version_info = $update_cache->response[ $this->name ]; 
  147.  
  148.  
  149. // Restore our filter 
  150. add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) ); 
  151.  
  152. if ( ! empty( $update_cache->response[ $this->name ] ) && version_compare( $this->version, $version_info->new_version, '<' ) ) { 
  153.  
  154. // build a plugin list row, with update notification 
  155. $wp_list_table = _get_list_table( 'WP_Plugins_List_Table' ); 
  156. # <tr class="plugin-update-tr"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange"> 
  157. echo '<tr class="plugin-update-tr" id="' . $this->slug . '-update" data-slug="' . $this->slug . '" data-plugin="' . $this->slug . '/' . $file . '">'; 
  158. echo '<td colspan="3" class="plugin-update colspanchange">'; 
  159. echo '<div class="update-message notice inline notice-warning notice-alt">'; 
  160.  
  161. $changelog_link = self_admin_url( 'index.php?edd_sl_action=view_plugin_changelog&plugin=' . $this->name . '&slug=' . $this->slug . '&TB_iframe=true&width=772&height=911' ); 
  162.  
  163. if ( empty( $version_info->download_link ) ) { 
  164. printf( 
  165. /** translators: %1$s plugin name, %3$s plugin version, %2$s and %4$s are html tags */ 
  166. esc_html__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s.', 'polylang' ),  
  167. esc_html( $version_info->name ),  
  168. '<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',  
  169. esc_html( $version_info->new_version ),  
  170. '</a>' 
  171. ); 
  172. } else { 
  173. printf( 
  174. /** translators: %1$s plugin name, %3$s plugin version, %2$s, %4$s, %5$s and %6$s are html tags */ 
  175. esc_html__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s or %5$supdate now%6$s.', 'polylang' ),  
  176. esc_html( $version_info->name ),  
  177. '<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',  
  178. esc_html( $version_info->new_version ),  
  179. '</a>',  
  180. '<a href="' . esc_url( wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $this->name, 'upgrade-plugin_' . $this->name ) ) .'">',  
  181. '</a>' 
  182. ); 
  183.  
  184. do_action( "in_plugin_update_message-{$file}", $plugin, $version_info ); 
  185.  
  186. echo '</div></td></tr>'; 
  187.  
  188. /** 
  189. * Updates information on the "View version x.x details" page with custom data. 
  190. * @uses api_request() 
  191. * @param mixed $_data 
  192. * @param string $_action 
  193. * @param object $_args 
  194. * @return object $_data 
  195. */ 
  196. public function plugins_api_filter( $_data, $_action = '', $_args = null ) { 
  197.  
  198. if ( $_action != 'plugin_information' ) { 
  199.  
  200. return $_data; 
  201.  
  202.  
  203. if ( ! isset( $_args->slug ) || ( $_args->slug != $this->slug ) ) { 
  204.  
  205. return $_data; 
  206.  
  207.  
  208. $to_send = array( 
  209. 'slug' => $this->slug,  
  210. 'is_ssl' => is_ssl(),  
  211. 'fields' => array( 
  212. 'banners' => array(),  
  213. 'reviews' => false 
  214. ); 
  215.  
  216. $cache_key = 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ); 
  217.  
  218. // Get the transient where we store the api request for this plugin for 24 hours 
  219. $edd_api_request_transient = $this->get_cached_version_info( $cache_key ); 
  220.  
  221. //If we have no transient-saved value, run the API, set a fresh transient with the API value, and return that value too right now. 
  222. if ( empty( $edd_api_request_transient ) ) { 
  223.  
  224. $api_response = $this->api_request( 'plugin_information', $to_send ); 
  225.  
  226. // Expires in 3 hours 
  227. $this->set_version_info_cache( $api_response, $cache_key ); 
  228.  
  229. if ( false !== $api_response ) { 
  230. $_data = $api_response; 
  231.  
  232. } else { 
  233. $_data = $edd_api_request_transient; 
  234.  
  235. // Convert sections into an associative array, since we're getting an object, but Core expects an array. 
  236. if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) { 
  237. $new_sections = array(); 
  238. foreach ( $_data->sections as $key => $key ) { 
  239. $new_sections[ $key ] = $key; 
  240.  
  241. $_data->sections = $new_sections; 
  242.  
  243. // Convert banners into an associative array, since we're getting an object, but Core expects an array. 
  244. if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) { 
  245. $new_banners = array(); 
  246. foreach ( $_data->banners as $key => $key ) { 
  247. $new_banners[ $key ] = $key; 
  248.  
  249. $_data->banners = $new_banners; 
  250.  
  251. return $_data; 
  252.  
  253. /** 
  254. * Disable SSL verification in order to prevent download update failures 
  255. * @param array $args 
  256. * @param string $url 
  257. * @return object $array 
  258. */ 
  259. public function http_request_args( $args, $url ) { 
  260. // If it is an https request and we are performing a package download, disable ssl verification 
  261. if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) { 
  262. $args['sslverify'] = false; 
  263. return $args; 
  264.  
  265. /** 
  266. * Calls the API and, if successfull, returns the object delivered by the API. 
  267. * @uses get_bloginfo() 
  268. * @uses wp_remote_post() 
  269. * @uses is_wp_error() 
  270. * @param string $_action The requested action. 
  271. * @param array $_data Parameters for the API action. 
  272. * @return false|object 
  273. */ 
  274. private function api_request( $_action, $_data ) { 
  275.  
  276. global $wp_version; 
  277.  
  278. $data = array_merge( $this->api_data, $_data ); 
  279.  
  280. if ( $data['slug'] != $this->slug ) { 
  281. return; 
  282.  
  283. if( $this->api_url == trailingslashit (home_url() ) ) { 
  284. return false; // Don't allow a plugin to ping itself 
  285.  
  286. $api_params = array( 
  287. 'edd_action' => 'get_version',  
  288. 'license' => ! empty( $data['license'] ) ? $data['license'] : '',  
  289. 'item_name' => isset( $data['item_name'] ) ? $data['item_name'] : false,  
  290. 'item_id' => isset( $data['item_id'] ) ? $data['item_id'] : false,  
  291. 'version' => isset( $data['version'] ) ? $data['version'] : false,  
  292. 'slug' => $data['slug'],  
  293. 'author' => $data['author'],  
  294. 'url' => home_url(),  
  295. 'beta' => ! empty( $data['beta'] ),  
  296. ); 
  297.  
  298. $request = wp_remote_post( $this->api_url, array( 'timeout' => 15, 'sslverify' => false, 'body' => $api_params ) ); 
  299.  
  300. if ( ! is_wp_error( $request ) ) { 
  301. $request = json_decode( wp_remote_retrieve_body( $request ) ); 
  302.  
  303. if ( $request && isset( $request->sections ) ) { 
  304. $request->sections = maybe_unserialize( $request->sections ); 
  305. } else { 
  306. $request = false; 
  307.  
  308. if ( $request && isset( $request->banners ) ) { 
  309. $request->banners = maybe_unserialize( $request->banners ); 
  310.  
  311. if( ! empty( $request->sections ) ) { 
  312. foreach( $request->sections as $key => $section ) { 
  313. $request->$key = (array) $section; 
  314.  
  315. return $request; 
  316.  
  317. public function show_changelog() { 
  318.  
  319. global $edd_plugin_data; 
  320.  
  321. if( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' != $_REQUEST['edd_sl_action'] ) { 
  322. return; 
  323.  
  324. if( empty( $_REQUEST['plugin'] ) ) { 
  325. return; 
  326.  
  327. if( empty( $_REQUEST['slug'] ) ) { 
  328. return; 
  329.  
  330. if( ! current_user_can( 'update_plugins' ) ) { 
  331. wp_die( __( 'You do not have permission to install plugin updates', 'polylang' ), __( 'Error', 'polylang' ), array( 'response' => 403 ) ); 
  332.  
  333. $data = $edd_plugin_data[ $_REQUEST['slug'] ]; 
  334. $beta = ! empty( $data['beta'] ) ? true : false; 
  335. $cache_key = md5( 'edd_plugin_' . sanitize_key( $_REQUEST['plugin'] ) . '_' . $beta . '_version_info' ); 
  336. $version_info = $this->get_cached_version_info( $cache_key ); 
  337.  
  338. if( false === $version_info ) { 
  339.  
  340. $api_params = array( 
  341. 'edd_action' => 'get_version',  
  342. 'item_name' => isset( $data['item_name'] ) ? $data['item_name'] : false,  
  343. 'item_id' => isset( $data['item_id'] ) ? $data['item_id'] : false,  
  344. 'slug' => $_REQUEST['slug'],  
  345. 'author' => $data['author'],  
  346. 'url' => home_url(),  
  347. 'beta' => ! empty( $data['beta'] ) 
  348. ); 
  349.  
  350. $request = wp_remote_post( $this->api_url, array( 'timeout' => 15, 'sslverify' => false, 'body' => $api_params ) ); 
  351.  
  352. if ( ! is_wp_error( $request ) ) { 
  353. $version_info = json_decode( wp_remote_retrieve_body( $request ) ); 
  354.  
  355.  
  356. if ( ! empty( $version_info ) && isset( $version_info->sections ) ) { 
  357. $version_info->sections = maybe_unserialize( $version_info->sections ); 
  358. } else { 
  359. $version_info = false; 
  360.  
  361. if( ! empty( $version_info ) ) { 
  362. foreach( $version_info->sections as $key => $section ) { 
  363. $version_info->$key = (array) $section; 
  364.  
  365. $this->set_version_info_cache( $version_info, $cache_key ); 
  366.  
  367.  
  368. if( ! empty( $version_info ) && isset( $version_info->sections['changelog'] ) ) { 
  369. echo '<div style="background:#fff;padding:10px;">' . $version_info->sections['changelog'] . '</div>'; 
  370.  
  371. exit; 
  372.  
  373. public function get_cached_version_info( $cache_key = '' ) { 
  374.  
  375. if( empty( $cache_key ) ) { 
  376. $cache_key = $this->cache_key; 
  377.  
  378. $cache = get_option( $cache_key ); 
  379.  
  380. if( empty( $cache['timeout'] ) || current_time( 'timestamp' ) > $cache['timeout'] ) { 
  381. return false; // Cache is expired 
  382.  
  383. return json_decode( $cache['value'] ); 
  384.  
  385.  
  386. public function set_version_info_cache( $value = '', $cache_key = '' ) { 
  387.  
  388. if( empty( $cache_key ) ) { 
  389. $cache_key = $this->cache_key; 
  390.  
  391. $data = array( 
  392. 'timeout' => strtotime( '+3 hours', current_time( 'timestamp' ) ),  
  393. 'value' => json_encode( $value ) 
  394. ); 
  395.  
  396. update_option( $cache_key, $data ); 
  397.  
  398.