PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / trunk
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites vtrunk
4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 2.6.1 All 38 releases
← All changes | freemius/includes/class-fs-plugin-updater.php +1627 -1584 2.5.3trunk View file →
@@ -1,1584 +1,1627 @@
1 -<?php
2 - /**
3 - * @package Freemius
4 - * @copyright Copyright (c) 2015, Freemius, Inc.
5 - * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 - * @since 1.0.4
7 - *
8 - * @link https://github.com/easydigitaldownloads/EDD-License-handler/blob/master/EDD_SL_Plugin_Updater.php
9 - */
10 -
11 - if ( ! defined( 'ABSPATH' ) ) {
12 - exit;
13 - }
14 -
15 - class FS_Plugin_Updater {
16 -
17 - /**
18 - * @var Freemius
19 - * @since 1.0.4
20 - */
21 - private $_fs;
22 - /**
23 - * @var FS_Logger
24 - * @since 1.0.4
25 - */
26 - private $_logger;
27 - /**
28 - * @var object
29 - * @since 1.1.8.1
30 - */
31 - private $_update_details;
32 - /**
33 - * @var array
34 - * @since 2.1.2
35 - */
36 - private $_translation_updates;
37 -
38 - private static $_upgrade_basename = null;
39 -
40 - #--------------------------------------------------------------------------------
41 - #region Singleton
42 - #--------------------------------------------------------------------------------
43 -
44 - /**
45 - * @var FS_Plugin_Updater[]
46 - * @since 2.0.0
47 - */
48 - private static $_INSTANCES = array();
49 -
50 - /**
51 - * @param Freemius $freemius
52 - *
53 - * @return FS_Plugin_Updater
54 - */
55 - static function instance( Freemius $freemius ) {
56 - $key = $freemius->get_id();
57 -
58 - if ( ! isset( self::$_INSTANCES[ $key ] ) ) {
59 - self::$_INSTANCES[ $key ] = new self( $freemius );
60 - }
61 -
62 - return self::$_INSTANCES[ $key ];
63 - }
64 -
65 - #endregion
66 -
67 - private function __construct( Freemius $freemius ) {
68 - $this->_fs = $freemius;
69 -
70 - $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $freemius->get_slug() . '_updater', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
71 -
72 - $this->filters();
73 - }
74 -
75 - /**
76 - * Initiate required filters.
77 - *
78 - * @author Vova Feldman (@svovaf)
79 - * @since 1.0.4
80 - */
81 - private function filters() {
82 - // Override request for plugin information
83 - add_filter( 'plugins_api', array( &$this, 'plugins_api_filter' ), 10, 3 );
84 -
85 - $this->add_transient_filters();
86 -
87 - /**
88 - * If user has the premium plugin's code but do NOT have an active license,
89 - * encourage him to upgrade by showing that there's a new release, but instead
90 - * of showing an update link, show upgrade link to the pricing page.
91 - *
92 - * @since 1.1.6
93 - *
94 - */
95 - // WP 2.9+
96 - add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
97 - &$this,
98 - 'catch_plugin_update_row'
99 - ), 9 );
100 - add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
101 - &$this,
102 - 'edit_and_echo_plugin_update_row'
103 - ), 11, 2 );
104 -
105 - if ( ! $this->_fs->has_any_active_valid_license() ) {
106 - add_action( 'admin_head', array( &$this, 'catch_plugin_information_dialog_contents' ) );
107 - }
108 -
109 - if ( ! WP_FS__IS_PRODUCTION_MODE ) {
110 - add_filter( 'http_request_host_is_external', array(
111 - $this,
112 - 'http_request_host_is_external_filter'
113 - ), 10, 3 );
114 - }
115 -
116 - if ( $this->_fs->is_premium() ) {
117 - if ( ! $this->is_correct_folder_name() ) {
118 - add_filter( 'upgrader_post_install', array( &$this, '_maybe_update_folder_name' ), 10, 3 );
119 - }
120 -
121 - add_filter( 'upgrader_pre_install', array( 'FS_Plugin_Updater', '_store_basename_for_source_adjustment' ), 1, 2 );
122 - add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
123 -
124 - if ( ! $this->_fs->has_any_active_valid_license() ) {
125 - add_filter( 'wp_prepare_themes_for_js', array( &$this, 'change_theme_update_info_html' ), 10, 1 );
126 - }
127 - }
128 - }
129 -
130 - /**
131 - * @author Leo Fajardo (@leorw)
132 - * @since 2.1.4
133 - */
134 - 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 - ) {
139 - return;
140 - }
141 -
142 - add_action( 'admin_footer', array( &$this, 'edit_and_echo_plugin_information_dialog_contents' ), 0, 1 );
143 -
144 - ob_start();
145 - }
146 -
147 - /**
148 - * @author Leo Fajardo (@leorw)
149 - * @since 2.1.4
150 - *
151 - * @param string $hook_suffix
152 - */
153 - function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) {
154 - if (
155 - 'plugin-information' !== fs_request_get( 'tab', false ) ||
156 - $this->_fs->get_slug() !== fs_request_get( 'plugin', false )
157 - ) {
158 - return;
159 - }
160 -
161 - $license = $this->_fs->_get_license();
162 -
163 - $subscription = ( is_object( $license ) && ! $license->is_lifetime() ) ?
164 - $this->_fs->_get_subscription( $license->id ) :
165 - null;
166 -
167 - $contents = ob_get_clean();
168 -
169 - $update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' );
170 -
171 - if ( false !== $update_button_id_attribute_pos ) {
172 - $update_button_start_pos = strrpos(
173 - substr( $contents, 0, $update_button_id_attribute_pos ),
174 - '<a'
175 - );
176 -
177 - $update_button_end_pos = ( strpos( $contents, '</a>', $update_button_id_attribute_pos ) + strlen( '</a>' ) );
178 -
179 - /**
180 - * The part of the contents without the update button.
181 - *
182 - * @author Leo Fajardo (@leorw)
183 - * @since 2.2.5
184 - */
185 - $modified_contents = substr( $contents, 0, $update_button_start_pos );
186 -
187 - $update_button = substr( $contents, $update_button_start_pos, ( $update_button_end_pos - $update_button_start_pos ) );
188 -
189 - /**
190 - * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license,
191 - * the text will be "Renew license" and will link to the checkout page with the license's billing cycle
192 - * and quota. If there's no license, the text will be "Buy license" and will link to the pricing page.
193 - */
194 - $update_button = preg_replace(
195 - '/(\<a.+)(id="plugin_update_from_iframe")(.+href=")([^\s]+)(".*\>)(.+)(\<\/a>)/is',
196 - is_object( $license ) ?
197 - sprintf(
198 - '$1$3%s$5%s$7',
199 - $this->_fs->checkout_url(
200 - is_object( $subscription ) ?
201 - ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
202 - WP_FS__PERIOD_LIFETIME,
203 - false,
204 - array( 'licenses' => $license->quota )
205 - ),
206 - fs_text_inline( 'Renew license', 'renew-license', $this->_fs->get_slug() )
207 - ) :
208 - sprintf(
209 - '$1$3%s$5%s$7',
210 - $this->_fs->pricing_url(),
211 - fs_text_inline( 'Buy license', 'buy-license', $this->_fs->get_slug() )
212 - ),
213 - $update_button
214 - );
215 -
216 - /**
217 - * Append the modified button.
218 - *
219 - * @author Leo Fajardo (@leorw)
220 - * @since 2.2.5
221 - */
222 - $modified_contents .= $update_button;
223 -
224 - /**
225 - * Append the remaining part of the contents after the update button.
226 - *
227 - * @author Leo Fajardo (@leorw)
228 - * @since 2.2.5
229 - */
230 - $modified_contents .= substr( $contents, $update_button_end_pos );
231 -
232 - $contents = $modified_contents;
233 - }
234 -
235 - echo $contents;
236 - }
237 -
238 - /**
239 - * @author Vova Feldman (@svovaf)
240 - * @since 2.0.0
241 - */
242 - private function add_transient_filters() {
243 - if (
244 - $this->_fs->is_premium() &&
245 - $this->_fs->is_registered() &&
246 - ! FS_Permission_Manager::instance( $this->_fs )->is_essentials_tracking_allowed()
247 - ) {
248 - $this->_logger->log( 'Opted out sites cannot receive automatic software updates.' );
249 -
250 - return;
251 - }
252 -
253 - add_filter( 'pre_set_site_transient_update_plugins', array(
254 - &$this,
255 - 'pre_set_site_transient_update_plugins_filter'
256 - ) );
257 -
258 - add_filter( 'pre_set_site_transient_update_themes', array(
259 - &$this,
260 - 'pre_set_site_transient_update_plugins_filter'
261 - ) );
262 - }
263 -
264 - /**
265 - * @author Vova Feldman (@svovaf)
266 - * @since 2.0.0
267 - */
268 - private function remove_transient_filters() {
269 - remove_filter( 'pre_set_site_transient_update_plugins', array(
270 - &$this,
271 - 'pre_set_site_transient_update_plugins_filter'
272 - ) );
273 -
274 - remove_filter( 'pre_set_site_transient_update_themes', array(
275 - &$this,
276 - 'pre_set_site_transient_update_plugins_filter'
277 - ) );
278 - }
279 -
280 - /**
281 - * Capture plugin update row by turning output buffering.
282 - *
283 - * @author Vova Feldman (@svovaf)
284 - * @since 1.1.6
285 - */
286 - function catch_plugin_update_row() {
287 - ob_start();
288 - }
289 -
290 - /**
291 - * Overrides default update message format with "renew your license" message.
292 - *
293 - * @author Vova Feldman (@svovaf)
294 - * @since 1.1.6
295 - *
296 - * @param string $file
297 - * @param array $plugin_data
298 - */
299 - function edit_and_echo_plugin_update_row( $file, $plugin_data ) {
300 - $plugin_update_row = ob_get_clean();
301 -
302 - $current = get_site_transient( 'update_plugins' );
303 - if ( ! isset( $current->response[ $file ] ) ) {
304 - echo $plugin_update_row;
305 -
306 - return;
307 - }
308 -
309 - $r = $current->response[ $file ];
310 -
311 - $has_beta_update = $this->_fs->has_beta_update();
312 -
313 - if ( $this->_fs->has_any_active_valid_license() ) {
314 - if ( $has_beta_update ) {
315 - /**
316 - * Turn the "new version" text into "new Beta version".
317 - *
318 - * Sample input:
319 - * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
320 - * Output:
321 - * There is a new Beta version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
322 - *
323 - * @author Leo Fajardo (@leorw)
324 - * @since 2.3.0
325 - */
326 - $plugin_update_row = preg_replace(
327 - '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
328 - (
329 - '$1' .
330 - sprintf(
331 - fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
332 - $has_beta_update ?
333 - fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
334 - fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() ),
335 - $this->_fs->get_plugin_title()
336 - ) .
337 - ' ' .
338 - '$3' .
339 - '$6'
340 - ),
341 - $plugin_update_row
342 - );
343 - }
344 - } else {
345 - /**
346 - * Turn the "new version" text into a link that opens the plugin information dialog when clicked and
347 - * make the "View version x details" text link to the checkout page instead of opening the plugin
348 - * information dialog when clicked.
349 - *
350 - * Sample input:
351 - * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
352 - * Output:
353 - * There is a <a href="...>new version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
354 - * OR
355 - * There is a <a href="...>new Beta version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
356 - *
357 - * @author Leo Fajardo (@leorw)
358 - */
359 - $plugin_update_row = preg_replace(
360 - '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
361 - (
362 - '$1' .
363 - sprintf(
364 - fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
365 - sprintf(
366 - '<a href="$4"%s>%s</a>',
367 - '$5',
368 - $has_beta_update ?
369 - fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
370 - fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() )
371 - ),
372 - $this->_fs->get_plugin_title()
373 - ) .
374 - ' ' .
375 - $this->_fs->version_upgrade_checkout_link( $r->new_version ) .
376 - '$6'
377 - ),
378 - $plugin_update_row
379 - );
380 - }
381 -
382 - if (
383 - $this->_fs->is_plugin() &&
384 - isset( $r->upgrade_notice ) &&
385 - strlen( trim( $r->upgrade_notice ) ) > 0
386 - ) {
387 - $slug = $this->_fs->get_slug();
388 -
389 - $upgrade_notice_html = sprintf(
390 - '<p class="notice fs-upgrade-notice fs-slug-%1$s fs-type-%2$s" data-slug="%1$s" data-type="%2$s"><strong>%3$s</strong> %4$s</p>',
391 - $slug,
392 - $this->_fs->get_module_type(),
393 - fs_text_inline( 'Important Upgrade Notice:', 'upgrade_notice', $slug ),
394 - esc_html( $r->upgrade_notice )
395 - );
396 -
397 - $plugin_update_row = str_replace( '</div>', '</div>' . $upgrade_notice_html, $plugin_update_row );
398 - }
399 -
400 - echo $plugin_update_row;
401 - }
402 -
403 - /**
404 - * @author Leo Fajardo (@leorw)
405 - * @since 2.0.2
406 - *
407 - * @param array $prepared_themes
408 - *
409 - * @return array
410 - */
411 - function change_theme_update_info_html( $prepared_themes ) {
412 - $theme_basename = $this->_fs->get_plugin_basename();
413 -
414 - if ( ! isset( $prepared_themes[ $theme_basename ] ) ) {
415 - return $prepared_themes;
416 - }
417 -
418 - $themes_update = get_site_transient( 'update_themes' );
419 - if ( ! isset( $themes_update->response[ $theme_basename ] ) ||
420 - empty( $themes_update->response[ $theme_basename ]['package'] )
421 - ) {
422 - return $prepared_themes;
423 - }
424 -
425 - $prepared_themes[ $theme_basename ]['update'] = preg_replace(
426 - '/(\<p.+>)(.+)(\<a.+\<a.+)\.(.+\<\/p\>)/is',
427 - '$1 $2 ' . $this->_fs->version_upgrade_checkout_link( $themes_update->response[ $theme_basename ]['new_version'] ) .
428 - '$4',
429 - $prepared_themes[ $theme_basename ]['update']
430 - );
431 -
432 - // Set to false to prevent the "Update now" link for the context theme from being shown on the "Themes" page.
433 - $prepared_themes[ $theme_basename ]['hasPackage'] = false;
434 -
435 - return $prepared_themes;
436 - }
437 -
438 - /**
439 - * Since WP version 3.6, a new security feature was added that denies access to repository with a local ip.
440 - * During development mode we want to be able updating plugin versions via our localhost repository. This
441 - * filter white-list all domains including "api.freemius".
442 - *
443 - * @link http://www.emanueletessore.com/wordpress-download-failed-valid-url-provided/
444 - *
445 - * @author Vova Feldman (@svovaf)
446 - * @since 1.0.4
447 - *
448 - * @param bool $allow
449 - * @param string $host
450 - * @param string $url
451 - *
452 - * @return bool
453 - */
454 - function http_request_host_is_external_filter( $allow, $host, $url ) {
455 - return ( false !== strpos( $host, 'freemius' ) ) ? true : $allow;
456 - }
457 -
458 - /**
459 - * Check for Updates at the defined API endpoint and modify the update array.
460 - *
461 - * This function dives into the update api just when WordPress creates its update array,
462 - * then adds a custom API call and injects the custom plugin data retrieved from the API.
463 - * It is reassembled from parts of the native WordPress plugin update code.
464 - * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
465 - *
466 - * @author Vova Feldman (@svovaf)
467 - * @since 1.0.4
468 - *
469 - * @uses FS_Api
470 - *
471 - * @param object $transient_data Update array build by WordPress.
472 - *
473 - * @return object Modified update array with custom plugin data.
474 - */
475 - function pre_set_site_transient_update_plugins_filter( $transient_data ) {
476 - $this->_logger->entrance();
477 -
478 - /**
479 - * "plugins" or "themes".
480 - *
481 - * @author Leo Fajardo (@leorw)
482 - * @since 1.2.2
483 - */
484 - $module_type = $this->_fs->get_module_type() . 's';
485 -
486 - /**
487 - * Ensure that we don't mix plugins update info with themes update info.
488 - *
489 - * @author Leo Fajardo (@leorw)
490 - * @since 1.2.2
491 - */
492 - if ( "pre_set_site_transient_update_{$module_type}" !== current_filter() ) {
493 - return $transient_data;
494 - }
495 -
496 - if ( empty( $transient_data ) ||
497 - defined( 'WP_FS__UNINSTALL_MODE' )
498 - ) {
499 - return $transient_data;
500 - }
501 -
502 - global $wp_current_filter;
503 -
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 - ) {
511 - /**
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
518 - */
519 - $this->_update_details = null;
520 - $current_plugin_version = $this->_fs->get_plugin_version( true );
521 - }
522 - }
523 -
524 - if ( ! isset( $this->_update_details ) ) {
525 - // 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 - );
532 -
533 - $this->_update_details = false;
534 -
535 - if ( is_object( $new_version ) && $this->is_new_version_premium( $new_version ) ) {
536 - $this->_logger->log( 'Found newer plugin version ' . $new_version->version );
537 -
538 - /**
539 - * Cache plugin details locally since set_site_transient( 'update_plugins' )
540 - * called multiple times and the non wp.org plugins are filtered after the
541 - * call to .org.
542 - *
543 - * @since 1.1.8.1
544 - */
545 - $this->_update_details = $this->get_update_details( $new_version );
546 - }
547 - }
548 -
549 - // Alias.
550 - $basename = $this->_fs->premium_plugin_basename();
551 -
552 - if ( is_object( $this->_update_details ) ) {
553 - if ( isset( $transient_data->no_update ) ) {
554 - unset( $transient_data->no_update[ $basename ] );
555 - }
556 -
557 - if ( ! isset( $transient_data->response ) ) {
558 - $transient_data->response = array();
559 - }
560 -
561 - // Add plugin to transient data.
562 - $transient_data->response[ $basename ] = $this->_fs->is_plugin() ?
563 - $this->_update_details :
564 - (array) $this->_update_details;
565 - } else {
566 - if ( isset( $transient_data->response ) ) {
567 - /**
568 - * Ensure that there's no update data for the plugin to prevent upgrading the premium version to the latest free version.
569 - *
570 - * @author Leo Fajardo (@leorw)
571 - * @since 2.3.0
572 - */
573 - unset( $transient_data->response[ $basename ] );
574 - }
575 -
576 - if ( ! isset( $transient_data->no_update ) ) {
577 - $transient_data->no_update = array();
578 - }
579 -
580 - /**
581 - * Add product to no_update transient data to properly integrate with WP 5.5 auto-updates UI.
582 - *
583 - * @since 2.4.1
584 - * @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
585 - */
586 - $transient_data->no_update[ $basename ] = $this->_fs->is_plugin() ?
587 - (object) array(
588 - 'id' => $basename,
589 - 'slug' => $this->_fs->get_slug(),
590 - 'plugin' => $basename,
591 - 'new_version' => $this->_fs->get_plugin_version(),
592 - 'url' => '',
593 - 'package' => '',
594 - 'icons' => array(),
595 - 'banners' => array(),
596 - 'banners_rtl' => array(),
597 - 'tested' => '',
598 - 'requires_php' => '',
599 - 'compatibility' => new stdClass(),
600 - ) :
601 - array(
602 - 'theme' => $basename,
603 - 'new_version' => $this->_fs->get_plugin_version(),
604 - 'url' => '',
605 - 'package' => '',
606 - 'requires' => '',
607 - 'requires_php' => '',
608 - );
609 - }
610 -
611 - $slug = $this->_fs->get_slug();
612 -
613 - if ( $this->_fs->is_org_repo_compliant() && $this->_fs->is_freemium() ) {
614 - if ( ! isset( $this->_translation_updates ) ) {
615 - $this->_translation_updates = array();
616 -
617 - $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug );
618 - if ( ! empty( $translation_updates ) ) {
619 - $this->_translation_updates = $translation_updates;
620 - }
621 - }
622 -
623 - if ( ! empty( $this->_translation_updates ) ) {
624 - $all_translation_updates = ( isset( $transient_data->translations ) && is_array( $transient_data->translations ) ) ?
625 - $transient_data->translations :
626 - array();
627 -
628 - $current_plugin_translation_updates_map = array();
629 - foreach ( $all_translation_updates as $key => $translation_update ) {
630 - if ( $module_type === ( $translation_update['type'] . 's' ) && $slug === $translation_update['slug'] ) {
631 - $current_plugin_translation_updates_map[ $translation_update['language'] ] = $translation_update;
632 - unset( $all_translation_updates[ $key ] );
633 - }
634 - }
635 -
636 - foreach ( $this->_translation_updates as $translation_update ) {
637 - $lang = $translation_update['language'];
638 - if ( ! isset( $current_plugin_translation_updates_map[ $lang ] ) ||
639 - version_compare( $translation_update['version'], $current_plugin_translation_updates_map[ $lang ]['version'], '>' )
640 - ) {
641 - $current_plugin_translation_updates_map[ $lang ] = $translation_update;
642 - }
643 - }
644 -
645 - $transient_data->translations = array_merge( $all_translation_updates, array_values( $current_plugin_translation_updates_map ) );
646 - }
647 - }
648 -
649 - return $transient_data;
650 - }
651 -
652 - /**
653 - * Get module's required data for the updates' mechanism.
654 - *
655 - * @author Vova Feldman (@svovaf)
656 - * @since 2.0.0
657 - *
658 - * @param \FS_Plugin_Tag $new_version
659 - *
660 - * @return object
661 - */
662 - function get_update_details( FS_Plugin_Tag $new_version ) {
663 - $update = new stdClass();
664 - $update->slug = $this->_fs->get_slug();
665 - $update->new_version = $new_version->version;
666 - $update->url = WP_FS__ADDRESS;
667 - $update->package = $new_version->url;
668 - $update->tested = self::get_tested_wp_version( $new_version->tested_up_to_version );
669 - $update->requires = $new_version->requires_platform_version;
670 - $update->requires_php = $new_version->requires_programming_language_version;
671 -
672 - $icon = $this->_fs->get_local_icon_url();
673 -
674 - if ( ! empty( $icon ) ) {
675 - $update->icons = array(
676 -// '1x' => $icon,
677 -// '2x' => $icon,
678 - 'default' => $icon,
679 - );
680 - }
681 -
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 - }
692 - }
693 -
694 - $update->{$this->_fs->get_module_type()} = $this->_fs->get_plugin_basename();
695 -
696 - return $update;
697 - }
698 -
699 - /**
700 - * @author Leo Fajardo (@leorw)
701 - * @since 2.3.0
702 - *
703 - * @param FS_Plugin_Tag $new_version
704 - *
705 - * @return bool
706 - */
707 - 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 - }
712 -
713 - parse_str( $query_str, $params );
714 -
715 - return ( isset( $params['is_premium'] ) && 'true' == $params['is_premium'] );
716 - }
717 -
718 - /**
719 - * Update the updates transient with the module's update information.
720 - *
721 - * This method is required for multisite environment.
722 - * If a module is site activated (not network) and not on the main site,
723 - * the module will NOT be executed on the network level, therefore, the
724 - * custom updates logic will not be executed as well, so unless we force
725 - * the injection of the update into the updates transient, premium updates
726 - * will not work.
727 - *
728 - * @author Vova Feldman (@svovaf)
729 - * @since 2.0.0
730 - *
731 - * @param \FS_Plugin_Tag $new_version
732 - */
733 - function set_update_data( FS_Plugin_Tag $new_version ) {
734 - $this->_logger->entrance();
735 -
736 - if ( ! $this->is_new_version_premium( $new_version ) ) {
737 - return;
738 - }
739 -
740 - $transient_key = "update_{$this->_fs->get_module_type()}s";
741 -
742 - $transient_data = get_site_transient( $transient_key );
743 -
744 - $transient_data = is_object( $transient_data ) ?
745 - $transient_data :
746 - new stdClass();
747 -
748 - // Alias.
749 - $basename = $this->_fs->get_plugin_basename();
750 - $is_plugin = $this->_fs->is_plugin();
751 -
752 - if ( ! isset( $transient_data->response ) ||
753 - ! is_array( $transient_data->response )
754 - ) {
755 - $transient_data->response = array();
756 - } else if ( ! empty( $transient_data->response[ $basename ] ) ) {
757 - $version = $is_plugin ?
758 - ( ! empty( $transient_data->response[ $basename ]->new_version ) ?
759 - $transient_data->response[ $basename ]->new_version :
760 - null
761 - ) : ( ! empty( $transient_data->response[ $basename ]['new_version'] ) ?
762 - $transient_data->response[ $basename ]['new_version'] :
763 - null
764 - );
765 -
766 - if ( $version == $new_version->version ) {
767 - // The update data is already set.
768 - return;
769 - }
770 - }
771 -
772 - // Remove the added filters.
773 - $this->remove_transient_filters();
774 -
775 - $this->_update_details = $this->get_update_details( $new_version );
776 -
777 - // Set update data in transient.
778 - $transient_data->response[ $basename ] = $is_plugin ?
779 - $this->_update_details :
780 - (array) $this->_update_details;
781 -
782 - if ( ! isset( $transient_data->checked ) ||
783 - ! is_array( $transient_data->checked )
784 - ) {
785 - $transient_data->checked = array();
786 - }
787 -
788 - // Flag the module as if it was already checked.
789 - $transient_data->checked[ $basename ] = $this->_fs->get_plugin_version();
790 - $transient_data->last_checked = time();
791 -
792 - set_site_transient( $transient_key, $transient_data );
793 -
794 - $this->add_transient_filters();
795 - }
796 -
797 - /**
798 - * @author Leo Fajardo (@leorw)
799 - * @since 2.0.2
800 - */
801 - function delete_update_data() {
802 - $this->_logger->entrance();
803 -
804 - $transient_key = "update_{$this->_fs->get_module_type()}s";
805 -
806 - $transient_data = get_site_transient( $transient_key );
807 -
808 - // Alias
809 - $basename = $this->_fs->get_plugin_basename();
810 -
811 - if ( ! is_object( $transient_data ) ||
812 - ! isset( $transient_data->response ) ||
813 - ! is_array( $transient_data->response ) ||
814 - empty( $transient_data->response[ $basename ] )
815 - ) {
816 - return;
817 - }
818 -
819 - unset( $transient_data->response[ $basename ] );
820 -
821 - // Remove the added filters.
822 - $this->remove_transient_filters();
823 -
824 - set_site_transient( $transient_key, $transient_data );
825 -
826 - $this->add_transient_filters();
827 - }
828 -
829 - /**
830 - * Try to fetch plugin's info from .org repository.
831 - *
832 - * @author Vova Feldman (@svovaf)
833 - * @since 1.0.5
834 - *
835 - * @param string $action
836 - * @param object $args
837 - *
838 - * @return bool|mixed
839 - */
840 - 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' ) ) ) {
843 - $url = set_url_scheme( $url, 'https' );
844 - }
845 -
846 - $args = array(
847 - 'timeout' => 15,
848 - 'body' => array(
849 - 'action' => $action,
850 - 'request' => serialize( $args )
851 - )
852 - );
853 -
854 - $request = wp_remote_post( $url, $args );
855 -
856 - if ( is_wp_error( $request ) ) {
857 - return false;
858 - }
859 -
860 - $res = maybe_unserialize( wp_remote_retrieve_body( $request ) );
861 -
862 - if ( ! is_object( $res ) && ! is_array( $res ) ) {
863 - return false;
864 - }
865 -
866 - return $res;
867 - }
868 -
869 - /**
870 - * Fetches module translation updates from wordpress.org.
871 - *
872 - * @author Leo Fajardo (@leorw)
873 - * @since 2.1.2
874 - *
875 - * @param string $module_type
876 - * @param string $slug
877 - *
878 - * @return array|null
879 - */
880 - private function fetch_wp_org_module_translation_updates( $module_type, $slug ) {
881 - $plugin_data = $this->_fs->get_plugin_data();
882 -
883 - $locales = array_values( get_available_languages() );
884 - $locales = apply_filters( "{$module_type}_update_check_locales", $locales );
885 - $locales = array_unique( $locales );
886 -
887 - $plugin_basename = $this->_fs->get_plugin_basename();
888 - if ( 'themes' === $module_type ) {
889 - $plugin_basename = $slug;
890 - }
891 -
892 - global $wp_version;
893 -
894 - $request_args = array(
895 - 'timeout' => 15,
896 - 'body' => array(
897 - "{$module_type}" => json_encode(
898 - array(
899 - "{$module_type}" => array(
900 - $plugin_basename => array(
901 - 'Name' => trim( str_replace( $this->_fs->get_plugin()->premium_suffix, '', $plugin_data['Name'] ) ),
902 - 'Author' => $plugin_data['Author'],
903 - )
904 - )
905 - )
906 - ),
907 - 'translations' => json_encode( $this->get_installed_translations( $module_type, $slug ) ),
908 - 'locale' => json_encode( $locales )
909 - ),
910 - 'user-agent' => ( 'WordPress/' . $wp_version . '; ' . home_url( '/' ) )
911 - );
912 -
913 - $url = "http://api.wordpress.org/{$module_type}/update-check/1.1/";
914 - if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) {
915 - $url = set_url_scheme( $url, 'https' );
916 - }
917 -
918 - $raw_response = Freemius::safe_remote_post(
919 - $url,
920 - $request_args,
921 - WP_FS__TIME_24_HOURS_IN_SEC,
922 - WP_FS__TIME_12_HOURS_IN_SEC,
923 - false
924 - );
925 -
926 - if ( is_wp_error( $raw_response ) ) {
927 - return null;
928 - }
929 -
930 - $response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
931 -
932 - if ( ! is_array( $response ) ) {
933 - return null;
934 - }
935 -
936 - if ( ! isset( $response['translations'] ) || empty( $response['translations'] ) ) {
937 - return null;
938 - }
939 -
940 - return $response['translations'];
941 - }
942 -
943 - /**
944 - * @author Leo Fajardo (@leorw)
945 - * @since 2.1.2
946 - *
947 - * @param string $module_type
948 - * @param string $slug
949 - *
950 - * @return array
951 - */
952 - private function get_installed_translations( $module_type, $slug ) {
953 - if ( function_exists( 'wp_get_installed_translations' ) ) {
954 - return wp_get_installed_translations( $module_type );
955 - }
956 -
957 - $dir = "/{$module_type}";
958 -
959 - if ( ! is_dir( WP_LANG_DIR . $dir ) )
960 - return array();
961 -
962 - $files = scandir( WP_LANG_DIR . $dir );
963 - if ( ! $files )
964 - return array();
965 -
966 - $language_data = array();
967 -
968 - foreach ( $files as $file ) {
969 - if ( 0 !== strpos( $file, $slug ) ) {
970 - continue;
971 - }
972 -
973 - if ( '.' === $file[0] || is_dir( WP_LANG_DIR . "{$dir}/{$file}" ) ) {
974 - continue;
975 - }
976 -
977 - if ( substr( $file, -3 ) !== '.po' ) {
978 - continue;
979 - }
980 -
981 - if ( ! preg_match( '/(?:(.+)-)?([a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?).po/', $file, $match ) ) {
982 - continue;
983 - }
984 -
985 - if ( ! in_array( substr( $file, 0, -3 ) . '.mo', $files ) ) {
986 - continue;
987 - }
988 -
989 - list( , $textdomain, $language ) = $match;
990 -
991 - if ( '' === $textdomain ) {
992 - $textdomain = 'default';
993 - }
994 -
995 - $language_data[ $textdomain ][ $language ] = wp_get_pomo_file_data( WP_LANG_DIR . "{$dir}/{$file}" );
996 - }
997 -
998 - return $language_data;
999 - }
1000 -
1001 - /**
1002 - * Updates information on the "View version x.x details" page with custom data.
1003 - *
1004 - * @author Vova Feldman (@svovaf)
1005 - * @since 1.0.4
1006 - *
1007 - * @uses FS_Api
1008 - *
1009 - * @param object $data
1010 - * @param string $action
1011 - * @param mixed $args
1012 - *
1013 - * @return object
1014 - */
1015 - function plugins_api_filter( $data, $action = '', $args = null ) {
1016 - $this->_logger->entrance();
1017 -
1018 - if ( ( 'plugin_information' !== $action ) ||
1019 - ! isset( $args->slug )
1020 - ) {
1021 - return $data;
1022 - }
1023 -
1024 - $addon = false;
1025 - $is_addon = false;
1026 - $addon_version = false;
1027 -
1028 - if ( $this->_fs->get_slug() !== $args->slug ) {
1029 - $addon = $this->_fs->get_addon_by_slug( $args->slug );
1030 -
1031 - if ( ! is_object( $addon ) ) {
1032 - return $data;
1033 - }
1034 -
1035 - if ( $this->_fs->is_addon_activated( $addon->id ) ) {
1036 - $addon_version = $this->_fs->get_addon_instance( $addon->id )->get_plugin_version();
1037 - } else if ( $this->_fs->is_addon_installed( $addon->id ) ) {
1038 - $addon_plugin_data = get_plugin_data(
1039 - ( WP_PLUGIN_DIR . '/' . $this->_fs->get_addon_basename( $addon->id ) ),
1040 - false,
1041 - false
1042 - );
1043 -
1044 - if ( ! empty( $addon_plugin_data ) ) {
1045 - $addon_version = $addon_plugin_data['Version'];
1046 - }
1047 - }
1048 -
1049 - $is_addon = true;
1050 - }
1051 -
1052 - $plugin_in_repo = false;
1053 - if ( ! $is_addon ) {
1054 - // Try to fetch info from .org repository.
1055 - $data = self::_fetch_plugin_info_from_repository( $action, $args );
1056 -
1057 - $plugin_in_repo = ( false !== $data );
1058 - }
1059 -
1060 - if ( ! $plugin_in_repo ) {
1061 - $data = $args;
1062 -
1063 - // Fetch as much as possible info from local files.
1064 - $plugin_local_data = $this->_fs->get_plugin_data();
1065 - $data->name = $plugin_local_data['Name'];
1066 - $data->author = $plugin_local_data['Author'];
1067 - $data->sections = array(
1068 - 'description' => 'Upgrade ' . $plugin_local_data['Name'] . ' to latest.',
1069 - );
1070 -
1071 - // @todo Store extra plugin info on Freemius or parse readme.txt markup.
1072 - /*$info = $this->_fs->get_api_site_scope()->call('/information.json');
1073 -
1074 -if ( !isset($info->error) ) {
1075 - $data = $info;
1076 -}*/
1077 - }
1078 -
1079 - $plugin_version = $is_addon ?
1080 - $addon_version :
1081 - $this->_fs->get_plugin_version();
1082 -
1083 - // Get plugin's newest update.
1084 - $new_version = $this->get_latest_download_details( $is_addon ? $addon->id : false, $plugin_version );
1085 -
1086 - if ( ! is_object( $new_version ) || empty( $new_version->version ) ) {
1087 - $data->version = $plugin_version;
1088 - } else {
1089 - if ( $is_addon ) {
1090 - $data->name = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1091 - $data->slug = $addon->slug;
1092 - $data->url = WP_FS__ADDRESS;
1093 - $data->package = $new_version->url;
1094 - }
1095 -
1096 - if ( ! $plugin_in_repo ) {
1097 - $data->last_updated = ! is_null( $new_version->updated ) ? $new_version->updated : $new_version->created;
1098 - $data->requires = $new_version->requires_platform_version;
1099 - $data->requires_php = $new_version->requires_programming_language_version;
1100 - $data->tested = $new_version->tested_up_to_version;
1101 - }
1102 -
1103 - $data->version = $new_version->version;
1104 - $data->download_link = $new_version->url;
1105 -
1106 - if ( isset( $new_version->readme ) && is_object( $new_version->readme ) ) {
1107 - $new_version_readme_data = $new_version->readme;
1108 - if ( isset( $new_version_readme_data->sections ) ) {
1109 - $new_version_readme_data->sections = (array) $new_version_readme_data->sections;
1110 - } else {
1111 - $new_version_readme_data->sections = array();
1112 - }
1113 -
1114 - if ( isset( $data->sections ) ) {
1115 - if ( isset( $data->sections['screenshots'] ) ) {
1116 - $new_version_readme_data->sections['screenshots'] = $data->sections['screenshots'];
1117 - }
1118 -
1119 - if ( isset( $data->sections['reviews'] ) ) {
1120 - $new_version_readme_data->sections['reviews'] = $data->sections['reviews'];
1121 - }
1122 - }
1123 -
1124 - if ( isset( $new_version_readme_data->banners ) ) {
1125 - $new_version_readme_data->banners = (array) $new_version_readme_data->banners;
1126 - } else if ( isset( $data->banners ) ) {
1127 - $new_version_readme_data->banners = $data->banners;
1128 - }
1129 -
1130 - $wp_org_sections = array(
1131 - 'author',
1132 - 'author_profile',
1133 - 'rating',
1134 - 'ratings',
1135 - 'num_ratings',
1136 - 'support_threads',
1137 - 'support_threads_resolved',
1138 - 'active_installs',
1139 - 'added',
1140 - 'homepage'
1141 - );
1142 -
1143 - foreach ( $wp_org_sections as $wp_org_section ) {
1144 - if ( isset( $data->{$wp_org_section} ) ) {
1145 - $new_version_readme_data->{$wp_org_section} = $data->{$wp_org_section};
1146 - }
1147 - }
1148 -
1149 - $data = $new_version_readme_data;
1150 - }
1151 - }
1152 -
1153 - if ( ! empty( $data->tested ) ) {
1154 - $data->tested = self::get_tested_wp_version( $data->tested );
1155 - }
1156 -
1157 - return $data;
1158 - }
1159 -
1160 - /**
1161 - * @since 2.5.3 If the current WordPress version is a patch of the tested version (e.g., 6.1.2 is a patch of 6.1), then override the tested version with the patch so developers won't need to release a new version just to bump the latest supported WP version.
1162 - *
1163 - * @param string|null $tested_up_to
1164 - *
1165 - * @return string|null
1166 - */
1167 - private static function get_tested_wp_version( $tested_up_to ) {
1168 - $current_wp_version = get_bloginfo( 'version' );
1169 -
1170 - return ( ! empty($tested_up_to) && fs_starts_with( $current_wp_version, $tested_up_to . '.' ) ) ?
1171 - $current_wp_version :
1172 - $tested_up_to;
1173 - }
1174 -
1175 - /**
1176 - * @author Vova Feldman (@svovaf)
1177 - * @since 1.2.1.7
1178 - *
1179 - * @param number|bool $addon_id
1180 - * @param bool|string $newer_than Since 2.2.1
1181 - * @param bool|string $fetch_readme Since 2.2.1
1182 - *
1183 - * @return object
1184 - */
1185 - 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 );
1187 - }
1188 -
1189 - /**
1190 - * Checks if a given basename has a matching folder name
1191 - * with the current context plugin.
1192 - *
1193 - * @author Vova Feldman (@svovaf)
1194 - * @since 1.2.1.6
1195 - *
1196 - * @return bool
1197 - */
1198 - private function is_correct_folder_name() {
1199 - return ( $this->_fs->get_target_folder_name() == trim( dirname( $this->_fs->get_plugin_basename() ), '/\\' ) );
1200 - }
1201 -
1202 - /**
1203 - * This is a special after upgrade handler for migrating modules
1204 - * that didn't use the '-premium' suffix folder structure before
1205 - * the migration.
1206 - *
1207 - * @author Vova Feldman (@svovaf)
1208 - * @since 1.2.1.6
1209 - *
1210 - * @param bool $response Install response.
1211 - * @param array $hook_extra Extra arguments passed to hooked filters.
1212 - * @param array $result Installation result data.
1213 - *
1214 - * @return bool
1215 - */
1216 - function _maybe_update_folder_name( $response, $hook_extra, $result ) {
1217 - $basename = $this->_fs->get_plugin_basename();
1218 -
1219 - if ( true !== $response ||
1220 - empty( $hook_extra ) ||
1221 - empty( $hook_extra['plugin'] ) ||
1222 - $basename !== $hook_extra['plugin']
1223 - ) {
1224 - return $response;
1225 - }
1226 -
1227 - $active_plugins_basenames = get_option( 'active_plugins' );
1228 -
1229 - foreach ( $active_plugins_basenames as $key => $active_plugin_basename ) {
1230 - if ( $basename === $active_plugin_basename ) {
1231 - // Get filename including extension.
1232 - $filename = basename( $basename );
1233 -
1234 - $new_basename = plugin_basename(
1235 - trailingslashit( $this->_fs->is_premium() ? $this->_fs->get_premium_slug() : $this->_fs->get_slug() ) .
1236 - $filename
1237 - );
1238 -
1239 - // Verify that the expected correct path exists.
1240 - if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $new_basename ) ) ) {
1241 - // Override active plugin name.
1242 - $active_plugins_basenames[ $key ] = $new_basename;
1243 - update_option( 'active_plugins', $active_plugins_basenames );
1244 - }
1245 -
1246 - break;
1247 - }
1248 - }
1249 -
1250 - return $response;
1251 - }
1252 -
1253 - #----------------------------------------------------------------------------------
1254 - #region Auto Activation
1255 - #----------------------------------------------------------------------------------
1256 -
1257 - /**
1258 - * Installs and active a plugin when explicitly requested that from a 3rd party service.
1259 - *
1260 - * This logic was inspired by the TGMPA GPL licensed library by Thomas Griffin.
1261 - *
1262 - * @link http://tgmpluginactivation.com/
1263 - *
1264 - * @author Vova Feldman
1265 - * @since 1.2.1.7
1266 - *
1267 - * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
1268 - *
1269 - * @uses WP_Filesystem
1270 - * @uses WP_Error
1271 - * @uses WP_Upgrader
1272 - * @uses Plugin_Upgrader
1273 - * @uses Plugin_Installer_Skin
1274 - * @uses Plugin_Upgrader_Skin
1275 - *
1276 - * @param number|bool $plugin_id
1277 - *
1278 - * @return array
1279 - */
1280 - function install_and_activate_plugin( $plugin_id = false ) {
1281 - if ( ! empty( $plugin_id ) && ! FS_Plugin::is_valid_id( $plugin_id ) ) {
1282 - // Invalid plugin ID.
1283 - return array(
1284 - 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1285 - 'code' => 'invalid_module_id',
1286 - );
1287 - }
1288 -
1289 - $is_addon = false;
1290 - if ( FS_Plugin::is_valid_id( $plugin_id ) &&
1291 - $plugin_id != $this->_fs->get_id()
1292 - ) {
1293 - $addon = $this->_fs->get_addon( $plugin_id );
1294 -
1295 - if ( ! is_object( $addon ) ) {
1296 - // Invalid add-on ID.
1297 - return array(
1298 - 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1299 - 'code' => 'invalid_module_id',
1300 - );
1301 - }
1302 -
1303 - $slug = $addon->slug;
1304 - $premium_slug = $addon->premium_slug;
1305 - $title = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1306 -
1307 - $is_addon = true;
1308 - } else {
1309 - $slug = $this->_fs->get_slug();
1310 - $premium_slug = $this->_fs->get_premium_slug();
1311 - $title = $this->_fs->get_plugin_title() .
1312 - ( $this->_fs->is_addon() ? ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' ) : '' );
1313 - }
1314 -
1315 - if ( $this->is_premium_plugin_active( $plugin_id ) ) {
1316 - // Premium version already activated.
1317 - return array(
1318 - 'message' => $is_addon ?
1319 - $this->_fs->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ) :
1320 - $this->_fs->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
1321 - 'code' => 'premium_installed',
1322 - );
1323 - }
1324 -
1325 - $latest_version = $this->get_latest_download_details( $plugin_id, false, false );
1326 - $target_folder = $premium_slug;
1327 -
1328 - // Prep variables for Plugin_Installer_Skin class.
1329 - $extra = array();
1330 - $extra['slug'] = $target_folder;
1331 - $source = $latest_version->url;
1332 - $api = null;
1333 -
1334 - $install_url = add_query_arg(
1335 - array(
1336 - 'action' => 'install-plugin',
1337 - 'plugin' => urlencode( $slug ),
1338 - ),
1339 - 'update.php'
1340 - );
1341 -
1342 - if ( ! class_exists( 'Plugin_Upgrader', false ) ) {
1343 - // Include required resources for the installation.
1344 - require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1345 - }
1346 -
1347 - $skin_args = array(
1348 - 'type' => 'web',
1349 - 'title' => sprintf( $this->_fs->get_text_inline( 'Installing plugin: %s', 'installing-plugin-x' ), $title ),
1350 - 'url' => esc_url_raw( $install_url ),
1351 - 'nonce' => 'install-plugin_' . $slug,
1352 - 'plugin' => '',
1353 - 'api' => $api,
1354 - 'extra' => $extra,
1355 - );
1356 -
1357 -// $skin = new Automatic_Upgrader_Skin( $skin_args );
1358 -// $skin = new Plugin_Installer_Skin( $skin_args );
1359 - $skin = new WP_Ajax_Upgrader_Skin( $skin_args );
1360 -
1361 - // Create a new instance of Plugin_Upgrader.
1362 - $upgrader = new Plugin_Upgrader( $skin );
1363 -
1364 - // Perform the action and install the plugin from the $source urldecode().
1365 - add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
1366 -
1367 - $install_result = $upgrader->install( $source );
1368 -
1369 - remove_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1 );
1370 -
1371 - if ( is_wp_error( $install_result ) ) {
1372 - return array(
1373 - 'message' => $install_result->get_error_message(),
1374 - 'code' => $install_result->get_error_code(),
1375 - );
1376 - } elseif ( is_wp_error( $skin->result ) ) {
1377 - return array(
1378 - 'message' => $skin->result->get_error_message(),
1379 - 'code' => $skin->result->get_error_code(),
1380 - );
1381 - } elseif ( $skin->get_errors()->get_error_code() ) {
1382 - return array(
1383 - 'message' => $skin->get_error_messages(),
1384 - 'code' => 'unknown',
1385 - );
1386 - } elseif ( is_null( $install_result ) ) {
1387 - global $wp_filesystem;
1388 -
1389 - $error_code = 'unable_to_connect_to_filesystem';
1390 - $error_message = $this->_fs->get_text_inline( 'Unable to connect to the filesystem. Please confirm your credentials.' );
1391 -
1392 - // Pass through the error from WP_Filesystem if one was raised.
1393 - if ( $wp_filesystem instanceof WP_Filesystem_Base &&
1394 - is_wp_error( $wp_filesystem->errors ) &&
1395 - $wp_filesystem->errors->get_error_code()
1396 - ) {
1397 - $error_message = $wp_filesystem->errors->get_error_message();
1398 - }
1399 -
1400 - return array(
1401 - 'message' => $error_message,
1402 - 'code' => $error_code,
1403 - );
1404 - }
1405 -
1406 - // Grab the full path to the main plugin's file.
1407 - $plugin_activate = $upgrader->plugin_info();
1408 -
1409 - // Try to activate the plugin.
1410 - $activation_result = $this->try_activate_plugin( $plugin_activate );
1411 -
1412 - if ( is_wp_error( $activation_result ) ) {
1413 - return array(
1414 - 'message' => $activation_result->get_error_message(),
1415 - 'code' => $activation_result->get_error_code(),
1416 - );
1417 - }
1418 -
1419 - return $skin->get_upgrade_messages();
1420 - }
1421 -
1422 - /**
1423 - * Tries to activate a plugin. If fails, returns the error.
1424 - *
1425 - * @author Vova Feldman
1426 - * @since 1.2.1.7
1427 - *
1428 - * @param string $file_path Path within wp-plugins/ to main plugin file.
1429 - * This determines the styling of the output messages.
1430 - *
1431 - * @return bool|WP_Error
1432 - */
1433 - protected function try_activate_plugin( $file_path ) {
1434 - $activate = activate_plugin( $file_path, '', $this->_fs->is_network_active() );
1435 -
1436 - return is_wp_error( $activate ) ?
1437 - $activate :
1438 - true;
1439 - }
1440 -
1441 - /**
1442 - * Check if a premium module version is already active.
1443 - *
1444 - * @author Vova Feldman
1445 - * @since 1.2.1.7
1446 - *
1447 - * @param number|bool $plugin_id
1448 - *
1449 - * @return bool
1450 - */
1451 - private function is_premium_plugin_active( $plugin_id = false ) {
1452 - if ( $plugin_id != $this->_fs->get_id() ) {
1453 - return $this->_fs->is_addon_activated( $plugin_id, true );
1454 - }
1455 -
1456 - return is_plugin_active( $this->_fs->premium_plugin_basename() );
1457 - }
1458 -
1459 - /**
1460 - * Store the basename since it's not always available in the `_maybe_adjust_source_dir` method below.
1461 - *
1462 - * @author Leo Fajardo (@leorw)
1463 - * @since 2.2.1
1464 - *
1465 - * @param bool|WP_Error $response Response.
1466 - * @param array $hook_extra Extra arguments passed to hooked filters.
1467 - *
1468 - * @return bool|WP_Error
1469 - */
1470 - static function _store_basename_for_source_adjustment( $response, $hook_extra ) {
1471 - if ( isset( $hook_extra['plugin'] ) ) {
1472 - self::$_upgrade_basename = $hook_extra['plugin'];
1473 - } else if ( isset( $hook_extra['theme'] ) ) {
1474 - self::$_upgrade_basename = $hook_extra['theme'];
1475 - } else {
1476 - self::$_upgrade_basename = null;
1477 - }
1478 -
1479 - return $response;
1480 - }
1481 -
1482 - /**
1483 - * Adjust the plugin directory name if necessary.
1484 - * Assumes plugin has a folder (not a single file plugin).
1485 - *
1486 - * The final destination directory of a plugin is based on the subdirectory name found in the
1487 - * (un)zipped source. In some cases this subdirectory name is not the same as the expected
1488 - * slug and the plugin will not be recognized as installed. This is fixed by adjusting
1489 - * the temporary unzipped source subdirectory name to the expected plugin slug.
1490 - *
1491 - * @author Vova Feldman
1492 - * @since 1.2.1.7
1493 - * @since 2.2.1 The method was converted to static since when the admin update bulk products via the Updates section, the logic applies the `upgrader_source_selection` filter for every product that is being updated.
1494 - *
1495 - * @param string $source Path to upgrade/zip-file-name.tmp/subdirectory/.
1496 - * @param string $remote_source Path to upgrade/zip-file-name.tmp.
1497 - * @param \WP_Upgrader $upgrader Instance of the upgrader which installs the plugin.
1498 - *
1499 - * @return string|WP_Error
1500 - */
1501 - static function _maybe_adjust_source_dir( $source, $remote_source, $upgrader ) {
1502 - if ( ! is_object( $GLOBALS['wp_filesystem'] ) ) {
1503 - return $source;
1504 - }
1505 -
1506 - $basename = self::$_upgrade_basename;
1507 - $is_theme = false;
1508 -
1509 - // Figure out what the slug is supposed to be.
1510 - if ( isset( $upgrader->skin->options['extra'] ) ) {
1511 - // Set by the auto-install logic.
1512 - $desired_slug = $upgrader->skin->options['extra']['slug'];
1513 - } else if ( ! empty( $basename ) ) {
1514 - /**
1515 - * If it doesn't end with ".php", it's a theme.
1516 - *
1517 - * @author Leo Fajardo (@leorw)
1518 - * @since 2.2.1
1519 - */
1520 - $is_theme = ( ! fs_ends_with( $basename, '.php' ) );
1521 -
1522 - $desired_slug = ( ! $is_theme ) ?
1523 - dirname( $basename ) :
1524 - // Theme slug
1525 - $basename;
1526 - } else {
1527 - // Can't figure out the desired slug, stop the execution.
1528 - return $source;
1529 - }
1530 -
1531 - if ( is_multisite() ) {
1532 - /**
1533 - * If we are running in a multisite environment and the product is not network activated,
1534 - * the instance will not exist anyway. Therefore, try to update the source if necessary
1535 - * regardless if the Freemius instance of the product exists or not.
1536 - *
1537 - * @author Vova Feldman
1538 - */
1539 - } else if ( ! empty( $basename ) ) {
1540 - $fs = Freemius::get_instance_by_file(
1541 - $basename,
1542 - $is_theme ?
1543 - WP_FS__MODULE_TYPE_THEME :
1544 - WP_FS__MODULE_TYPE_PLUGIN
1545 - );
1546 -
1547 - if ( ! is_object( $fs ) ) {
1548 - /**
1549 - * If the Freemius instance does not exist on a non-multisite network environment, it means that:
1550 - * 1. The product is not powered by Freemius; OR
1551 - * 2. The product is not activated, therefore, we don't mind if after the update the folder name will change.
1552 - *
1553 - * @author Leo Fajardo (@leorw)
1554 - * @since 2.2.1
1555 - */
1556 - return $source;
1557 - }
1558 - }
1559 -
1560 - $subdir_name = untrailingslashit( str_replace( trailingslashit( $remote_source ), '', $source ) );
1561 -
1562 - if ( ! empty( $subdir_name ) && $subdir_name !== $desired_slug ) {
1563 - $from_path = untrailingslashit( $source );
1564 - $to_path = trailingslashit( $remote_source ) . $desired_slug;
1565 -
1566 - if ( true === $GLOBALS['wp_filesystem']->move( $from_path, $to_path ) ) {
1567 - return trailingslashit( $to_path );
1568 - }
1569 -
1570 - return new WP_Error(
1571 - 'rename_failed',
1572 - fs_text_inline( 'The remote plugin package does not contain a folder with the desired slug and renaming did not work.', 'module-package-rename-failure' ),
1573 - array(
1574 - 'found' => $subdir_name,
1575 - 'expected' => $desired_slug
1576 - )
1577 - );
1578 - }
1579 -
1580 - return $source;
1581 - }
1582 -
1583 - #endregion
1584 - }
1 +<?php
2 + /**
3 + * @package Freemius
4 + * @copyright Copyright (c) 2015, Freemius, Inc.
5 + * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 + * @since 1.0.4
7 + *
8 + * @link https://github.com/easydigitaldownloads/EDD-License-handler/blob/master/EDD_SL_Plugin_Updater.php
9 + */
10 +
11 + if ( ! defined( 'ABSPATH' ) ) {
12 + exit;
13 + }
14 +
15 + class FS_Plugin_Updater {
16 +
17 + /**
18 + * @var Freemius
19 + * @since 1.0.4
20 + */
21 + private $_fs;
22 + /**
23 + * @var FS_Logger
24 + * @since 1.0.4
25 + */
26 + private $_logger;
27 + /**
28 + * @var object
29 + * @since 1.1.8.1
30 + */
31 + private $_update_details;
32 + /**
33 + * @var array
34 + * @since 2.1.2
35 + */
36 + private $_translation_updates;
37 +
38 + private static $_upgrade_basename = null;
39 +
40 + const UPDATES_CHECK_CACHE_EXPIRATION = ( WP_FS__TIME_24_HOURS_IN_SEC / 24 );
41 +
42 + #--------------------------------------------------------------------------------
43 + #region Singleton
44 + #--------------------------------------------------------------------------------
45 +
46 + /**
47 + * @var FS_Plugin_Updater[]
48 + * @since 2.0.0
49 + */
50 + private static $_INSTANCES = array();
51 +
52 + /**
53 + * @param Freemius $freemius
54 + *
55 + * @return FS_Plugin_Updater
56 + */
57 + static function instance( Freemius $freemius ) {
58 + $key = $freemius->get_id();
59 +
60 + if ( ! isset( self::$_INSTANCES[ $key ] ) ) {
61 + self::$_INSTANCES[ $key ] = new self( $freemius );
62 + }
63 +
64 + return self::$_INSTANCES[ $key ];
65 + }
66 +
67 + #endregion
68 +
69 + private function __construct( Freemius $freemius ) {
70 + $this->_fs = $freemius;
71 +
72 + $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $freemius->get_slug() . '_updater', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
73 +
74 + $this->filters();
75 + }
76 +
77 + /**
78 + * Initiate required filters.
79 + *
80 + * @author Vova Feldman (@svovaf)
81 + * @since 1.0.4
82 + */
83 + private function filters() {
84 + // Override request for plugin information
85 + add_filter( 'plugins_api', array( &$this, 'plugins_api_filter' ), 10, 3 );
86 +
87 + $this->add_transient_filters();
88 +
89 + /**
90 + * If user has the premium plugin's code but do NOT have an active license,
91 + * encourage him to upgrade by showing that there's a new release, but instead
92 + * of showing an update link, show upgrade link to the pricing page.
93 + *
94 + * @since 1.1.6
95 + *
96 + */
97 + // WP 2.9+
98 + add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
99 + &$this,
100 + 'catch_plugin_update_row'
101 + ), 9 );
102 + add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
103 + &$this,
104 + 'edit_and_echo_plugin_update_row'
105 + ), 11, 2 );
106 +
107 + if ( ! $this->_fs->has_any_active_valid_license() ) {
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' ) );
111 + }
112 +
113 + if ( ! WP_FS__IS_PRODUCTION_MODE ) {
114 + add_filter( 'http_request_host_is_external', array(
115 + $this,
116 + 'http_request_host_is_external_filter'
117 + ), 10, 3 );
118 + }
119 +
120 + if ( $this->_fs->is_premium() ) {
121 + if ( ! $this->is_correct_folder_name() ) {
122 + add_filter( 'upgrader_post_install', array( &$this, '_maybe_update_folder_name' ), 10, 3 );
123 + }
124 +
125 + add_filter( 'upgrader_pre_install', array( 'FS_Plugin_Updater', '_store_basename_for_source_adjustment' ), 1, 2 );
126 + add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
127 +
128 + if ( ! $this->_fs->has_any_active_valid_license() ) {
129 + add_filter( 'wp_prepare_themes_for_js', array( &$this, 'change_theme_update_info_html' ), 10, 1 );
130 + }
131 + }
132 + }
133 +
134 + /**
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)
172 + * @since 2.1.4
173 + */
174 + function catch_plugin_information_dialog_contents() {
175 + if ( ! $this->is_plugin_information_dialog_for_plugin() ) {
176 + return;
177 + }
178 +
179 + add_action( 'admin_footer', array( &$this, 'edit_and_echo_plugin_information_dialog_contents' ), 0, 1 );
180 +
181 + ob_start();
182 + }
183 +
184 + /**
185 + * @author Leo Fajardo (@leorw)
186 + * @since 2.1.4
187 + *
188 + * @param string $hook_suffix
189 + */
190 + function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) {
191 + if (
192 + 'plugin-information' !== fs_request_get( 'tab', false ) ||
193 + $this->_fs->get_slug() !== fs_request_get_raw( 'plugin', false )
194 + ) {
195 + return;
196 + }
197 +
198 + $license = $this->_fs->_get_license();
199 +
200 + $subscription = ( is_object( $license ) && ! $license->is_lifetime() ) ?
201 + $this->_fs->_get_subscription( $license->id ) :
202 + null;
203 +
204 + $contents = ob_get_clean();
205 +
206 + $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_install_from_iframe"' );
207 +
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 ),
215 + '<a'
216 + );
217 +
218 + $install_or_update_button_end_pos = ( strpos( $contents, '</a>', $install_or_update_button_id_attribute_pos ) + strlen( '</a>' ) );
219 +
220 + /**
221 + * The part of the contents without the update button.
222 + *
223 + * @author Leo Fajardo (@leorw)
224 + * @since 2.2.5
225 + */
226 + $modified_contents = substr( $contents, 0, $install_or_update_button_start_pos );
227 +
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 ) );
229 +
230 + /**
231 + * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license,
232 + * the text will be "Renew license" and will link to the checkout page with the license's billing cycle
233 + * and quota. If there's no license, the text will be "Buy license" and will link to the pricing page.
234 + */
235 + $install_or_update_button = preg_replace(
236 + '/(\<a.+)(id="plugin_(install|update)_from_iframe")(.+href=")([^\s]+)(".*\>)(.+)(\<\/a>)/is',
237 + is_object( $license ) ?
238 + sprintf(
239 + '$1$4%s$6%s$8',
240 + $this->_fs->checkout_url(
241 + is_object( $subscription ) ?
242 + ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
243 + WP_FS__PERIOD_LIFETIME,
244 + false,
245 + array( 'licenses' => $license->quota )
246 + ),
247 + fs_text_inline( 'Renew license', 'renew-license', $this->_fs->get_slug() )
248 + ) :
249 + sprintf(
250 + '$1$4%s$6%s$8',
251 + $this->_fs->pricing_url(),
252 + fs_text_inline( 'Buy license', 'buy-license', $this->_fs->get_slug() )
253 + ),
254 + $install_or_update_button
255 + );
256 +
257 + /**
258 + * Append the modified button.
259 + *
260 + * @author Leo Fajardo (@leorw)
261 + * @since 2.2.5
262 + */
263 + $modified_contents .= $install_or_update_button;
264 +
265 + /**
266 + * Append the remaining part of the contents after the update button.
267 + *
268 + * @author Leo Fajardo (@leorw)
269 + * @since 2.2.5
270 + */
271 + $modified_contents .= substr( $contents, $install_or_update_button_end_pos );
272 +
273 + $contents = $modified_contents;
274 + }
275 +
276 + echo $contents;
277 + }
278 +
279 + /**
280 + * @author Vova Feldman (@svovaf)
281 + * @since 2.0.0
282 + */
283 + private function add_transient_filters() {
284 + if (
285 + $this->_fs->is_premium() &&
286 + $this->_fs->is_registered() &&
287 + ! FS_Permission_Manager::instance( $this->_fs )->is_essentials_tracking_allowed()
288 + ) {
289 + $this->_logger->log( 'Opted out sites cannot receive automatic software updates.' );
290 +
291 + return;
292 + }
293 +
294 + add_filter( 'pre_set_site_transient_update_plugins', array(
295 + &$this,
296 + 'pre_set_site_transient_update_plugins_filter'
297 + ) );
298 +
299 + add_filter( 'pre_set_site_transient_update_themes', array(
300 + &$this,
301 + 'pre_set_site_transient_update_plugins_filter'
302 + ) );
303 + }
304 +
305 + /**
306 + * @author Vova Feldman (@svovaf)
307 + * @since 2.0.0
308 + */
309 + private function remove_transient_filters() {
310 + remove_filter( 'pre_set_site_transient_update_plugins', array(
311 + &$this,
312 + 'pre_set_site_transient_update_plugins_filter'
313 + ) );
314 +
315 + remove_filter( 'pre_set_site_transient_update_themes', array(
316 + &$this,
317 + 'pre_set_site_transient_update_plugins_filter'
318 + ) );
319 + }
320 +
321 + /**
322 + * Capture plugin update row by turning output buffering.
323 + *
324 + * @author Vova Feldman (@svovaf)
325 + * @since 1.1.6
326 + */
327 + function catch_plugin_update_row() {
328 + ob_start();
329 + }
330 +
331 + /**
332 + * Overrides default update message format with "renew your license" message.
333 + *
334 + * @author Vova Feldman (@svovaf)
335 + * @since 1.1.6
336 + *
337 + * @param string $file
338 + * @param array $plugin_data
339 + */
340 + function edit_and_echo_plugin_update_row( $file, $plugin_data ) {
341 + $plugin_update_row = ob_get_clean();
342 +
343 + $current = get_site_transient( 'update_plugins' );
344 + if ( ! isset( $current->response[ $file ] ) ) {
345 + echo $plugin_update_row;
346 +
347 + return;
348 + }
349 +
350 + $r = $current->response[ $file ];
351 +
352 + $has_beta_update = $this->_fs->has_beta_update();
353 +
354 + if ( $this->_fs->has_any_active_valid_license() ) {
355 + if ( $has_beta_update ) {
356 + /**
357 + * Turn the "new version" text into "new Beta version".
358 + *
359 + * Sample input:
360 + * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
361 + * Output:
362 + * There is a new Beta version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
363 + *
364 + * @author Leo Fajardo (@leorw)
365 + * @since 2.3.0
366 + */
367 + $plugin_update_row = preg_replace(
368 + '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
369 + (
370 + '$1' .
371 + sprintf(
372 + fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
373 + $has_beta_update ?
374 + fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
375 + fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() ),
376 + $this->_fs->get_plugin_title()
377 + ) .
378 + ' ' .
379 + '$3' .
380 + '$6'
381 + ),
382 + $plugin_update_row
383 + );
384 + }
385 + } else {
386 + /**
387 + * Turn the "new version" text into a link that opens the plugin information dialog when clicked and
388 + * make the "View version x details" text link to the checkout page instead of opening the plugin
389 + * information dialog when clicked.
390 + *
391 + * Sample input:
392 + * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
393 + * Output:
394 + * There is a <a href="...>new version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
395 + * OR
396 + * There is a <a href="...>new Beta version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
397 + *
398 + * @author Leo Fajardo (@leorw)
399 + */
400 + $plugin_update_row = preg_replace(
401 + '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
402 + (
403 + '$1' .
404 + sprintf(
405 + fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
406 + sprintf(
407 + '<a href="$4"%s>%s</a>',
408 + '$5',
409 + $has_beta_update ?
410 + fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
411 + fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() )
412 + ),
413 + $this->_fs->get_plugin_title()
414 + ) .
415 + ' ' .
416 + $this->_fs->version_upgrade_checkout_link( $r->new_version ) .
417 + '$6'
418 + ),
419 + $plugin_update_row
420 + );
421 + }
422 +
423 + if (
424 + $this->_fs->is_plugin() &&
425 + isset( $r->upgrade_notice ) &&
426 + strlen( trim( $r->upgrade_notice ) ) > 0
427 + ) {
428 + $slug = $this->_fs->get_slug();
429 +
430 + $upgrade_notice_html = sprintf(
431 + '<p class="notice fs-upgrade-notice fs-slug-%1$s fs-type-%2$s" data-slug="%1$s" data-type="%2$s"><strong>%3$s</strong> %4$s</p>',
432 + $slug,
433 + $this->_fs->get_module_type(),
434 + fs_text_inline( 'Important Upgrade Notice:', 'upgrade_notice', $slug ),
435 + esc_html( $r->upgrade_notice )
436 + );
437 +
438 + $plugin_update_row = str_replace( '</div>', '</div>' . $upgrade_notice_html, $plugin_update_row );
439 + }
440 +
441 + echo $plugin_update_row;
442 + }
443 +
444 + /**
445 + * @author Leo Fajardo (@leorw)
446 + * @since 2.0.2
447 + *
448 + * @param array $prepared_themes
449 + *
450 + * @return array
451 + */
452 + function change_theme_update_info_html( $prepared_themes ) {
453 + $theme_basename = $this->_fs->get_plugin_basename();
454 +
455 + if ( ! isset( $prepared_themes[ $theme_basename ] ) ) {
456 + return $prepared_themes;
457 + }
458 +
459 + $themes_update = get_site_transient( 'update_themes' );
460 + if ( ! isset( $themes_update->response[ $theme_basename ] ) ||
461 + empty( $themes_update->response[ $theme_basename ]['package'] )
462 + ) {
463 + return $prepared_themes;
464 + }
465 +
466 + $prepared_themes[ $theme_basename ]['update'] = preg_replace(
467 + '/(\<p.+>)(.+)(\<a.+\<a.+)\.(.+\<\/p\>)/is',
468 + '$1 $2 ' . $this->_fs->version_upgrade_checkout_link( $themes_update->response[ $theme_basename ]['new_version'] ) .
469 + '$4',
470 + $prepared_themes[ $theme_basename ]['update']
471 + );
472 +
473 + // Set to false to prevent the "Update now" link for the context theme from being shown on the "Themes" page.
474 + $prepared_themes[ $theme_basename ]['hasPackage'] = false;
475 +
476 + return $prepared_themes;
477 + }
478 +
479 + /**
480 + * Since WP version 3.6, a new security feature was added that denies access to repository with a local ip.
481 + * During development mode we want to be able updating plugin versions via our localhost repository. This
482 + * filter white-list all domains including "api.freemius".
483 + *
484 + * @link http://www.emanueletessore.com/wordpress-download-failed-valid-url-provided/
485 + *
486 + * @author Vova Feldman (@svovaf)
487 + * @since 1.0.4
488 + *
489 + * @param bool $allow
490 + * @param string $host
491 + * @param string $url
492 + *
493 + * @return bool
494 + */
495 + function http_request_host_is_external_filter( $allow, $host, $url ) {
496 + return ( false !== strpos( $host, 'freemius' ) ) ? true : $allow;
497 + }
498 +
499 + /**
500 + * Check for Updates at the defined API endpoint and modify the update array.
501 + *
502 + * This function dives into the update api just when WordPress creates its update array,
503 + * then adds a custom API call and injects the custom plugin data retrieved from the API.
504 + * It is reassembled from parts of the native WordPress plugin update code.
505 + * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
506 + *
507 + * @author Vova Feldman (@svovaf)
508 + * @since 1.0.4
509 + *
510 + * @uses FS_Api
511 + *
512 + * @param object $transient_data Update array build by WordPress.
513 + *
514 + * @return object Modified update array with custom plugin data.
515 + */
516 + function pre_set_site_transient_update_plugins_filter( $transient_data ) {
517 + $this->_logger->entrance();
518 +
519 + /**
520 + * "plugins" or "themes".
521 + *
522 + * @author Leo Fajardo (@leorw)
523 + * @since 1.2.2
524 + */
525 + $module_type = $this->_fs->get_module_type() . 's';
526 +
527 + /**
528 + * Ensure that we don't mix plugins update info with themes update info.
529 + *
530 + * @author Leo Fajardo (@leorw)
531 + * @since 1.2.2
532 + */
533 + if ( "pre_set_site_transient_update_{$module_type}" !== current_filter() ) {
534 + return $transient_data;
535 + }
536 +
537 + if ( empty( $transient_data ) ||
538 + defined( 'WP_FS__UNINSTALL_MODE' )
539 + ) {
540 + return $transient_data;
541 + }
542 +
543 + // Alias.
544 + $basename = $this->_fs->premium_plugin_basename();
545 +
546 + global $wp_current_filter;
547 +
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 ) && (
557 + /**
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.
560 + */
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;
570 + }
571 +
572 + if ( ! isset( $this->_update_details ) ) {
573 + // Get plugin's newest update.
574 + $new_version = $this->_fs->get_update( false, fs_request_get_bool( 'force-check' ) );
575 +
576 + $this->_update_details = false;
577 +
578 + if ( is_object( $new_version ) && $this->is_new_version_premium( $new_version ) ) {
579 + $this->_logger->log( 'Found newer plugin version ' . $new_version->version );
580 +
581 + /**
582 + * Cache plugin details locally since set_site_transient( 'update_plugins' )
583 + * called multiple times and the non wp.org plugins are filtered after the
584 + * call to .org.
585 + *
586 + * @since 1.1.8.1
587 + */
588 + $this->_update_details = $this->get_update_details( $new_version );
589 + }
590 + }
591 +
592 + if ( is_object( $this->_update_details ) ) {
593 + if ( isset( $transient_data->no_update ) ) {
594 + unset( $transient_data->no_update[ $basename ] );
595 + }
596 +
597 + if ( ! isset( $transient_data->response ) ) {
598 + $transient_data->response = array();
599 + }
600 +
601 + // Add plugin to transient data.
602 + $transient_data->response[ $basename ] = $this->_fs->is_plugin() ?
603 + $this->_update_details :
604 + (array) $this->_update_details;
605 + } else {
606 + if ( isset( $transient_data->response ) ) {
607 + /**
608 + * Ensure that there's no update data for the plugin to prevent upgrading the premium version to the latest free version.
609 + *
610 + * @author Leo Fajardo (@leorw)
611 + * @since 2.3.0
612 + */
613 + unset( $transient_data->response[ $basename ] );
614 + }
615 +
616 + if ( ! isset( $transient_data->no_update ) ) {
617 + $transient_data->no_update = array();
618 + }
619 +
620 + /**
621 + * Add product to no_update transient data to properly integrate with WP 5.5 auto-updates UI.
622 + *
623 + * @since 2.4.1
624 + * @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
625 + */
626 + $transient_data->no_update[ $basename ] = $this->_fs->is_plugin() ?
627 + (object) array(
628 + 'id' => $basename,
629 + 'slug' => $this->_fs->get_slug(),
630 + 'plugin' => $basename,
631 + 'new_version' => $this->_fs->get_plugin_version(),
632 + 'url' => '',
633 + 'package' => '',
634 + 'icons' => array(),
635 + 'banners' => array(),
636 + 'banners_rtl' => array(),
637 + 'tested' => '',
638 + 'requires_php' => '',
639 + 'compatibility' => new stdClass(),
640 + ) :
641 + array(
642 + 'theme' => $basename,
643 + 'new_version' => $this->_fs->get_plugin_version(),
644 + 'url' => '',
645 + 'package' => '',
646 + 'requires' => '',
647 + 'requires_php' => '',
648 + );
649 + }
650 +
651 + $slug = $this->_fs->get_slug();
652 +
653 + if ( $this->can_fetch_data_from_wp_org() ) {
654 + if ( ! isset( $this->_translation_updates ) ) {
655 + $this->_translation_updates = array();
656 +
657 + $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug );
658 + if ( ! empty( $translation_updates ) ) {
659 + $this->_translation_updates = $translation_updates;
660 + }
661 + }
662 +
663 + if ( ! empty( $this->_translation_updates ) ) {
664 + $all_translation_updates = ( isset( $transient_data->translations ) && is_array( $transient_data->translations ) ) ?
665 + $transient_data->translations :
666 + array();
667 +
668 + $current_plugin_translation_updates_map = array();
669 + foreach ( $all_translation_updates as $key => $translation_update ) {
670 + if ( $module_type === ( $translation_update['type'] . 's' ) && $slug === $translation_update['slug'] ) {
671 + $current_plugin_translation_updates_map[ $translation_update['language'] ] = $translation_update;
672 + unset( $all_translation_updates[ $key ] );
673 + }
674 + }
675 +
676 + foreach ( $this->_translation_updates as $translation_update ) {
677 + $lang = $translation_update['language'];
678 + if ( ! isset( $current_plugin_translation_updates_map[ $lang ] ) ||
679 + version_compare( $translation_update['version'], $current_plugin_translation_updates_map[ $lang ]['version'], '>' )
680 + ) {
681 + $current_plugin_translation_updates_map[ $lang ] = $translation_update;
682 + }
683 + }
684 +
685 + $transient_data->translations = array_merge( $all_translation_updates, array_values( $current_plugin_translation_updates_map ) );
686 + }
687 + }
688 +
689 + return $transient_data;
690 + }
691 +
692 + /**
693 + * Get module's required data for the updates' mechanism.
694 + *
695 + * @author Vova Feldman (@svovaf)
696 + * @since 2.0.0
697 + *
698 + * @param \FS_Plugin_Tag $new_version
699 + *
700 + * @return object
701 + */
702 + function get_update_details( FS_Plugin_Tag $new_version ) {
703 + $update = new stdClass();
704 + $update->slug = $this->_fs->get_slug();
705 + $update->new_version = $new_version->version;
706 + $update->url = WP_FS__ADDRESS;
707 + $update->package = $new_version->url;
708 + $update->tested = self::get_tested_wp_version( $new_version->tested_up_to_version );
709 + $update->requires = $new_version->requires_platform_version;
710 + $update->requires_php = $new_version->requires_programming_language_version;
711 +
712 + $icon = $this->_fs->get_local_icon_url();
713 +
714 + if ( ! empty( $icon ) ) {
715 + $update->icons = array(
716 +// '1x' => $icon,
717 +// '2x' => $icon,
718 + 'default' => $icon,
719 + );
720 + }
721 +
722 + if ( $this->_fs->is_premium() && ! empty( $new_version->upgrade_notice ) ) {
723 + $update->upgrade_notice = $new_version->upgrade_notice;
724 + }
725 +
726 + $update->{$this->_fs->get_module_type()} = $this->_fs->get_plugin_basename();
727 +
728 + return $update;
729 + }
730 +
731 + /**
732 + * @author Leo Fajardo (@leorw)
733 + * @since 2.3.0
734 + *
735 + * @param FS_Plugin_Tag $new_version
736 + *
737 + * @return bool
738 + */
739 + private function is_new_version_premium( FS_Plugin_Tag $new_version ) {
740 + $params = fs_parse_url_params( $new_version->url );
741 +
742 + return ( isset( $params['is_premium'] ) && 'true' == $params['is_premium'] );
743 + }
744 +
745 + /**
746 + * Update the updates transient with the module's update information.
747 + *
748 + * This method is required for multisite environment.
749 + * If a module is site activated (not network) and not on the main site,
750 + * the module will NOT be executed on the network level, therefore, the
751 + * custom updates logic will not be executed as well, so unless we force
752 + * the injection of the update into the updates transient, premium updates
753 + * will not work.
754 + *
755 + * @author Vova Feldman (@svovaf)
756 + * @since 2.0.0
757 + *
758 + * @param \FS_Plugin_Tag $new_version
759 + */
760 + function set_update_data( FS_Plugin_Tag $new_version ) {
761 + $this->_logger->entrance();
762 +
763 + if ( ! $this->is_new_version_premium( $new_version ) ) {
764 + return;
765 + }
766 +
767 + $transient_key = "update_{$this->_fs->get_module_type()}s";
768 +
769 + $transient_data = get_site_transient( $transient_key );
770 +
771 + $transient_data = is_object( $transient_data ) ?
772 + $transient_data :
773 + new stdClass();
774 +
775 + // Alias.
776 + $basename = $this->_fs->get_plugin_basename();
777 + $is_plugin = $this->_fs->is_plugin();
778 +
779 + if ( ! isset( $transient_data->response ) ||
780 + ! is_array( $transient_data->response )
781 + ) {
782 + $transient_data->response = array();
783 + } else if ( ! empty( $transient_data->response[ $basename ] ) ) {
784 + $version = $is_plugin ?
785 + ( ! empty( $transient_data->response[ $basename ]->new_version ) ?
786 + $transient_data->response[ $basename ]->new_version :
787 + null
788 + ) : ( ! empty( $transient_data->response[ $basename ]['new_version'] ) ?
789 + $transient_data->response[ $basename ]['new_version'] :
790 + null
791 + );
792 +
793 + if ( $version == $new_version->version ) {
794 + // The update data is already set.
795 + return;
796 + }
797 + }
798 +
799 + // Remove the added filters.
800 + $this->remove_transient_filters();
801 +
802 + $this->_update_details = $this->get_update_details( $new_version );
803 +
804 + // Set update data in transient.
805 + $transient_data->response[ $basename ] = $is_plugin ?
806 + $this->_update_details :
807 + (array) $this->_update_details;
808 +
809 + if ( ! isset( $transient_data->checked ) ||
810 + ! is_array( $transient_data->checked )
811 + ) {
812 + $transient_data->checked = array();
813 + }
814 +
815 + // Flag the module as if it was already checked.
816 + $transient_data->checked[ $basename ] = $this->_fs->get_plugin_version();
817 + $transient_data->last_checked = time();
818 +
819 + set_site_transient( $transient_key, $transient_data );
820 +
821 + $this->add_transient_filters();
822 + }
823 +
824 + /**
825 + * @author Leo Fajardo (@leorw)
826 + * @since 2.0.2
827 + */
828 + function delete_update_data() {
829 + $this->_logger->entrance();
830 +
831 + $transient_key = "update_{$this->_fs->get_module_type()}s";
832 +
833 + $transient_data = get_site_transient( $transient_key );
834 +
835 + // Alias
836 + $basename = $this->_fs->get_plugin_basename();
837 +
838 + if ( ! is_object( $transient_data ) ||
839 + ! isset( $transient_data->response ) ||
840 + ! is_array( $transient_data->response ) ||
841 + empty( $transient_data->response[ $basename ] )
842 + ) {
843 + return;
844 + }
845 +
846 + unset( $transient_data->response[ $basename ] );
847 +
848 + // Remove the added filters.
849 + $this->remove_transient_filters();
850 +
851 + set_site_transient( $transient_key, $transient_data );
852 +
853 + $this->add_transient_filters();
854 + }
855 +
856 + /**
857 + * Try to fetch plugin's info from .org repository.
858 + *
859 + * @author Vova Feldman (@svovaf)
860 + * @since 1.0.5
861 + *
862 + * @param string $action
863 + * @param object $args
864 + *
865 + * @return bool|mixed
866 + */
867 + static function _fetch_plugin_info_from_repository( $action, $args ) {
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' ) ) ) {
878 + $url = set_url_scheme( $url, 'https' );
879 + }
880 +
881 + // The new endpoint version serves only GET requests.
882 + $request = wp_remote_get( $url, array( 'timeout' => 15 ) );
883 +
884 + if ( is_wp_error( $request ) ) {
885 + return false;
886 + }
887 +
888 + $res = json_decode( wp_remote_retrieve_body( $request ), true );
889 +
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 ) ) {
896 + return false;
897 + }
898 +
899 + return $res;
900 + }
901 +
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 + /**
913 + * Fetches module translation updates from wordpress.org.
914 + *
915 + * @author Leo Fajardo (@leorw)
916 + * @since 2.1.2
917 + *
918 + * @param string $module_type
919 + * @param string $slug
920 + *
921 + * @return array|null
922 + */
923 + private function fetch_wp_org_module_translation_updates( $module_type, $slug ) {
924 + $plugin_data = $this->_fs->get_plugin_data();
925 +
926 + $locales = array_values( get_available_languages() );
927 + $locales = apply_filters( "{$module_type}_update_check_locales", $locales );
928 + $locales = array_unique( $locales );
929 +
930 + $plugin_basename = $this->_fs->get_plugin_basename();
931 + if ( 'themes' === $module_type ) {
932 + $plugin_basename = $slug;
933 + }
934 +
935 + global $wp_version;
936 +
937 + $request_args = array(
938 + 'timeout' => 15,
939 + 'body' => array(
940 + "{$module_type}" => json_encode(
941 + array(
942 + "{$module_type}" => array(
943 + $plugin_basename => array(
944 + 'Name' => trim( str_replace( $this->_fs->get_plugin()->premium_suffix, '', $plugin_data['Name'] ) ),
945 + 'Author' => $plugin_data['Author'],
946 + )
947 + )
948 + )
949 + ),
950 + 'translations' => json_encode( $this->get_installed_translations( $module_type, $slug ) ),
951 + 'locale' => json_encode( $locales )
952 + ),
953 + 'user-agent' => ( 'WordPress/' . $wp_version . '; ' . home_url( '/' ) )
954 + );
955 +
956 + $url = "http://api.wordpress.org/{$module_type}/update-check/1.1/";
957 + if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) {
958 + $url = set_url_scheme( $url, 'https' );
959 + }
960 +
961 + $raw_response = Freemius::safe_remote_post(
962 + $url,
963 + $request_args,
964 + WP_FS__TIME_24_HOURS_IN_SEC,
965 + WP_FS__TIME_12_HOURS_IN_SEC,
966 + false
967 + );
968 +
969 + if ( is_wp_error( $raw_response ) ) {
970 + return null;
971 + }
972 +
973 + $response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
974 +
975 + if ( ! is_array( $response ) ) {
976 + return null;
977 + }
978 +
979 + if ( ! isset( $response['translations'] ) || empty( $response['translations'] ) ) {
980 + return null;
981 + }
982 +
983 + return $response['translations'];
984 + }
985 +
986 + /**
987 + * @author Leo Fajardo (@leorw)
988 + * @since 2.1.2
989 + *
990 + * @param string $module_type
991 + * @param string $slug
992 + *
993 + * @return array
994 + */
995 + private function get_installed_translations( $module_type, $slug ) {
996 + if ( function_exists( 'wp_get_installed_translations' ) ) {
997 + return wp_get_installed_translations( $module_type );
998 + }
999 +
1000 + $dir = "/{$module_type}";
1001 +
1002 + if ( ! is_dir( WP_LANG_DIR . $dir ) )
1003 + return array();
1004 +
1005 + $files = scandir( WP_LANG_DIR . $dir );
1006 + if ( ! $files )
1007 + return array();
1008 +
1009 + $language_data = array();
1010 +
1011 + foreach ( $files as $file ) {
1012 + if ( 0 !== strpos( $file, $slug ) ) {
1013 + continue;
1014 + }
1015 +
1016 + if ( '.' === $file[0] || is_dir( WP_LANG_DIR . "{$dir}/{$file}" ) ) {
1017 + continue;
1018 + }
1019 +
1020 + if ( substr( $file, -3 ) !== '.po' ) {
1021 + continue;
1022 + }
1023 +
1024 + if ( ! preg_match( '/(?:(.+)-)?([a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?).po/', $file, $match ) ) {
1025 + continue;
1026 + }
1027 +
1028 + if ( ! in_array( substr( $file, 0, -3 ) . '.mo', $files ) ) {
1029 + continue;
1030 + }
1031 +
1032 + list( , $textdomain, $language ) = $match;
1033 +
1034 + if ( '' === $textdomain ) {
1035 + $textdomain = 'default';
1036 + }
1037 +
1038 + $language_data[ $textdomain ][ $language ] = wp_get_pomo_file_data( WP_LANG_DIR . "{$dir}/{$file}" );
1039 + }
1040 +
1041 + return $language_data;
1042 + }
1043 +
1044 + /**
1045 + * Updates information on the "View version x.x details" page with custom data.
1046 + *
1047 + * @author Vova Feldman (@svovaf)
1048 + * @since 1.0.4
1049 + *
1050 + * @uses FS_Api
1051 + *
1052 + * @param object $data
1053 + * @param string $action
1054 + * @param mixed $args
1055 + *
1056 + * @return object
1057 + */
1058 + function plugins_api_filter( $data, $action = '', $args = null ) {
1059 + $this->_logger->entrance();
1060 +
1061 + if ( ( 'plugin_information' !== $action ) ||
1062 + ! isset( $args->slug )
1063 + ) {
1064 + return $data;
1065 + }
1066 +
1067 + $addon = false;
1068 + $is_addon = false;
1069 + $addon_version = false;
1070 +
1071 + if ( $this->_fs->get_slug() !== $args->slug ) {
1072 + $addon = $this->_fs->get_addon_by_slug( $args->slug );
1073 +
1074 + if ( ! is_object( $addon ) ) {
1075 + return $data;
1076 + }
1077 +
1078 + if ( $this->_fs->is_addon_activated( $addon->id ) ) {
1079 + $addon_version = $this->_fs->get_addon_instance( $addon->id )->get_plugin_version();
1080 + } else if ( $this->_fs->is_addon_installed( $addon->id ) ) {
1081 + $addon_plugin_data = get_plugin_data(
1082 + ( WP_PLUGIN_DIR . '/' . $this->_fs->get_addon_basename( $addon->id ) ),
1083 + false,
1084 + false
1085 + );
1086 +
1087 + if ( ! empty( $addon_plugin_data ) ) {
1088 + $addon_version = $addon_plugin_data['Version'];
1089 + }
1090 + }
1091 +
1092 + $is_addon = true;
1093 + }
1094 +
1095 + $plugin_in_repo = false;
1096 + if ( ! $is_addon && $this->can_fetch_data_from_wp_org() ) {
1097 + // Try to fetch info from .org repository.
1098 + $data = self::_fetch_plugin_info_from_repository( $action, $args );
1099 +
1100 + $plugin_in_repo = ( false !== $data );
1101 + }
1102 +
1103 + if ( ! $plugin_in_repo ) {
1104 + $data = $args;
1105 +
1106 + // Fetch as much as possible info from local files.
1107 + $plugin_local_data = $this->_fs->get_plugin_data();
1108 + $data->name = $plugin_local_data['Name'];
1109 + $data->author = $plugin_local_data['Author'];
1110 + $data->sections = array(
1111 + 'description' => 'Upgrade ' . $plugin_local_data['Name'] . ' to latest.',
1112 + );
1113 +
1114 + // @todo Store extra plugin info on Freemius or parse readme.txt markup.
1115 + /*$info = $this->_fs->get_api_site_scope()->call('/information.json');
1116 +
1117 +if ( !isset($info->error) ) {
1118 + $data = $info;
1119 +}*/
1120 + }
1121 +
1122 + $plugin_version = $is_addon ?
1123 + $addon_version :
1124 + $this->_fs->get_plugin_version();
1125 +
1126 + // Get plugin's newest update.
1127 + $new_version = $this->get_latest_download_details( $is_addon ? $addon->id : false, $plugin_version );
1128 +
1129 + if ( ! is_object( $new_version ) || empty( $new_version->version ) ) {
1130 + $data->version = $plugin_version;
1131 + } else {
1132 + if ( $is_addon ) {
1133 + $data->name = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1134 + $data->slug = $addon->slug;
1135 + $data->url = WP_FS__ADDRESS;
1136 + $data->package = $new_version->url;
1137 + }
1138 +
1139 + if ( ! $plugin_in_repo ) {
1140 + $data->last_updated = ! is_null( $new_version->updated ) ? $new_version->updated : $new_version->created;
1141 + $data->requires = $new_version->requires_platform_version;
1142 + $data->requires_php = $new_version->requires_programming_language_version;
1143 + $data->tested = $new_version->tested_up_to_version;
1144 + }
1145 +
1146 + $data->version = $new_version->version;
1147 + $data->download_link = $new_version->url;
1148 +
1149 + if ( isset( $new_version->readme ) && is_object( $new_version->readme ) ) {
1150 + $new_version_readme_data = $new_version->readme;
1151 + if ( isset( $new_version_readme_data->sections ) ) {
1152 + $new_version_readme_data->sections = (array) $new_version_readme_data->sections;
1153 + } else {
1154 + $new_version_readme_data->sections = array();
1155 + }
1156 +
1157 + if ( isset( $data->sections ) ) {
1158 + if ( isset( $data->sections['screenshots'] ) ) {
1159 + $new_version_readme_data->sections['screenshots'] = $data->sections['screenshots'];
1160 + }
1161 +
1162 + if ( isset( $data->sections['reviews'] ) ) {
1163 + $new_version_readme_data->sections['reviews'] = $data->sections['reviews'];
1164 + }
1165 + }
1166 +
1167 + if ( isset( $new_version_readme_data->banners ) ) {
1168 + $new_version_readme_data->banners = (array) $new_version_readme_data->banners;
1169 + } else if ( isset( $data->banners ) ) {
1170 + $new_version_readme_data->banners = $data->banners;
1171 + }
1172 +
1173 + $wp_org_sections = array(
1174 + 'author',
1175 + 'author_profile',
1176 + 'rating',
1177 + 'ratings',
1178 + 'num_ratings',
1179 + 'support_threads',
1180 + 'support_threads_resolved',
1181 + 'active_installs',
1182 + 'added',
1183 + 'homepage'
1184 + );
1185 +
1186 + foreach ( $wp_org_sections as $wp_org_section ) {
1187 + if ( isset( $data->{$wp_org_section} ) ) {
1188 + $new_version_readme_data->{$wp_org_section} = $data->{$wp_org_section};
1189 + }
1190 + }
1191 +
1192 + $data = $new_version_readme_data;
1193 + }
1194 + }
1195 +
1196 + if ( ! empty( $data->tested ) ) {
1197 + $data->tested = self::get_tested_wp_version( $data->tested );
1198 + }
1199 +
1200 + return $data;
1201 + }
1202 +
1203 + /**
1204 + * @since 2.5.3 If the current WordPress version is a patch of the tested version (e.g., 6.1.2 is a patch of 6.1), then override the tested version with the patch so developers won't need to release a new version just to bump the latest supported WP version.
1205 + *
1206 + * @param string|null $tested_up_to
1207 + *
1208 + * @return string|null
1209 + */
1210 + private static function get_tested_wp_version( $tested_up_to ) {
1211 + $current_wp_version = get_bloginfo( 'version' );
1212 +
1213 + return ( ! empty($tested_up_to) && fs_starts_with( $current_wp_version, $tested_up_to . '.' ) ) ?
1214 + $current_wp_version :
1215 + $tested_up_to;
1216 + }
1217 +
1218 + /**
1219 + * @author Vova Feldman (@svovaf)
1220 + * @since 1.2.1.7
1221 + *
1222 + * @param number|bool $addon_id
1223 + * @param bool|string $newer_than Since 2.2.1
1224 + * @param bool|string $fetch_readme Since 2.2.1
1225 + *
1226 + * @return object
1227 + */
1228 + private function get_latest_download_details( $addon_id = false, $newer_than = false, $fetch_readme = true ) {
1229 + return $this->_fs->_fetch_latest_version( $addon_id, true, FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than, $fetch_readme );
1230 + }
1231 +
1232 + /**
1233 + * Checks if a given basename has a matching folder name
1234 + * with the current context plugin.
1235 + *
1236 + * @author Vova Feldman (@svovaf)
1237 + * @since 1.2.1.6
1238 + *
1239 + * @return bool
1240 + */
1241 + private function is_correct_folder_name() {
1242 + return ( $this->_fs->get_target_folder_name() == trim( dirname( $this->_fs->get_plugin_basename() ), '/\\' ) );
1243 + }
1244 +
1245 + /**
1246 + * This is a special after upgrade handler for migrating modules
1247 + * that didn't use the '-premium' suffix folder structure before
1248 + * the migration.
1249 + *
1250 + * @author Vova Feldman (@svovaf)
1251 + * @since 1.2.1.6
1252 + *
1253 + * @param bool $response Install response.
1254 + * @param array $hook_extra Extra arguments passed to hooked filters.
1255 + * @param array $result Installation result data.
1256 + *
1257 + * @return bool
1258 + */
1259 + function _maybe_update_folder_name( $response, $hook_extra, $result ) {
1260 + $basename = $this->_fs->get_plugin_basename();
1261 +
1262 + if ( true !== $response ||
1263 + empty( $hook_extra ) ||
1264 + empty( $hook_extra['plugin'] ) ||
1265 + $basename !== $hook_extra['plugin']
1266 + ) {
1267 + return $response;
1268 + }
1269 +
1270 + $active_plugins_basenames = get_option( 'active_plugins' );
1271 +
1272 + foreach ( $active_plugins_basenames as $key => $active_plugin_basename ) {
1273 + if ( $basename === $active_plugin_basename ) {
1274 + // Get filename including extension.
1275 + $filename = basename( $basename );
1276 +
1277 + $new_basename = plugin_basename(
1278 + trailingslashit( $this->_fs->is_premium() ? $this->_fs->get_premium_slug() : $this->_fs->get_slug() ) .
1279 + $filename
1280 + );
1281 +
1282 + // Verify that the expected correct path exists.
1283 + if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $new_basename ) ) ) {
1284 + // Override active plugin name.
1285 + $active_plugins_basenames[ $key ] = $new_basename;
1286 + update_option( 'active_plugins', $active_plugins_basenames );
1287 + }
1288 +
1289 + break;
1290 + }
1291 + }
1292 +
1293 + return $response;
1294 + }
1295 +
1296 + #----------------------------------------------------------------------------------
1297 + #region Auto Activation
1298 + #----------------------------------------------------------------------------------
1299 +
1300 + /**
1301 + * Installs and active a plugin when explicitly requested that from a 3rd party service.
1302 + *
1303 + * This logic was inspired by the TGMPA GPL licensed library by Thomas Griffin.
1304 + *
1305 + * @link http://tgmpluginactivation.com/
1306 + *
1307 + * @author Vova Feldman
1308 + * @since 1.2.1.7
1309 + *
1310 + * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
1311 + *
1312 + * @uses WP_Filesystem
1313 + * @uses WP_Error
1314 + * @uses WP_Upgrader
1315 + * @uses Plugin_Upgrader
1316 + * @uses Plugin_Installer_Skin
1317 + * @uses Plugin_Upgrader_Skin
1318 + *
1319 + * @param number|bool $plugin_id
1320 + *
1321 + * @return array
1322 + */
1323 + function install_and_activate_plugin( $plugin_id = false ) {
1324 + if ( ! empty( $plugin_id ) && ! FS_Plugin::is_valid_id( $plugin_id ) ) {
1325 + // Invalid plugin ID.
1326 + return array(
1327 + 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1328 + 'code' => 'invalid_module_id',
1329 + );
1330 + }
1331 +
1332 + $is_addon = false;
1333 + if ( FS_Plugin::is_valid_id( $plugin_id ) &&
1334 + $plugin_id != $this->_fs->get_id()
1335 + ) {
1336 + $addon = $this->_fs->get_addon( $plugin_id );
1337 +
1338 + if ( ! is_object( $addon ) ) {
1339 + // Invalid add-on ID.
1340 + return array(
1341 + 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1342 + 'code' => 'invalid_module_id',
1343 + );
1344 + }
1345 +
1346 + $slug = $addon->slug;
1347 + $premium_slug = $addon->premium_slug;
1348 + $title = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1349 +
1350 + $is_addon = true;
1351 + } else {
1352 + $slug = $this->_fs->get_slug();
1353 + $premium_slug = $this->_fs->get_premium_slug();
1354 + $title = $this->_fs->get_plugin_title() .
1355 + ( $this->_fs->is_addon() ? ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' ) : '' );
1356 + }
1357 +
1358 + if ( $this->is_premium_plugin_active( $plugin_id ) ) {
1359 + // Premium version already activated.
1360 + return array(
1361 + 'message' => $is_addon ?
1362 + $this->_fs->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ) :
1363 + $this->_fs->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
1364 + 'code' => 'premium_installed',
1365 + );
1366 + }
1367 +
1368 + $latest_version = $this->get_latest_download_details( $plugin_id, false, false );
1369 + $target_folder = $premium_slug;
1370 +
1371 + // Prep variables for Plugin_Installer_Skin class.
1372 + $extra = array();
1373 + $extra['slug'] = $target_folder;
1374 + $source = $latest_version->url;
1375 + $api = null;
1376 +
1377 + $install_url = add_query_arg(
1378 + array(
1379 + 'action' => 'install-plugin',
1380 + 'plugin' => urlencode( $slug ),
1381 + ),
1382 + 'update.php'
1383 + );
1384 +
1385 + if ( ! class_exists( 'Plugin_Upgrader', false ) ) {
1386 + // Include required resources for the installation.
1387 + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1388 + }
1389 +
1390 + $skin_args = array(
1391 + 'type' => 'web',
1392 + 'title' => sprintf( $this->_fs->get_text_inline( 'Installing plugin: %s', 'installing-plugin-x' ), $title ),
1393 + 'url' => esc_url_raw( $install_url ),
1394 + 'nonce' => 'install-plugin_' . $slug,
1395 + 'plugin' => '',
1396 + 'api' => $api,
1397 + 'extra' => $extra,
1398 + );
1399 +
1400 +// $skin = new Automatic_Upgrader_Skin( $skin_args );
1401 +// $skin = new Plugin_Installer_Skin( $skin_args );
1402 + $skin = new WP_Ajax_Upgrader_Skin( $skin_args );
1403 +
1404 + // Create a new instance of Plugin_Upgrader.
1405 + $upgrader = new Plugin_Upgrader( $skin );
1406 +
1407 + // Perform the action and install the plugin from the $source urldecode().
1408 + add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
1409 +
1410 + $install_result = $upgrader->install( $source );
1411 +
1412 + remove_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1 );
1413 +
1414 + if ( is_wp_error( $install_result ) ) {
1415 + return array(
1416 + 'message' => $install_result->get_error_message(),
1417 + 'code' => $install_result->get_error_code(),
1418 + );
1419 + } elseif ( is_wp_error( $skin->result ) ) {
1420 + return array(
1421 + 'message' => $skin->result->get_error_message(),
1422 + 'code' => $skin->result->get_error_code(),
1423 + );
1424 + } elseif ( $skin->get_errors()->get_error_code() ) {
1425 + return array(
1426 + 'message' => $skin->get_error_messages(),
1427 + 'code' => 'unknown',
1428 + );
1429 + } elseif ( is_null( $install_result ) ) {
1430 + global $wp_filesystem;
1431 +
1432 + $error_code = 'unable_to_connect_to_filesystem';
1433 + $error_message = $this->_fs->get_text_inline( 'Unable to connect to the filesystem. Please confirm your credentials.' );
1434 +
1435 + // Pass through the error from WP_Filesystem if one was raised.
1436 + if ( $wp_filesystem instanceof WP_Filesystem_Base &&
1437 + is_wp_error( $wp_filesystem->errors ) &&
1438 + $wp_filesystem->errors->get_error_code()
1439 + ) {
1440 + $error_message = $wp_filesystem->errors->get_error_message();
1441 + }
1442 +
1443 + return array(
1444 + 'message' => $error_message,
1445 + 'code' => $error_code,
1446 + );
1447 + }
1448 +
1449 + // Grab the full path to the main plugin's file.
1450 + $plugin_activate = $upgrader->plugin_info();
1451 +
1452 + // Try to activate the plugin.
1453 + $activation_result = $this->try_activate_plugin( $plugin_activate );
1454 +
1455 + if ( is_wp_error( $activation_result ) ) {
1456 + return array(
1457 + 'message' => $activation_result->get_error_message(),
1458 + 'code' => $activation_result->get_error_code(),
1459 + );
1460 + }
1461 +
1462 + return $skin->get_upgrade_messages();
1463 + }
1464 +
1465 + /**
1466 + * Tries to activate a plugin. If fails, returns the error.
1467 + *
1468 + * @author Vova Feldman
1469 + * @since 1.2.1.7
1470 + *
1471 + * @param string $file_path Path within wp-plugins/ to main plugin file.
1472 + * This determines the styling of the output messages.
1473 + *
1474 + * @return bool|WP_Error
1475 + */
1476 + protected function try_activate_plugin( $file_path ) {
1477 + $activate = activate_plugin( $file_path, '', $this->_fs->is_network_active() );
1478 +
1479 + return is_wp_error( $activate ) ?
1480 + $activate :
1481 + true;
1482 + }
1483 +
1484 + /**
1485 + * Check if a premium module version is already active.
1486 + *
1487 + * @author Vova Feldman
1488 + * @since 1.2.1.7
1489 + *
1490 + * @param number|bool $plugin_id
1491 + *
1492 + * @return bool
1493 + */
1494 + private function is_premium_plugin_active( $plugin_id = false ) {
1495 + if ( $plugin_id != $this->_fs->get_id() ) {
1496 + return $this->_fs->is_addon_activated( $plugin_id, true );
1497 + }
1498 +
1499 + return is_plugin_active( $this->_fs->premium_plugin_basename() );
1500 + }
1501 +
1502 + /**
1503 + * Store the basename since it's not always available in the `_maybe_adjust_source_dir` method below.
1504 + *
1505 + * @author Leo Fajardo (@leorw)
1506 + * @since 2.2.1
1507 + *
1508 + * @param bool|WP_Error $response Response.
1509 + * @param array $hook_extra Extra arguments passed to hooked filters.
1510 + *
1511 + * @return bool|WP_Error
1512 + */
1513 + static function _store_basename_for_source_adjustment( $response, $hook_extra ) {
1514 + if ( isset( $hook_extra['plugin'] ) ) {
1515 + self::$_upgrade_basename = $hook_extra['plugin'];
1516 + } else if ( isset( $hook_extra['theme'] ) ) {
1517 + self::$_upgrade_basename = $hook_extra['theme'];
1518 + } else {
1519 + self::$_upgrade_basename = null;
1520 + }
1521 +
1522 + return $response;
1523 + }
1524 +
1525 + /**
1526 + * Adjust the plugin directory name if necessary.
1527 + * Assumes plugin has a folder (not a single file plugin).
1528 + *
1529 + * The final destination directory of a plugin is based on the subdirectory name found in the
1530 + * (un)zipped source. In some cases this subdirectory name is not the same as the expected
1531 + * slug and the plugin will not be recognized as installed. This is fixed by adjusting
1532 + * the temporary unzipped source subdirectory name to the expected plugin slug.
1533 + *
1534 + * @author Vova Feldman
1535 + * @since 1.2.1.7
1536 + * @since 2.2.1 The method was converted to static since when the admin update bulk products via the Updates section, the logic applies the `upgrader_source_selection` filter for every product that is being updated.
1537 + *
1538 + * @param string $source Path to upgrade/zip-file-name.tmp/subdirectory/.
1539 + * @param string $remote_source Path to upgrade/zip-file-name.tmp.
1540 + * @param \WP_Upgrader $upgrader Instance of the upgrader which installs the plugin.
1541 + *
1542 + * @return string|WP_Error
1543 + */
1544 + static function _maybe_adjust_source_dir( $source, $remote_source, $upgrader ) {
1545 + if ( ! is_object( $GLOBALS['wp_filesystem'] ) ) {
1546 + return $source;
1547 + }
1548 +
1549 + $basename = self::$_upgrade_basename;
1550 + $is_theme = false;
1551 +
1552 + // Figure out what the slug is supposed to be.
1553 + if ( isset( $upgrader->skin->options['extra'] ) ) {
1554 + // Set by the auto-install logic.
1555 + $desired_slug = $upgrader->skin->options['extra']['slug'];
1556 + } else if ( ! empty( $basename ) ) {
1557 + /**
1558 + * If it doesn't end with ".php", it's a theme.
1559 + *
1560 + * @author Leo Fajardo (@leorw)
1561 + * @since 2.2.1
1562 + */
1563 + $is_theme = ( ! fs_ends_with( $basename, '.php' ) );
1564 +
1565 + $desired_slug = ( ! $is_theme ) ?
1566 + dirname( $basename ) :
1567 + // Theme slug
1568 + $basename;
1569 + } else {
1570 + // Can't figure out the desired slug, stop the execution.
1571 + return $source;
1572 + }
1573 +
1574 + if ( is_multisite() ) {
1575 + /**
1576 + * If we are running in a multisite environment and the product is not network activated,
1577 + * the instance will not exist anyway. Therefore, try to update the source if necessary
1578 + * regardless if the Freemius instance of the product exists or not.
1579 + *
1580 + * @author Vova Feldman
1581 + */
1582 + } else if ( ! empty( $basename ) ) {
1583 + $fs = Freemius::get_instance_by_file(
1584 + $basename,
1585 + $is_theme ?
1586 + WP_FS__MODULE_TYPE_THEME :
1587 + WP_FS__MODULE_TYPE_PLUGIN
1588 + );
1589 +
1590 + if ( ! is_object( $fs ) ) {
1591 + /**
1592 + * If the Freemius instance does not exist on a non-multisite network environment, it means that:
1593 + * 1. The product is not powered by Freemius; OR
1594 + * 2. The product is not activated, therefore, we don't mind if after the update the folder name will change.
1595 + *
1596 + * @author Leo Fajardo (@leorw)
1597 + * @since 2.2.1
1598 + */
1599 + return $source;
1600 + }
1601 + }
1602 +
1603 + $subdir_name = untrailingslashit( str_replace( trailingslashit( $remote_source ), '', $source ) );
1604 +
1605 + if ( ! empty( $subdir_name ) && $subdir_name !== $desired_slug ) {
1606 + $from_path = untrailingslashit( $source );
1607 + $to_path = trailingslashit( $remote_source ) . $desired_slug;
1608 +
1609 + if ( true === $GLOBALS['wp_filesystem']->move( $from_path, $to_path ) ) {
1610 + return trailingslashit( $to_path );
1611 + }
1612 +
1613 + return new WP_Error(
1614 + 'rename_failed',
1615 + fs_text_inline( 'The remote plugin package does not contain a folder with the desired slug and renaming did not work.', 'module-package-rename-failure' ),
1616 + array(
1617 + 'found' => $subdir_name,
1618 + 'expected' => $desired_slug
1619 + )
1620 + );
1621 + }
1622 +
1623 + return $source;
1624 + }
1625 +
1626 + #endregion
1627 + }