← All changes
|
libraries/freemius/includes/class-fs-plugin-updater.php
+119
-76
2.0.4
→
3.4
View file →
| @@ -36,8 +36,10 @@ | ||
| 36 | 36 | private $_translation_updates; |
| 37 | 37 | |
| 38 | 38 | private static $_upgrade_basename = null; |
| 39 | 39 | |
| 40 | + const UPDATES_CHECK_CACHE_EXPIRATION = ( WP_FS__TIME_24_HOURS_IN_SEC / 24 ); | |
| 41 | + | |
| 40 | 42 | #-------------------------------------------------------------------------------- |
| 41 | 43 | #region Singleton |
| 42 | 44 | #-------------------------------------------------------------------------------- |
| 43 | 45 | |
| @@ -103,8 +105,10 @@ | ||
| 103 | 105 | ), 11, 2 ); |
| 104 | 106 | |
| 105 | 107 | if ( ! $this->_fs->has_any_active_valid_license() ) { |
| 106 | 108 | add_action( 'admin_head', array( &$this, 'catch_plugin_information_dialog_contents' ) ); |
| 109 | + } else { | |
| 110 | + add_action( 'admin_footer', array( &$this, '_add_fs_allow_updater_and_dialog_request_param' ) ); | |
| 107 | 111 | } |
| 108 | 112 | |
| 109 | 113 | if ( ! WP_FS__IS_PRODUCTION_MODE ) { |
| 110 | 114 | add_filter( 'http_request_host_is_external', array( |
| @@ -128,15 +132,48 @@ | ||
| 128 | 132 | } |
| 129 | 133 | |
| 130 | 134 | /** |
| 131 | 135 | * @author Leo Fajardo (@leorw) |
| 136 | + * @since 2.7.4 | |
| 137 | + */ | |
| 138 | + function _add_fs_allow_updater_and_dialog_request_param() { | |
| 139 | + if ( ! $this->is_plugin_information_dialog_for_plugin() ) { | |
| 140 | + return; | |
| 141 | + } | |
| 142 | + ?> | |
| 143 | + <script type="text/javascript"> | |
| 144 | + if ( typeof jQuery !== 'undefined' ) { | |
| 145 | + jQuery( document ).on( 'wp-plugin-updating', function( event, args ) { | |
| 146 | + if ( typeof args === 'object' && args.slug && typeof args.slug === 'string' ) { | |
| 147 | + if ( <?php echo json_encode( $this->_fs->get_slug() ) ?> === args.slug ) { | |
| 148 | + args.fs_allow_updater_and_dialog = true; | |
| 149 | + } | |
| 150 | + } | |
| 151 | + } ); | |
| 152 | + } | |
| 153 | + </script> | |
| 154 | + <?php | |
| 155 | + } | |
| 156 | + | |
| 157 | + /** | |
| 158 | + * @author Leo Fajardo (@leorw) | |
| 159 | + * @since 2.7.4 | |
| 160 | + * | |
| 161 | + * @return bool | |
| 162 | + */ | |
| 163 | + private function is_plugin_information_dialog_for_plugin() { | |
| 164 | + return ( | |
| 165 | + 'plugin-information' === fs_request_get( 'tab', false ) && | |
| 166 | + $this->_fs->get_slug() === fs_request_get_raw( 'plugin', false ) | |
| 167 | + ); | |
| 168 | + } | |
| 169 | + | |
| 170 | + /** | |
| 171 | + * @author Leo Fajardo (@leorw) | |
| 132 | 172 | * @since 2.1.4 |
| 133 | 173 | */ |
| 134 | 174 | function catch_plugin_information_dialog_contents() { |
| 135 | - if ( | |
| 136 | - 'plugin-information' !== fs_request_get( 'tab', false ) || | |
| 137 | - $this->_fs->get_slug() !== fs_request_get( 'plugin', false ) | |
| 138 | - ) { | |
| 175 | + if ( ! $this->is_plugin_information_dialog_for_plugin() ) { | |
| 139 | 176 | return; |
| 140 | 177 | } |
| 141 | 178 | |
| 142 | 179 | add_action( 'admin_footer', array( &$this, 'edit_and_echo_plugin_information_dialog_contents' ), 0, 1 ); |
| @@ -152,9 +189,9 @@ | ||
| 152 | 189 | */ |
| 153 | 190 | function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { |
| 154 | 191 | if ( |
| 155 | 192 | 'plugin-information' !== fs_request_get( 'tab', false ) || |
| 156 | - $this->_fs->get_slug() !== fs_request_get( 'plugin', false ) | |
| 193 | + $this->_fs->get_slug() !== fs_request_get_raw( 'plugin', false ) | |
| 157 | 194 | ) { |
| 158 | 195 | return; |
| 159 | 196 | } |
| 160 | 197 | |
| @@ -165,17 +202,21 @@ | ||
| 165 | 202 | null; |
| 166 | 203 | |
| 167 | 204 | $contents = ob_get_clean(); |
| 168 | 205 | |
| 169 | - $update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' ); | |
| 206 | + $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_install_from_iframe"' ); | |
| 170 | 207 | |
| 171 | - if ( false !== $update_button_id_attribute_pos ) { | |
| 172 | - $update_button_start_pos = strrpos( | |
| 173 | - substr( $contents, 0, $update_button_id_attribute_pos ), | |
| 208 | + if ( false === $install_or_update_button_id_attribute_pos ) { | |
| 209 | + $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' ); | |
| 210 | + } | |
| 211 | + | |
| 212 | + if ( false !== $install_or_update_button_id_attribute_pos ) { | |
| 213 | + $install_or_update_button_start_pos = strrpos( | |
| 214 | + substr( $contents, 0, $install_or_update_button_id_attribute_pos ), | |
| 174 | 215 | '<a' |
| 175 | 216 | ); |
| 176 | 217 | |
| 177 | - $update_button_end_pos = ( strpos( $contents, '</a>', $update_button_id_attribute_pos ) + strlen( '</a>' ) ); | |
| 218 | + $install_or_update_button_end_pos = ( strpos( $contents, '</a>', $install_or_update_button_id_attribute_pos ) + strlen( '</a>' ) ); | |
| 178 | 219 | |
| 179 | 220 | /** |
| 180 | 221 | * The part of the contents without the update button. |
| 181 | 222 | * |
| @@ -181,11 +222,11 @@ | ||
| 181 | 222 | * |
| 182 | 223 | * @author Leo Fajardo (@leorw) |
| 183 | 224 | * @since 2.2.5 |
| 184 | 225 | */ |
| 185 | - $modified_contents = substr( $contents, 0, $update_button_start_pos ); | |
| 226 | + $modified_contents = substr( $contents, 0, $install_or_update_button_start_pos ); | |
| 186 | 227 | |
| 187 | - $update_button = substr( $contents, $update_button_start_pos, ( $update_button_end_pos - $update_button_start_pos ) ); | |
| 228 | + $install_or_update_button = substr( $contents, $install_or_update_button_start_pos, ( $install_or_update_button_end_pos - $install_or_update_button_start_pos ) ); | |
| 188 | 229 | |
| 189 | 230 | /** |
| 190 | 231 | * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license, |
| 191 | 232 | * the text will be "Renew license" and will link to the checkout page with the license's billing cycle |
| @@ -190,13 +231,13 @@ | ||
| 190 | 231 | * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license, |
| 191 | 232 | * the text will be "Renew license" and will link to the checkout page with the license's billing cycle |
| 192 | 233 | * and quota. If there's no license, the text will be "Buy license" and will link to the pricing page. |
| 193 | 234 | */ |
| 194 | - $update_button = preg_replace( | |
| 195 | - '/(\<a.+)(id="plugin_update_from_iframe")(.+href=")([^\s]+)(".*\>)(.+)(\<\/a>)/is', | |
| 235 | + $install_or_update_button = preg_replace( | |
| 236 | + '/(\<a.+)(id="plugin_(install|update)_from_iframe")(.+href=")([^\s]+)(".*\>)(.+)(\<\/a>)/is', | |
| 196 | 237 | is_object( $license ) ? |
| 197 | 238 | sprintf( |
| 198 | - '$1$3%s$5%s$7', | |
| 239 | + '$1$4%s$6%s$8', | |
| 199 | 240 | $this->_fs->checkout_url( |
| 200 | 241 | is_object( $subscription ) ? |
| 201 | 242 | ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) : |
| 202 | 243 | WP_FS__PERIOD_LIFETIME, |
| @@ -205,13 +246,13 @@ | ||
| 205 | 246 | ), |
| 206 | 247 | fs_text_inline( 'Renew license', 'renew-license', $this->_fs->get_slug() ) |
| 207 | 248 | ) : |
| 208 | 249 | sprintf( |
| 209 | - '$1$3%s$5%s$7', | |
| 250 | + '$1$4%s$6%s$8', | |
| 210 | 251 | $this->_fs->pricing_url(), |
| 211 | 252 | fs_text_inline( 'Buy license', 'buy-license', $this->_fs->get_slug() ) |
| 212 | 253 | ), |
| 213 | - $update_button | |
| 254 | + $install_or_update_button | |
| 214 | 255 | ); |
| 215 | 256 | |
| 216 | 257 | /** |
| 217 | 258 | * Append the modified button. |
| @@ -218,9 +259,9 @@ | ||
| 218 | 259 | * |
| 219 | 260 | * @author Leo Fajardo (@leorw) |
| 220 | 261 | * @since 2.2.5 |
| 221 | 262 | */ |
| 222 | - $modified_contents .= $update_button; | |
| 263 | + $modified_contents .= $install_or_update_button; | |
| 223 | 264 | |
| 224 | 265 | /** |
| 225 | 266 | * Append the remaining part of the contents after the update button. |
| 226 | 267 | * |
| @@ -226,9 +267,9 @@ | ||
| 226 | 267 | * |
| 227 | 268 | * @author Leo Fajardo (@leorw) |
| 228 | 269 | * @since 2.2.5 |
| 229 | 270 | */ |
| 230 | - $modified_contents .= substr( $contents, $update_button_end_pos ); | |
| 271 | + $modified_contents .= substr( $contents, $install_or_update_button_end_pos ); | |
| 231 | 272 | |
| 232 | 273 | $contents = $modified_contents; |
| 233 | 274 | } |
| 234 | 275 | |
| @@ -498,38 +539,40 @@ | ||
| 498 | 539 | ) { |
| 499 | 540 | return $transient_data; |
| 500 | 541 | } |
| 501 | 542 | |
| 543 | + // Alias. | |
| 544 | + $basename = $this->_fs->premium_plugin_basename(); | |
| 545 | + | |
| 502 | 546 | global $wp_current_filter; |
| 503 | 547 | |
| 504 | - $current_plugin_version = $this->_fs->get_plugin_version(); | |
| 505 | - | |
| 506 | - if ( ! empty( $wp_current_filter ) && 'upgrader_process_complete' === $wp_current_filter[0] ) { | |
| 507 | - if ( | |
| 508 | - is_null( $this->_update_details ) || | |
| 509 | - ( is_object( $this->_update_details ) && $this->_update_details->new_version !== $current_plugin_version ) | |
| 510 | - ) { | |
| 548 | + /** | |
| 549 | + * During bulk updates, avoid re-injecting update data for the plugin itself once it has already been updated. | |
| 550 | + * | |
| 551 | + * If the custom package is re-added to the transient after the plugin update, WordPress may detect the package again and incorrectly report "The plugin is at the latest version" for a pending update, since the custom package version matches the currently installed version. | |
| 552 | + * | |
| 553 | + * Behavior differs depending on how the bulk update is triggered. Please refer to the inline comments for each flow below for details. | |
| 554 | + */ | |
| 555 | + if ( | |
| 556 | + ! empty( $wp_current_filter ) && ( | |
| 511 | 557 | /** |
| 512 | - * After an update, clear the stored update details and reparse the plugin's main file in order to get | |
| 513 | - * the updated version's information and prevent the previous update information from showing up on the | |
| 514 | - * updates page. | |
| 515 | - * | |
| 516 | - * @author Leo Fajardo (@leorw) | |
| 517 | - * @since 2.3.1 | |
| 558 | + * update-core.php and other upgrader pages: | |
| 559 | + * The `upgrader_process_complete` action fires only once after all updates have finished. In this case, it is the current action (`$wp_current_filter[0]`), while `self::$_upgrade_basename` may contain any plugin basename. | |
| 518 | 560 | */ |
| 519 | - $this->_update_details = null; | |
| 520 | - $current_plugin_version = $this->_fs->get_plugin_version( true ); | |
| 521 | - } | |
| 561 | + 'upgrader_process_complete' === $wp_current_filter[0] || | |
| 562 | + /** | |
| 563 | + * AJAX bulk updates (e.g., from the Plugins page): | |
| 564 | + * The `upgrader_process_complete` action fires multiple times — once for each plugin after it finishes updating. In this flow, it is not the current action (`$wp_current_filter[0]`) because it is triggered from another action. Instead, we compare `self::$_upgrade_basename` with the basename of the plugin currently being updated, since the `upgrader_process_complete` action runs separately for each plugin. | |
| 565 | + */ | |
| 566 | + ( in_array( 'upgrader_process_complete', $wp_current_filter ) && self::$_upgrade_basename === $basename ) | |
| 567 | + ) | |
| 568 | + ) { | |
| 569 | + return $transient_data; | |
| 522 | 570 | } |
| 523 | 571 | |
| 524 | 572 | if ( ! isset( $this->_update_details ) ) { |
| 525 | 573 | // Get plugin's newest update. |
| 526 | - $new_version = $this->_fs->get_update( | |
| 527 | - false, | |
| 528 | - fs_request_get_bool( 'force-check' ), | |
| 529 | - WP_FS__TIME_24_HOURS_IN_SEC / 24, | |
| 530 | - $current_plugin_version | |
| 531 | - ); | |
| 574 | + $new_version = $this->_fs->get_update( false, fs_request_get_bool( 'force-check' ) ); | |
| 532 | 575 | |
| 533 | 576 | $this->_update_details = false; |
| 534 | 577 | |
| 535 | 578 | if ( is_object( $new_version ) && $this->is_new_version_premium( $new_version ) ) { |
| @@ -545,11 +588,8 @@ | ||
| 545 | 588 | $this->_update_details = $this->get_update_details( $new_version ); |
| 546 | 589 | } |
| 547 | 590 | } |
| 548 | 591 | |
| 549 | - // Alias. | |
| 550 | - $basename = $this->_fs->premium_plugin_basename(); | |
| 551 | - | |
| 552 | 592 | if ( is_object( $this->_update_details ) ) { |
| 553 | 593 | if ( isset( $transient_data->no_update ) ) { |
| 554 | 594 | unset( $transient_data->no_update[ $basename ] ); |
| 555 | 595 | } |
| @@ -609,9 +649,9 @@ | ||
| 609 | 649 | } |
| 610 | 650 | |
| 611 | 651 | $slug = $this->_fs->get_slug(); |
| 612 | 652 | |
| 613 | - if ( $this->_fs->is_org_repo_compliant() && $this->_fs->is_freemium() ) { | |
| 653 | + if ( $this->can_fetch_data_from_wp_org() ) { | |
| 614 | 654 | if ( ! isset( $this->_translation_updates ) ) { |
| 615 | 655 | $this->_translation_updates = array(); |
| 616 | 656 | |
| 617 | 657 | $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug ); |
| @@ -678,18 +718,10 @@ | ||
| 678 | 718 | 'default' => $icon, |
| 679 | 719 | ); |
| 680 | 720 | } |
| 681 | 721 | |
| 682 | - if ( $this->_fs->is_premium() ) { | |
| 683 | - $latest_tag = $this->_fs->_fetch_latest_version( $this->_fs->get_id(), false ); | |
| 684 | - | |
| 685 | - if ( | |
| 686 | - isset( $latest_tag->readme ) && | |
| 687 | - isset( $latest_tag->readme->upgrade_notice ) && | |
| 688 | - ! empty( $latest_tag->readme->upgrade_notice ) | |
| 689 | - ) { | |
| 690 | - $update->upgrade_notice = $latest_tag->readme->upgrade_notice; | |
| 691 | - } | |
| 722 | + if ( $this->_fs->is_premium() && ! empty( $new_version->upgrade_notice ) ) { | |
| 723 | + $update->upgrade_notice = $new_version->upgrade_notice; | |
| 692 | 724 | } |
| 693 | 725 | |
| 694 | 726 | $update->{$this->_fs->get_module_type()} = $this->_fs->get_plugin_basename(); |
| 695 | 727 | |
| @@ -704,15 +736,10 @@ | ||
| 704 | 736 | * |
| 705 | 737 | * @return bool |
| 706 | 738 | */ |
| 707 | 739 | private function is_new_version_premium( FS_Plugin_Tag $new_version ) { |
| 708 | - $query_str = parse_url( $new_version->url, PHP_URL_QUERY ); | |
| 709 | - if ( empty( $query_str ) ) { | |
| 710 | - return false; | |
| 711 | - } | |
| 740 | + $params = fs_parse_url_params( $new_version->url ); | |
| 712 | 741 | |
| 713 | - parse_str( $query_str, $params ); | |
| 714 | - | |
| 715 | 742 | return ( isset( $params['is_premium'] ) && 'true' == $params['is_premium'] ); |
| 716 | 743 | } |
| 717 | 744 | |
| 718 | 745 | /** |
| @@ -837,30 +864,36 @@ | ||
| 837 | 864 | * |
| 838 | 865 | * @return bool|mixed |
| 839 | 866 | */ |
| 840 | 867 | static function _fetch_plugin_info_from_repository( $action, $args ) { |
| 841 | - $url = $http_url = 'http://api.wordpress.org/plugins/info/1.0/'; | |
| 842 | - if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) { | |
| 868 | + $url = $http_url = 'http://api.wordpress.org/plugins/info/1.2/'; | |
| 869 | + $url = add_query_arg( | |
| 870 | + array( | |
| 871 | + 'action' => $action, | |
| 872 | + 'request' => $args, | |
| 873 | + ), | |
| 874 | + $url | |
| 875 | + ); | |
| 876 | + | |
| 877 | + if ( wp_http_supports( array( 'ssl' ) ) ) { | |
| 843 | 878 | $url = set_url_scheme( $url, 'https' ); |
| 844 | 879 | } |
| 845 | 880 | |
| 846 | - $args = array( | |
| 847 | - 'timeout' => 15, | |
| 848 | - 'body' => array( | |
| 849 | - 'action' => $action, | |
| 850 | - 'request' => serialize( $args ) | |
| 851 | - ) | |
| 852 | - ); | |
| 881 | + // The new endpoint version serves only GET requests. | |
| 882 | + $request = wp_remote_get( $url, array( 'timeout' => 15 ) ); | |
| 853 | 883 | |
| 854 | - $request = wp_remote_post( $url, $args ); | |
| 855 | - | |
| 856 | 884 | if ( is_wp_error( $request ) ) { |
| 857 | 885 | return false; |
| 858 | 886 | } |
| 859 | 887 | |
| 860 | - $res = maybe_unserialize( wp_remote_retrieve_body( $request ) ); | |
| 888 | + $res = json_decode( wp_remote_retrieve_body( $request ), true ); | |
| 861 | 889 | |
| 862 | - if ( ! is_object( $res ) && ! is_array( $res ) ) { | |
| 890 | + if ( is_array( $res ) ) { | |
| 891 | + // Object casting is required in order to match the info/1.0 format. We are not decoding directly into an object as we need some fields to remain an array (e.g., $res->sections). | |
| 892 | + $res = (object) $res; | |
| 893 | + } | |
| 894 | + | |
| 895 | + if ( ! is_object( $res ) || isset( $res->error ) ) { | |
| 863 | 896 | return false; |
| 864 | 897 | } |
| 865 | 898 | |
| 866 | 899 | return $res; |
| @@ -866,8 +899,18 @@ | ||
| 866 | 899 | return $res; |
| 867 | 900 | } |
| 868 | 901 | |
| 869 | 902 | /** |
| 903 | + * Returns true if the product can fetch data from WordPress.org. | |
| 904 | + * | |
| 905 | + * @author Leo Fajardo (@leorw) | |
| 906 | + * @since 2.7.4 | |
| 907 | + */ | |
| 908 | + private function can_fetch_data_from_wp_org() { | |
| 909 | + return ( $this->_fs->is_org_repo_compliant() && $this->_fs->is_freemium() ); | |
| 910 | + } | |
| 911 | + | |
| 912 | + /** | |
| 870 | 913 | * Fetches module translation updates from wordpress.org. |
| 871 | 914 | * |
| 872 | 915 | * @author Leo Fajardo (@leorw) |
| 873 | 916 | * @since 2.1.2 |
| @@ -1049,9 +1092,9 @@ | ||
| 1049 | 1092 | $is_addon = true; |
| 1050 | 1093 | } |
| 1051 | 1094 | |
| 1052 | 1095 | $plugin_in_repo = false; |
| 1053 | - if ( ! $is_addon ) { | |
| 1096 | + if ( ! $is_addon && $this->can_fetch_data_from_wp_org() ) { | |
| 1054 | 1097 | // Try to fetch info from .org repository. |
| 1055 | 1098 | $data = self::_fetch_plugin_info_from_repository( $action, $args ); |
| 1056 | 1099 | |
| 1057 | 1100 | $plugin_in_repo = ( false !== $data ); |
| @@ -1182,9 +1225,9 @@ | ||
| 1182 | 1225 | * |
| 1183 | 1226 | * @return object |
| 1184 | 1227 | */ |
| 1185 | 1228 | private function get_latest_download_details( $addon_id = false, $newer_than = false, $fetch_readme = true ) { |
| 1186 | - return $this->_fs->_fetch_latest_version( $addon_id, true, WP_FS__TIME_24_HOURS_IN_SEC, $newer_than, $fetch_readme ); | |
| 1229 | + return $this->_fs->_fetch_latest_version( $addon_id, true, FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than, $fetch_readme ); | |
| 1187 | 1230 | } |
| 1188 | 1231 | |
| 1189 | 1232 | /** |
| 1190 | 1233 | * Checks if a given basename has a matching folder name |