444 lines
13 KiB
PHP
444 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* GitHub Updater
|
|
*
|
|
* @author Andy Fragen
|
|
* @license GPL-2.0+
|
|
* @link https://github.com/afragen/github-updater
|
|
* @package github-updater
|
|
*/
|
|
|
|
namespace Fragen\GitHub_Updater;
|
|
|
|
use Fragen\Singleton;
|
|
use Fragen\GitHub_Updater\Traits\GHU_Trait;
|
|
|
|
/*
|
|
* Exit if called directly.
|
|
*/
|
|
if ( ! defined( 'WPINC' ) ) {
|
|
die;
|
|
}
|
|
|
|
/**
|
|
* Class Plugin
|
|
*
|
|
* Update a WordPress plugin from a GitHub repo.
|
|
*
|
|
* @author Andy Fragen
|
|
* @author Codepress
|
|
* @link https://github.com/codepress/github-plugin-updater
|
|
*/
|
|
class Plugin extends Base {
|
|
use GHU_Trait;
|
|
|
|
/**
|
|
* Rollback variable
|
|
*
|
|
* @var string branch
|
|
*/
|
|
public $tag = false;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct() {
|
|
parent::__construct();
|
|
$this->load_options();
|
|
|
|
// Get details of installed git sourced plugins.
|
|
$this->config = $this->get_plugin_meta();
|
|
|
|
if ( null === $this->config ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an array of configurations for the known plugins.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function get_plugin_configs() {
|
|
return $this->config;
|
|
}
|
|
|
|
/**
|
|
* Get details of Git-sourced plugins from those that are installed.
|
|
*
|
|
* @return array Indexed array of associative arrays of plugin details.
|
|
*/
|
|
protected function get_plugin_meta() {
|
|
// Ensure get_plugins() function is available.
|
|
include_once ABSPATH . '/wp-admin/includes/plugin.php';
|
|
|
|
$plugins = get_plugins();
|
|
$git_plugins = [];
|
|
|
|
/**
|
|
* Filter to add plugins not containing appropriate header line.
|
|
*
|
|
* @since 5.4.0
|
|
* @access public
|
|
*
|
|
* @param array $additions Listing of plugins to add.
|
|
* Default null.
|
|
* @param array $plugins Listing of all plugins.
|
|
* @param string 'plugin' Type being passed.
|
|
*/
|
|
$additions = apply_filters( 'github_updater_additions', null, $plugins, 'plugin' );
|
|
$plugins = array_merge( $plugins, (array) $additions );
|
|
|
|
foreach ( (array) $plugins as $plugin => $headers ) {
|
|
$git_plugin = [];
|
|
|
|
foreach ( (array) static::$extra_headers as $value ) {
|
|
$header = null;
|
|
|
|
if ( empty( $headers[ $value ] ) || false === stripos( $value, 'Plugin' ) ) {
|
|
continue;
|
|
}
|
|
|
|
$header_parts = explode( ' ', $value );
|
|
$repo_parts = $this->get_repo_parts( $header_parts[0], 'plugin' );
|
|
|
|
if ( $repo_parts['bool'] ) {
|
|
$header = $this->parse_header_uri( $headers[ $value ] );
|
|
if ( empty( $header ) ) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$header = $this->parse_extra_headers( $header, $headers, $header_parts, $repo_parts );
|
|
$current_branch = "current_branch_{$header['repo']}";
|
|
$branch = isset( static::$options[ $current_branch ] )
|
|
? static::$options[ $current_branch ]
|
|
: false;
|
|
|
|
$git_plugin['type'] = 'plugin';
|
|
$git_plugin['git'] = $repo_parts['git_server'];
|
|
$git_plugin['uri'] = "{$header['base_uri']}/{$header['owner_repo']}";
|
|
$git_plugin['enterprise'] = $header['enterprise_uri'];
|
|
$git_plugin['enterprise_api'] = $header['enterprise_api'];
|
|
$git_plugin['owner'] = $header['owner'];
|
|
$git_plugin['slug'] = $header['repo'];
|
|
$git_plugin['branch'] = $branch ?: 'master';
|
|
$git_plugin['file'] = $plugin;
|
|
$git_plugin['local_path'] = WP_PLUGIN_DIR . "/{$header['repo']}/";
|
|
|
|
$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $git_plugin['file'] );
|
|
$git_plugin['author'] = $plugin_data['AuthorName'];
|
|
$git_plugin['name'] = $plugin_data['Name'];
|
|
$git_plugin['homepage'] = $plugin_data['PluginURI'];
|
|
$git_plugin['local_version'] = strtolower( $plugin_data['Version'] );
|
|
$git_plugin['sections']['description'] = $plugin_data['Description'];
|
|
$git_plugin['languages'] = $header['languages'];
|
|
$git_plugin['ci_job'] = $header['ci_job'];
|
|
$git_plugin['release_asset'] = $header['release_asset'];
|
|
$git_plugin['broken'] = ( empty( $header['owner'] ) || empty( $header['repo'] ) );
|
|
|
|
$git_plugin['banners']['high'] =
|
|
file_exists( WP_PLUGIN_DIR . "/{$header['repo']}/assets/banner-1544x500.png" )
|
|
? WP_PLUGIN_URL . "/{$header['repo']}/assets/banner-1544x500.png"
|
|
: null;
|
|
|
|
$git_plugin['banners']['low'] =
|
|
file_exists( WP_PLUGIN_DIR . "/{$header['repo']}/assets/banner-772x250.png" )
|
|
? WP_PLUGIN_URL . "/{$header['repo']}/assets/banner-772x250.png"
|
|
: null;
|
|
|
|
$git_plugin['icons'] = [];
|
|
$icons = [
|
|
'svg' => 'icon.svg',
|
|
'1x_png' => 'icon-128x128.png',
|
|
'1x_jpg' => 'icon-128x128.jpg',
|
|
'2x_png' => 'icon-256x256.png',
|
|
'2x_jpg' => 'icon-256x256.jpg',
|
|
];
|
|
foreach ( $icons as $key => $filename ) {
|
|
$key = preg_replace( '/_png|_jpg/', '', $key );
|
|
$git_plugin['icons'][ $key ] = file_exists( $git_plugin['local_path'] . 'assets/' . $filename )
|
|
? WP_PLUGIN_URL . "/{$git_plugin['slug']}/assets/{$filename}"
|
|
: null;
|
|
}
|
|
}
|
|
|
|
// Exit if not git hosted plugin.
|
|
if ( empty( $git_plugin ) ) {
|
|
continue;
|
|
}
|
|
|
|
$git_plugins[ $git_plugin['slug'] ] = (object) $git_plugin;
|
|
}
|
|
|
|
return $git_plugins;
|
|
}
|
|
|
|
/**
|
|
* Get remote plugin meta to populate $config plugin objects.
|
|
* Calls to remote APIs to get data.
|
|
*/
|
|
public function get_remote_plugin_meta() {
|
|
$plugins = [];
|
|
foreach ( (array) $this->config as $plugin ) {
|
|
/**
|
|
* Filter to set if WP-Cron is disabled or if user wants to return to old way.
|
|
*
|
|
* @since 7.4.0
|
|
* @access public
|
|
*
|
|
* @param bool
|
|
*/
|
|
if ( ! $this->waiting_for_background_update( $plugin ) || static::is_wp_cli()
|
|
|| apply_filters( 'github_updater_disable_wpcron', false )
|
|
) {
|
|
$this->get_remote_repo_meta( $plugin );
|
|
} else {
|
|
$plugins[ $plugin->slug ] = $plugin;
|
|
}
|
|
|
|
// current_filter() check due to calling hook for shiny updates, don't show row twice.
|
|
if ( ! $plugin->release_asset && 'init' === current_filter() &&
|
|
( ! is_multisite() || is_network_admin() )
|
|
) {
|
|
add_action( "after_plugin_row_{$plugin->file}", [ $this, 'plugin_branch_switcher' ], 15, 3 );
|
|
}
|
|
}
|
|
|
|
$schedule_event = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? is_main_site() : true;
|
|
|
|
if ( $schedule_event ) {
|
|
if ( ! wp_next_scheduled( 'ghu_get_remote_plugin' ) &&
|
|
! $this->is_duplicate_wp_cron_event( 'ghu_get_remote_plugin' ) &&
|
|
! apply_filters( 'github_updater_disable_wpcron', false )
|
|
) {
|
|
wp_schedule_single_event( time(), 'ghu_get_remote_plugin', [ $plugins ] );
|
|
}
|
|
}
|
|
|
|
if ( ! static::is_wp_cli() ) {
|
|
$this->load_pre_filters();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load pre-update filters.
|
|
*/
|
|
public function load_pre_filters() {
|
|
add_filter( 'plugin_row_meta', [ $this, 'plugin_row_meta' ], 10, 2 );
|
|
add_filter( 'plugins_api', [ $this, 'plugins_api' ], 99, 3 );
|
|
add_filter( 'site_transient_update_plugins', [ $this, 'update_site_transient' ], 15, 1 );
|
|
}
|
|
|
|
/**
|
|
* Add branch switch row to plugins page.
|
|
*
|
|
* @param string $plugin_file
|
|
* @param \stdClass $plugin_data
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function plugin_branch_switcher( $plugin_file, $plugin_data ) {
|
|
if ( empty( static::$options['branch_switch'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$enclosure = $this->update_row_enclosure( $plugin_file, 'plugin', true );
|
|
$plugin = $this->get_repo_slugs( dirname( $plugin_file ) );
|
|
$nonced_update_url = wp_nonce_url(
|
|
$this->get_update_url( 'plugin', 'upgrade-plugin', $plugin_file ),
|
|
'upgrade-plugin_' . $plugin_file
|
|
);
|
|
|
|
if ( ! empty( $plugin ) ) {
|
|
$id = $plugin['slug'] . '-id';
|
|
$branches = isset( $this->config[ $plugin['slug'] ]->branches )
|
|
? $this->config[ $plugin['slug'] ]->branches
|
|
: null;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
// Get current branch.
|
|
$repo = $this->config[ $plugin['slug'] ];
|
|
$branch = Singleton::get_instance( 'Branch', $this )->get_current_branch( $repo );
|
|
|
|
$branch_switch_data = [];
|
|
$branch_switch_data['slug'] = $plugin['slug'];
|
|
$branch_switch_data['nonced_update_url'] = $nonced_update_url;
|
|
$branch_switch_data['id'] = $id;
|
|
$branch_switch_data['branch'] = $branch;
|
|
$branch_switch_data['branches'] = $branches;
|
|
|
|
/*
|
|
* Create after_plugin_row_
|
|
*/
|
|
echo $enclosure['open'];
|
|
$this->make_branch_switch_row( $branch_switch_data );
|
|
echo $enclosure['close'];
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add 'View details' link to plugins page.
|
|
*
|
|
* @param array $links
|
|
* @param string $file
|
|
*
|
|
* @return array $links
|
|
*/
|
|
public function plugin_row_meta( $links, $file ) {
|
|
$regex_pattern = '/<a href="(.*)">(.*)<\/a>/';
|
|
$repo = dirname( $file );
|
|
|
|
/*
|
|
* Sanity check for some commercial plugins.
|
|
*/
|
|
if ( ! isset( $links[2] ) ) {
|
|
return $links;
|
|
}
|
|
|
|
preg_match( $regex_pattern, $links[2], $matches );
|
|
|
|
/*
|
|
* Remove 'Visit plugin site' link in favor or 'View details' link.
|
|
*/
|
|
if ( array_key_exists( $repo, $this->config ) ) {
|
|
if ( null !== $repo ) {
|
|
unset( $links[2] );
|
|
$links[] = sprintf(
|
|
'<a href="%s" class="thickbox">%s</a>',
|
|
esc_url(
|
|
add_query_arg(
|
|
[
|
|
'tab' => 'plugin-information',
|
|
'plugin' => $repo,
|
|
'TB_iframe' => 'true',
|
|
'width' => 600,
|
|
'height' => 550,
|
|
],
|
|
network_admin_url( 'plugin-install.php' )
|
|
)
|
|
),
|
|
esc_html__( 'View details', 'github-updater' )
|
|
);
|
|
}
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
/**
|
|
* Put changelog in plugins_api, return WP.org data as appropriate
|
|
*
|
|
* @param bool $false
|
|
* @param string $action
|
|
* @param \stdClass $response
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function plugins_api( $false, $action, $response ) {
|
|
if ( ! ( 'plugin_information' === $action ) ) {
|
|
return $false;
|
|
}
|
|
|
|
$plugin = isset( $this->config[ $response->slug ] ) ? $this->config[ $response->slug ] : false;
|
|
|
|
// Skip if waiting for background update.
|
|
if ( $this->waiting_for_background_update( $plugin ) ) {
|
|
return $false;
|
|
}
|
|
|
|
// wp.org plugin.
|
|
if ( ! $plugin || ( $plugin->dot_org && 'master' === $plugin->branch ) ) {
|
|
return $false;
|
|
}
|
|
|
|
$response->slug = $plugin->slug;
|
|
$response->plugin_name = $plugin->name;
|
|
$response->name = $plugin->name;
|
|
$response->author = $plugin->author;
|
|
$response->homepage = $plugin->homepage;
|
|
$response->donate_link = $plugin->donate_link;
|
|
$response->version = $plugin->remote_version;
|
|
$response->sections = $plugin->sections;
|
|
$response->requires = $plugin->requires;
|
|
$response->requires_php = $plugin->requires_php;
|
|
$response->tested = $plugin->tested;
|
|
$response->downloaded = $plugin->downloaded;
|
|
$response->last_updated = $plugin->last_updated;
|
|
$response->download_link = $plugin->download_link;
|
|
$response->banners = $plugin->banners;
|
|
$response->icons = ! empty( $plugin->icons ) ? $plugin->icons : [];
|
|
$response->contributors = $plugin->contributors;
|
|
if ( ! $this->is_private( $plugin ) ) {
|
|
$response->num_ratings = $plugin->num_ratings;
|
|
$response->rating = $plugin->rating;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Hook into site_transient_update_plugins to update from GitHub.
|
|
*
|
|
* @param \stdClass $transient
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function update_site_transient( $transient ) {
|
|
foreach ( (array) $this->config as $plugin ) {
|
|
if ( $this->can_update_repo( $plugin ) ) {
|
|
$response = [
|
|
'slug' => $plugin->slug,
|
|
'plugin' => $plugin->file,
|
|
'new_version' => $plugin->remote_version,
|
|
'url' => $plugin->uri,
|
|
'package' => $plugin->download_link,
|
|
'icons' => $plugin->icons,
|
|
'tested' => $plugin->tested,
|
|
'requires_php' => $plugin->requires_php,
|
|
'branch' => $plugin->branch,
|
|
'branches' => array_keys( $plugin->branches ),
|
|
'type' => "{$plugin->git}-{$plugin->type}",
|
|
];
|
|
|
|
// Skip on RESTful updating.
|
|
if ( isset( $_GET['action'], $_GET['plugin'] ) &&
|
|
'github-updater-update' === $_GET['action'] &&
|
|
$response['slug'] === $_GET['plugin']
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Pull update from dot org if not overriding.
|
|
if ( ! $this->override_dot_org( 'plugin', $plugin ) ) {
|
|
continue;
|
|
}
|
|
|
|
$transient->response[ $plugin->file ] = (object) $response;
|
|
} else {
|
|
/**
|
|
* Filter to return array of overrides to dot org.
|
|
*
|
|
* @since 8.5.0
|
|
* @return array
|
|
*/
|
|
$overrides = apply_filters( 'github_updater_override_dot_org', [] );
|
|
if ( isset( $transient->response[ $plugin->file ] ) && in_array( $plugin->file, $overrides, true ) ) {
|
|
unset( $transient->response[ $plugin->file ] );
|
|
}
|
|
}
|
|
|
|
// Set transient on rollback.
|
|
if ( isset( $_GET['plugin'], $_GET['rollback'] ) && $plugin->file === $_GET['plugin']
|
|
) {
|
|
$transient->response[ $plugin->file ] = $this->set_rollback_transient( 'plugin', $plugin );
|
|
}
|
|
}
|
|
|
|
return $transient;
|
|
}
|
|
}
|