Move into wp-content path

Signed-off-by: Adrian Nöthlich <git@promasu.tech>
This commit is contained in:
2019-08-31 00:48:20 +02:00
parent f523d8ccc0
commit 3724cc6edd
342 changed files with 108652 additions and 0 deletions

View File

@@ -0,0 +1,735 @@
<?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\API_Common;
use Fragen\GitHub_Updater\Traits\GHU_Trait;
use Fragen\GitHub_Updater\Traits\Basic_Auth_Loader;
use Fragen\GitHub_Updater\API\GitHub_API;
use Fragen\GitHub_Updater\API\Bitbucket_API;
use Fragen\GitHub_Updater\API\Bitbucket_Server_API;
use Fragen\GitHub_Updater\API\GitLab_API;
use Fragen\GitHub_Updater\API\Gitea_API;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class API
*/
class API {
use API_Common, GHU_Trait, Basic_Auth_Loader;
/**
* Holds HTTP error code from API call.
*
* @var array ( $this->type->slug => $code )
*/
protected static $error_code = [];
/**
* Holds site options.
*
* @var array $options
*/
protected static $options;
/**
* Holds extra headers.
*
* @var array $extra_headers
*/
protected static $extra_headers;
/**
* Variable for setting update transient hours.
*
* @var integer
*/
protected $hours = 12;
/**
* Variable to hold all repository remote info.
*
* @access protected
* @var array
*/
protected $response = [];
/**
* Variable to hold AWS redirect URL.
*
* @var string|\WP_Error $redirect
*/
protected $redirect;
/**
* API constructor.
*/
public function __construct() {
static::$options = $this->get_class_vars( 'Base', 'options' );
static::$extra_headers = Singleton::get_instance( 'Base', $this )->add_headers( [] );
}
/**
* Adds custom user agent for GitHub Updater.
*
* @access public
*
* @param array $args Existing HTTP Request arguments.
* @param string $url URL being passed.
*
* @return array Amended HTTP Request arguments.
*/
public static function http_request_args( $args, $url ) {
$args['sslverify'] = true;
if ( false === stripos( $args['user-agent'], 'GitHub Updater' ) ) {
$args['user-agent'] .= '; GitHub Updater - https://github.com/afragen/github-updater';
$args['wp-rest-cache'] = [ 'tag' => 'github-updater' ];
}
return $args;
}
/**
* Add data in Settings page.
*
* @param object $git Git API object.
*/
public function settings_hook( $git ) {
add_action(
'github_updater_add_settings',
function ( $auth_required ) use ( $git ) {
$git->add_settings( $auth_required );
}
);
add_filter( 'github_updater_add_repo_setting_field', [ $this, 'add_setting_field' ], 10, 2 );
}
/**
* Add data to the setting_field in Settings.
*
* @param array $fields
* @param array $repo
*
* @return array
*/
public function add_setting_field( $fields, $repo ) {
if ( ! empty( $fields ) ) {
return $fields;
}
return $this->get_repo_api( $repo->git, $repo )->add_repo_setting_field();
}
/**
* Get repo's API.
*
* @param string $git (github|bitbucket|gitlab|gitea)
* @param bool|\stdClass $repo
*
* @return \Fragen\GitHub_Updater\API\Bitbucket_API|
* \Fragen\GitHub_Updater\API\Bitbucket_Server_API|
* \Fragen\GitHub_Updater\API\Gitea_API|
* \Fragen\GitHub_Updater\API\GitHub_API|
* \Fragen\GitHub_Updater\API\GitLab_API $repo_api
*/
public function get_repo_api( $git, $repo = false ) {
$repo_api = null;
$repo = $repo ?: new \stdClass();
switch ( $git ) {
case 'github':
$repo_api = new GitHub_API( $repo );
break;
case 'bitbucket':
if ( ! empty( $repo->enterprise ) ) {
$repo_api = new Bitbucket_Server_API( $repo );
} else {
$repo_api = new Bitbucket_API( $repo );
}
break;
case 'gitlab':
$repo_api = new GitLab_API( $repo );
break;
case 'gitea':
$repo_api = new Gitea_API( $repo );
break;
}
return $repo_api;
}
/**
* Add Install settings fields.
*
* @param object $git Git API from caller.
*/
public function add_install_fields( $git ) {
add_action(
'github_updater_add_install_settings_fields',
function ( $type ) use ( $git ) {
$git->add_install_settings_fields( $type );
}
);
}
/**
* Call the API and return a json decoded body.
* Create error messages.
*
* @link http://developer.github.com/v3/
*
* @param string $url The URL to send the request to.
*
* @return boolean|\stdClass
*/
protected function api( $url ) {
add_filter( 'http_request_args', [ $this, 'http_request_args' ], 10, 2 );
$type = $this->return_repo_type();
$response = wp_remote_get( $this->get_api_url( $url ) );
$code = (int) wp_remote_retrieve_response_code( $response );
$allowed_codes = [ 200, 404 ];
remove_filter( 'http_request_args', [ $this, 'http_request_args' ] );
if ( is_wp_error( $response ) ) {
Singleton::get_instance( 'Messages', $this )->create_error_message( $response );
return $response;
}
if ( ! in_array( $code, $allowed_codes, true ) ) {
static::$error_code = array_merge(
static::$error_code,
[
$this->type->slug => [
'repo' => $this->type->slug,
'code' => $code,
'name' => $this->type->name,
'git' => $this->type->git,
],
]
);
if ( 'github' === $type['git'] ) {
GitHub_API::ratelimit_reset( $response, $this->type->slug );
}
Singleton::get_instance( 'Messages', $this )->create_error_message( $type['git'] );
return false;
}
// Gitea doesn't return json encoded raw file.
$response = $this->convert_body_string_to_json( $response );
return json_decode( wp_remote_retrieve_body( $response ) );
}
/**
* Convert response body to JSON.
*
* @param mixed $response (JSON|string)
* @return mixed $response JSON encoded.
*/
private function convert_body_string_to_json( $response ) {
if ( $this instanceof Gitea_API || $this instanceof Bitbucket_API || $this instanceof Bitbucket_Server_API ) {
$body = wp_remote_retrieve_body( $response );
if ( null === json_decode( $body ) ) {
$response['body'] = json_encode( $body );
}
}
return $response;
}
/**
* Return repo data for API calls.
*
* @access protected
*
* @return array
*/
protected function return_repo_type() {
$arr = [];
$arr['type'] = $this->type->type;
switch ( $this->type->git ) {
case 'github':
$arr['git'] = 'github';
$arr['base_uri'] = 'https://api.github.com';
$arr['base_download'] = 'https://github.com';
break;
case 'bitbucket':
$arr['git'] = 'bitbucket';
if ( empty( $this->type->enterprise ) ) {
$arr['base_uri'] = 'https://api.bitbucket.org';
$arr['base_download'] = 'https://bitbucket.org';
} else {
$arr['base_uri'] = $this->type->enterprise_api;
$arr['base_download'] = $this->type->enterprise;
}
break;
case 'gitlab':
$arr['git'] = 'gitlab';
$arr['base_uri'] = 'https://gitlab.com/api/v4';
$arr['base_download'] = 'https://gitlab.com';
break;
case 'gitea':
$arr['git'] = 'gitea';
$arr['base_uri'] = $this->type->enterprise . '/api/v1';
$arr['base_download'] = $this->type->enterprise;
break;
}
return $arr;
}
/**
* Return API url.
*
* @access protected
*
* @param string $endpoint The endpoint to access.
* @param bool|string $download_link The plugin or theme download link. Defaults to false.
*
* @return string $endpoint
*/
protected function get_api_url( $endpoint, $download_link = false ) {
$type = $this->return_repo_type();
$segments = [
'owner' => $this->type->owner,
'repo' => $this->type->slug,
'branch' => empty( $this->type->branch ) ? 'master' : $this->type->branch,
];
foreach ( $segments as $segment => $value ) {
$endpoint = str_replace( ':' . $segment, sanitize_text_field( $value ), $endpoint );
}
$repo_api = $this->get_repo_api( $type['git'], $this->type );
switch ( $type['git'] ) {
case 'github':
if ( ! $this->type->enterprise && $download_link ) {
$type['base_download'] = $type['base_uri'];
break;
}
if ( $this->type->enterprise_api ) {
$type['base_download'] = $this->type->enterprise_api;
$type['base_uri'] = null;
if ( $download_link ) {
break;
}
}
$endpoint = $repo_api->add_endpoints( $this, $endpoint );
break;
case 'gitlab':
if ( ! $this->type->enterprise && $download_link ) {
break;
}
if ( $this->type->enterprise ) {
$type['base_download'] = $this->type->enterprise;
$type['base_uri'] = null;
if ( $download_link ) {
break;
}
}
$endpoint = $repo_api->add_endpoints( $this, $endpoint );
break;
case 'bitbucket':
$this->load_authentication_hooks();
if ( $this->type->enterprise_api ) {
if ( $download_link ) {
$type['base_download'] = $type['base_uri'];
break;
}
$endpoint = $repo_api->add_endpoints( $this, $endpoint );
return $this->type->enterprise_api . $endpoint;
}
if ( $download_link && 'release_asset' === self::$method ) {
$type['base_download'] = $type['base_uri'];
}
$endpoint = $repo_api->add_endpoints( $this, $endpoint );
break;
case 'gitea':
if ( $download_link ) {
$type['base_download'] = $type['base_uri'];
break;
}
$endpoint = $repo_api->add_endpoints( $this, $endpoint );
break;
default:
break;
}
$base = $download_link ? $type['base_download'] : $type['base_uri'];
return $base . $endpoint;
}
/**
* Query wp.org for plugin/theme information.
*
* @access protected
*
* @return bool|int|mixed|string|\WP_Error
*/
protected function get_dot_org_data() {
$response = isset( $this->response['dot_org'] ) ? $this->response['dot_org'] : false;
if ( ! $response ) {
$url = "https://api.wordpress.org/{$this->type->type}s/info/1.1/";
$url = add_query_arg(
[
'action' => "{$this->type->type}_information",
rawurlencode( 'request[slug]' ) => $this->type->slug,
],
$url
);
$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
Singleton::get_instance( 'Messages', $this )->create_error_message( $response );
return false;
}
$response = json_decode( $response['body'] );
$response = ! empty( $response ) && ! isset( $response->error ) ? 'in dot org' : 'not in dot org';
$this->set_repo_cache( 'dot_org', $response );
}
return 'in dot org' === $response;
}
/**
* Add appropriate access token to endpoint.
*
* @access protected
*
* @param GitHub_API|GitLab_API $git Class containing the GitAPI used.
* @param string $endpoint The endpoint being accessed.
*
* @return string $endpoint
*/
protected function add_access_token_endpoint( $git, $endpoint ) {
// This will return if checking during shiny updates.
if ( null === static::$options ) {
return $endpoint;
}
$key = null;
$token = null;
$token_enterprise = null;
switch ( $git->type->git ) {
case 'github':
$key = 'access_token';
$token = 'github_access_token';
$token_enterprise = 'github_enterprise_token';
break;
case 'gitlab':
$key = 'private_token';
$token = 'gitlab_access_token';
$token_enterprise = 'gitlab_enterprise_token';
break;
case 'gitea':
$key = 'access_token';
$token = 'gitea_access_token';
$token_enterprise = 'gitea_access_token';
break;
case 'bitbucket':
return $endpoint;
}
// Add hosted access token.
if ( ! empty( static::$options[ $token ] ) ) {
$endpoint = add_query_arg( $key, static::$options[ $token ], $endpoint );
}
// Add Enterprise access token.
if ( ! empty( $git->type->enterprise ) &&
! empty( static::$options[ $token_enterprise ] )
) {
$endpoint = remove_query_arg( $key, $endpoint );
$endpoint = add_query_arg( $key, static::$options[ $token_enterprise ], $endpoint );
}
// Add repo access token.
if ( ! empty( static::$options[ $git->type->slug ] ) ) {
$endpoint = remove_query_arg( $key, $endpoint );
$endpoint = add_query_arg( $key, static::$options[ $git->type->slug ], $endpoint );
}
return $endpoint;
}
/**
* Test to exit early if no update available, saves API calls.
*
* @param array|bool $response
* @param bool $branch
*
* @return bool
*/
protected function exit_no_update( $response, $branch = false ) {
/**
* Filters the return value of exit_no_update.
*
* @since 6.0.0
* @return bool `true` will exit this function early, default will not.
*/
if ( apply_filters( 'ghu_always_fetch_update', false ) ) {
return false;
}
if ( $branch ) {
return empty( static::$options['branch_switch'] );
}
return ! isset( $_POST['ghu_refresh_cache'] ) && ! $response && ! $this->can_update_repo( $this->type );
}
/**
* Validate wp_remote_get response.
*
* @access protected
*
* @param \stdClass $response The response.
*
* @return bool true if invalid
*/
protected function validate_response( $response ) {
return empty( $response ) || isset( $response->message ) || is_wp_error( $response );
}
/**
* Check if a local file for the repository exists.
* Only checks the root directory of the repository.
*
* @access protected
*
* @param string $filename The filename to check for.
*
* @return bool
*/
protected function local_file_exists( $filename ) {
return file_exists( $this->type->local_path . $filename );
}
/**
* Sort tags and set object data.
*
* @param array $parsed_tags
*
* @return bool
*/
protected function sort_tags( $parsed_tags ) {
if ( empty( $parsed_tags ) ) {
return false;
}
list($tags, $rollback) = $parsed_tags;
usort( $tags, 'version_compare' );
krsort( $rollback );
$newest_tag = array_slice( $tags, -1, 1, true );
$newest_tag_key = key( $newest_tag );
$newest_tag = $tags[ $newest_tag_key ];
$this->type->newest_tag = $newest_tag;
$this->type->tags = $tags;
$this->type->rollback = $rollback;
return true;
}
/**
* Get local file info if no update available. Save API calls.
*
* @param \stdClass $repo Repo data.
* @param string $file
*
* @return null|string
*/
protected function get_local_info( $repo, $file ) {
$response = false;
if ( isset( $_POST['ghu_refresh_cache'] ) ) {
return $response;
}
if ( is_dir( $repo->local_path ) &&
file_exists( $repo->local_path . $file )
) {
$response = file_get_contents( $repo->local_path . $file );
}
return $response;
}
/**
* Set repo object file info.
*
* @param array $response Repo data.
*/
protected function set_file_info( $response ) {
$this->type->transient = $response;
$this->type->remote_version = strtolower( $response['Version'] );
$this->type->requires_php = ! empty( $response['Requires PHP'] ) ? $response['Requires PHP'] : false;
$this->type->requires = ! empty( $response['Requires WP'] ) ? $response['Requires WP'] : null;
$this->type->dot_org = $response['dot_org'];
}
/**
* Add remote data to type object.
*
* @access protected
*/
protected function add_meta_repo_object() {
$this->type->rating = $this->make_rating( $this->type->repo_meta );
$this->type->last_updated = $this->type->repo_meta['last_updated'];
$this->type->num_ratings = $this->type->repo_meta['watchers'];
$this->type->is_private = $this->type->repo_meta['private'];
}
/**
* Create some sort of rating from 0 to 100 for use in star ratings.
* I'm really just making this up, more based upon popularity.
*
* @param array $repo_meta
*
* @return integer
*/
protected function make_rating( $repo_meta ) {
$watchers = ! empty( $repo_meta['watchers'] ) ? $repo_meta['watchers'] : 0;
$forks = ! empty( $repo_meta['forks'] ) ? $repo_meta['forks'] : 0;
$open_issues = ! empty( $repo_meta['open_issues'] ) ? $repo_meta['open_issues'] : 0;
$rating = abs( (int) round( $watchers + ( $forks * 1.5 ) - ( $open_issues * 0.1 ) ) );
if ( 100 < $rating ) {
return 100;
}
return $rating;
}
/**
* Set data from readme.txt.
* Prefer changelog from CHANGES.md.
*
* @param array $readme Array of parsed readme.txt data.
*
* @return bool
*/
protected function set_readme_info( $readme ) {
foreach ( (array) $this->type->sections as $section => $value ) {
if ( 'description' === $section ) {
continue;
}
$readme['sections'][ $section ] = $value;
}
$readme['remaining_content'] = ! empty( $readme['remaining_content'] ) ? $readme['remaining_content'] : null;
if ( empty( $readme['sections']['other_notes'] ) ) {
unset( $readme['sections']['other_notes'] );
} else {
$readme['sections']['other_notes'] .= $readme['remaining_content'];
}
unset( $readme['sections']['screenshots'], $readme['sections']['installation'] );
$readme['sections'] = ! empty( $readme['sections'] ) ? $readme['sections'] : [];
$this->type->sections = array_merge( (array) $this->type->sections, (array) $readme['sections'] );
$this->type->tested = isset( $readme['tested'] ) ? $readme['tested'] : null;
$this->type->requires = isset( $readme['requires'] ) ? $readme['requires'] : null;
$this->type->requires_php = isset( $readme['requires_php'] ) ? $readme['requires_php'] : null;
$this->type->donate_link = isset( $readme['donate_link'] ) ? $readme['donate_link'] : null;
$this->type->contributors = isset( $readme['contributors'] ) ? $readme['contributors'] : null;
return true;
}
/**
* Return the redirect download link for a release asset.
* AWS download link sets a link expiration of ONLY 5 minutes.
*
* @since 6.1.0
* @uses Requests, requires WP 4.6
*
* @param string $asset Release asset URI from git host.
*
* @return string|bool|\stdClass Release asset URI from AWS.
*/
protected function get_release_asset_redirect( $asset, $aws = false ) {
if ( ! $asset ) {
return false;
}
// Unset release asset url if older than 5 min to account for AWS expiration.
if ( $aws && ( time() - strtotime( '-12 hours', $this->response['timeout'] ) ) >= 300 ) {
unset( $this->response['release_asset_redirect'] );
}
$response = isset( $this->response['release_asset_redirect'] ) ? $this->response['release_asset_redirect'] : false;
if ( $this->exit_no_update( $response ) ) {
return false;
}
if ( ! $response ) {
add_action( 'requests-requests.before_redirect', [ $this, 'set_redirect' ], 10, 1 );
add_filter( 'http_request_args', [ $this, 'set_aws_release_asset_header' ] );
$url = $this->add_access_token_endpoint( $this, $asset );
wp_remote_get( $url );
remove_filter( 'http_request_args', [ $this, 'set_aws_release_asset_header' ] );
}
if ( ! empty( $this->redirect ) ) {
$this->set_repo_cache( 'release_asset_redirect', $this->redirect );
return $this->redirect;
}
return $response;
}
/**
* Set HTTP header for following AWS release assets.
*
* @since 6.1.0
*
* @param array $args
* @param string $url
*
* @return mixed $args
*/
public function set_aws_release_asset_header( $args, $url = '' ) {
$args['headers']['accept'] = 'application/octet-stream';
return $args;
}
/**
* Set AWS redirect URL from action hook.
*
* @uses `requests-requests.before_redirect` Action hook.
*
* @param string $location
* @return void
*/
public function set_redirect( $location ) {
$this->redirect = $location;
}
}

View File

@@ -0,0 +1,181 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
/**
* Interface API_Interface
*/
interface API_Interface {
/**
* Read the remote file and parse headers.
*
* @access public
*
* @param string $file Filename.
*
* @return mixed
*/
public function get_remote_info( $file);
/**
* Get remote info for tags.
*
* @access public
*
* @return mixed
*/
public function get_remote_tag();
/**
* Read the remote CHANGES.md file.
*
* @access public
*
* @param string $changes Changelog filename.
*
* @return mixed
*/
public function get_remote_changes( $changes);
/**
* Read and parse remote readme.txt.
*
* @access public
*
* @return mixed
*/
public function get_remote_readme();
/**
* Read the repository meta from API.
*
* @access public
*
* @return mixed
*/
public function get_repo_meta();
/**
* Create array of branches and download links as array.
*
* @access public
*
* @return bool
*/
public function get_remote_branches();
/**
* Get release asset URL.
*
* @return string|bool
*/
public function get_release_asset();
/**
* Construct $this->type->download_link using Repository Contents API.
*
* @access public
*
* @param bool $branch_switch For direct branch switching. Defaults to false.
*
* @return string URL for download link.
*/
public function construct_download_link( $branch_switch = false);
/**
* Create endpoints.
*
* @access public
*
* @param GitHub_API|Bitbucket_API|Bitbucket_Server_API|GitLab_API $git
* @param string $endpoint
*
* @return string $endpoint
*/
public function add_endpoints( $git, $endpoint);
/**
* Parse API response call and return only array of tag numbers.
*
* @access public
*
* @param \stdClass|array $response API response.
*
* @return array|\stdClass Array of tag numbers, object is error.
*/
public function parse_tag_response( $response);
/**
* Parse API response and return array of meta variables.
*
* @access public
*
* @param \stdClass|array $response API response.
*
* @return array|\stdClass Array of meta variables.
*/
public function parse_meta_response( $response);
/**
* Parse API response and return array with changelog.
*
* @access public
*
* @param \stdClass|array $response API response.
*
* @return array|\stdClass $arr Array of changes in base64, object if error.
*/
public function parse_changelog_response( $response);
/**
* Parse API response and return array of branch data.
*
* @access public
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response );
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field();
/**
* Add settings for each API.
*
* @param array $auth_required
*
* @return mixed
*/
public function add_settings( $auth_required);
/**
* Add remote install settings fields.
*
* @param string $type plugin|theme.
*/
public function add_install_settings_fields( $type);
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed $install
*/
public function remote_install( $headers, $install);
}

View File

@@ -0,0 +1,584 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\Singleton;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Branch;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Bitbucket_API
*
* Get remote data from a Bitbucket repo.
*
* @author Andy Fragen
*/
class Bitbucket_API extends API implements API_Interface {
/**
* Constructor.
*
* @access public
*
* @param \stdClass $type The repo type.
*/
public function __construct( $type ) {
parent::__construct();
$this->type = $type;
$this->response = $this->get_repo_cache();
$branch = new Branch( $this->response );
if ( ! empty( $type->branch ) ) {
$this->type->branch = ! empty( $branch->cache['current_branch'] )
? $branch->cache['current_branch']
: $type->branch;
}
$this->set_default_credentials();
$this->settings_hook( $this );
$this->add_settings_subtab();
$this->add_install_fields( $this );
}
/**
* Set default credentials if option not set.
*/
protected function set_default_credentials() {
$running_servers = Singleton::get_instance( 'Base', $this )->get_running_git_servers();
$set_credentials = false;
if ( $this instanceof Bitbucket_API ) {
$username = 'bitbucket_username';
$password = 'bitbucket_password';
}
if ( $this instanceof Bitbucket_Server_API ) {
$username = 'bitbucket_server_username';
$password = 'bitbucket_server_password';
}
if ( ! isset( static::$options[ $username ] ) ) {
static::$options[ $username ] = null;
$set_credentials = true;
}
if ( ! isset( static::$options[ $password ] ) ) {
static::$options[ $password ] = null;
$set_credentials = true;
}
if ( ( empty( static::$options[ $username ] ) || empty( static::$options[ $password ] ) ) &&
( ( 'bitbucket_username' === $username &&
in_array( 'bitbucket', $running_servers, true ) ) ||
( 'bitbucket_server_username' === $username &&
in_array( 'bbserver', $running_servers, true ) ) )
) {
Singleton::get_instance( 'Messages', $this )->create_error_message( 'bitbucket' );
static::$error_code['bitbucket'] = [ 'code' => 401 ];
}
if ( $set_credentials ) {
add_site_option( 'github_updater', static::$options );
}
}
/**
* Read the remote file and parse headers.
*
* @access public
*
* @param string $file The file.
*
* @return bool
*/
public function get_remote_info( $file ) {
return $this->get_remote_api_info( 'bitbucket', $file, "/2.0/repositories/:owner/:repo/src/:branch/{$file}" );
}
/**
* Get the remote info for tags.
*
* @access public
*
* @return bool
*/
public function get_remote_tag() {
return $this->get_remote_api_tag( 'bitbucket', '/2.0/repositories/:owner/:repo/refs/tags' );
}
/**
* Read the remote CHANGES.md file.
*
* @access public
*
* @param string $changes The changelog filename.
*
* @return bool
*/
public function get_remote_changes( $changes ) {
return $this->get_remote_api_changes( 'bitbucket', $changes, "/2.0/repositories/:owner/:repo/src/:branch/{$changes}" );
}
/**
* Read and parse remote readme.txt.
*
* @return bool
*/
public function get_remote_readme() {
return $this->get_remote_api_readme( 'bitbucket', '/2.0/repositories/:owner/:repo/src/:branch/readme.txt' );
}
/**
* Read the repository meta from API
*
* @return bool
*/
public function get_repo_meta() {
return $this->get_remote_api_repo_meta( 'bitbucket', '/2.0/repositories/:owner/:repo' );
}
/**
* Create array of branches and download links as array.
*
* @return bool
*/
public function get_remote_branches() {
return $this->get_remote_api_branches( 'bitbucket', '/2.0/repositories/:owner/:repo/refs/branches' );
}
/**
* Return the Bitbucket release asset URL.
*
* @return string
*/
public function get_release_asset() {
return $this->get_api_release_asset( 'bitbucket', '/2.0/repositories/:owner/:repo/downloads' );
}
/**
* Construct $this->type->download_link using Bitbucket API
*
* @param boolean $branch_switch For direct branch changing. Defaults to false.
*
* @return string $endpoint
*/
public function construct_download_link( $branch_switch = false ) {
self::$method = 'download_link';
$download_link_base = $this->get_api_url( '/:owner/:repo/get/', true );
$endpoint = '';
// Release asset.
if ( $this->type->release_asset && '0.0.0' !== $this->type->newest_tag ) {
$release_asset = $this->get_release_asset();
return $this->get_release_asset_redirect( $release_asset, true );
}
/*
* If a branch has been given, use branch.
* If branch is master (default) and tags are used, use newest tag.
*/
if ( 'master' !== $this->type->branch || empty( $this->type->tags ) ) {
if ( ! empty( $this->type->enterprise_api ) ) {
$endpoint = add_query_arg( 'at', $this->type->branch, $endpoint );
} else {
$endpoint .= $this->type->branch . '.zip';
}
} else {
if ( ! empty( $this->type->enterprise_api ) ) {
$endpoint = add_query_arg( 'at', $this->type->newest_tag, $endpoint );
} else {
$endpoint .= $this->type->newest_tag . '.zip';
}
}
/*
* Create endpoint for branch switching.
*/
if ( $branch_switch ) {
if ( ! empty( $this->type->enterprise_api ) ) {
$endpoint = add_query_arg( 'at', $branch_switch, $endpoint );
} else {
$endpoint = $branch_switch . '.zip';
}
}
$download_link = $download_link_base . $endpoint;
/**
* Filter download link so developers can point to specific ZipFile
* to use as a download link during a branch switch.
*
* @since 8.8.0
*
* @param string $download_link Download URL.
* @param /stdClass $this->type Repository object.
* @param string $branch_switch Branch or tag for rollback or branch switching.
*/
return apply_filters( 'github_updater_post_construct_download_link', $download_link, $this->type, $branch_switch );
}
/**
* Create Bitbucket API endpoints.
*
* @param Bitbucket_API|API $git
* @param string $endpoint
*
* @return string|void $endpoint
*/
public function add_endpoints( $git, $endpoint ) {
switch ( $git::$method ) {
case 'file':
case 'readme':
case 'meta':
case 'changes':
case 'translation':
case 'release_asset':
case 'download_link':
break;
case 'tags':
case 'branches':
$endpoint = add_query_arg(
[
'pagelen' => '100',
'sort' => '-name',
],
$endpoint
);
break;
default:
break;
}
$endpoint = $this->add_access_token_endpoint( $git, $endpoint );
return $endpoint;
}
/**
* Parse API response call and return only array of tag numbers.
*
* @param \stdClass $response Response from API call.
*
* @return array|\stdClass Array of tag numbers, object is error.
*/
public function parse_tag_response( $response ) {
if ( ! isset( $response->values ) || $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
array_map(
function ( $e ) use ( &$arr ) {
$arr[] = $e->name;
return $arr;
},
(array) $response->values
);
return $arr;
}
/**
* Parse API response and return array of meta variables.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of meta variables.
*/
public function parse_meta_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['private'] = $e->is_private;
$arr['last_updated'] = $e->updated_on;
$arr['watchers'] = 0;
$arr['forks'] = 0;
$arr['open_issues'] = 0;
}
);
return $arr;
}
/**
* Parse API response and return array with changelog in base64.
*
* @param \stdClass|array $response Response from API call.
*
* @return array|\stdClass $arr Array of changes in base64, object if error.
*/
public function parse_changelog_response( $response ) {
}
/**
* Parse API response and return array of branch data.
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$branches = [];
foreach ( $response as $branch ) {
$branches[ $branch->name ]['download'] = $this->construct_download_link( $branch->name );
$branches[ $branch->name ]['commit_hash'] = $branch->target->hash;
$branches[ $branch->name ]['commit_timestamp'] = $branch->target->date;
}
return $branches;
}
/**
* Parse tags and create download links.
*
* @param \stdClass|array $response Response from API call.
* @param string $repo_type
*
* @return array
*/
protected function parse_tags( $response, $repo_type ) {
$tags = [];
$rollback = [];
foreach ( (array) $response as $tag ) {
// $download_base = implode(
// '/',
// [
// $repo_type['base_download'],
// $this->type->owner,
// $this->type->owner,
// 'get/',
// ]
// );
$download_base = "{$repo_type['base_download']}/{$this->type->owner}/{$this->type->owner}/get/";
$tags[] = $tag;
$rollback[ $tag ] = $download_base . $tag . '.zip';
}
return [ $tags, $rollback ];
}
/**
* Add settings for Bitbucket Username and Password.
*
* @param array $auth_required
*
* @return void
*/
public function add_settings( $auth_required ) {
add_settings_section(
'bitbucket_user',
esc_html__( 'Bitbucket Private Settings', 'github-updater' ),
[ $this, 'print_section_bitbucket_username' ],
'github_updater_bitbucket_install_settings'
);
add_settings_field(
'bitbucket_username',
esc_html__( 'Bitbucket Username', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_bitbucket_install_settings',
'bitbucket_user',
[ 'id' => 'bitbucket_username' ]
);
add_settings_field(
'bitbucket_password',
esc_html__( 'Bitbucket Password', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_bitbucket_install_settings',
'bitbucket_user',
[
'id' => 'bitbucket_password',
'token' => true,
]
);
/*
* Show section for private Bitbucket repositories.
*/
if ( $auth_required['bitbucket_private'] ) {
add_settings_section(
'bitbucket_id',
esc_html__( 'Bitbucket Private Repositories', 'github-updater' ),
[ $this, 'print_section_bitbucket_info' ],
'github_updater_bitbucket_install_settings'
);
}
}
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field() {
$setting_field['page'] = 'github_updater_bitbucket_install_settings';
$setting_field['section'] = 'bitbucket_id';
$setting_field['callback_method'] = [
Singleton::get_instance( 'Settings', $this ),
'token_callback_checkbox',
];
return $setting_field;
}
/**
* Add subtab to Settings page.
*/
private function add_settings_subtab() {
add_filter(
'github_updater_add_settings_subtabs',
function ( $subtabs ) {
return array_merge( $subtabs, [ 'bitbucket' => esc_html__( 'Bitbucket', 'github-updater' ) ] );
}
);
}
/**
* Print the Bitbucket repo Settings text.
*/
public function print_section_bitbucket_info() {
esc_html_e( 'Check box if private repository. Leave unchecked for public repositories.', 'github-updater' );
}
/**
* Print the Bitbucket user/pass Settings text.
*/
public function print_section_bitbucket_username() {
esc_html_e( 'Enter your personal Bitbucket username and password.', 'github-updater' );
}
/**
* Add remote install settings fields.
*
* @param string $type
*/
public function add_install_settings_fields( $type ) {
if ( ( empty( static::$options['bitbucket_username'] ) ||
empty( static::$options['bitbucket_password'] ) ) ||
( empty( static::$options['bitbucket_server_username'] ) ||
empty( static::$options['bitbucket_server_password'] ) )
) {
add_settings_field(
'bitbucket_username',
esc_html__( 'Bitbucket Username', 'github-updater' ),
[ $this, 'bitbucket_username' ],
'github_updater_install_' . $type,
$type
);
add_settings_field(
'bitbucket_password',
esc_html__( 'Bitbucket Password', 'github-updater' ),
[ $this, 'bitbucket_password' ],
'github_updater_install_' . $type,
$type
);
}
add_settings_field(
'is_private',
esc_html__( 'Private Bitbucket Repository', 'github-updater' ),
[ $this, 'is_private_repo' ],
'github_updater_install_' . $type,
$type
);
}
/**
* Setting for private repo for remote install.
*/
public function is_private_repo() {
?>
<label for="is_private">
<input class="bitbucket_setting" type="checkbox" id="is_private" name="is_private" <?php checked( '1', false ); ?> >
<br>
<span class="description">
<?php esc_html_e( 'Check for private Bitbucket repositories.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Bitbucket username for remote install.
*/
public function bitbucket_username() {
?>
<label for="bitbucket_username">
<input class="bitbucket_setting" type="text" style="width:50%;" id="bitbucket_username" name="bitbucket_username" value="">
<br>
<span class="description">
<?php esc_html_e( 'Enter Bitbucket username.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Bitbucket password for remote install.
*/
public function bitbucket_password() {
?>
<label for="bitbucket_password">
<input class="bitbucket_setting" type="password" style="width:50%;" id="bitbucket_password" name="bitbucket_password" value="" autocomplete="new-password">
<br>
<span class="description">
<?php esc_html_e( 'Enter Bitbucket password.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed $install
*/
public function remote_install( $headers, $install ) {
$bitbucket_org = true;
if ( 'bitbucket.org' === $headers['host'] || empty( $headers['host'] ) ) {
$base = 'https://bitbucket.org';
$headers['host'] = 'bitbucket.org';
} else {
$base = $headers['base_uri'];
$bitbucket_org = false;
}
if ( $bitbucket_org ) {
$install['download_link'] = "{$base}/{$install['github_updater_repo']}/get/{$install['github_updater_branch']}.zip";
if ( isset( $install['is_private'] ) ) {
$install['options'][ $install['repo'] ] = 1;
}
if ( ! empty( $install['bitbucket_username'] ) ) {
$install['options']['bitbucket_username'] = $install['bitbucket_username'];
}
if ( ! empty( $install['bitbucket_password'] ) ) {
$install['options']['bitbucket_password'] = $install['bitbucket_password'];
}
}
return $install;
}
}

View File

@@ -0,0 +1,451 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\Singleton;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Readme_Parser;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Bitbucket_Server_API
*
* Get remote data from a self-hosted Bitbucket Server repo.
* Assumes an owner == project_key
* Group URI: https://bitbucket.example.com/projects/<owner>/<repo>
* User URI: https://bitbucket.example.com/users/<owner>/<repo>
*
* @link https://docs.atlassian.com/bitbucket-server/rest/5.3.1/bitbucket-rest.html
*
* @author Andy Fragen
* @author Bjorn Wijers
*/
class Bitbucket_Server_API extends Bitbucket_API {
/**
* Constructor.
*
* @param \stdClass $type
*/
public function __construct( $type ) {
parent::__construct( $type );
$this->add_settings_subtab();
}
/**
* Read the remote file and parse headers.
*
* @param string $file Filename.
*
* @return bool
*/
public function get_remote_info( $file ) {
return $this->get_remote_api_info( 'bbserver', $file, "/1.0/:owner/repos/:repo/browse/{$file}" );
}
/**
* Read the repository meta from API
*
* @return bool
*/
public function get_repo_meta() {
return $this->get_remote_api_repo_meta( 'bbserver', '/1.0/:owner/repos/:repo' );
}
/**
* Get the remote info for tags.
*
* @access public
*
* @return bool
*/
public function get_remote_tag() {
return $this->get_remote_api_tag( 'bbserver', '/1.0/:owner/repos/:repo/tags' );
}
/**
* Read and parse remote readme.txt.
*
* @return bool
*/
public function get_remote_readme() {
return $this->get_remote_api_readme( 'bbserver', '/1.0/:owner/repos/:repo/raw/readme.txt' );
}
/**
* Read the remote CHANGES.md file
*
* @param string $changes Changelog filename.
*
* @return bool
*/
public function get_remote_changes( $changes ) {
return $this->get_remote_api_changes( 'bbserver', $changes, "/1.0/:owner/repos/:repo/raw/{$changes}" );
}
/**
* Create array of branches and download links as array.
*
* @return bool
*/
public function get_remote_branches() {
return $this->get_remote_api_branches( 'bbserver', '/1.0/:owner/repos/:repo/branches' );
}
/**
* Return the Bitbucket Sever release asset URL.
*
* @return string
*/
public function get_release_asset() {
// TODO: make this work.
// return $this->get_api_release_asset( 'bbserver', '/1.0/:owner/:repo/downloads' );
}
/**
* Construct $this->type->download_link using Bitbucket Server API.
*
* Downloads requires the official stash-archive plugin which enables
* subdirectory support using the prefix query argument.
*
* @link https://bitbucket.org/atlassian/stash-archive
*
* @param boolean $branch_switch for direct branch changing.
*
* @return string $endpoint
*/
public function construct_download_link( $branch_switch = false ) {
self::$method = 'download_link';
$download_link_base = $this->get_api_url( '/latest/:owner/repos/:repo/archive', true );
$endpoint = $this->add_endpoints( $this, '' );
if ( $branch_switch ) {
$endpoint = urldecode( add_query_arg( 'at', $branch_switch, $endpoint ) );
}
return $download_link_base . $endpoint;
}
/**
* Create Bitbucket Server API endpoints.
*
* @param Bitbucket_Server_API|API $git
* @param string $endpoint
*
* @return string $endpoint
*/
public function add_endpoints( $git, $endpoint ) {
switch ( self::$method ) {
case 'meta':
case 'translation':
case 'branches':
break;
case 'file':
case 'readme':
$endpoint = add_query_arg( 'at', $git->type->branch, $endpoint );
break;
case 'changes':
$endpoint = add_query_arg(
[
'at' => $git->type->branch,
'raw' => '',
],
$endpoint
);
break;
case 'tags':
case 'download_link':
/*
* Add a prefix query argument to create a subdirectory with the same name
* as the repo, e.g. 'my-repo' becomes 'my-repo/'
* Required for using stash-archive.
*/
$defaults = [
'prefix' => $git->type->slug . '/',
'at' => $git->type->branch,
'format' => 'zip',
];
$endpoint = add_query_arg( $defaults, $endpoint );
if ( ! empty( $git->type->tags ) ) {
$endpoint = urldecode( add_query_arg( 'at', $git->type->newest_tag, $endpoint ) );
}
break;
default:
break;
}
return $endpoint;
}
/**
* Combines separate text lines from API response into one string with \n line endings.
* Code relying on raw text can now parse it.
*
* @param string|\stdClass|mixed $response
*
* @return string Combined lines of text returned by API
*/
protected function bbserver_recombine_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$remote_info_file = '';
if ( isset( $response->lines ) ) {
foreach ( (array) $response->lines as $line ) {
$remote_info_file .= $line->text . "\n";
}
}
return $remote_info_file;
}
/**
* Parse API response and return array of meta variables.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of meta variables.
*/
public function parse_meta_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['private'] = ! $e->public;
$arr['last_updated'] = null;
$arr['watchers'] = 0;
$arr['forks'] = 0;
$arr['open_issues'] = 0;
}
);
return $arr;
}
/**
* Parse API response and return array with changelog.
*
* @param string $response Response from API call.
*
* @return void
*/
public function parse_changelog_response( $response ) {
}
/**
* Parse API response and return object with readme body.
*
* @param string|\stdClass $response
*
* @return void
*/
protected function parse_readme_response( $response ) {
}
/**
* Parse API response and return array of branch data.
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$branches = [];
foreach ( $response as $branch ) {
$branches[ $branch->displayId ]['download'] = $this->construct_download_link( $branch->displayId );
$branches[ $branch->displayId ]['commit_hash'] = $branch->latestCommit;
}
return $branches;
}
/**
* Parse API response call and return only array of tag numbers.
*
* @param \stdClass $response Response from API call.
*
* @return array|\stdClass Array of tag numbers, object is error.
*/
public function parse_tag_response( $response ) {
if ( ! isset( $response->values ) || $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
array_map(
function ( $e ) use ( &$arr ) {
$arr[] = $e->displayId;
return $arr;
},
(array) $response->values
);
return $arr;
}
/**
* Parse tags and create download links.
*
* @param \stdClass|array $response Response from API call.
* @param string $repo_type
*
* @return array
*/
protected function parse_tags( $response, $repo_type ) {
$tags = [];
$rollback = [];
foreach ( (array) $response as $tag ) {
$download_base = "{$repo_type['base_uri']}/latest/{$this->type->owner}/repos/{$this->type->slug}/archive";
$download_base = $this->add_endpoints( $this, $download_base );
$tags[] = $tag;
$rollback[ $tag ] = add_query_arg( 'at', $tag, $download_base );
}
return [ $tags, $rollback ];
}
/**
* Add settings for Bitbucket Server Username and Password.
*
* @param array $auth_required
*
* @return void
*/
public function add_settings( $auth_required ) {
add_settings_section(
'bitbucket_server_user',
esc_html__( 'Bitbucket Server Private Settings', 'github-updater' ),
[ $this, 'print_section_bitbucket_username' ],
'github_updater_bbserver_install_settings'
);
add_settings_field(
'bitbucket_server_username',
esc_html__( 'Bitbucket Server Username', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_bbserver_install_settings',
'bitbucket_server_user',
[ 'id' => 'bitbucket_server_username' ]
);
add_settings_field(
'bitbucket_server_password',
esc_html__( 'Bitbucket Server Password', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_bbserver_install_settings',
'bitbucket_server_user',
[
'id' => 'bitbucket_server_password',
'token' => true,
]
);
/*
* Show section for private Bitbucket Server repositories.
*/
if ( $auth_required['bitbucket_server'] ) {
add_settings_section(
'bitbucket_server_id',
esc_html__( 'Bitbucket Server Private Repositories', 'github-updater' ),
[ $this, 'print_section_bitbucket_info' ],
'github_updater_bbserver_install_settings'
);
}
}
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field() {
$setting_field['page'] = 'github_updater_bbserver_install_settings';
$setting_field['section'] = 'bitbucket_server_id';
$setting_field['callback_method'] = [
Singleton::get_instance( 'Settings', $this ),
'token_callback_checkbox',
];
return $setting_field;
}
/**
* Add subtab to Settings page.
*/
private function add_settings_subtab() {
add_filter(
'github_updater_add_settings_subtabs',
function ( $subtabs ) {
return array_merge( $subtabs, [ 'bbserver' => esc_html__( 'Bitbucket Server', 'github-updater' ) ] );
}
);
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return array $install
*/
public function remote_install( $headers, $install ) {
$bitbucket_org = true;
if ( 'bitbucket.org' === $headers['host'] || empty( $headers['host'] ) ) {
$base = 'https://bitbucket.org';
$headers['host'] = 'bitbucket.org';
} else {
$base = $headers['base_uri'];
$bitbucket_org = false;
}
if ( ! $bitbucket_org ) {
$install['download_link'] = "{$base}/rest/api/latest/{$headers['owner']}/repos/{$headers['repo']}/archive";
$install['download_link'] = add_query_arg(
[
'prefix' => $headers['repo'] . '/',
'at' => $install['github_updater_branch'],
'format' => 'zip',
],
$install['download_link']
);
if ( isset( $install['is_private'] ) ) {
$install['options'][ $install['repo'] ] = 1;
}
if ( ! empty( $install['bitbucket_username'] ) ) {
$install['options']['bitbucket_server_username'] = $install['bitbucket_username'];
}
if ( ! empty( $install['bitbucket_password'] ) ) {
$install['options']['bitbucket_server_password'] = $install['bitbucket_password'];
}
}
return $install;
}
}

View File

@@ -0,0 +1,549 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\Singleton;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Branch;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class GitHub_API
*
* Get remote data from a GitHub repo.
*
* @author Andy Fragen
*/
class GitHub_API extends API implements API_Interface {
/**
* Constructor.
*
* @param \stdClass $type
*/
public function __construct( $type ) {
parent::__construct();
$this->type = $type;
$this->response = $this->get_repo_cache();
$branch = new Branch( $this->response );
if ( ! empty( $type->branch ) ) {
$this->type->branch = ! empty( $branch->cache['current_branch'] )
? $branch->cache['current_branch']
: $type->branch;
}
$this->settings_hook( $this );
$this->add_settings_subtab();
$this->add_install_fields( $this );
}
/**
* Read the remote file and parse headers.
*
* @param string $file Filename.
*
* @return bool
*/
public function get_remote_info( $file ) {
return $this->get_remote_api_info( 'github', $file, "/repos/:owner/:repo/contents/{$file}" );
}
/**
* Get remote info for tags.
*
* @return bool
*/
public function get_remote_tag() {
return $this->get_remote_api_tag( 'github', '/repos/:owner/:repo/tags' );
}
/**
* Read the remote CHANGES.md file.
*
* @param string $changes Changelog filename.
*
* @return bool
*/
public function get_remote_changes( $changes ) {
return $this->get_remote_api_changes( 'github', $changes, "/repos/:owner/:repo/contents/{$changes}" );
}
/**
* Read and parse remote readme.txt.
*
* @return bool
*/
public function get_remote_readme() {
$this->get_remote_api_readme( 'github', '/repos/:owner/:repo/contents/readme.txt' );
}
/**
* Read the repository meta from API.
*
* @return bool
*/
public function get_repo_meta() {
return $this->get_remote_api_repo_meta( 'github', '/repos/:owner/:repo' );
}
/**
* Create array of branches and download links as array.
*
* @return bool
*/
public function get_remote_branches() {
return $this->get_remote_api_branches( 'github', '/repos/:owner/:repo/branches' );
}
/**
* Return the GitHub release asset URL.
*
* @return string|bool
*/
public function get_release_asset() {
return $this->get_api_release_asset( 'github', '/repos/:owner/:repo/releases/latest' );
}
/**
* Construct $this->type->download_link using Repository Contents API.
*
* @url http://developer.github.com/v3/repos/contents/#get-archive-link
*
* @param boolean $branch_switch for direct branch changing.
*
* @return string $endpoint
*/
public function construct_download_link( $branch_switch = false ) {
self::$method = 'download_link';
$download_link_base = $this->get_api_url( '/repos/:owner/:repo/zipball/', true );
$endpoint = '';
// Release asset.
if ( $this->type->release_asset && '0.0.0' !== $this->type->newest_tag ) {
$release_asset = $this->get_release_asset();
if ( property_exists( $this->type, 'is_private' ) && $this->type->is_private ) {
return $this->get_release_asset_redirect( $release_asset, true );
}
return $release_asset;
}
/*
* If a branch has been given, use branch.
* If branch is master (default) and tags are used, use newest tag.
*/
if ( 'master' !== $this->type->branch || empty( $this->type->tags ) ) {
$endpoint .= $this->type->branch;
} else {
$endpoint .= $this->type->newest_tag;
}
// Create endpoint for branch switching.
if ( $branch_switch ) {
$endpoint = $branch_switch;
}
$endpoint = $this->add_access_token_endpoint( $this, $endpoint );
$download_link = $download_link_base . $endpoint;
/**
* Filter download link so developers can point to specific ZipFile
* to use as a download link during a branch switch.
*
* @since 8.8.0
*
* @param string $download_link Download URL.
* @param /stdClass $this->type Repository object.
* @param string $branch_switch Branch or tag for rollback or branch switching.
*/
return apply_filters( 'github_updater_post_construct_download_link', $download_link, $this->type, $branch_switch );
}
/**
* Create GitHub API endpoints.
*
* @param GitHub_API|API $git
* @param string $endpoint
*
* @return string $endpoint
*/
public function add_endpoints( $git, $endpoint ) {
switch ( $git::$method ) {
case 'file':
case 'readme':
case 'changes':
$endpoint = add_query_arg( 'ref', $git->type->branch, $endpoint );
break;
case 'meta':
case 'tags':
case 'download_link':
case 'release_asset':
case 'translation':
break;
case 'branches':
$endpoint = add_query_arg( 'per_page', '100', $endpoint );
break;
default:
break;
}
$endpoint = $this->add_access_token_endpoint( $git, $endpoint );
/*
* If GitHub Enterprise return this endpoint.
*/
if ( ! empty( $git->type->enterprise_api ) ) {
return $git->type->enterprise_api . $endpoint;
}
return $endpoint;
}
/**
* Calculate and store time until rate limit reset.
*
* @param array $response HTTP headers.
* @param string $repo Repo name.
*/
public static function ratelimit_reset( $response, $repo ) {
if ( isset( $response['headers']['x-ratelimit-reset'] ) ) {
$reset = (int) $response['headers']['x-ratelimit-reset'];
$wait = date( 'i', $reset - time() );
static::$error_code[ $repo ] = array_merge(
static::$error_code[ $repo ],
[
'git' => 'github',
'wait' => $wait,
]
);
}
}
/**
* Parse API response call and return only array of tag numbers.
*
* @param \stdClass|array $response Response from API call.
*
* @return \stdClass|array $arr Array of tag numbers, object is error.
*/
public function parse_tag_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
array_map(
function ( $e ) use ( &$arr ) {
$arr[] = $e->name;
return $arr;
},
(array) $response
);
return $arr;
}
/**
* Parse API response and return array of meta variables.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of meta variables.
*/
public function parse_meta_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['private'] = $e->private;
$arr['last_updated'] = $e->pushed_at;
$arr['watchers'] = $e->watchers;
$arr['forks'] = $e->forks;
$arr['open_issues'] = $e->open_issues;
}
);
return $arr;
}
/**
* Parse API response and return array with changelog in base64.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of changes in base64.
*/
public function parse_changelog_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['changes'] = $e->content;
}
);
return $arr;
}
/**
* Parse API response and return array of branch data.
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$branches = [];
foreach ( $response as $branch ) {
$branches[ $branch->name ]['download'] = $this->construct_download_link( $branch->name );
$branches[ $branch->name ]['commit_hash'] = $branch->commit->sha;
$branches[ $branch->name ]['commit_api'] = $branch->commit->url;
}
return $branches;
}
/**
* Parse tags and create download links.
*
* @param \stdClass|array $response Response from API call.
* @param array $repo_type
*
* @return array
*/
protected function parse_tags( $response, $repo_type ) {
$tags = [];
$rollback = [];
foreach ( (array) $response as $tag ) {
$download_base = implode(
'/',
[
$repo_type['base_uri'],
'repos',
$this->type->owner,
$this->type->slug,
'zipball/',
]
);
$tags[] = $tag;
$rollback[ $tag ] = $download_base . $tag;
}
return [ $tags, $rollback ];
}
/**
* Add settings for GitHub Personal Access Token.
*
* @param array $auth_required
*
* @return void
*/
public function add_settings( $auth_required ) {
add_settings_section(
'github_access_token',
esc_html__( 'GitHub Personal Access Token', 'github-updater' ),
[ $this, 'print_section_github_access_token' ],
'github_updater_github_install_settings'
);
add_settings_field(
'github_access_token',
esc_html__( 'GitHub.com Access Token', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_github_install_settings',
'github_access_token',
[
'id' => 'github_access_token',
'token' => true,
]
);
if ( $auth_required['github_enterprise'] ) {
add_settings_field(
'github_enterprise_token',
esc_html__( 'GitHub Enterprise Access Token', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_github_install_settings',
'github_access_token',
[
'id' => 'github_enterprise_token',
'token' => true,
]
);
}
/*
* Show section for private GitHub repositories.
*/
if ( $auth_required['github_private'] || $auth_required['github_enterprise'] ) {
add_settings_section(
'github_id',
esc_html__( 'GitHub Private Settings', 'github-updater' ),
[ $this, 'print_section_github_info' ],
'github_updater_github_install_settings'
);
}
}
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field() {
$setting_field['page'] = 'github_updater_github_install_settings';
$setting_field['section'] = 'github_id';
$setting_field['callback_method'] = [
Singleton::get_instance( 'Settings', $this ),
'token_callback_text',
];
return $setting_field;
}
/**
* Print the GitHub text.
*/
public function print_section_github_info() {
esc_html_e( 'Enter your GitHub Access Token. Leave empty for public repositories.', 'github-updater' );
}
/**
* Print the GitHub Personal Access Token text.
*/
public function print_section_github_access_token() {
esc_html_e( 'Enter your personal GitHub.com or GitHub Enterprise Access Token to avoid API access limits.', 'github-updater' );
}
/**
* Add remote install settings fields.
*
* @param string $type plugin|theme.
*/
public function add_install_settings_fields( $type ) {
add_settings_field(
'github_access_token',
esc_html__( 'GitHub Access Token', 'github-updater' ),
[ $this, 'github_access_token' ],
'github_updater_install_' . $type,
$type
);
}
/**
* Add subtab to Settings page.
*/
private function add_settings_subtab() {
add_filter(
'github_updater_add_settings_subtabs',
function ( $subtabs ) {
return array_merge( $subtabs, [ 'github' => esc_html__( 'GitHub', 'github-updater' ) ] );
}
);
}
/**
* GitHub Access Token for remote install.
*/
public function github_access_token() {
?>
<label for="github_access_token">
<input class="github_setting" type="password" style="width:50%;" id="github_access_token" name="github_access_token" value="" autocomplete="new-password">
<br>
<span class="description">
<?php esc_html_e( 'Enter GitHub Access Token for private GitHub repositories.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed
*/
public function remote_install( $headers, $install ) {
$github_com = true;
$options['github_access_token'] = isset( static::$options['github_access_token'] ) ? static::$options['github_access_token'] : null;
$options['github_enterprise_token'] = isset( static::$options['github_enterprise_token'] ) ? static::$options['github_enterprise_token'] : null;
if ( 'github.com' === $headers['host'] || empty( $headers['host'] ) ) {
$base = 'https://api.github.com';
$headers['host'] = 'github.com';
} else {
$base = $headers['base_uri'] . '/api/v3';
$github_com = false;
}
$install['download_link'] = "{$base}/repos/{$install['github_updater_repo']}/zipball/{$install['github_updater_branch']}";
// If asset is entered install it.
if ( false !== stripos( $headers['uri'], 'releases/download' ) ) {
$install['download_link'] = $headers['uri'];
}
/*
* Add/Save access token if present.
*/
if ( ! empty( $install['github_access_token'] ) ) {
$install['options'][ $install['repo'] ] = $install['github_access_token'];
if ( $github_com ) {
$install['options']['github_access_token'] = $install['github_access_token'];
} else {
$install['options']['github_enterprise_token'] = $install['github_access_token'];
}
}
if ( $github_com ) {
$token = ! empty( $install['options']['github_access_token'] )
? $install['options']['github_access_token']
: $options['github_access_token'];
} else {
$token = ! empty( $install['options']['github_enterprise_token'] )
? $install['options']['github_enterprise_token']
: $options['github_enterprise_token'];
}
if ( ! empty( $token ) ) {
$install['download_link'] = add_query_arg( 'access_token', $token, $install['download_link'] );
}
if ( ! empty( static::$options['github_access_token'] ) ) {
unset( $install['options']['github_access_token'] );
}
if ( ! empty( static::$options['github_enterprise_token'] ) ) {
unset( $install['options']['github_enterprise_token'] );
}
return $install;
}
}

View File

@@ -0,0 +1,645 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\Singleton;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Branch;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class GitLab_API
*
* Get remote data from a GitLab repo.
*
* @author Andy Fragen
*/
class GitLab_API extends API implements API_Interface {
/**
* Constructor.
*
* @param \stdClass $type
*/
public function __construct( $type ) {
parent::__construct();
$this->type = $type;
$this->response = $this->get_repo_cache();
$branch = new Branch( $this->response );
if ( ! empty( $type->branch ) ) {
$this->type->branch = ! empty( $branch->cache['current_branch'] )
? $branch->cache['current_branch']
: $type->branch;
}
$this->set_default_credentials();
$this->settings_hook( $this );
$this->add_settings_subtab();
$this->add_install_fields( $this );
}
/**
* Set default credentials if option not set.
*/
protected function set_default_credentials() {
$running_servers = Singleton::get_instance( 'Base', $this )->get_running_git_servers();
$set_credentials = false;
if ( ! isset( static::$options['gitlab_access_token'] ) ) {
static::$options['gitlab_access_token'] = null;
$set_credentials = true;
}
if ( ! isset( static::$options['gitlab_enterprise_token'] ) ) {
static::$options['gitlab_enterprise_token'] = null;
$set_credentials = true;
}
if ( ( empty( static::$options['gitlab_enterprise_token'] ) &&
in_array( 'gitlabce', $running_servers, true ) ) ||
( empty( static::$options['gitlab_access_token'] ) &&
in_array( 'gitlab', $running_servers, true ) )
) {
$this->gitlab_error_notices();
}
if ( $set_credentials ) {
add_site_option( 'github_updater', static::$options );
}
}
/**
* Read the remote file and parse headers.
*
* @param string $file Filename.
*
* @return bool
*/
public function get_remote_info( $file ) {
$id = $this->get_gitlab_id();
return $this->get_remote_api_info( 'gitlab', $file, "/projects/{$id}/repository/files/{$file}" );
}
/**
* Get remote info for tags.
*
* @return bool
*/
public function get_remote_tag() {
$id = $this->get_gitlab_id();
return $this->get_remote_api_tag( 'gitlab', "/projects/{$id}/repository/tags" );
}
/**
* Read the remote CHANGES.md file.
*
* @param string $changes Changelog filename.
*
* @return bool
*/
public function get_remote_changes( $changes ) {
$id = $this->get_gitlab_id();
return $this->get_remote_api_changes( 'gitlab', $changes, "/projects/{$id}/repository/files/{$changes}" );
}
/**
* Read and parse remote readme.txt.
*
* @return bool
*/
public function get_remote_readme() {
$id = $this->get_gitlab_id();
return $this->get_remote_api_readme( 'gitlab', "/projects/{$id}/repository/files/readme.txt" );
}
/**
* Read the repository meta from API.
*
* @return bool
*/
public function get_repo_meta() {
$response = isset( $this->response['meta'] ) ? $this->response['meta'] : false;
if ( ! $response ) {
self::$method = 'meta';
$project = isset( $this->response['project'] ) ? $this->response['project'] : false;
// exit if transient is empty.
if ( ! $project ) {
return false;
}
$response = ( $this->type->slug === $project->path ) ? $project : false;
if ( $response ) {
$response = $this->parse_meta_response( $response );
$this->set_repo_cache( 'meta', $response );
$this->set_repo_cache( 'project', null );
}
}
if ( $this->validate_response( $response ) ) {
return false;
}
$this->type->repo_meta = $response;
$this->add_meta_repo_object();
return true;
}
/**
* Create array of branches and download links as array.
*
* @return bool
*/
public function get_remote_branches() {
$id = $this->get_gitlab_id();
return $this->get_remote_api_branches( 'gitlab', "/projects/{$id}/repository/branches" );
}
/**
* Get GitLab release asset download link.
*
* @return string|bool
*/
public function get_release_asset() {
return $this->get_api_release_asset( 'gitlab', "/projects/{$this->response['project_id']}/jobs/artifacts/{$this->type->newest_tag}/download" );
}
/**
* Construct $this->type->download_link using GitLab API v4.
*
* @param boolean $branch_switch for direct branch changing.
*
* @return string $endpoint
*/
public function construct_download_link( $branch_switch = false ) {
self::$method = 'download_link';
$download_link_base = $this->get_api_url( "/projects/{$this->get_gitlab_id()}/repository/archive.zip" );
$download_link_base = remove_query_arg( 'private_token', $download_link_base );
$endpoint = '';
$endpoint = add_query_arg( 'sha', $this->type->branch, $endpoint );
// Release asset.
if ( $this->type->ci_job && '0.0.0' !== $this->type->newest_tag ) {
$release_asset = $this->get_release_asset();
return $release_asset;
}
// If branch is master (default) and tags are used, use newest tag.
if ( 'master' === $this->type->branch && ! empty( $this->type->tags ) ) {
$endpoint = add_query_arg( 'sha', $this->type->newest_tag, $endpoint );
}
// Create endpoint for branch switching.
if ( $branch_switch ) {
$endpoint = add_query_arg( 'sha', $branch_switch, $endpoint );
}
$endpoint = $this->add_access_token_endpoint( $this, $endpoint );
$download_link = $download_link_base . $endpoint;
/**
* Filter download link so developers can point to specific ZipFile
* to use as a download link during a branch switch.
*
* @since 8.8.0
*
* @param string $download_link Download URL.
* @param /stdClass $this->type Repository object.
* @param string $branch_switch Branch or tag for rollback or branch switching.
*/
return apply_filters( 'github_updater_post_construct_download_link', $download_link, $this->type, $branch_switch );
}
/**
* Create GitLab API endpoints.
*
* @param GitLab_API|API $git
* @param string $endpoint
*
* @return string $endpoint
*/
public function add_endpoints( $git, $endpoint ) {
switch ( $git::$method ) {
case 'projects':
$endpoint = add_query_arg( 'per_page', '100', $endpoint );
break;
case 'meta':
case 'tags':
case 'branches':
case 'download_link':
break;
case 'file':
case 'changes':
case 'readme':
$endpoint = add_query_arg( 'ref', $git->type->branch, $endpoint );
break;
case 'translation':
$endpoint = add_query_arg( 'ref', 'master', $endpoint );
break;
case 'release_asset':
$endpoint = add_query_arg( 'job', $git->type->ci_job, $endpoint );
break;
default:
break;
}
$endpoint = $this->add_access_token_endpoint( $git, $endpoint );
/*
* If GitLab CE/Enterprise return this endpoint.
*/
if ( ! empty( $git->type->enterprise_api ) ) {
return $git->type->enterprise_api . $endpoint;
}
return $endpoint;
}
/**
* Get GitLab project ID and project meta.
*
* @return string|int
*/
public function get_gitlab_id() {
$id = null;
$response = isset( $this->response['project_id'] ) ? $this->response['project_id'] : false;
if ( ! $response ) {
self::$method = 'projects';
$id = implode( '/', [ $this->type->owner, $this->type->slug ] );
$id = rawurlencode( $id );
$response = $this->api( '/projects/' . $id );
if ( $this->validate_response( $response ) ) {
return $id;
}
if ( $response && $this->type->slug === $response->path ) {
$id = $response->id;
$this->set_repo_cache( 'project_id', $id );
$this->set_repo_cache( 'project', $response );
}
return $id;
}
return $response;
}
/**
* Parse API response call and return only array of tag numbers.
*
* @param \stdClass|array $response Response from API call for tags.
*
* @return \stdClass|array Array of tag numbers, object is error.
*/
public function parse_tag_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
array_map(
function ( $e ) use ( &$arr ) {
$arr[] = $e->name;
return $arr;
},
(array) $response
);
return $arr;
}
/**
* Parse API response and return array of meta variables.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of meta variables.
*/
public function parse_meta_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['private'] = isset( $e->visibility ) && 'private' === $e->visibility ? true : false;
$arr['private'] = isset( $e->public ) ? ! $e->public : $arr['private'];
$arr['last_updated'] = $e->last_activity_at;
$arr['watchers'] = 0;
$arr['forks'] = $e->forks_count;
$arr['open_issues'] = isset( $e->open_issues_count ) ? $e->open_issues_count : 0;
}
);
return $arr;
}
/**
* Parse API response and return array with changelog in base64.
*
* @param \stdClass|array $response Response from API call.
*
* @return array|\stdClass $arr Array of changes in base64, object if error.
*/
public function parse_changelog_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['changes'] = $e->content;
}
);
return $arr;
}
/**
* Parse API response and return array of branch data.
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$branches = [];
foreach ( $response as $branch ) {
$branches[ $branch->name ]['download'] = $this->construct_download_link( $branch->name );
$branches[ $branch->name ]['commit_hash'] = $branch->commit->id;
$branches[ $branch->name ]['commit_timestamp'] = $branch->commit->committed_date;
}
return $branches;
}
/**
* Parse tags and create download links.
*
* @param \stdClass|array $response Response from API call.
* @param array $repo_type
*
* @return array
*/
protected function parse_tags( $response, $repo_type ) {
$tags = [];
$rollback = [];
foreach ( (array) $response as $tag ) {
$download_link = "/projects/{$this->get_gitlab_id()}/repository/archive.zip";
$download_link = $this->get_api_url( $download_link );
$download_link = add_query_arg( 'sha', $tag, $download_link );
$tags[] = $tag;
$rollback[ $tag ] = $download_link;
}
return [ $tags, $rollback ];
}
/**
* Add settings for GitLab.com, GitLab Community Edition.
* or GitLab Enterprise Access Token.
*
* @param array $auth_required
*
* @return void
*/
public function add_settings( $auth_required ) {
if ( $auth_required['gitlab'] || $auth_required['gitlab_enterprise'] ) {
add_settings_section(
'gitlab_settings',
esc_html__( 'GitLab Personal Access Token', 'github-updater' ),
[ $this, 'print_section_gitlab_token' ],
'github_updater_gitlab_install_settings'
);
}
if ( $auth_required['gitlab_private'] ) {
add_settings_section(
'gitlab_id',
esc_html__( 'GitLab Private Settings', 'github-updater' ),
[ $this, 'print_section_gitlab_info' ],
'github_updater_gitlab_install_settings'
);
}
if ( $auth_required['gitlab'] ) {
add_settings_field(
'gitlab_access_token',
esc_html__( 'GitLab.com Access Token', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_gitlab_install_settings',
'gitlab_settings',
[
'id' => 'gitlab_access_token',
'token' => true,
]
);
}
if ( $auth_required['gitlab_enterprise'] ) {
add_settings_field(
'gitlab_enterprise_token',
esc_html__( 'GitLab CE or GitLab Enterprise Personal Access Token', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_gitlab_install_settings',
'gitlab_settings',
[
'id' => 'gitlab_enterprise_token',
'token' => true,
]
);
}
}
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field() {
$setting_field['page'] = 'github_updater_gitlab_install_settings';
$setting_field['section'] = 'gitlab_id';
$setting_field['callback_method'] = [
Singleton::get_instance( 'Settings', $this ),
'token_callback_text',
];
return $setting_field;
}
/**
* Add subtab to Settings page.
*/
private function add_settings_subtab() {
add_filter(
'github_updater_add_settings_subtabs',
function ( $subtabs ) {
return array_merge( $subtabs, [ 'gitlab' => esc_html__( 'GitLab', 'github-updater' ) ] );
}
);
}
/**
* Print the GitLab Settings text.
*/
public function print_section_gitlab_info() {
esc_html_e( 'Enter your repository specific GitLab Access Token.', 'github-updater' );
}
/**
* Print the GitLab Access Token Settings text.
*/
public function print_section_gitlab_token() {
esc_html_e( 'Enter your GitLab.com, GitLab CE, or GitLab Enterprise Access Token.', 'github-updater' );
}
/**
* Add remote install settings fields.
*
* @param string $type
*/
public function add_install_settings_fields( $type ) {
add_settings_field(
'gitlab_access_token',
esc_html__( 'GitLab Access Token', 'github-updater' ),
[ $this, 'gitlab_access_token' ],
'github_updater_install_' . $type,
$type
);
}
/**
* GitLab Access Token for remote install.
*/
public function gitlab_access_token() {
?>
<label for="gitlab_access_token">
<input class="gitlab_setting" type="password" style="width:50%;" id="gitlab_access_token" name="gitlab_access_token" value="" autocomplete="new-password">
<br>
<span class="description">
<?php esc_html_e( 'Enter GitLab Access Token for private GitLab repositories.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Display GitLab error admin notices.
*/
public function gitlab_error_notices() {
add_action( is_multisite() ? 'network_admin_notices' : 'admin_notices', [ $this, 'gitlab_error' ] );
}
/**
* Generate error message for missing GitLab Private Token.
*/
public function gitlab_error() {
$auth_required = $this->get_class_vars( 'Settings', 'auth_required' );
$error_code = $this->get_error_codes();
if ( ! isset( $error_code['gitlab'] ) &&
( ( empty( static::$options['gitlab_enterprise_token'] ) &&
$auth_required['gitlab_enterprise'] ) ||
( empty( static::$options['gitlab_access_token'] ) &&
$auth_required['gitlab'] ) )
) {
self::$error_code['gitlab'] = [ 'error' => true ];
if ( ! \PAnD::is_admin_notice_active( 'gitlab-error-1' ) ) {
return;
}
?>
<div data-dismissible="gitlab-error-1" class="error notice is-dismissible">
<p>
<?php esc_html_e( 'You must set a GitLab.com, GitLab CE, or GitLab Enterprise Access Token.', 'github-updater' ); ?>
</p>
</div>
<?php
}
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed $install
*/
public function remote_install( $headers, $install ) {
$gitlab_com = true;
$options['gitlab_access_token'] = isset( static::$options['gitlab_access_token'] ) ? static::$options['gitlab_access_token'] : null;
$options['gitlab_enterprise_token'] = isset( static::$options['gitlab_enterprise_token'] ) ? static::$options['gitlab_enterprise_token'] : null;
if ( 'gitlab.com' === $headers['host'] || empty( $headers['host'] ) ) {
$base = 'https://gitlab.com';
$headers['host'] = 'gitlab.com';
} else {
$base = $headers['base_uri'];
$gitlab_com = false;
}
$id = rawurlencode( $install['github_updater_repo'] );
$install['download_link'] = "{$base}/api/v4/projects/{$id}/repository/archive.zip";
$install['download_link'] = add_query_arg( 'sha', $install['github_updater_branch'], $install['download_link'] );
/*
* Add/Save access token if present.
*/
if ( ! empty( $install['gitlab_access_token'] ) ) {
$install['options'][ $install['repo'] ] = $install['gitlab_access_token'];
if ( $gitlab_com ) {
$install['options']['gitlab_access_token'] = $install['gitlab_access_token'];
} else {
$install['options']['gitlab_enterprise_token'] = $install['gitlab_access_token'];
}
}
if ( $gitlab_com ) {
$token = ! empty( $install['options']['gitlab_access_token'] )
? $install['options']['gitlab_access_token']
: $options['gitlab_access_token'];
} else {
$token = ! empty( $install['options']['gitlab_enterprise_token'] )
? $install['options']['gitlab_enterprise_token']
: $options['gitlab_enterprise_token'];
}
if ( ! empty( $token ) ) {
$install['download_link'] = add_query_arg( 'private_token', $token, $install['download_link'] );
}
if ( ! empty( static::$options['gitlab_access_token'] ) ) {
unset( $install['options']['gitlab_access_token'] );
}
if ( ! empty( static::$options['gitlab_enterprise_token'] ) ) {
unset( $install['options']['gitlab_enterprise_token'] );
}
return $install;
}
}

View File

@@ -0,0 +1,514 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\Singleton;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Branch;
use Fragen\GitHub_Updater\Traits\GHU_Trait;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Gitea_API
*
* Get remote data from a Gitea repo.
*
* @author Andy Fragen
* @author Marco Betschart
*/
class Gitea_API extends API implements API_Interface {
use GHU_Trait;
/**
* Constructor.
*
* @param \stdClass $type
*/
public function __construct( $type ) {
parent::__construct();
$this->type = $type;
$this->response = $this->get_repo_cache();
$branch = new Branch( $this->response );
if ( ! empty( $type->branch ) ) {
$this->type->branch = ! empty( $branch->cache['current_branch'] )
? $branch->cache['current_branch']
: $type->branch;
}
$this->set_default_credentials();
$this->settings_hook( $this );
$this->add_settings_subtab();
$this->add_install_fields( $this );
}
/**
* Set default credentials if option not set.
*/
protected function set_default_credentials() {
$running_servers = Singleton::get_instance( 'Base', $this )->get_running_git_servers();
$set_credentials = false;
if ( ! isset( static::$options['gitea_access_token'] ) ) {
static::$options['gitea_access_token'] = null;
$set_credentials = true;
}
if ( empty( static::$options['gitea_access_token'] ) &&
in_array( 'gitea', $running_servers, true )
) {
$this->gitea_error_notices();
}
if ( $set_credentials ) {
add_site_option( 'github_updater', static::$options );
}
}
/**
* Read the remote file and parse headers.
*
* @param string $file Filename.
*
* @return bool
*/
public function get_remote_info( $file ) {
return $this->get_remote_api_info( 'gitea', $file, "/repos/:owner/:repo/raw/:branch/{$file}" );
}
/**
* Get remote info for tags.
*
* @return bool
*/
public function get_remote_tag() {
return $this->get_remote_api_tag( 'gitea', '/repos/:owner/:repo/releases' );
}
/**
* Read the remote CHANGES.md file.
*
* @param string $changes Changelog filename.
*
* @return mixed
*/
public function get_remote_changes( $changes ) {
return $this->get_remote_api_changes( 'gitea', $changes, "/repos/:owner/:repo/raw/:branch/{$changes}" );
}
/**
* Read and parse remote readme.txt.
*
* @return mixed
*/
public function get_remote_readme() {
return $this->get_remote_api_readme( 'gitea', '/repos/:owner/:repo/raw/:branch/readme.txt' );
}
/**
* Read the repository meta from API.
*
* @return mixed
*/
public function get_repo_meta() {
return $this->get_remote_api_repo_meta( 'gitea', '/repos/:owner/:repo' );
}
/**
* Create array of branches and download links as array.
*
* @return mixed
*/
public function get_remote_branches() {
return $this->get_remote_api_branches( 'gitea', '/repos/:owner/:repo/branches' );
}
/**
* Get Gitea release asset.
*
* @return false
*/
public function get_release_asset() {
// TODO: eventually figure this out.
return false;
}
/**
* Construct $this->type->download_link using Gitea API.
*
* @param boolean $branch_switch for direct branch changing.
*
* @return string $endpoint
*/
public function construct_download_link( $branch_switch = false ) {
self::$method = 'download_link';
$download_link_base = $this->get_api_url( '/repos/:owner/:repo/archive/', true );
$endpoint = '';
/*
* If a branch has been given, use branch.
* If branch is master (default) and tags are used, use newest tag.
*/
if ( 'master' !== $this->type->branch || empty( $this->type->tags ) ) {
$endpoint .= $this->type->branch . '.zip';
} else {
$endpoint .= $this->type->newest_tag . '.zip';
}
// Create endpoint for branch switching.
if ( $branch_switch ) {
$endpoint = $branch_switch . '.zip';
}
$endpoint = $this->add_access_token_endpoint( $this, $endpoint );
$download_link = $download_link_base . $endpoint;
/**
* Filter download link so developers can point to specific ZipFile
* to use as a download link during a branch switch.
*
* @since 8.8.0
*
* @param string $download_link Download URL.
* @param /stdClass $this->type Repository object.
* @param string $branch_switch Branch or tag for rollback or branch switching.
*/
return apply_filters( 'github_updater_post_construct_download_link', $download_link, $this->type, $branch_switch );
}
/**
* Create Gitea API endpoints.
*
* @param Gitea_API|API $git
* @param string $endpoint
*
* @return string $endpoint
*/
public function add_endpoints( $git, $endpoint ) {
switch ( $git::$method ) {
case 'file':
case 'readme':
case 'meta':
case 'tags':
case 'changes':
case 'translation':
case 'download_link':
break;
case 'branches':
$endpoint = add_query_arg( 'per_page', '100', $endpoint );
break;
default:
break;
}
$endpoint = $this->add_access_token_endpoint( $git, $endpoint );
return $endpoint;
}
/**
* Parse API response call and return only array of tag numbers.
*
* @param \stdClass|array $response Response from API call for tags.
*
* @return \stdClass|array Array of tag numbers, object is error.
*/
public function parse_tag_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
array_map(
function ( $e ) use ( &$arr ) {
$arr[] = $e->tag_name;
return $arr;
},
(array) $response
);
return $arr;
}
/**
* Parse API response and return array of meta variables.
*
* @param \stdClass|array $response Response from API call.
*
* @return array $arr Array of meta variables.
*/
public function parse_meta_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$arr = [];
$response = [ $response ];
array_filter(
$response,
function ( $e ) use ( &$arr ) {
$arr['private'] = $e->private;
$arr['last_updated'] = $e->updated_at;
$arr['watchers'] = $e->watchers_count;
$arr['forks'] = $e->forks_count;
$arr['open_issues'] = isset( $e->open_issues_count ) ? $e->open_issues_count : 0;
}
);
return $arr;
}
/**
* Parse API response and return array with changelog in base64.
*
* @param \stdClass|array $response Response from API call.
*
* @return array|\stdClass $arr Array of changes in base64, object if error.
*/
public function parse_changelog_response( $response ) {
}
/**
* Parse API response and return array of branch data.
*
* @param \stdClass $response API response.
*
* @return array Array of branch data.
*/
public function parse_branch_response( $response ) {
if ( $this->validate_response( $response ) ) {
return $response;
}
$branches = [];
foreach ( $response as $branch ) {
$branches[ $branch->name ]['download'] = $this->construct_download_link( $branch->name );
$branches[ $branch->name ]['commit_hash'] = $branch->commit->id;
$branches[ $branch->name ]['commit_timestamp'] = $branch->commit->timestamp;
}
return $branches;
}
/**
* Parse tags and create download links.
*
* @param \stdClass|array $response Response from API call.
* @param array $repo_type
*
* @return array
*/
protected function parse_tags( $response, $repo_type ) {
$tags = [];
$rollback = [];
foreach ( (array) $response as $tag ) {
$download_link = implode(
'/',
[
$repo_type['base_uri'],
'repos',
$this->type->owner,
$this->type->slug,
'archive/',
]
);
$tags[] = $tag;
$rollback[ $tag ] = $download_link . $tag . '.zip';
}
return [ $tags, $rollback ];
}
/**
* Add settings for Gitea Access Token.
*
* @param array $auth_required
*
* @return void
*/
public function add_settings( $auth_required ) {
if ( $auth_required['gitea'] ) {
add_settings_section(
'gitea_settings',
esc_html__( 'Gitea Access Token', 'github-updater' ),
[ $this, 'print_section_gitea_token' ],
'github_updater_gitea_install_settings'
);
}
if ( $auth_required['gitea_private'] ) {
add_settings_section(
'gitea_id',
esc_html__( 'Gitea Private Settings', 'github-updater' ),
[ $this, 'print_section_gitea_info' ],
'github_updater_gitea_install_settings'
);
}
if ( $auth_required['gitea'] ) {
add_settings_field(
'gitea_access_token',
esc_html__( 'Gitea Access Token', 'github-updater' ),
[ Singleton::get_instance( 'Settings', $this ), 'token_callback_text' ],
'github_updater_gitea_install_settings',
'gitea_settings',
[
'id' => 'gitea_access_token',
'token' => true,
]
);
}
}
/**
* Add values for individual repo add_setting_field().
*
* @return mixed
*/
public function add_repo_setting_field() {
$setting_field['page'] = 'github_updater_gitea_install_settings';
$setting_field['section'] = 'gitea_id';
$setting_field['callback_method'] = [
Singleton::get_instance( 'Settings', $this ),
'token_callback_text',
];
return $setting_field;
}
/**
* Add subtab to Settings page.
*/
private function add_settings_subtab() {
add_filter(
'github_updater_add_settings_subtabs',
function ( $subtabs ) {
return array_merge( $subtabs, [ 'gitea' => esc_html__( 'Gitea', 'github-updater' ) ] );
}
);
}
/**
* Print the Gitea Settings text.
*/
public function print_section_gitea_info() {
esc_html_e( 'Enter your repository specific Gitea Access Token.', 'github-updater' );
}
/**
* Print the Gitea Access Token Settings text.
*/
public function print_section_gitea_token() {
esc_html_e( 'Enter your Gitea Access Token.', 'github-updater' );
}
/**
* Add remote install settings fields.
*
* @param string $type
*/
public function add_install_settings_fields( $type ) {
add_settings_field(
'gitea_access_token',
esc_html__( 'Gitea Access Token', 'github-updater' ),
[ $this, 'gitea_access_token' ],
'github_updater_install_' . $type,
$type
);
}
/**
* Gitea Access Token for remote install.
*/
public function gitea_access_token() {
?>
<label for="gitea_access_token">
<input class="gitea_setting" type="password" style="width:50%;" id="gitea_access_token" name="gitea_access_token" value="" autocomplete="new-password">
<br>
<span class="description">
<?php esc_html_e( 'Enter Gitea Access Token for private Gitea repositories.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Display Gitea error admin notices.
*/
public function gitea_error_notices() {
add_action( is_multisite() ? 'network_admin_notices' : 'admin_notices', [ $this, 'gitea_error' ] );
}
/**
* Generate error message for missing Gitea Access Token.
*/
public function gitea_error() {
$auth_required = $this->get_class_vars( 'Settings', 'auth_required' );
$error_code = $this->get_error_codes();
if ( ! isset( $error_code['gitea'] ) &&
empty( static::$options['gitea_access_token'] ) &&
$auth_required['gitea']
) {
self::$error_code['gitea'] = [ 'error' => true ];
if ( ! \PAnD::is_admin_notice_active( 'gitea-error-1' ) ) {
return;
}
?>
<div data-dismissible="gitea-error-1" class="error notice is-dismissible">
<p>
<?php esc_html_e( 'You must set a Gitea Access Token.', 'github-updater' ); ?>
</p>
</div>
<?php
}
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed $install
*/
public function remote_install( $headers, $install ) {
$options['gitea_access_token'] = isset( static::$options['gitea_access_token'] ) ? static::$options['gitea_access_token'] : null;
$base = $headers['base_uri'] . '/api/v1';
$install['download_link'] = "{$base}/repos/{$install['github_updater_repo']}/archive/{$install['github_updater_branch']}.zip";
/*
* Add/Save access token if present.
*/
if ( ! empty( $install['gitea_access_token'] ) ) {
$install['options'][ $install['repo'] ] = $install['gitea_access_token'];
$install['options']['gitea_access_token'] = $install['gitea_access_token'];
}
$token = ! empty( $install['options']['gitea_access_token'] )
? $install['options']['gitea_access_token']
: $options['gitea_access_token'];
if ( ! empty( $token ) ) {
$install['download_link'] = add_query_arg( 'access_token', $token, $install['download_link'] );
}
if ( ! empty( static::$options['gitea_access_token'] ) ) {
unset( $install['options']['gitea_access_token'] );
}
return $install;
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\API;
use Fragen\GitHub_Updater\Traits\GHU_Trait;
/**
* Class Language_Pack_API
*/
class Language_Pack_API extends API {
use GHU_Trait;
/**
* Constructor.
*
* @param \stdClass $type
*/
public function __construct( $type ) {
parent::__construct();
self::$method = 'translation';
$this->type = $type;
$this->response = $this->get_repo_cache();
}
/**
* Get/process Language Packs.
*
* @param array $headers Array of headers of Language Pack.
*
* @return bool When invalid response.
*/
public function get_language_pack( $headers ) {
$response = ! empty( $this->response['languages'] ) ? $this->response['languages'] : false;
if ( ! $response ) {
$response = $this->get_language_pack_json( $this->type->git, $headers, $response );
if ( $response ) {
foreach ( $response as $locale ) {
$package = $this->process_language_pack_package( $this->type->git, $locale, $headers );
$response->{$locale->language}->package = $package;
$response->{$locale->language}->type = $this->type->type;
$response->{$locale->language}->version = $this->type->local_version;
}
$this->set_repo_cache( 'languages', $response );
} else {
return false;
}
}
$this->type->language_packs = $response;
return true;
}
/**
* Get language-pack.json from appropriate host.
*
* @param string $git ( github|bitbucket|gitlab|gitea ).
* @param array $headers
* @param mixed $response API response.
*
* @return array|bool|mixed
*/
private function get_language_pack_json( $git, $headers, $response ) {
switch ( $git ) {
case 'github':
$response = $this->api( '/repos/' . $headers['owner'] . '/' . $headers['repo'] . '/contents/language-pack.json' );
$response = isset( $response->content )
? json_decode( base64_decode( $response->content ) )
: null;
break;
case 'bitbucket':
$response = $this->api( '/2.0/repositories/' . $headers['owner'] . '/' . $headers['repo'] . '/src/master/language-pack.json' );
break;
case 'gitlab':
$id = rawurlencode( $headers['owner'] . '/' . $headers['repo'] );
$response = $this->api( '/projects/' . $id . '/repository/files/language-pack.json' );
$response = isset( $response->content )
? json_decode( base64_decode( $response->content ) )
: null;
break;
case 'gitea':
$response = $this->api( '/repos/' . $headers['owner'] . '/' . $headers['repo'] . '/raw/master/language-pack.json' );
$response = isset( $response->content )
? json_decode( base64_decode( $response->content ) )
: null;
break;
}
if ( $this->validate_response( $response ) ) {
return false;
}
return $response;
}
/**
* Process $package for update transient.
*
* @param string $git ( github|bitbucket|gitlab|gitea ).
* @param string $locale
* @param array $headers
*
* @return array|null|string
*/
private function process_language_pack_package( $git, $locale, $headers ) {
$package = null;
switch ( $git ) {
case 'github':
$package = [ 'https://github.com', $headers['owner'], $headers['repo'], 'blob/master' ];
$package = implode( '/', $package ) . $locale->package;
$package = add_query_arg( [ 'raw' => 'true' ], $package );
break;
case 'bitbucket':
$package = [ 'https://bitbucket.org', $headers['owner'], $headers['repo'], 'raw/master' ];
$package = implode( '/', $package ) . $locale->package;
break;
case 'gitlab':
$package = [ 'https://gitlab.com', $headers['owner'], $headers['repo'], 'raw/master' ];
$package = implode( '/', $package ) . $locale->package;
break;
case 'gitea':
// TODO: make sure this works as expected.
$package = [ $headers['uri'], 'raw/master' ];
$package = implode( '/', $package ) . $local->package;
break;
}
return $package;
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\API;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Zipfile_API
*
* Remote install from a Zipfile.
*
* @author Andy Fragen
*/
class Zipfile_API {
/**
* Add remote install settings fields.
*
* @param string $type plugin|theme.
*/
public function add_install_settings_fields( $type ) {
add_settings_field(
'zipfile_slug',
esc_html__( 'Zipfile Slug', 'github-updater' ),
[ $this, 'zipfile_slug' ],
'github_updater_install_' . $type,
$type
);
}
/**
* Set repo slug for remote install.
*/
public function zipfile_slug() {
?>
<label for="zipfile_slug">
<input class="zipfile_setting" type="text" style="width:50%;" id="zipfile_slug" name="zipfile_slug" value="" placeholder="my-repo-slug">
<br>
<span class="description">
<?php esc_html_e( 'Enter plugin or theme slug.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Add remote install feature, create endpoint.
*
* @param array $headers
* @param array $install
*
* @return mixed $install
*/
public function remote_install( $headers, $install ) {
$install['download_link'] = ! empty( $headers['uri'] ) ? $headers['uri'] : $headers['original'];
$install['github_updater_install_repo'] = $install['zipfile_slug'];
return $install;
}
}

View File

@@ -0,0 +1,957 @@
<?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;
use Fragen\GitHub_Updater\Traits\Basic_Auth_Loader;
use Fragen\GitHub_Updater\API\Bitbucket_API;
use Fragen\GitHub_Updater\API\Language_Pack_API;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Base
*
* Update a WordPress plugin or theme from a Git-based repo.
*
* @author Andy Fragen
*/
class Base {
use GHU_Trait, Basic_Auth_Loader;
/**
* Variable for holding extra theme and plugin headers.
*
* @var array
*/
public static $extra_headers = [];
/**
* Holds the values to be used in the fields callbacks.
*
* @var array
*/
public static $options;
/**
* Holds git server types.
*
* @var array
*/
public static $git_servers = [ 'github' => 'GitHub' ];
/**
* Holds extra repo header types.
*
* @var array
*/
protected static $extra_repo_headers = [
'Languages' => 'Languages',
'CIJob' => 'CI Job',
];
/**
* Holds an array of installed git APIs.
*
* @var array
*/
public static $installed_apis = [ 'github_api' => true ];
/**
* Stores the object calling Basic_Auth_Loader.
*
* @access public
* @var \stdClass
*/
public $caller;
/**
* Store details of all repositories that are installed.
*
* @var \stdClass
*/
protected $config;
/**
* Constructor.
*/
public function __construct() {
$this->set_installed_apis();
}
/**
* Set boolean for installed API classes.
*/
protected function set_installed_apis() {
if ( file_exists( __DIR__ . '/API/Bitbucket_API.php' ) ) {
self::$installed_apis['bitbucket_api'] = true;
self::$git_servers['bitbucket'] = 'Bitbucket';
} else {
self::$installed_apis['bitbucket_api'] = false;
}
self::$installed_apis['bitbucket_server_api'] = file_exists( __DIR__ . '/API/Bitbucket_Server_API.php' );
if ( file_exists( __DIR__ . '/API/GitLab_API.php' ) ) {
self::$installed_apis['gitlab_api'] = true;
self::$git_servers['gitlab'] = 'GitLab';
} else {
self::$installed_apis['gitlab_api'] = false;
}
if ( file_exists( __DIR__ . '/API/Gitea_API.php' ) ) {
self::$installed_apis['gitea_api'] = true;
self::$git_servers['gitea'] = 'Gitea';
} else {
self::$installed_apis['gitea_api'] = false;
}
if ( file_exists( __DIR__ . '/API/Zipfile_API.php' ) ) {
self::$installed_apis['zipfile_api'] = true;
self::$git_servers['zipfile'] = 'Zipfile';
} else {
self::$installed_apis['zipfile_api'] = false;
}
}
/**
* Load Plugin, Theme, and Settings with correct capabiltiies and on selective admin pages.
*
* @return bool
*/
public function load() {
if ( ! apply_filters( 'github_updater_hide_settings', false ) ) {
Singleton::get_instance( 'Settings', $this )->run();
}
if ( ! Singleton::get_instance( 'Init', $this )->can_update() ) {
return false;
}
// Run GitHub Updater upgrade functions.
$upgrade = new GHU_Upgrade();
$upgrade->run();
// Load plugin stylesheet.
add_action(
'admin_enqueue_scripts',
function () {
wp_register_style( 'github-updater', plugins_url( basename( GITHUB_UPDATER_DIR ) ) . '/css/github-updater.css' );
wp_enqueue_style( 'github-updater' );
}
);
if ( isset( $_POST['ghu_refresh_cache'] ) ) {
/**
* Fires later in cycle when Refreshing Cache.
*
* @since 6.0.0
*/
do_action( 'ghu_refresh_transients' );
}
$this->get_meta_plugins();
$this->get_meta_themes();
return true;
}
/**
* Performs actual plugin metadata fetching.
*/
public function get_meta_plugins() {
if ( Singleton::get_instance( 'Init', $this )->can_update() ) {
Singleton::get_instance( 'Plugin', $this )->get_remote_plugin_meta();
}
}
/**
* Performs actual theme metadata fetching.
*/
public function get_meta_themes() {
if ( Singleton::get_instance( 'Init', $this )->can_update() ) {
Singleton::get_instance( 'Theme', $this )->get_remote_theme_meta();
}
}
/**
* AJAX endpoint for REST updates.
*/
public function ajax_update() {
Singleton::get_instance( 'Rest_Update', $this )->process_request();
}
/**
* Run background processes.
* Piggyback on built-in update function to get metadata.
* Set update transients for remote management.
*/
public function background_update() {
add_action( 'wp_update_plugins', [ $this, 'get_meta_plugins' ] );
add_action( 'wp_update_themes', [ $this, 'get_meta_themes' ] );
add_action( 'ghu_get_remote_plugin', [ $this, 'run_cron_batch' ], 10, 1 );
add_action( 'ghu_get_remote_theme', [ $this, 'run_cron_batch' ], 10, 1 );
add_action( 'wp_ajax_nopriv_ithemes_sync_request', [ $this, 'get_meta_remote_management' ] );
add_action( 'update_option_auto_updater.lock', [ $this, 'get_meta_remote_management' ] );
( new Remote_Management() )->set_update_transients();
}
/**
* Calls $this->get_meta_plugins() and $this->get_meta_themes()
* for remote management services.
*/
public function get_meta_remote_management() {
$this->get_meta_plugins();
$this->get_meta_themes();
}
/**
* Allows developers to use 'github_updater_set_options' hook to set access tokens or other settings.
* Saves results of filter hook to self::$options.
* Single plugin/theme should not be using both hooks.
*
* Hook requires return of associative element array.
* $key === repo-name and $value === token
* e.g. array( 'repo-name' => 'access_token' );
*/
public function set_options_filter() {
$config = apply_filters( 'github_updater_set_options', [] );
if ( empty( $config ) ) {
$config = function_exists( 'apply_filters_deprecated' )
? apply_filters_deprecated( 'github_updater_token_distribution', [ null ], '6.1.0', 'github_updater_set_options' )
: apply_filters( 'github_updater_token_distribution', [] );
}
if ( ! empty( $config ) ) {
$config = $this->sanitize( $config );
self::$options = array_merge( get_site_option( 'github_updater' ), $config );
update_site_option( 'github_updater', self::$options );
}
}
/**
* Add extra headers to get_plugins() or wp_get_themes().
*
* @param array $extra_headers
*
* @return array
*/
public function add_headers( $extra_headers ) {
$ghu_extra_headers = [
'RequiresWP' => 'Requires WP',
'RequiresPHP' => 'Requires PHP',
'ReleaseAsset' => 'Release Asset',
];
$uri_types = [
'PluginURI' => ' Plugin URI',
'ThemeURI' => ' Theme URI',
];
foreach ( self::$git_servers as $server ) {
foreach ( $uri_types as $uri_key => $uri_value ) {
$ghu_extra_headers[ $server . $uri_key ] = $server . $uri_value;
}
foreach ( self::$extra_repo_headers as $header_key => $header_value ) {
$ghu_extra_headers[ $server . $header_key ] = $server . ' ' . $header_value;
}
}
self::$extra_headers = array_unique( array_merge( self::$extra_headers, $ghu_extra_headers ) );
$extra_headers = array_merge( (array) $extra_headers, $ghu_extra_headers );
ksort( self::$extra_headers );
return $extra_headers;
}
/**
* Runs on wp-cron job to get remote repo meta in background.
*
* @param array $batches
*/
public function run_cron_batch( array $batches ) {
foreach ( $batches as $repo ) {
$this->get_remote_repo_meta( $repo );
}
}
/**
* Get remote repo meta data for plugins or themes.
* Calls remote APIs for data.
*
* @param \stdClass $repo
*
* @return bool
*/
public function get_remote_repo_meta( $repo ) {
$file = 'style.css';
if ( false !== stripos( $repo->type, 'plugin' ) ) {
$file = basename( $repo->file );
}
$repo_api = Singleton::get_instance( 'API', $this )->get_repo_api( $repo->git, $repo );
if ( null === $repo_api ) {
return false;
}
$this->{$repo->type} = $repo;
$this->set_defaults( $repo->type );
if ( $repo_api->get_remote_info( $file ) ) {
if ( ! self::is_wp_cli() ) {
if ( ! apply_filters( 'github_updater_run_at_scale', false ) ) {
$repo_api->get_repo_meta();
$changelog = $this->get_changelog_filename( $repo );
if ( $changelog ) {
$repo_api->get_remote_changes( $changelog );
}
$repo_api->get_remote_readme();
}
if ( ! empty( self::$options['branch_switch'] ) ) {
$repo_api->get_remote_branches();
}
}
$repo_api->get_remote_tag();
$repo->download_link = $repo_api->construct_download_link();
$language_pack = new Language_Pack( $repo, new Language_Pack_API( $repo ) );
$language_pack->run();
}
$this->remove_hooks( $repo_api );
return true;
}
/**
* Set default values for plugin/theme.
*
* @param string $type
*/
protected function set_defaults( $type ) {
if ( ! isset( self::$options['branch_switch'] ) ) {
self::$options['branch_switch'] = null;
}
if ( ! isset( $this->$type->slug ) ) {
$this->$type = new \stdClass();
$this->$type->slug = null;
} elseif ( ! isset( self::$options[ $this->$type->slug ] ) ) {
self::$options[ $this->$type->slug ] = null;
add_site_option( 'github_updater', self::$options );
}
$this->$type->remote_version = '0.0.0';
$this->$type->newest_tag = '0.0.0';
$this->$type->download_link = null;
$this->$type->tags = [];
$this->$type->rollback = [];
$this->$type->branches = [];
$this->$type->requires = null;
$this->$type->tested = null;
$this->$type->donate_link = null;
$this->$type->contributors = [];
$this->$type->downloaded = 0;
$this->$type->last_updated = null;
$this->$type->rating = 0;
$this->$type->num_ratings = 0;
$this->$type->transient = [];
$this->$type->repo_meta = [];
$this->$type->watchers = 0;
$this->$type->forks = 0;
$this->$type->open_issues = 0;
$this->$type->requires = false;
$this->$type->requires_php = false;
}
/**
* Get filename of changelog and return.
*
* @param \stdClass $repo
*
* @return bool|string
*/
protected function get_changelog_filename( $repo ) {
$changelogs = [ 'CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md' ];
$changes = null;
$local_files = null;
if ( is_dir( $repo->local_path ) ) {
$local_files = scandir( $repo->local_path, 0 );
}
$changes = array_intersect( (array) $local_files, $changelogs );
$changes = array_pop( $changes );
if ( ! empty( $changes ) ) {
return $changes;
}
return false;
}
/**
* Remove hooks after use.
*
* @param \stdClass $repo_api
*/
public function remove_hooks( $repo_api ) {
remove_filter( 'extra_theme_headers', [ $this, 'add_headers' ] );
remove_filter( 'extra_plugin_headers', [ $this, 'add_headers' ] );
if ( $repo_api instanceof Bitbucket_API ) {
$this->remove_authentication_hooks();
}
}
/**
* Checks if dupicate wp-cron event exists.
*
* @param string $event Name of wp-cron event.
*
* @return bool
*/
public function is_duplicate_wp_cron_event( $event ) {
$cron = _get_cron_array();
foreach ( $cron as $timestamp => $cronhooks ) {
if ( key( $cronhooks ) === $event ) {
$this->is_cron_overdue( $cron, $timestamp );
return true;
}
}
return false;
}
/**
* Check to see if wp-cron event is overdue by 24 hours and report error message.
*
* @param array $cron
* @param int $timestamp
*/
public function is_cron_overdue( $cron, $timestamp ) {
$overdue = ( ( time() - $timestamp ) / HOUR_IN_SECONDS ) > 24;
if ( $overdue ) {
$error_msg = esc_html__( 'There may be a problem with WP-Cron. A GitHub Updater WP-Cron event is overdue.', 'github-updater' );
$error = new \WP_Error( 'github_updater_cron_error', $error_msg );
Singleton::get_instance( 'Messages', $this )->create_error_message( $error );
}
}
/**
* Used for renaming of sources to ensure correct directory name.
*
* @since WordPress 4.4.0 The $hook_extra parameter became available.
*
* @param string $source
* @param string $remote_source
* @param \Plugin_Upgrader|\Theme_Upgrader $upgrader
* @param array $hook_extra
*
* @return string
*/
public function upgrader_source_selection( $source, $remote_source, $upgrader, $hook_extra = null ) {
global $wp_filesystem;
$slug = null;
$repo = null;
$new_source = null;
$upgrader_object = null;
/*
* Rename plugins.
*/
if ( $upgrader instanceof \Plugin_Upgrader ) {
$upgrader_object = Singleton::get_instance( 'Plugin', $this );
if ( isset( $hook_extra['plugin'] ) ) {
$slug = dirname( $hook_extra['plugin'] );
$new_source = trailingslashit( $remote_source ) . $slug;
}
}
/*
* Rename themes.
*/
if ( $upgrader instanceof \Theme_Upgrader ) {
$upgrader_object = Singleton::get_instance( 'Theme', $this );
if ( isset( $hook_extra['theme'] ) ) {
$slug = $hook_extra['theme'];
$new_source = trailingslashit( $remote_source ) . $slug;
}
}
$repo = $this->get_repo_slugs( $slug, $upgrader_object );
/*
* Not GitHub Updater plugin/theme.
*/
if ( ! isset( $_POST['github_updater_repo'] ) && empty( $repo ) ) {
return $source;
}
/*
* Remote install source.
*/
$install_options = $this->get_class_vars( 'Install', 'install' );
if ( empty( $repo ) && isset( $install_options['github_updater_install_repo'] ) ) {
$slug = $install_options['github_updater_install_repo'];
$new_source = trailingslashit( $remote_source ) . $slug;
self::$options['remote_install'] = true;
}
Singleton::get_instance( 'Branch', $this )->set_branch_on_switch( $slug );
$new_source = $this->fix_misnamed_directory( $new_source, $remote_source, $upgrader_object, $slug );
$new_source = $this->fix_release_asset_directory( $new_source, $remote_source, $upgrader_object, $slug );
$wp_filesystem->move( $source, $new_source );
return trailingslashit( $new_source );
}
/**
* Correctly rename an initially misnamed directory.
* This usually occurs when initial installation not using GitHub Updater.
* May cause plugin/theme deactivation.
*
* @param string $new_source
* @param string $remote_source
* @param Plugin|Theme $upgrader_object
* @param string $slug
*
* @return string $new_source
*/
private function fix_misnamed_directory( $new_source, $remote_source, $upgrader_object, $slug ) {
if ( ! array_key_exists( $slug, (array) $upgrader_object->config ) &&
! isset( self::$options['remote_install'] )
) {
if ( $upgrader_object instanceof Plugin ) {
foreach ( (array) $upgrader_object->config as $plugin ) {
if ( $slug === $plugin->slug ) {
$new_source = trailingslashit( $remote_source ) . $slug;
break;
}
}
}
if ( $upgrader_object instanceof Theme ) {
foreach ( (array) $upgrader_object->config as $theme ) {
if ( $slug === $theme->slug ) {
$new_source = trailingslashit( $remote_source ) . $slug;
break;
}
}
}
}
return $new_source;
}
/**
* Fix the directory structure of certain release assests.
*
* GitLab release assets have a different download directory structure.
* Bitbucket release assets need to be copied into a containing directory.
*
* @param string $new_source
* @param string $remote_source
* @param Plugin|Theme $upgrader_object
* @param string $slug
*
* @return string $new_source
*/
private function fix_release_asset_directory( $new_source, $remote_source, $upgrader_object, $slug ) {
global $wp_filesystem;
if ( isset( $upgrader_object->config[ $slug ]->release_asset ) &&
$upgrader_object->config[ $slug ]->release_asset ) {
if ( 'gitlab' === $upgrader_object->config[ $slug ]->git ) {
$new_source = trailingslashit( dirname( $remote_source ) ) . $slug;
add_filter( 'upgrader_post_install', [ $this, 'upgrader_post_install' ], 10, 3 );
}
if ( 'bitbucket' === $upgrader_object->config[ $slug ]->git ) {
$temp_source = trailingslashit( dirname( $remote_source ) ) . $slug;
$wp_filesystem->move( $remote_source, $temp_source );
wp_mkdir_p( $new_source );
copy_dir( $temp_source, $new_source );
$wp_filesystem->delete( $temp_source, true );
}
}
return $new_source;
}
/**
* Delete $source when updating from GitLab Release Asset.
*
* @param bool $true
* @param array $hook_extra
* @param array $result
*
* @return mixed
*/
public function upgrader_post_install( $true, $hook_extra, $result ) {
global $wp_filesystem;
$wp_filesystem->delete( $result['source'], true );
remove_filter( 'upgrader_post_install', [ $this, 'upgrader_post_install' ] );
return $result;
}
/**
* Set array with normal repo names.
* Fix name even if installed without renaming originally, eg <repo>-master
*
* @param string $slug
* @param Base|Plugin|Theme $upgrader_object
*
* @return array
*/
protected function get_repo_slugs( $slug, $upgrader_object = null ) {
$arr = [];
$rename = explode( '-', $slug );
array_pop( $rename );
$rename = implode( '-', $rename );
if ( null === $upgrader_object ) {
$upgrader_object = $this;
}
$rename = isset( $upgrader_object->config[ $slug ] ) ? $slug : $rename;
foreach ( (array) $upgrader_object->config as $repo ) {
// Check repo slug or directory name for match.
$slug_check = [
$repo->slug,
dirname( $repo->file ),
];
// Exact match.
if ( \in_array( $slug, $slug_check, true ) ) {
$arr['slug'] = $repo->slug;
break;
}
// Soft match, there may still be an exact $slug match.
if ( \in_array( $rename, $slug_check, true ) ) {
$arr['slug'] = $repo->slug;
}
}
return $arr;
}
/**
* Update transient for rollback or branch switch.
*
* @param string $type plugin|theme.
* @param \stdClass $repo
* @param bool $set_transient Default false, if true then set update transient.
*
* @return array $rollback Rollback transient.
*/
protected function set_rollback_transient( $type, $repo, $set_transient = false ) {
$repo_api = Singleton::get_instance( 'API', $this )->get_repo_api( $repo->git, $repo );
$this->tag = isset( $_GET['rollback'] ) ? $_GET['rollback'] : false;
$slug = 'plugin' === $type ? $repo->file : $repo->slug;
$download_link = $repo_api->construct_download_link( $this->tag );
/**
* Filter download link so developers can point to specific ZipFile
* to use as a download link during a branch switch.
*
* @since 8.6.0
*
* @param string $download_link Download URL.
* @param /stdClass $repo
* @param string $this->tag Branch or tag for rollback.
*/
$download_link = apply_filters_deprecated(
'github_updater_set_rollback_package',
[ $download_link, $repo, $this->tag ],
'8.8.0',
'github_updater_post_construct_download_link'
);
$rollback = [
$type => $slug,
'new_version' => $this->tag,
'url' => $repo->uri,
'package' => $download_link,
'branch' => $repo->branch,
'branches' => $repo->branches,
'type' => $repo->type,
];
if ( 'plugin' === $type ) {
$rollback['slug'] = $repo->slug;
$rollback = (object) $rollback;
}
return $rollback;
}
/**
* Check to see if wp-cron/background updating has finished.
*
* @param null $repo
*
* @return bool true when waiting for background job to finish.
*/
protected function waiting_for_background_update( $repo = null ) {
$caches = [];
if ( null !== $repo ) {
$cache = isset( $repo->slug ) ? $this->get_repo_cache( $repo->slug ) : null;
return empty( $cache );
}
$repos = array_merge(
Singleton::get_instance( 'Plugin', $this )->get_plugin_configs(),
Singleton::get_instance( 'Theme', $this )->get_theme_configs()
);
foreach ( $repos as $git_repo ) {
$caches[ $git_repo->slug ] = $this->get_repo_cache( $git_repo->slug );
}
$waiting = array_filter(
$caches,
function ( $e ) {
return empty( $e );
}
);
return ! empty( $waiting );
}
/**
* Create repo parts.
*
* @param string $repo
* @param string $type plugin|theme.
*
* @return mixed
*/
protected function get_repo_parts( $repo, $type ) {
$arr['bool'] = false;
$pattern = '/' . strtolower( $repo ) . '_/';
$type = preg_replace( $pattern, '', $type );
$repo_types = [
'GitHub' => 'github_' . $type,
'Bitbucket' => 'bitbucket_' . $type,
'GitLab' => 'gitlab_' . $type,
'Gitea' => 'gitea_' . $type,
];
$repo_base_uris = [
'GitHub' => 'https://github.com/',
'Bitbucket' => 'https://bitbucket.org/',
'GitLab' => 'https://gitlab.com/',
'Gitea' => '',
];
if ( array_key_exists( $repo, $repo_types ) ) {
$arr['type'] = $repo_types[ $repo ];
$arr['git_server'] = strtolower( $repo );
$arr['base_uri'] = $repo_base_uris[ $repo ];
$arr['bool'] = true;
foreach ( self::$extra_repo_headers as $key => $value ) {
$arr[ $key ] = $repo . ' ' . $value;
}
}
return $arr;
}
/**
* Return correct update row opening and closing tags for Shiny Updates.
*
* @param string $repo_name
* @param string $type plugin|theme.
* @param bool $branch_switcher
*
* @return array
*/
protected function update_row_enclosure( $repo_name, $type, $branch_switcher = false ) {
global $wp_version;
$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
$repo_base = $repo_name;
$shiny_classes = ' notice inline notice-warning notice-alt';
if ( 'plugin' === $type ) {
$repo_base = dirname( $repo_name );
}
$open = '<tr class="plugin-update-tr" data-slug="' . esc_attr( $repo_base ) . '" data-plugin="' . esc_attr( $repo_name ) . '">
<td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange">
<div class="update-message">';
$enclosure = [
'open' => $open,
'close' => '</div></td></tr>',
];
if ( version_compare( $wp_version, '4.6', '>=' ) ) {
$open_p = '<p>';
$close_p = '</p>';
if ( $branch_switcher ) {
$open_p = '';
$close_p = '';
}
$enclosure = [
'open' => substr_replace( $open, $shiny_classes, -2, 0 ) . $open_p,
'close' => $close_p . '</div></td></tr>',
];
}
return $enclosure;
}
/**
* Make branch switch row.
*
* @param array $data Parameters for creating branch switching row.
*
* @return void
*/
protected function make_branch_switch_row( $data ) {
$rollback = empty( $this->config[ $data['slug'] ]->rollback ) ? [] : $this->config[ $data['slug'] ]->rollback;
printf(
/* translators: 1: branch name, 2: jQuery dropdown, 3: closing tag */
esc_html__( 'Current branch is `%1$s`, try %2$sanother version%3$s', 'github-updater' ),
$data['branch'],
'<a href="#" onclick="jQuery(\'#' . $data['id'] . '\').toggle();return false;">',
'</a>.'
);
print '<ul id="' . $data['id'] . '" style="display:none; width: 100%;">';
if ( null !== $data['branches'] ) {
foreach ( array_keys( $data['branches'] ) as $branch ) {
printf(
'<li><a href="%s%s" aria-label="' . esc_html__( 'Switch to branch ', 'github-updater' ) . $branch . '">%s</a></li>',
$data['nonced_update_url'],
'&rollback=' . rawurlencode( $branch ),
esc_attr( $branch )
);
}
}
if ( ! empty( $rollback ) ) {
$rollback = array_keys( $rollback );
usort( $rollback, 'version_compare' );
krsort( $rollback );
$rollback = array_splice( $rollback, 0, 4, true );
array_shift( $rollback ); // Dump current tag.
foreach ( $rollback as $tag ) {
printf(
'<li><a href="%s%s" aria-label="' . esc_html__( 'Switch to release ', 'github-updater' ) . $tag . '">%s</a></li>',
$data['nonced_update_url'],
'&rollback=' . rawurlencode( $tag ),
esc_attr( $tag )
);
}
}
if ( empty( $rollback ) ) {
esc_html_e( 'No previous tags to rollback to.', 'github-updater' );
}
print '</ul>';
}
/**
* Generate update URL.
*
* @param string $type ( plugin or theme ).
* @param string $action
* @param string $repo_name
*
* @return string
*/
protected function get_update_url( $type, $action, $repo_name ) {
$update_url = esc_attr(
add_query_arg(
[
'action' => $action,
$type => rawurlencode( $repo_name ),
],
self_admin_url( 'update.php' )
)
);
return $update_url;
}
/**
* Parse Enterprise, Languages, Release Asset, and CI Job headers for plugins and themes.
*
* @param array $header
* @param array|\WP_Theme $headers
* @param array $header_parts
* @param array $repo_parts
*
* @return array $header
*/
protected function parse_extra_headers( $header, $headers, $header_parts, $repo_parts ) {
$hosted_domains = [ 'github.com', 'bitbucket.org', 'gitlab.com' ];
$theme = null;
$header['enterprise_uri'] = null;
$header['enterprise_api'] = null;
$header['languages'] = null;
$header['ci_job'] = false;
$header['release_asset'] = false;
if ( ! empty( $header['host'] ) && ! in_array( $header['host'], $hosted_domains, true ) ) {
$header['enterprise_uri'] = $header['base_uri'];
$header['enterprise_api'] = trim( $header['enterprise_uri'], '/' );
switch ( $header_parts[0] ) {
case 'GitHub':
$header['enterprise_api'] .= '/api/v3';
break;
case 'GitLab':
$header['enterprise_api'] .= '/api/v4';
break;
case 'Bitbucket':
$header['enterprise_api'] .= '/rest/api';
break;
}
}
if ( $headers instanceof \WP_Theme ) {
$theme = $headers;
$headers = [];
$headers['Release Asset'] = '';
$header['release_asset'] = 'true' === $theme->get( 'Release Asset' );
}
$self_hosted_parts = array_keys( self::$extra_repo_headers );
foreach ( $self_hosted_parts as $part ) {
if ( $theme instanceof \WP_Theme ) {
$headers[ $repo_parts[ $part ] ] = $theme->get( $repo_parts[ $part ] );
}
if ( array_key_exists( $repo_parts[ $part ], $headers ) &&
! empty( $headers[ $repo_parts[ $part ] ] )
) {
switch ( $part ) {
case 'Languages':
$header['languages'] = $headers[ $repo_parts[ $part ] ];
break;
case 'CIJob':
$header['ci_job'] = $headers[ $repo_parts[ $part ] ];
break;
}
}
}
$header['release_asset'] = ! $header['release_asset'] && isset( $headers['Release Asset'] ) ? 'true' === $headers['Release Asset'] : $header['release_asset'];
return $header;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Bootstrap
*/
class Bootstrap {
/**
* Holds main plugin file.
*
* @var $file
*/
protected $file;
/**
* Holds main plugin directory.
*
* @var $dir
*/
protected $dir;
/**
* Constructor.
*
* @param string $file Main plugin file.
* @return void
*/
public function __construct( $file ) {
$this->file = $file;
$this->dir = dirname( $file );
}
/**
* Run the bootstrap.
*
* @return void
*/
public function run() {
add_action(
'init',
function() {
load_plugin_textdomain( 'github-updater' );
}
);
define( 'GITHUB_UPDATER_DIR', $this->dir );
// Load Autoloader.
require_once $this->dir . '/vendor/autoload.php';
register_activation_hook( $this->file, array( new Init(), 'rename_on_activation' ) );
( new Init() )->run();
/**
* Initialize Persist Admin notices Dismissal.
*
* @link https://github.com/collizo4sky/persist-admin-notices-dismissal
*/
add_action( 'admin_init', array( 'PAnD', 'init' ) );
}
}

View File

@@ -0,0 +1,108 @@
<?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\GitHub_Updater\Traits\GHU_Trait;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Branch
*/
class Branch {
use GHU_Trait;
/**
* Holds repo cache data.
*
* @access public
* @var null
*/
public $cache;
/**
* Holds site options.
*
* @var array $options
*/
private static $options;
/**
* Branch constructor.
*
* @access public
*
* @param null $cache
*/
public function __construct( $cache = null ) {
$this->cache = $cache;
$this->load_options();
self::$options = $this->get_class_vars( 'Base', 'options' );
}
/**
* Get the current repo branch.
*
* @access public
*
* @param \stdClass $repo
*
* @return mixed
*/
public function get_current_branch( $repo ) {
$current_branch = ! empty( $this->cache['current_branch'] )
? $this->cache['current_branch']
: $repo->branch;
return $current_branch;
}
/**
* Set current branch on branch switch.
*
* @access public
*
* @param string $repo Repository slug.
*/
public function set_branch_on_switch( $repo ) {
$this->cache = $this->get_repo_cache( $repo );
if ( isset( $_GET['action'], $_GET['rollback'], $this->cache['branches'] ) &&
( 'upgrade-plugin' === $_GET['action'] || 'upgrade-theme' === $_GET['action'] )
) {
$current_branch = array_key_exists( $_GET['rollback'], $this->cache['branches'] )
? $_GET['rollback']
: 'master';
$this->set_repo_cache( 'current_branch', $current_branch, $repo );
self::$options[ 'current_branch_' . $repo ] = $current_branch;
update_site_option( 'github_updater', self::$options );
}
}
/**
* Set current branch on install and update options.
*
* @access public
*
* @param array $install Array of install data.
*/
public function set_branch_on_install( $install ) {
$this->set_repo_cache( 'current_branch', $install['github_updater_branch'], $install['repo'] );
self::$options[ 'current_branch_' . $install['repo'] ] = $install['github_updater_branch'];
update_site_option( 'github_updater', self::$options );
}
}

View File

@@ -0,0 +1,65 @@
<?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\GitHub_Updater\Traits\GHU_Trait;
/**
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class GHU_Upgrade
*/
class GHU_Upgrade {
use GHU_Trait;
/**
* DB version.
*
* @var int
*/
private $db_version = 8312;
/**
* Run update check against db_version.
*/
public function run() {
$options = $this->get_class_vars( 'Base', 'options' );
$db_version = isset( $options['db_version'] ) ? (int) $options['db_version'] : 6000;
if ( $db_version === $this->db_version ) {
return;
}
switch ( $db_version ) {
case $db_version < $this->db_version:
$this->delete_flush_cache();
break;
default:
break;
}
$options = array_merge( (array) $options, [ 'db_version' => (int) $this->db_version ] );
update_site_option( 'github_updater', $options );
}
/**
* Flush caches and delete cached options.
*/
private function delete_flush_cache() {
wp_cache_flush();
$this->delete_all_cached_data();
}
}

View File

@@ -0,0 +1,151 @@
<?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\GitHub_Updater\Traits\GHU_Trait;
use Fragen\GitHub_Updater\Traits\Basic_Auth_Loader;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Init
*/
class Init extends Base {
use GHU_Trait, Basic_Auth_Loader;
/**
* Constuctor.
*
* @return void
*/
public function __construct() {
parent::__construct();
$this->load_options();
}
/**
* Rename on activation.
*
* Correctly renames the slug when GitHub Updater is installed
* via FTP or from plugin upload.
*
* Set current branch to `develop` if appropriate.
*
* `rename()` causes activation to fail.
*
* @return void
*/
public function rename_on_activation() {
$plugin_dir = trailingslashit( WP_PLUGIN_DIR );
$slug = isset( $_GET['plugin'] ) ? $_GET['plugin'] : false;
$exploded = explode( '-', dirname( $slug ) );
if ( in_array( 'develop', $exploded, true ) ) {
$options = $this->get_class_vars( 'Base', 'options' );
update_site_option( 'github_updater', array_merge( $options, [ 'current_branch_github-updater' => 'develop' ] ) );
}
if ( $slug && 'github-updater/github-updater.php' !== $slug ) {
@rename( $plugin_dir . dirname( $slug ), $plugin_dir . 'github-updater' );
}
}
/**
* Let's get going.
*/
public function run() {
if ( ! static::is_heartbeat() ) {
$this->load_hooks();
}
if ( static::is_wp_cli() ) {
include_once __DIR__ . '/WP_CLI/CLI.php';
include_once __DIR__ . '/WP_CLI/CLI_Integration.php';
}
}
/**
* Load relevant action/filter hooks.
* Use 'init' hook for user capabilities.
*/
protected function load_hooks() {
add_action( 'init', [ $this, 'load' ] );
add_action( 'init', [ $this, 'background_update' ] );
add_action( 'init', [ $this, 'set_options_filter' ] );
add_action( 'wp_ajax_github-updater-update', [ $this, 'ajax_update' ] );
add_action( 'wp_ajax_nopriv_github-updater-update', [ $this, 'ajax_update' ] );
// Load hook for shiny updates Basic Authentication headers.
if ( self::is_doing_ajax() ) {
$this->load_authentication_hooks();
}
add_filter( 'extra_theme_headers', [ $this, 'add_headers' ] );
add_filter( 'extra_plugin_headers', [ $this, 'add_headers' ] );
add_filter( 'upgrader_source_selection', [ $this, 'upgrader_source_selection' ], 10, 4 );
// Needed for updating from update-core.php.
if ( ! self::is_doing_ajax() ) {
add_filter( 'upgrader_pre_download', [ $this, 'upgrader_pre_download' ], 10, 3 );
}
}
/**
* Checks current user capabilities and admin pages.
*
* @return bool
*/
public function can_update() {
global $pagenow;
// WP-CLI access has full capabilities.
if ( static::is_wp_cli() ) {
return true;
}
$can_user_update = current_user_can( 'update_plugins' ) && current_user_can( 'update_themes' );
$this->load_options();
$admin_pages = [
'plugins.php',
'plugin-install.php',
'themes.php',
'theme-install.php',
'update-core.php',
'update.php',
'options-general.php',
'options.php',
'settings.php',
'edit.php',
];
// Needed for sequential shiny updating.
if ( isset( $_POST['action'] ) && in_array( $_POST['action'], [ 'update-plugin', 'update-theme' ], true ) ) {
$admin_pages[] = 'admin-ajax.php';
}
/**
* Filter $admin_pages to be able to adjust the pages where GitHub Updater runs.
*
* @since 8.0.0
*
* @param array $admin_pages Default array of admin pages where GitHub Updater runs.
*/
$admin_pages = array_unique( apply_filters( 'github_updater_add_admin_pages', $admin_pages ) );
return $can_user_update && in_array( $pagenow, $admin_pages, true );
}
}

View File

@@ -0,0 +1,560 @@
<?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;
use Fragen\GitHub_Updater\Traits\Basic_Auth_Loader;
use Fragen\GitHub_Updater\WP_CLI\CLI_Plugin_Installer_Skin;
use Fragen\GitHub_Updater\WP_CLI\CLI_Theme_Installer_Skin;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Install
*
* Install <author>/<repo> directly from GitHub Updater.
*/
class Install {
use GHU_Trait, Basic_Auth_Loader;
/**
* Class options.
*
* @var array
*/
protected static $install = [];
/**
* Hold local copy of GitHub Updater options.
*
* @var mixed
*/
private static $options;
/**
* Hold local copy of installed APIs.
*
* @var mixed
*/
private static $installed_apis;
/**
* Hold local copy of git servers.
*
* @var mixed
*/
private static $git_servers;
/**
* Constructor.
*/
public function __construct() {
self::$options = $this->get_class_vars( 'Base', 'options' );
self::$installed_apis = $this->get_class_vars( 'Base', 'installed_apis' );
self::$git_servers = $this->get_class_vars( 'Base', 'git_servers' );
}
/**
* Let's set up the Install tabs.
* Need class-wp-upgrader.php for upgrade classes.
*
* @return void
*/
public function run() {
$this->load_js();
$this->add_settings_tabs();
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
/**
* Load javascript for Install.
*
* @return void
*/
public function load_js() {
add_action(
'admin_enqueue_scripts',
function () {
wp_register_script( 'ghu-install', plugins_url( basename( GITHUB_UPDATER_DIR ) . '/js/ghu-install-vanilla.js' ), [], false, true );
wp_enqueue_script( 'ghu-install' );
}
);
}
/**
* Adds Install tabs to Settings page.
*/
public function add_settings_tabs() {
$install_tabs = [];
if ( current_user_can( 'install_plugins' ) ) {
$install_tabs['github_updater_install_plugin'] = esc_html__( 'Install Plugin', 'github-updater' );
}
if ( current_user_can( 'install_themes' ) ) {
$install_tabs['github_updater_install_theme'] = esc_html__( 'Install Theme', 'github-updater' );
}
add_filter(
'github_updater_add_settings_tabs',
function ( $tabs ) use ( $install_tabs ) {
return array_merge( $tabs, $install_tabs );
}
);
add_action(
'github_updater_add_admin_page',
function ( $tab ) {
$this->add_admin_page( $tab );
}
);
}
/**
* Add Settings page data via action hook.
*
* @uses 'github_updater_add_admin_page' action hook
*
* @param string $tab Name of tab.
*/
public function add_admin_page( $tab ) {
if ( 'github_updater_install_plugin' === $tab ) {
$this->install( 'plugin' );
$this->create_form( 'plugin' );
}
if ( 'github_updater_install_theme' === $tab ) {
$this->install( 'theme' );
$this->create_form( 'theme' );
}
}
/**
* Install remote plugin or theme.
*
* @param string $type
* @param array $config
*
* @return bool
*/
public function install( $type, $config = null ) {
$this->set_install_post_data( $config );
if ( isset( $_POST['option_page'] ) && 'github_updater_install' === $_POST['option_page'] ) {
if ( empty( $_POST['github_updater_branch'] ) ) {
$_POST['github_updater_branch'] = 'master';
}
// Exit early if no repo entered.
if ( empty( $_POST['github_updater_repo'] ) ) {
echo '<h3>';
esc_html_e( 'A repository URI is required.', 'github-updater' );
echo '</h3>';
return false;
}
// Transform URI to owner/repo.
$headers = $this->parse_header_uri( $_POST['github_updater_repo'] );
$_POST['github_updater_repo'] = $headers['owner_repo'];
self::$install = $this->sanitize( $_POST );
self::$install['repo'] = self::$install['github_updater_install_repo'] = $headers['repo'];
/*
* Create GitHub endpoint.
* Save Access Token if present.
* Check for GitHub Self-Hosted.
*/
if ( 'github' === self::$install['github_updater_api'] ) {
self::$install = Singleton::get_instance( 'API\GitHub_API', $this, new \stdClass() )->remote_install( $headers, self::$install );
}
/*
* Create Bitbucket endpoint and instantiate class Bitbucket_API.
* Save private setting if present.
* Ensures `maybe_authenticate_http()` is available.
*/
if ( 'bitbucket' === self::$install['github_updater_api'] ) {
$this->load_authentication_hooks();
if ( self::$installed_apis['bitbucket_api'] ) {
self::$install = Singleton::get_instance( 'API\Bitbucket_API', $this, new \stdClass() )->remote_install( $headers, self::$install );
}
if ( self::$installed_apis['bitbucket_server_api'] ) {
self::$install = Singleton::get_instance( 'API\Bitbucket_Server_API', $this, new \stdClass() )->remote_install( $headers, self::$install );
}
}
/*
* Create GitLab endpoint.
* Save Access Token if present.
* Check for GitLab Self-Hosted.
*/
if ( 'gitlab' === self::$install['github_updater_api'] ) {
if ( self::$installed_apis['gitlab_api'] ) {
self::$install = Singleton::get_instance( 'API\GitLab_API', $this, new \stdClass() )->remote_install( $headers, self::$install );
}
}
/*
* Create Gitea endpoint.
* Save Access Token if present.
*/
if ( 'gitea' === self::$install['github_updater_api'] ) {
if ( self::$installed_apis['gitea_api'] ) {
self::$install = Singleton::get_instance( 'API\Gitea_API', $this, new \stdClass() )->remote_install( $headers, self::$install );
}
}
/*
* Install from Zipfile.
*/
if ( 'zipfile' === self::$install['github_updater_api'] ) {
self::$install = Singleton::get_instance( 'API\Zipfile_API', $this )->remote_install( $headers, self::$install );
}
if ( isset( self::$install['options'] ) ) {
$this->save_options_on_install( self::$install['options'] );
}
$url = self::$install['download_link'];
$upgrader = $this->get_upgrader( $type, $url );
// Install the repo from the $source urldecode() and save branch setting.
if ( $upgrader && $upgrader->install( $url ) ) {
Singleton::get_instance( 'Branch', $this )->set_branch_on_install( self::$install );
} else {
return false;
}
}
return true;
}
/**
* Save options set during installation.
*
* @param array $install_options Array of options from remote install process.
* @return void
*/
private function save_options_on_install( $install_options ) {
self::$options = array_merge( self::$options, $install_options );
update_site_option( 'github_updater', self::$options );
}
/**
* Set remote install data into $_POST.
*
* @param array $config Data for a remote install.
*/
private function set_install_post_data( $config ) {
if ( ! isset( $config['uri'] ) ) {
return;
}
$headers = $this->parse_header_uri( $config['uri'] );
$api = false !== strpos( $headers['host'], '.com' )
? rtrim( $headers['host'], '.com' )
: rtrim( $headers['host'], '.org' );
$api = isset( $config['git'] ) ? $config['git'] : $api;
$_POST['github_updater_repo'] = $config['uri'];
$_POST['github_updater_branch'] = $config['branch'];
$_POST['github_updater_api'] = $api;
$_POST['option_page'] = 'github_updater_install';
switch ( $api ) {
case 'github':
$_POST['github_access_token'] = $config['private'] ?: null;
break;
case 'bitbucket':
$_POST['is_private'] = $config['private'] ? '1' : null;
break;
case 'gitlab':
$_POST['gitlab_access_token'] = $config['private'] ?: null;
break;
case 'gitea':
$_POST['gitea_access_token'] = $config['private'] ?: null;
break;
case 'zipfile':
$_POST['zipfile_slug'] = $config['slug'];
break;
}
}
/**
* Get the appropriate upgrader for remote installation.
*
* @param string $type 'plugin' | 'theme'.
* @param string $url URL of the repository to be installed.
*
* @return bool|\Plugin_Upgrader|\Theme_Upgrader
*/
private function get_upgrader( $type, $url ) {
$nonce = wp_nonce_url( $url );
$upgrader = false;
if ( 'plugin' === $type ) {
$plugin = self::$install['repo'];
// Create a new instance of Plugin_Upgrader.
$skin = static::is_wp_cli()
? new CLI_Plugin_Installer_Skin()
: new \Plugin_Installer_Skin( compact( 'type', 'url', 'nonce', 'plugin' ) );
$upgrader = new \Plugin_Upgrader( $skin );
add_filter(
'install_plugin_complete_actions',
[
$this,
'install_plugin_complete_actions',
],
10,
3
);
}
if ( 'theme' === $type ) {
$theme = self::$install['repo'];
// Create a new instance of Theme_Upgrader.
$skin = static::is_wp_cli()
? new CLI_Theme_Installer_Skin()
: new \Theme_Installer_Skin( compact( 'type', 'url', 'nonce', 'theme' ) );
$upgrader = new \Theme_Upgrader( $skin );
add_filter(
'install_theme_complete_actions',
[
$this,
'install_theme_complete_actions',
],
10,
3
);
}
return $upgrader;
}
/**
* Create Install Plugin or Install Theme page.
*
* @param string $type
*/
public function create_form( $type ) {
// Bail if installing.
if ( isset( $_POST['option_page'] ) && 'github_updater_install' === $_POST['option_page'] ) {
return;
}
$this->register_settings( $type ); ?>
<form method="post">
<?php
settings_fields( 'github_updater_install' );
do_settings_sections( 'github_updater_install_' . $type );
if ( 'plugin' === $type ) {
submit_button( esc_html__( 'Install Plugin', 'github-updater' ) );
}
if ( 'theme' === $type ) {
submit_button( esc_html__( 'Install Theme', 'github-updater' ) );
}
?>
</form>
<?php
}
/**
* Add settings sections.
*
* @param string $type
*/
public function register_settings( $type ) {
$repo_type = null;
// Place translatable strings into variables.
if ( 'plugin' === $type ) {
$repo_type = esc_html__( 'Plugin', 'github-updater' );
}
if ( 'theme' === $type ) {
$repo_type = esc_html__( 'Theme', 'github-updater' );
}
register_setting(
'github_updater_install',
'github_updater_install_' . $type,
[ $this, 'sanitize' ]
);
add_settings_section(
$type,
/* translators: variable is 'Plugin' or 'Theme' */
sprintf( esc_html__( 'GitHub Updater Install %s', 'github-updater' ), $repo_type ),
[],
'github_updater_install_' . $type
);
add_settings_field(
$type . '_repo',
/* translators: variable is 'Plugin' or 'Theme' */
sprintf( esc_html__( '%s URI', 'github-updater' ), $repo_type ),
[ $this, 'get_repo' ],
'github_updater_install_' . $type,
$type
);
add_settings_field(
$type . '_branch',
esc_html__( 'Repository Branch', 'github-updater' ),
[ $this, 'branch' ],
'github_updater_install_' . $type,
$type
);
add_settings_field(
$type . '_api',
esc_html__( 'Remote Repository Host', 'github-updater' ),
[ $this, 'install_api' ],
'github_updater_install_' . $type,
$type
);
/**
* Action hook to add git API install settings fields.
*
* @since 8.0.0
*
* @param string $type 'plugin'|'theme'.
*/
do_action( 'github_updater_add_install_settings_fields', $type );
// Load install settings fields for existing APIs that are not loaded.
$running_servers = $this->get_running_git_servers();
$git_servers = $this->get_class_vars( 'Base', 'git_servers' );
$servers_not_running = array_diff( array_flip( $git_servers ), $running_servers );
if ( ! empty( $servers_not_running ) ) {
foreach ( array_keys( $servers_not_running ) as $server ) {
$class = 'API\\' . $server . '_API';
Singleton::get_instance( $class, $this )->add_install_settings_fields( $type );
}
}
}
/**
* Repo setting.
*/
public function get_repo() {
?>
<label for="github_updater_repo">
<input type="text" style="width:50%;" id="github_updater_repo" name="github_updater_repo" value="" autofocus>
<br>
<span class="description">
<?php esc_html_e( 'URI is case sensitive.', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* Branch setting.
*/
public function branch() {
?>
<label for="github_updater_branch">
<input type="text" style="width:50%;" id="github_updater_branch" name="github_updater_branch" value="" placeholder="master">
<br>
<span class="description">
<?php esc_html_e( 'Enter branch name or leave empty for `master`', 'github-updater' ); ?>
</span>
</label>
<?php
}
/**
* API setting.
*/
public function install_api() {
?>
<label for="github_updater_api">
<select id="github_updater_api" name="github_updater_api">
<?php foreach ( self::$git_servers as $key => $value ) : ?>
<?php if ( self::$installed_apis[ $key . '_api' ] ) : ?>
<option value="<?php esc_attr_e( $key ); ?>" <?php selected( $key ); ?> >
<?php esc_html_e( $value ); ?>
</option>
<?php endif ?>
<?php endforeach ?>
</select>
</label>
<?php
}
/**
* Remove activation links after plugin installation as no method to get $plugin_file.
*
* @param array $install_actions
* @param mixed $api
* @param string $plugin_file
*
* @return mixed
*/
public function install_plugin_complete_actions( $install_actions, $api, $plugin_file ) {
unset( $install_actions['activate_plugin'], $install_actions['network_activate'] );
return $install_actions;
}
/**
* Fix activation links after theme installation, no method to get proper theme name.
*
* @param array $install_actions
* @param mixed $api
* @param mixed $theme_info
*
* @return mixed
*/
public function install_theme_complete_actions( $install_actions, $api, $theme_info ) {
if ( isset( $install_actions['preview'] ) ) {
unset( $install_actions['preview'] );
}
$stylesheet = self::$install['repo'];
$activate_link = add_query_arg(
[
'action' => 'activate',
// 'template' => rawurlencode( $template ),
'stylesheet' => rawurlencode( $stylesheet ),
],
admin_url( 'themes.php' )
);
$activate_link = esc_url( wp_nonce_url( $activate_link, 'switch-theme_' . $stylesheet ) );
$install_actions['activate'] = '<a href="' . $activate_link . '" class="activatelink"><span aria-hidden="true">' . esc_attr__( 'Activate', 'github-updater' ) . '</span><span class="screen-reader-text">' . esc_attr__( 'Activate', 'github-updater' ) . ' &#8220;' . $stylesheet . '&#8221;</span></a>';
if ( is_network_admin() && current_user_can( 'manage_network_themes' ) ) {
$network_activate_link = add_query_arg(
[
'action' => 'enable',
'theme' => rawurlencode( $stylesheet ),
],
network_admin_url( 'themes.php' )
);
$network_activate_link = esc_url( wp_nonce_url( $network_activate_link, 'enable-theme_' . $stylesheet ) );
$install_actions['network_enable'] = '<a href="' . $network_activate_link . '" target="_parent">' . esc_attr_x( 'Network Enable', 'This refers to a network activation in a multisite installation', 'github-updater' ) . '</a>';
unset( $install_actions['activate'] );
}
ksort( $install_actions );
return $install_actions;
}
}

View File

@@ -0,0 +1,124 @@
<?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;
use Fragen\GitHub_Updater\API\Language_Pack_API;
/**
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Language_Pack
*/
class Language_Pack {
use GHU_Trait;
/**
* Variable containing the plugin/theme object.
*
* @var Plugin|Theme
*/
protected $repo;
/**
* Variable containing the Language_Pack_API.
*
* @var Language_Pack_API
*/
private $repo_api;
/**
* Language_Pack constructor.
*
* @param Plugin|Theme $repo Plugin/Theme object.
* @param Language_Pack_API $api Language_Pack_API object.
*/
public function __construct( $repo, Language_Pack_API $api ) {
if ( null === $repo->languages ) {
return;
}
$this->repo = $repo;
$this->repo_api = $api;
}
/**
* Do the Language Pack integration.
*/
public function run() {
if ( null === $this->repo ) {
return false;
}
$headers = $this->parse_header_uri( $this->repo->languages );
$this->repo_api->get_language_pack( $headers );
add_filter( 'site_transient_update_plugins', [ $this, 'update_site_transient' ] );
add_filter( 'site_transient_update_themes', [ $this, 'update_site_transient' ] );
}
/**
* Add language translations to update_plugins or update_themes transients.
*
* @param mixed $transient Update transient.
*
* @return mixed
*/
public function update_site_transient( $transient ) {
$locales = get_available_languages();
$locales = ! empty( $locales ) ? $locales : [ get_locale() ];
$repos = [];
if ( ! isset( $transient->translations ) ) {
return $transient;
}
if ( 'site_transient_update_plugins' === current_filter() ) {
$repos = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$translations = wp_get_installed_translations( 'plugins' );
}
if ( 'site_transient_update_themes' === current_filter() ) {
$repos = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$translations = wp_get_installed_translations( 'themes' );
}
$repos = array_filter(
$repos,
function ( $e ) {
return isset( $e->language_packs );
}
);
foreach ( $repos as $repo ) {
foreach ( $locales as $locale ) {
$lang_pack_mod = isset( $repo->language_packs->$locale )
? strtotime( $repo->language_packs->$locale->updated )
: 0;
$translation_mod = isset( $translations[ $repo->slug ][ $locale ] )
? strtotime( $translations[ $repo->slug ][ $locale ]['PO-Revision-Date'] )
: 0;
if ( $lang_pack_mod > $translation_mod ) {
$transient->translations[] = (array) $repo->language_packs->$locale;
}
}
}
$transient->translations = array_unique( $transient->translations, SORT_REGULAR );
return $transient;
}
}

View File

@@ -0,0 +1,195 @@
<?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\GitHub_Updater\Traits\GHU_Trait;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Messages
*/
class Messages {
use GHU_Trait;
/**
* Holds WP_Error message.
*
* @var string
*/
public static $error_message = '';
/**
* Display message when API returns other than 200 or 404.
*
* @param string $type
*
* @return bool
*/
public function create_error_message( $type = '' ) {
global $pagenow;
$update_pages = [ 'update-core.php', 'plugins.php', 'themes.php' ];
$settings_pages = [ 'settings.php', 'options-general.php' ];
if (
( ( ! isset( $_GET['page'] ) || 'github-updater' !== $_GET['page'] ) &&
in_array( $pagenow, $settings_pages, true ) ) ||
! in_array( $pagenow, array_merge( $update_pages, $settings_pages ), true )
) {
return false;
}
if ( is_admin() && ! static::is_doing_ajax() ) {
switch ( $type ) {
case is_wp_error( $type ):
self::$error_message = $type->get_error_message();
add_action(
is_multisite() ? 'network_admin_notices' : 'admin_notices',
[
$this,
'show_wp_error',
]
);
break;
case 'waiting':
if ( ! apply_filters( 'github_updater_disable_wpcron', false ) ) {
add_action( is_multisite() ? 'network_admin_notices' : 'admin_notices', [ $this, 'waiting' ] );
}
// no break.
case 'git':
default:
add_action(
is_multisite() ? 'network_admin_notices' : 'admin_notices',
[
$this,
'show_403_error_message',
]
);
add_action(
is_multisite() ? 'network_admin_notices' : 'admin_notices',
[
$this,
'show_401_error_message',
]
);
}
}
return true;
}
/**
* Create error message for 403 error.
* Usually 403 as API rate limit max out.
*/
public function show_403_error_message() {
$_403 = false;
$error_code = $this->get_error_codes();
foreach ( (array) $error_code as $repo ) {
if ( ( ! $_403 && isset( $repo['code'], $repo['git'] ) )
&& 403 === $repo['code'] && 'github' === $repo['git'] ) {
$_403 = true;
if ( ! \PAnD::is_admin_notice_active( '403-error-1' ) ) {
return;
} ?>
<div data-dismissible="403-error-1" class="notice-error notice is-dismissible">
<p>
<?php
esc_html_e( 'GitHub Updater Error Code:', 'github-updater' );
echo ' ' . $repo['code'];
?>
<br>
<?php
printf(
/* translators: %s: wait time */
esc_html__( 'GitHub API&#8217;s rate limit will reset in %s minutes.', 'github-updater' ),
$repo['wait']
);
echo '<br>';
printf(
/* translators: %s: GitHub personal access token URL */
wp_kses_post( __( 'It looks like you are running into GitHub API rate limits. Be sure and configure a <a href="%s">Personal Access Token</a> to avoid this issue.', 'github-updater' ) ),
esc_url( 'https://help.github.com/articles/creating-an-access-token-for-command-line-use/' )
);
?>
</p>
</div>
<?php
}
}
}
/**
* Create error message or 401 (Authentication Error) error.
* Usually 401 as private repo with no token set or incorrect user/pass.
*/
public function show_401_error_message() {
$_401 = false;
$error_code = $this->get_error_codes();
foreach ( (array) $error_code as $repo ) {
if ( ( ! $_401 && isset( $repo['code'] ) ) && 401 === $repo['code'] ) {
$_401 = true;
if ( ! \PAnD::is_admin_notice_active( '401-error-1' ) ) {
return;
}
?>
<div data-dismissible="401-error-1" class="notice-error notice is-dismissible">
<p>
<?php
esc_html_e( 'GitHub Updater Error Code:', 'github-updater' );
echo ' ' . $repo['code'];
?>
<br>
<?php esc_html_e( 'There is probably an access token or password error on the GitHub Updater Settings page.', 'github-updater' ); ?>
</p>
</div>
<?php
}
}
}
/**
* Generate error message for WP_Error.
*/
public function show_wp_error() {
?>
<div class="notice-error notice">
<p>
<?php
esc_html_e( 'GitHub Updater Error Code:', 'github-updater' );
echo ' ' . self::$error_message;
?>
</p>
</div>
<?php
}
/**
* Generate information message when waiting for WP-Cron to finish.
*/
public function waiting() {
?>
<div class="notice-info notice is-dismissible">
<p>
<?php esc_html_e( 'GitHub Updater Information', 'github-updater' ); ?>
<br>
<?php esc_html_e( 'Please be patient while WP-Cron finishes making API calls.', 'github-updater' ); ?>
</p>
</div>
<?php
}
}

View File

@@ -0,0 +1,443 @@
<?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;
}
}

View File

@@ -0,0 +1,198 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
* @uses https://meta.trac.wordpress.org/browser/sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/readme
*/
namespace Fragen\GitHub_Updater;
use WordPressdotorg\Plugin_Directory\Readme\Parser;
use Parsedown;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Readme_Parser
*/
class Readme_Parser extends Parser {
/**
* Holds absolute filepath to temp readme file.
*
* @var string
*/
protected $readme_path;
/**
* Constructor.
*
* Convert file contents string to temporary file.
* Pass file path into class-parser.php.
* Delete temporary file when finished.
*
* @param string $file
*
* @return void
*/
public function __construct( $file ) {
$file_path = trailingslashit( get_temp_dir() ) . md5( $file ) . '-tmp-readme.txt';
/**
* Filter location of temporary readme filepath.
*
* @since 8.7.0
*
* @param string $file_path Absolute filepath to temp readme file.
*/
$this->readme_path = apply_filters( 'github_updater_temp_readme_filepath', $file_path );
$this->readme_path = file_put_contents( $this->readme_path, $file ) ? $this->readme_path : false;
parent::__construct( $this->readme_path );
}
/**
* Parse text into markdown.
*
* @param string $text
*
* @return string
*/
protected function parse_markdown( $text ) {
static $markdown = null;
if ( null === $markdown ) {
$markdown = new Parsedown();
}
return $markdown->text( $text );
}
/**
* Return parsed readme.txt as array.
*
* @return array $data
*/
public function parse_data() {
$data = [];
foreach ( get_object_vars( $this ) as $key => $value ) {
$data[ $key ] = 'contributors' === $key ? $this->create_contributors( $value ) : $value;
}
$data = $this->faq_as_h4( $data );
$data = $this->readme_section_as_h4( 'changelog', $data );
$data = $this->readme_section_as_h4( 'description', $data );
@unlink( $this->readme_path );
return $data;
}
/**
* Sanitize contributors.
*
* @param array $users
*
* @return array
*/
protected function sanitize_contributors( $users ) {
return $users;
}
/**
* Create contributor data.
*
* @param array $users
*
* @return array $contributors
*/
private function create_contributors( $users ) {
global $wp_version;
$contributors = [];
foreach ( (array) $users as $contributor ) {
$contributors[ $contributor ]['display_name'] = $contributor;
$contributors[ $contributor ]['profile'] = '//profiles.wordpress.org/' . $contributor;
$contributors[ $contributor ]['avatar'] = 'https://wordpress.org/grav-redirect.php?user=' . $contributor;
if ( version_compare( $wp_version, '5.1-alpha', '<' ) ) {
$contributors[ $contributor ] = '//profiles.wordpress.org/' . $contributor;
}
}
return $contributors;
}
/**
* Converts FAQ from dictionary list to h4 style.
*
* @param array $data Array of parsed readme data.
*
* @return array $data
*/
public function faq_as_h4( $data ) {
if ( empty( $data['faq'] ) ) {
return $data;
}
unset( $data['sections']['faq'] );
$data['sections']['faq'] = '';
foreach ( $data['faq'] as $question => $answer ) {
$data['sections']['faq'] .= "<h4>{$question}</h4>\n{$answer}\n";
}
return $data;
}
/**
* Converts wp.org readme section items to h4 style.
*
* @param string $section Readme section.
* @param array $data Array of parsed readme data.
*
* @return array $data
*/
public function readme_section_as_h4( $section, $data ) {
if ( empty( $data['sections'][ $section ] ) || false !== strpos( $data['sections'][ $section ], '<h4>' ) ) {
return $data;
}
$pattern = '~<p>=(.*)=</p>~';
$replace = '<h4>$1</h4>';
$data['sections'][ $section ] = preg_replace( $pattern, $replace, $data['sections'][ $section ] );
return $data;
}
/**
* Replace parent method as some users don't have `mb_strrpos()`.
*
* @access protected
*
* @param string $desc
* @param int $length
*
* @return string
*/
protected function trim_length( $desc, $length = 150 ) {
if ( mb_strlen( $desc ) > $length ) {
$desc = mb_substr( $desc, 0, $length ) . ' &hellip;';
// If not a full sentence, and one ends within 20% of the end, trim it to that.
if ( function_exists( 'mb_strrpos' ) ) {
$pos = mb_strrpos( $desc, '.' );
} else {
$pos = strrpos( $desc, '.' );
}
if ( $pos > ( 0.8 * $length ) && '.' !== mb_substr( $desc, -1 ) ) {
$desc = mb_substr( $desc, 0, $pos + 1 );
}
}
return trim( $desc );
}
}

View File

@@ -0,0 +1,315 @@
<?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\GitHub_Updater\Traits\GHU_Trait;
use Fragen\Singleton;
/**
* Class Remote_Management
*/
class Remote_Management {
use GHU_Trait;
/**
* Holds the values for remote management settings.
*
* @var array $option_remote
*/
public static $options_remote;
/**
* Supported remote management services.
*
* @var array $remote_management
*/
public static $remote_management = [
'ithemes_sync' => 'iThemes Sync',
'infinitewp' => 'InfiniteWP',
'managewp' => 'ManageWP',
'mainwp' => 'MainWP',
];
/**
* Holds the value for the Remote Management API key.
*
* @var string $api_key
*/
private static $api_key;
/**
* Remote_Management constructor.
*/
public function __construct() {
$this->load_options();
$this->ensure_api_key_is_set();
}
/**
* Load site options.
*/
private function load_options() {
self::$options_remote = get_site_option( 'github_updater_remote_management', [] );
self::$api_key = get_site_option( 'github_updater_api_key' );
}
/**
* Ensure api key is set.
*/
public function ensure_api_key_is_set() {
if ( ! self::$api_key ) {
update_site_option( 'github_updater_api_key', md5( uniqid( \rand(), true ) ) );
}
}
/**
* Load needed action/filter hooks.
*/
public function load_hooks() {
add_action( 'admin_init', [ $this, 'remote_management_page_init' ] );
add_action(
'github_updater_update_settings',
function ( $post_data ) {
$this->save_settings( $post_data );
}
);
add_filter( 'github_updater_add_admin_pages', [ $this, 'extra_admin_pages' ] );
$this->add_settings_tabs();
}
/**
* Return list of pages where GitHub Updater loads/runs.
*
* @param array $admin_pages Default list of pages where GitHub Updater loads.
*
* @return array $admin_pages
*/
public function extra_admin_pages( $admin_pages = [] ) {
$extra_admin_pages = [];
foreach ( array_keys( self::$remote_management ) as $key ) {
if ( ! empty( self::$options_remote[ $key ] ) ) {
$extra_admin_pages = [ 'index.php' ];
break;
}
}
return array_merge( $admin_pages, $extra_admin_pages );
}
/**
* Save Remote Management settings.
*
* @uses 'github_updater_update_settings' action hook
* @uses 'github_updater_save_redirect' filter hook
*
* @param array $post_data $_POST data.
*/
public function save_settings( $post_data ) {
if ( isset( $post_data['option_page'] ) &&
'github_updater_remote_management' === $post_data['option_page']
) {
$options = isset( $post_data['github_updater_remote_management'] )
? $post_data['github_updater_remote_management']
: [];
update_site_option( 'github_updater_remote_management', (array) $this->sanitize( $options ) );
add_filter(
'github_updater_save_redirect',
function ( $option_page ) {
return array_merge( $option_page, [ 'github_updater_remote_management' ] );
}
);
}
}
/**
* Adds Remote Management tab to Settings page.
*/
public function add_settings_tabs() {
$install_tabs = [ 'github_updater_remote_management' => esc_html__( 'Remote Management', 'github-updater' ) ];
add_filter(
'github_updater_add_settings_tabs',
function ( $tabs ) use ( $install_tabs ) {
return array_merge( $tabs, $install_tabs );
}
);
add_filter(
'github_updater_add_admin_page',
function ( $tab, $action ) {
$this->add_admin_page( $tab, $action );
},
10,
2
);
}
/**
* Add Settings page data via action hook.
*
* @uses 'github_updater_add_admin_page' action hook
*
* @param string $tab Tab name.
* @param string $action Form action.
*/
public function add_admin_page( $tab, $action ) {
if ( 'github_updater_remote_management' === $tab ) {
$action = add_query_arg( 'tab', $tab, $action ); ?>
<form class="settings" method="post" action="<?php esc_attr_e( $action ); ?>">
<?php
settings_fields( 'github_updater_remote_management' );
do_settings_sections( 'github_updater_remote_settings' );
submit_button();
?>
</form>
<?php
$reset_api_action = add_query_arg( [ 'github_updater_reset_api_key' => true ], $action );
?>
<form class="settings no-sub-tabs" method="post" action="<?php esc_attr_e( $reset_api_action ); ?>">
<?php submit_button( esc_html__( 'Reset RESTful key', 'github-updater' ) ); ?>
</form>
<?php
}
}
/**
* Settings for Remote Management.
*/
public function remote_management_page_init() {
register_setting(
'github_updater_remote_management',
'github_updater_remote_settings',
[ $this, 'sanitize' ]
);
add_settings_section(
'remote_management',
esc_html__( 'Remote Management', 'github-updater' ),
[ $this, 'print_section_remote_management' ],
'github_updater_remote_settings'
);
foreach ( self::$remote_management as $id => $name ) {
add_settings_field(
$id,
null,
[ $this, 'token_callback_checkbox_remote' ],
'github_updater_remote_settings',
'remote_management',
[
'id' => $id,
'title' => esc_html( $name ),
]
);
}
}
/**
* Print the Remote Management text.
*/
public function print_section_remote_management() {
if ( empty( self::$api_key ) ) {
$this->load_options();
}
$api_url = add_query_arg(
[
'action' => 'github-updater-update',
'key' => self::$api_key,
],
admin_url( 'admin-ajax.php' )
);
?>
<p>
<?php
printf(
wp_kses_post(
/* translators: %s: Link to wiki */
__( 'Please refer to the <a href="%s">wiki</a> for complete list of attributes. RESTful endpoints begin at:', 'github-updater' )
),
'https://github.com/afragen/github-updater/wiki/Remote-Management---RESTful-Endpoints'
);
?>
<br>
<span style="font-family:monospace;"><?php echo $api_url; ?></span>
<p>
<?php esc_html_e( 'Use of Remote Management services may result increase some page load speeds only for `admin` level users in the dashboard.', 'github-updater' ); ?>
</p>
<?php
}
/**
* Get the settings option array and print one of its values.
* For remote management settings.
*
* @param array $args Checkbox args.
*
* @return bool|void
*/
public function token_callback_checkbox_remote( $args ) {
$checked = isset( self::$options_remote[ $args['id'] ] ) ? self::$options_remote[ $args['id'] ] : null;
?>
<label for="<?php esc_attr_e( $args['id'] ); ?>">
<input type="checkbox" id="<?php esc_attr_e( $args['id'] ); ?>" name="github_updater_remote_management[<?php esc_attr_e( $args['id'] ); ?>]" value="1" <?php checked( '1', $checked ); ?> >
<?php echo $args['title']; ?>
</label>
<?php
}
/**
* Reset RESTful API key.
* Deleting site option will cause it to be re-created.
*
* @return bool
*/
public function reset_api_key() {
if ( isset( $_REQUEST['tab'], $_REQUEST['github_updater_reset_api_key'] ) &&
'github_updater_remote_management' === $_REQUEST['tab']
) {
$_POST = $_REQUEST;
$_POST['_wp_http_referer'] = $_SERVER['HTTP_REFERER'];
delete_site_option( 'github_updater_api_key' );
return true;
}
return false;
}
/**
* Set site transients for 'update_plugins' and 'update_themes' for remote management.
*
* Only call if any remote management options are present and only if on a page specified
* to run remote management.
*
* @return void
*/
public function set_update_transients() {
if ( empty( self::$options_remote ) ) {
return;
}
$remote_management_pages = $this->extra_admin_pages();
if ( $this->is_current_page( $remote_management_pages ) ) {
add_filter( 'github_updater_add_admin_pages', [ $this, 'extra_admin_pages' ] );
add_filter( 'site_transient_update_plugins', [ Singleton::get_instance( 'Plugin', $this ), 'update_site_transient' ], 10, 1 );
add_filter( 'site_transient_update_themes', [ Singleton::get_instance( 'Theme', $this ), 'update_site_transient' ], 10, 1 );
Singleton::get_instance( 'Base', $this )->get_meta_remote_management();
$current_plugins = get_site_transient( 'update_plugins' );
$current_themes = get_site_transient( 'update_themes' );
set_site_transient( 'update_plugins', $current_plugins );
set_site_transient( 'update_themes', $current_themes );
remove_filter( 'github_updater_add_admin_pages', [ $this, 'extra_admin_pages' ] );
}
}
}

View File

@@ -0,0 +1,314 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen, Mikael Lindqvist
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater;
use Fragen\Singleton;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
/**
* Class Rest_Update
*
* Updates a single plugin or theme, in a way suitable for rest requests.
* This class inherits from Base in order to be able to call the
* set_defaults function.
*/
class Rest_Update extends Base {
/**
* Holds REST Upgrader Skin.
*
* @var Rest_Upgrader_Skin $upgrader_skin
*/
protected $upgrader_skin;
/**
* Constructor.
*/
public function __construct() {
parent::__construct();
$this->load_options();
$this->upgrader_skin = new Rest_Upgrader_Skin();
}
/**
* Update plugin.
*
* @param string $plugin_slug
* @param string $tag
*
* @throws \UnexpectedValueException Plugin not found or not updatable.
*/
public function update_plugin( $plugin_slug, $tag = 'master' ) {
$plugin = null;
$is_plugin_active = false;
foreach ( (array) Singleton::get_instance( 'Plugin', $this )->get_plugin_configs() as $config_entry ) {
if ( $config_entry->slug === $plugin_slug ) {
$plugin = $config_entry;
break;
}
}
if ( ! $plugin ) {
throw new \UnexpectedValueException( 'Plugin not found or not updatable with GitHub Updater: ' . $plugin_slug );
}
if ( is_plugin_active( $plugin->file ) ) {
$is_plugin_active = true;
}
$this->get_remote_repo_meta( $plugin );
$repo_api = Singleton::get_instance( 'API', $this )->get_repo_api( $plugin->git, $plugin );
$update = [
'slug' => $plugin->slug,
'plugin' => $plugin->file,
'new_version' => null,
'url' => $plugin->uri,
'package' => $repo_api->construct_download_link( $tag ),
];
add_filter(
'site_transient_update_plugins',
function ( $current ) use ( $plugin, $update ) {
$current->response[ $plugin->file ] = (object) $update;
return $current;
}
);
$upgrader = new \Plugin_Upgrader( $this->upgrader_skin );
$upgrader->upgrade( $plugin->file );
if ( $is_plugin_active ) {
$activate = is_multisite() ? activate_plugin( $plugin->file, null, true ) : activate_plugin( $plugin->file );
if ( ! $activate ) {
$this->upgrader_skin->messages[] = 'Plugin reactivated successfully.';
}
}
}
/**
* Update a single theme.
*
* @param string $theme_slug
* @param string $tag
*
* @throws \UnexpectedValueException Theme not found or not updatable.
*/
public function update_theme( $theme_slug, $tag = 'master' ) {
$theme = null;
foreach ( (array) Singleton::get_instance( 'Theme', $this )->get_theme_configs() as $config_entry ) {
if ( $config_entry->slug === $theme_slug ) {
$theme = $config_entry;
break;
}
}
if ( ! $theme ) {
throw new \UnexpectedValueException( 'Theme not found or not updatable with GitHub Updater: ' . $theme_slug );
}
$this->get_remote_repo_meta( $theme );
$repo_api = Singleton::get_instance( 'API', $this )->get_repo_api( $theme->git, $theme );
$update = [
'theme' => $theme->slug,
'new_version' => null,
'url' => $theme->uri,
'package' => $repo_api->construct_download_link( $tag ),
];
add_filter(
'site_transient_update_themes',
function ( $current ) use ( $theme, $update ) {
$current->response[ $theme->slug ] = $update;
return $current;
}
);
$upgrader = new \Theme_Upgrader( $this->upgrader_skin );
$upgrader->upgrade( $theme->slug );
}
/**
* Is there an error?
*/
public function is_error() {
return $this->upgrader_skin->error;
}
/**
* Get messages during update.
*/
public function get_messages() {
return $this->upgrader_skin->messages;
}
/**
* Process request.
*
* Relies on data in $_REQUEST, prints out json and exits.
* If the request came through a webhook, and if the branch in the
* webhook matches the branch specified by the url, use the latest
* update available as specified in the webhook payload.
*
* @throws \UnexpectedValueException Under multiple bad or missing params.
*/
public function process_request() {
$start = microtime( true );
try {
if ( ! isset( $_REQUEST['key'] ) ||
get_site_option( 'github_updater_api_key' ) !== $_REQUEST['key']
) {
throw new \UnexpectedValueException( 'Bad API key.' );
}
/**
* Allow access into the REST Update process.
*
* @since 7.6.0
* @access public
*/
do_action( 'github_updater_pre_rest_process_request' );
$tag = 'master';
if ( isset( $_REQUEST['tag'] ) ) {
$tag = $_REQUEST['tag'];
} elseif ( isset( $_REQUEST['committish'] ) ) {
$tag = $_REQUEST['committish'];
}
$this->get_webhook_source();
$current_branch = $this->get_local_branch();
$override = isset( $_REQUEST['override'] );
if ( $tag !== $current_branch && ! $override ) {
throw new \UnexpectedValueException( 'Webhook tag and current branch are not matching. Consider using `override` query arg.' );
}
if ( isset( $_REQUEST['plugin'] ) ) {
$this->update_plugin( $_REQUEST['plugin'], $tag );
} elseif ( isset( $_REQUEST['theme'] ) ) {
$this->update_theme( $_REQUEST['theme'], $tag );
} else {
throw new \UnexpectedValueException( 'No plugin or theme specified for update.' );
}
} catch ( \Exception $e ) {
$http_response = [
'success' => false,
'messages' => $e->getMessage(),
'webhook' => $_GET,
'elapsed_time' => round( ( microtime( true ) - $start ) * 1000, 2 ) . ' ms',
];
$this->log_exit( $http_response, 417 );
}
$response = [
'success' => true,
'messages' => $this->get_messages(),
'webhook' => $_GET,
'elapsed_time' => round( ( microtime( true ) - $start ) * 1000, 2 ) . ' ms',
];
if ( $this->is_error() ) {
$response['success'] = false;
$this->log_exit( $response, 417 );
}
$this->log_exit( $response, 200 );
}
/**
* Returns the current branch of the local repository referenced in the webhook.
*
* @return string $current_branch Default return is 'master'.
*/
private function get_local_branch() {
$repo = false;
if ( isset( $_REQUEST['plugin'] ) ) {
$repos = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$repo = isset( $repos[ $_REQUEST['plugin'] ] ) ? $repos[ $_REQUEST['plugin'] ] : false;
}
if ( isset( $_REQUEST['theme'] ) ) {
$repos = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$repo = isset( $repos[ $_REQUEST['theme'] ] ) ? $repos[ $_REQUEST['theme'] ] : false;
}
$current_branch = $repo ?
Singleton::get_instance( 'Branch', $this )->get_current_branch( $repo ) :
'master';
return $current_branch;
}
/**
* Sets the source of the webhook to $_GET variable.
*/
private function get_webhook_source() {
switch ( $_SERVER ) {
case isset( $_SERVER['HTTP_X_GITHUB_EVENT'] ):
$webhook_source = 'GitHub webhook';
break;
case isset( $_SERVER['HTTP_X_EVENT_KEY'] ):
$webhook_source = 'Bitbucket webhook';
break;
case isset( $_SERVER['HTTP_X_GITLAB_EVENT'] ):
$webhook_source = 'GitLab webhook';
break;
case isset( $_SERVER['HTTP_X_GITEA_EVENT'] ):
$webhook_source = 'Gitea webhook';
break;
default:
$webhook_source = 'browser';
break;
}
$_GET['webhook_source'] = $webhook_source;
}
/**
* Append $response to debug.log and wp_die().
*
* @param array $response
* @param int $code
*
* 128 == JSON_PRETTY_PRINT
* 64 == JSON_UNESCAPED_SLASHES
*/
private function log_exit( $response, $code ) {
$json_encode_flags = 128 | 64;
error_log( json_encode( $response, $json_encode_flags ) );
/**
* Action hook after processing REST process.
*
* @since 8.6.0
*
* @param array $response
* @param int $code HTTP response.
*/
do_action( 'github_updater_post_rest_process_request', $response, $code );
unset( $response['success'] );
if ( 200 === $code ) {
wp_die( wp_send_json_success( $response, $code ) );
} else {
wp_die( wp_send_json_error( $response, $code ) );
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen, Mikael Lindqvist
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
/**
* Class Rest_Upgrader_Skin
*
* Extends WP_Upgrader_Skin and collects outputed messages for later
* processing, rather than printing them out.
*/
class Rest_Upgrader_Skin extends \WP_Upgrader_Skin {
/**
* Holds messages.
*
* @var array $messages
*/
public $messages = [];
/**
* Boolean if errors are present.
*
* @var bool $error
*/
public $error;
/**
* Overrides the feedback method.
* Adds the feedback string to the messages array.
*
* @param string $string
*/
public function feedback( $string ) {
if ( isset( $this->upgrader->strings[ $string ] ) ) {
$string = $this->upgrader->strings[ $string ];
}
if ( false !== strpos( $string, '%' ) ) {
$args = func_get_args();
$args = array_splice( $args, 1 );
if ( $args ) {
$args = array_map( 'strip_tags', $args );
$args = array_map( 'esc_html', $args );
$string = vsprintf( $string, $args );
}
}
if ( empty( $string ) ) {
return;
}
$this->messages[] = $string;
}
/**
* Set the error flag to true, then let the base class handle the rest.
*
* @param mixed $errors
*/
public function error( $errors ) {
$this->error = true;
parent::error( $errors );
}
/**
* Do nothing.
*
* @param mixed $type
*/
protected function decrement_update_count( $type ) {
}
/**
* Do nothing.
*/
public function header() {
}
/**
* Do nothing.
*/
public function footer() {
}
}

View File

@@ -0,0 +1,804 @@
<?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 Settings
*
* Add a settings page.
*
* @author Andy Fragen
*/
class Settings extends Base {
use GHU_Trait;
/**
* Holds the plugin basename.
*
* @var string
*/
private $ghu_plugin_name = 'github-updater/github-updater.php';
/**
* Holds boolean on whether or not the repo requires authentication.
*
* @var array
*/
public static $auth_required = [
'github_private' => false,
'github_enterprise' => false,
'bitbucket_private' => false,
'bitbucket_server' => false,
'gitlab_private' => false,
'gitlab_enterprise' => false,
'gitea_private' => false,
];
/**
* Constructor.
*/
public function __construct() {
parent::__construct();
$this->refresh_caches();
$this->load_options();
}
/**
* Check for cache refresh.
*/
protected function refresh_caches() {
if ( isset( $_POST['ghu_refresh_cache'] ) && ! ( $this instanceof Messages ) ) {
$this->delete_all_cached_data();
}
}
/**
* Let's get going.
*/
public function run() {
$this->load_hooks();
// Need to ensure these classes are activated here for hooks to fire.
if ( $this->is_current_page( [ 'options.php', 'options-general.php', 'settings.php' ] ) ) {
Singleton::get_instance( 'Install', $this )->run();
Singleton::get_instance( 'Remote_Management', $this )->load_hooks();
}
}
/**
* Load relevant action/filter hooks.
*/
protected function load_hooks() {
add_action( is_multisite() ? 'network_admin_menu' : 'admin_menu', [ $this, 'add_plugin_page' ] );
add_action( 'network_admin_edit_github-updater', [ $this, 'update_settings' ] );
add_filter(
is_multisite()
? 'network_admin_plugin_action_links_' . $this->ghu_plugin_name
: 'plugin_action_links_' . $this->ghu_plugin_name,
[ $this, 'plugin_action_links' ]
);
if ( $this->is_current_page( [ 'options.php', 'options-general.php', 'settings.php', 'edit.php' ] ) ) {
add_action( 'admin_init', [ $this, 'update_settings' ] );
add_action( 'admin_init', [ $this, 'page_init' ] );
}
}
/**
* Define tabs for Settings page.
* By defining in a method, strings can be translated.
*
* @access private
* @return array
*/
private function settings_tabs() {
$tabs = [ 'github_updater_settings' => esc_html__( 'Settings', 'github-updater' ) ];
/**
* Filter settings tabs.
*
* @since 8.0.0
*
* @param array $tabs Array of default tabs.
*/
return apply_filters( 'github_updater_add_settings_tabs', $tabs );
}
/**
* Set up the Settings Sub-tabs.
*
* @access private
* @return array
*/
private function settings_sub_tabs() {
$subtabs = [ 'github_updater' => esc_html__( 'GitHub Updater', 'github-updater' ) ];
$gits = $this->get_running_git_servers();
$gits[] = in_array( 'gitlabce', $gits, true ) ? 'gitlab' : null;
$gits = array_unique( $gits );
$git_subtab = [];
$ghu_subtabs = [];
/**
* Filter subtabs to be able to add subtab from git API class.
*
* @since 8.0.0
*
* @param array $ghu_subtabs Array of added subtabs.
*
* @return array $subtabs Array of subtabs.
*/
$ghu_subtabs = apply_filters( 'github_updater_add_settings_subtabs', $ghu_subtabs );
foreach ( $gits as $git ) {
if ( array_key_exists( $git, $ghu_subtabs ) ) {
$git_subtab[ $git ] = $ghu_subtabs[ $git ];
}
}
$subtabs = array_merge( $subtabs, $git_subtab );
return $subtabs;
}
/**
* Add options page.
*/
public function add_plugin_page() {
$parent = is_multisite() ? 'settings.php' : 'options-general.php';
$capability = is_multisite() ? 'manage_network' : 'manage_options';
add_submenu_page(
$parent,
esc_html__( 'GitHub Updater Settings', 'github-updater' ),
esc_html__( 'GitHub Updater', 'github-updater' ),
$capability,
'github-updater',
[ $this, 'create_admin_page' ]
);
}
/**
* Renders setting tabs.
*
* Walks through the object's tabs array and prints them one by one.
* Provides the heading for the settings page.
*
* @access private
*/
private function options_tabs() {
$current_tab = isset( $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : 'github_updater_settings';
echo '<nav class="nav-tab-wrapper" aria-label="Secondary menu">';
foreach ( $this->settings_tabs() as $key => $name ) {
$active = ( $current_tab === $key ) ? 'nav-tab-active' : '';
echo '<a class="nav-tab ' . $active . '" href="?page=github-updater&tab=' . $key . '">' . $name . '</a>';
}
echo '</nav>';
}
/**
* Render the settings sub-tabs.
*
* @access private
*/
private function options_sub_tabs() {
$current_tab = isset( $_GET['subtab'] ) ? esc_attr( $_GET['subtab'] ) : 'github_updater';
echo '<nav class="nav-tab-wrapper" aria-label="Tertiary menu">';
foreach ( $this->settings_sub_tabs() as $key => $name ) {
$active = ( $current_tab === $key ) ? 'nav-tab-active' : '';
echo '<a class="nav-tab ' . $active . '" href="?page=github-updater&tab=github_updater_settings&subtab=' . $key . '">' . $name . '</a>';
}
echo '</nav>';
}
/**
* Options page callback.
*/
public function create_admin_page() {
$action = is_multisite() ? 'edit.php?action=github-updater' : 'options.php';
$tab = isset( $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : 'github_updater_settings';
$subtab = isset( $_GET['subtab'] ) ? esc_attr( $_GET['subtab'] ) : 'github_updater';
$logo = plugins_url( basename( GITHUB_UPDATER_DIR ) . '/assets/GitHub_Updater_logo_small.png' ); ?>
<div class="wrap github-updater-settings">
<h1>
<a href="https://github.com/afragen/github-updater" target="_blank"><img src="<?php esc_attr_e( $logo ); ?>" alt="GitHub Updater logo" /></a><br>
<?php esc_html_e( 'GitHub Updater', 'github-updater' ); ?>
</h1>
<?php $this->options_tabs(); ?>
<?php $this->admin_page_notices(); ?>
<?php if ( 'github_updater_settings' === $tab ) : ?>
<?php $this->options_sub_tabs(); ?>
<form class="settings" method="post" action="<?php esc_attr_e( $action ); ?>">
<?php
settings_fields( 'github_updater' );
if ( 'github_updater' === $subtab ) {
do_settings_sections( 'github_updater_install_settings' );
$this->add_hidden_settings_sections();
} else {
do_settings_sections( 'github_updater_' . $subtab . '_install_settings' );
$this->display_ghu_repos( $subtab );
$this->add_hidden_settings_sections( $subtab );
}
submit_button();
?>
</form>
<?php $refresh_transients = add_query_arg( [ 'github_updater_refresh_transients' => true ], $action ); ?>
<form class="settings" method="post" action="<?php esc_attr_e( $refresh_transients ); ?>">
<?php submit_button( esc_html__( 'Refresh Cache', 'github-updater' ), 'primary', 'ghu_refresh_cache' ); ?>
</form>
<?php endif; ?>
<?php
/**
* Action hook to add admin page data to appropriate $tab.
*
* @since 8.0.0
*
* @param string $tab Name of tab.
* @param string $action Save action for appropriate WordPress installation.
* Single site or Multisite.
*/
do_action( 'github_updater_add_admin_page', $tab, $action );
?>
</div>
<?php
}
/**
* Display appropriate notice for Settings page actions.
*/
private function admin_page_notices() {
$display = ( isset( $_GET['updated'] ) && is_multisite() )
|| isset( $_GET['reset'] )
|| isset( $_GET['refresh_transients'] );
if ( $display ) {
echo '<div class="updated"><p>';
}
if ( ( isset( $_GET['updated'] ) && '1' === $_GET['updated'] ) && is_multisite() ) {
esc_html_e( 'Settings saved.', 'github-updater' );
} elseif ( isset( $_GET['reset'] ) && '1' === $_GET['reset'] ) {
esc_html_e( 'RESTful key reset.', 'github-updater' );
} elseif ( isset( $_GET['refresh_transients'] ) && '1' === $_GET['refresh_transients'] ) {
esc_html_e( 'Cache refreshed.', 'github-updater' );
}
if ( $display ) {
echo '</p></div>';
}
}
/**
* Register and add settings.
* Check to see if it's a private repo.
*/
public function page_init() {
if ( static::is_doing_ajax() ) {
return;
}
register_setting(
'github_updater',
'github_updater',
[ $this, 'sanitize' ]
);
$this->ghu_tokens();
/*
* Add basic plugin settings.
*/
add_settings_section(
'github_updater_settings',
esc_html__( 'GitHub Updater Settings', 'github-updater' ),
[ $this, 'print_section_ghu_settings' ],
'github_updater_install_settings'
);
add_settings_field(
'branch_switch',
null,
[ $this, 'token_callback_checkbox' ],
'github_updater_install_settings',
'github_updater_settings',
[
'id' => 'branch_switch',
'title' => esc_html__( 'Enable Branch Switching', 'github-updater' ),
]
);
/**
* Hook to add Git API settings.
*
* @since 8.0.0
*
* @param array $auth_required Array containing authorization needs of git APIs.
*/
do_action( 'github_updater_add_settings', static::$auth_required );
}
/**
* Create and return settings fields for private repositories.
*/
public function ghu_tokens() {
$ghu_options_keys = [];
$ghu_plugins = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$ghu_themes = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$ghu_tokens = array_merge( $ghu_plugins, $ghu_themes );
foreach ( $ghu_tokens as $token ) {
$type = '<span class="dashicons dashicons-admin-plugins"></span>&nbsp;';
$setting_field = [];
$ghu_options_keys[ $token->slug ] = null;
/*
* Check to see if it's a private repo or Enterprise and set variables.
*/
$this->set_auth_required( $token );
/*
* Next if not a private repo or token field not empty.
*/
if ( ! $this->is_private( $token ) ) {
continue;
}
if ( 'theme' === $token->type ) {
$type = '<span class="dashicons dashicons-admin-appearance"></span>&nbsp;';
}
$setting_field['id'] = $token->slug;
$setting_field['title'] = $type . esc_html( $token->name );
$repo_setting_field = apply_filters( 'github_updater_add_repo_setting_field', [], $token, $token->git );
if ( empty( $repo_setting_field ) ) {
continue;
}
$setting_field = array_merge( $setting_field, $repo_setting_field );
$setting_field['callback'] = $token->slug;
$title = 'token_callback_checkbox' !== $setting_field['callback_method'][1] ? $setting_field['title'] : null;
add_settings_field(
$setting_field['id'],
$title,
$setting_field['callback_method'],
$setting_field['page'],
$setting_field['section'],
[
'id' => $setting_field['callback'],
'token' => true,
'title' => $setting_field['title'],
]
);
}
if ( ! $this->waiting_for_background_update() ) {
$this->unset_stale_options( $ghu_options_keys, $ghu_tokens );
} else {
Singleton::get_instance( 'Messages', $this )->create_error_message( 'waiting' );
}
}
/**
* Check current saved options and unset if repos not present.
*
* @param array $ghu_options_keys
* @param array $ghu_tokens
*/
public function unset_stale_options( $ghu_options_keys, $ghu_tokens ) {
$running_servers = $this->get_running_git_servers();
$ghu_unset_keys = array_diff_key( static::$options, $ghu_options_keys );
$always_unset = [
'db_version',
'branch_switch',
'github_access_token',
'github_enterprise_token',
];
if ( in_array( 'bitbucket', $running_servers, true ) ) {
$always_unset = array_merge(
$always_unset,
[
'bitbucket_username',
'bitbucket_password',
]
);
}
if ( in_array( 'bbserver', $running_servers, true ) ) {
$always_unset = array_merge(
$always_unset,
[
'bitbucket_server_username',
'bitbucket_server_password',
]
);
}
array_map(
function ( $e ) use ( &$ghu_unset_keys ) {
unset( $ghu_unset_keys[ $e ] );
},
$always_unset
);
$auth_required = static::$auth_required;
$auth_required_unset = [
'github_enterprise' => 'github_enterprise_token',
'gitlab' => 'gitlab_access_token',
'gitlab_enterprise' => 'gitlab_enterprise_token',
'gitea' => 'gitea_access_token',
];
array_map(
function ( $e ) use ( &$ghu_unset_keys, $auth_required, $auth_required_unset ) {
$key = array_search( $e, $auth_required_unset, true );
if ( $auth_required[ $key ] ) {
unset( $ghu_unset_keys[ $e ] );
}
},
$auth_required_unset
);
// Unset if current_branch AND if associated with repo.
array_map(
function ( $e ) use ( &$ghu_unset_keys, $ghu_tokens, &$reset_keys ) {
$key = array_search( $e, $ghu_unset_keys, true );
$repo = str_replace( 'current_branch_', '', $key );
if ( array_key_exists( $key, $ghu_unset_keys )
&& false !== strpos( $key, 'current_branch' )
) {
unset( $ghu_unset_keys[ $key ] );
}
if ( ! array_key_exists( $repo, $ghu_tokens ) ) {
$reset_keys[ $key ] = $e;
}
},
$ghu_unset_keys
);
$ghu_unset_keys = array_merge( $ghu_unset_keys, (array) $reset_keys );
if ( ! empty( $ghu_unset_keys ) ) {
foreach ( $ghu_unset_keys as $key => $value ) {
unset( static::$options[ $key ] );
}
update_site_option( 'github_updater', static::$options );
}
}
/**
* Check to see if it's an enterprise or private repo and set variables.
*
* @param \stdClass $token Repo data.
*/
private function set_auth_required( $token ) {
// Set booleans for Enterprise repos.
if ( $token->enterprise ) {
static::$auth_required['github_enterprise'] = static::$auth_required['github_enterprise']
?: 'github' === $token->git;
static::$auth_required['gitlab_enterprise'] = static::$auth_required['gitlab_enterprise']
?: 'gitlab' === $token->git;
static::$auth_required['bitbucket_server'] = static::$auth_required['bitbucket_server']
?: 'bitbucket' === $token->git;
}
// Set booleans for private repos.
if ( $this->is_private( $token ) ) {
static::$auth_required['github_private'] = static::$auth_required['github_private']
?: 'github' === $token->git;
static::$auth_required['bitbucket_private'] = static::$auth_required['bitbucket_private']
?: 'bitbucket' === $token->git;
static::$auth_required['gitlab_private'] = static::$auth_required['gitlab_private']
?: 'gitlab' === $token->git;
static::$auth_required['gitea_private'] = static::$auth_required['gitea_private']
?: 'gitea' === $token->git;
}
// Always set to true.
static::$auth_required['gitlab'] = true;
static::$auth_required['gitea'] = true;
}
/**
* Print the GitHub Updater Settings text.
*/
public function print_section_ghu_settings() {
$this->display_dot_org_overrides();
echo '<p>' . esc_html__( 'Check to enable branch switching from the Plugins or Themes page.', 'github-updater' ) . '</p>';
}
/**
* Display plugins/themes that are overridden using the filter hook.
*
* @uses `github_updater_override_dot_org` filter hook
* @return void
*/
private function display_dot_org_overrides() {
$plugins = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$themes = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$dashicon_plugin = '<span class="dashicons dashicons-admin-plugins"></span>&nbsp;&nbsp;';
$dashicon_theme = '<span class="dashicons dashicons-admin-appearance"></span>&nbsp;&nbsp;';
/**
* Filter to return array of overrides to dot org.
*
* @since 8.5.0
* @return array
*/
$overrides = apply_filters( 'github_updater_override_dot_org', [] );
if ( ! empty( $overrides ) ) {
echo '<h4>' . esc_html__( 'Overridden Plugins and Themes', 'github-updater' ) . '</h4>';
echo '<p>' . esc_html__( 'The following plugins or themes might exist on wp.org, but any updates will be downloaded from their respective git repositories.', 'github-updater' ) . '</p>';
foreach ( $plugins as $plugin ) {
if ( in_array( $plugin->file, $overrides, true ) ) {
echo '<p>' . $dashicon_plugin . $plugin->name . '</p>';
}
}
foreach ( $themes as $theme ) {
if ( in_array( $theme->slug, $overrides, true ) ) {
echo '<p>' . $dashicon_theme . $theme->name . '</p>';
}
}
echo '<br>';
}
}
/**
* Get the settings option array and print one of its values.
*
* @param array $args
*/
public function token_callback_text( $args ) {
$name = isset( static::$options[ $args['id'] ] ) ? esc_attr( static::$options[ $args['id'] ] ) : '';
$type = isset( $args['token'] ) ? 'password' : 'text';
?>
<label for="<?php esc_attr( $args['id'] ); ?>">
<input class="ghu-callback-text" type="<?php esc_attr_e( $type ); ?>" id="<?php esc_attr( $args['id'] ); ?>" name="github_updater[<?php esc_attr_e( $args['id'] ); ?>]" value="<?php esc_attr_e( $name ); ?>">
</label>
<?php
}
/**
* Get the settings option array and print one of its values.
*
* @param array $args
*/
public function token_callback_checkbox( $args ) {
$checked = isset( static::$options[ $args['id'] ] ) ? static::$options[ $args['id'] ] : null;
?>
<label for="<?php esc_attr_e( $args['id'] ); ?>">
<input type="checkbox" id="<?php esc_attr_e( $args['id'] ); ?>" name="github_updater[<?php esc_attr_e( $args['id'] ); ?>]" value="1" <?php checked( '1', $checked ); ?> >
<?php echo $args['title']; ?>
</label>
<?php
}
/**
* Update settings for single site or network activated.
*
* @link http://wordpress.stackexchange.com/questions/64968/settings-api-in-multisite-missing-update-message
* @link http://benohead.com/wordpress-network-wide-plugin-settings/
*/
public function update_settings() {
if ( isset( $_POST['option_page'] ) &&
'github_updater' === $_POST['option_page']
) {
$options = $this->filter_options();
update_site_option( 'github_updater', $this->sanitize( $options ) );
}
/**
* Save $options in add-on classes.
*
* @since 8.0.0
*/
do_action( 'github_updater_update_settings', $_POST );
$this->redirect_on_save();
}
/**
* Filter options to remove unchecked checkbox options.
*
* @access private
*
* @return array|mixed
*/
private function filter_options() {
$options = static::$options;
// Remove checkbox options, only after background update complete.
if ( ! $this->waiting_for_background_update() ) {
$options = array_filter(
$options,
function ( $e ) {
return '1' !== $e;
}
);
}
$options = array_merge( $options, $_POST['github_updater'] );
return $options;
}
/**
* Redirect to correct Settings tab on Save.
*/
protected function redirect_on_save() {
$update = false;
$refresh_transients = $this->refresh_transients();
$reset_api_key = Singleton::get_instance( 'Remote_Management', $this )->reset_api_key();
/**
* Filter to add to $option_page array.
*
* @since 8.0.0
* @return array
*/
$option_page = apply_filters( 'github_updater_save_redirect', [ 'github_updater' ] );
if ( ( isset( $_POST['action'] ) && 'update' === $_POST['action'] ) &&
( isset( $_POST['option_page'] ) && in_array( $_POST['option_page'], $option_page, true ) )
) {
$update = true;
}
$redirect_url = is_multisite() ? network_admin_url( 'settings.php' ) : admin_url( 'options-general.php' );
if ( $update || $refresh_transients || $reset_api_key ) {
$query = isset( $_POST['_wp_http_referer'] ) ? parse_url( $_POST['_wp_http_referer'], PHP_URL_QUERY ) : null;
parse_str( $query, $arr );
$arr['tab'] = ! empty( $arr['tab'] ) ? $arr['tab'] : 'github_updater_settings';
$arr['subtab'] = ! empty( $arr['subtab'] ) ? $arr['subtab'] : 'github_updater';
$location = add_query_arg(
[
'page' => 'github-updater',
'tab' => $arr['tab'],
'subtab' => $arr['subtab'],
'refresh_transients' => $refresh_transients,
'reset' => $reset_api_key,
'updated' => $update,
],
$redirect_url
);
wp_safe_redirect( $location );
exit;
}
}
/**
* Clear GitHub Updater transients.
*
* @return bool
*/
private function refresh_transients() {
if ( isset( $_REQUEST['github_updater_refresh_transients'] ) ) {
$_POST = $_REQUEST;
return true;
}
return false;
}
/**
* Add setting link to plugin page.
* Applied to the list of links to display on the plugins page (beside the activate/deactivate links).
*
* @link http://codex.wordpress.org/Plugin_API/Filter_Reference/plugin_action_links_(plugin_file_name)
*
* @param array $links
*
* @return array
*/
public function plugin_action_links( $links ) {
$settings_page = is_multisite() ? 'settings.php' : 'options-general.php';
$link = [ '<a href="' . esc_url( network_admin_url( $settings_page ) ) . '?page=github-updater">' . esc_html__( 'Settings', 'github-updater' ) . '</a>' ];
return array_merge( $links, $link );
}
/**
* Create settings sections that are hidden.
* Required to preserve subtab settings during saves.
*
* @param array $subtab Subtab to display.
*/
private function add_hidden_settings_sections( $subtab = [] ) {
$subtabs = array_keys( $this->settings_sub_tabs() );
$hide_tabs = array_diff( $subtabs, (array) $subtab, [ 'github_updater' ] );
if ( ! empty( $subtab ) ) {
echo '<div id="github_updater" class="hide-github-updater-settings">';
do_settings_sections( 'github_updater_install_settings' );
echo '</div>';
}
foreach ( $hide_tabs as $hide_tab ) {
echo '<div id="' . $hide_tab . '" class="hide-github-updater-settings">';
do_settings_sections( 'github_updater_' . $hide_tab . '_install_settings' );
echo '</div>';
}
}
/**
* Write out listing of installed plugins and themes using GitHub Updater.
* Places a lock dashicon after the repo name if it's a private repo.
* Places a WordPress dashicon after the repo name if it's in dot org.
*
* @param string $git (github|bitbucket|bbserver|gitlab|gitea)
*/
private function display_ghu_repos( $git ) {
$lock_title = esc_html__( 'This is a private repository.', 'github-updater' );
$broken_title = esc_html__( 'This repository has not connected to the API or was unable to connect.', 'github-updater' );
$dot_org_title = esc_html__( 'This repository is hosted on WordPress.org.', 'github-updater' );
$plugins = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$themes = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$repos = array_merge( $plugins, $themes );
$bbserver = [ 'bitbucket', 'bbserver' ];
$type_repos = array_filter(
$repos,
function ( $e ) use ( $git, $bbserver ) {
if ( ! empty( $e->enterprise ) && in_array( $git, $bbserver, true ) ) {
return false !== stripos( $e->git, 'bitbucket' ) && 'bbserver' === $git;
}
return false !== stripos( $e->git, $git );
}
);
$display_data = array_map(
function ( $e ) {
return [
'type' => $e->type,
'slug' => $e->slug,
'file' => isset( $e->file ) ? $e->file : $e->slug,
'branch' => $e->branch,
'name' => $e->name,
'private' => isset( $e->is_private ) ? $e->is_private : false,
'broken' => ! isset( $e->remote_version ) || '0.0.0' === $e->remote_version,
'dot_org' => isset( $e->dot_org ) ? $e->dot_org : false,
];
},
$type_repos
);
$lock = '&nbsp;<span title="' . $lock_title . '" class="dashicons dashicons-lock"></span>';
$broken = '&nbsp;<span title="' . $broken_title . '" style="color:#f00;" class="dashicons dashicons-warning"></span>';
$dot_org = '&nbsp;<span title="' . $dot_org_title . '" class="dashicons dashicons-wordpress"></span></span>';
printf( '<h2>' . esc_html__( 'Installed Plugins and Themes', 'github-updater' ) . '</h2>' );
foreach ( $display_data as $data ) {
$dashicon = false !== strpos( $data['type'], 'theme' )
? '<span class="dashicons dashicons-admin-appearance"></span>&nbsp;&nbsp;'
: '<span class="dashicons dashicons-admin-plugins"></span>&nbsp;&nbsp;';
$is_private = $data['private'] ? $lock : null;
$is_broken = $data['broken'] ? $broken : null;
$override = $this->override_dot_org( $data['type'], $data );
$is_dot_org = $data['dot_org'] && ! $override ? $dot_org : null;
printf( '<p>' . $dashicon . $data['name'] . $is_private . $is_dot_org . $is_broken . '</p>' );
}
}
}

View File

@@ -0,0 +1,676 @@
<?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;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Class Theme
*
* Update a WordPress theme from a GitHub repo.
*
* @author Andy Fragen
* @author Seth Carstens
* @link https://github.com/WordPress-Phoenix/whitelabel-framework
* @author UCF Web Communications
* @link https://github.com/UCF/Theme-Updater
*/
class Theme extends Base {
/**
* Rollback variable.
*
* @var number
*/
protected $tag = false;
/**
* Constructor.
*/
public function __construct() {
parent::__construct();
$this->load_options();
// Get details of installed git sourced themes.
$this->config = $this->get_theme_meta();
if ( null === $this->config ) {
return;
}
}
/**
* Returns an array of configurations for the known themes.
*
* @return array
*/
public function get_theme_configs() {
return $this->config;
}
/**
* Delete cache of current theme.
* This is needed in case `wp_get_theme()` is called in earlier or in a mu-plugin.
* This action results in the extra headers not being added.
*
* @link https://github.com/afragen/github-updater/issues/586
*/
private function delete_current_theme_cache() {
$cache_hash = md5( get_stylesheet_directory() );
wp_cache_delete( 'theme-' . $cache_hash, 'themes' );
}
/**
* Reads in WP_Theme class of each theme.
* Populates variable array.
*
* @return array Indexed array of associative arrays of theme details.
*/
protected function get_theme_meta() {
$this->delete_current_theme_cache();
$git_themes = [];
$themes = wp_get_themes( [ 'errors' => null ] );
/**
* Filter to add themes not containing appropriate header line.
*
* @since 5.4.0
* @access public
*
* @param array $additions Listing of themes to add.
* Default null.
* @param array $themes Listing of all themes.
* @param string 'theme' Type being passed.
*/
$additions = apply_filters( 'github_updater_additions', null, $themes, 'theme' );
foreach ( (array) $themes as $theme ) {
$git_theme = [];
foreach ( (array) static::$extra_headers as $value ) {
$header = null;
$repo_uri = $theme->get( $value );
/**
* Get $repo_uri from themes added to GitHub Updater via hook.
*/
foreach ( (array) $additions as $addition ) {
if ( $theme->stylesheet === $addition['slug'] ) {
if ( ! empty( $addition[ $value ] ) ) {
$repo_uri = $addition[ $value ];
break;
}
}
}
if ( empty( $repo_uri ) || false === stripos( $value, 'Theme' ) ) {
continue;
}
$header_parts = explode( ' ', $value );
$repo_parts = $this->get_repo_parts( $header_parts[0], 'theme' );
if ( $repo_parts['bool'] ) {
$header = $this->parse_header_uri( $repo_uri );
if ( empty( $header ) || $theme->stylesheet !== $header['repo'] ) {
continue;
}
}
$header = $this->parse_extra_headers( $header, $theme, $header_parts, $repo_parts );
$current_branch = "current_branch_{$header['repo']}";
$branch = isset( static::$options[ $current_branch ] )
? static::$options[ $current_branch ]
: false;
$git_theme['type'] = 'theme';
$git_theme['git'] = $repo_parts['git_server'];
$git_theme['uri'] = "{$header['base_uri']}/{$header['owner_repo']}";
$git_theme['enterprise'] = $header['enterprise_uri'];
$git_theme['enterprise_api'] = $header['enterprise_api'];
$git_theme['owner'] = $header['owner'];
$git_theme['slug'] = $header['repo'];
$git_theme['file'] = "{$header['repo']}/style.css";
$git_theme['name'] = $theme->get( 'Name' );
$git_theme['theme_uri'] = $theme->get( 'ThemeURI' );
$git_theme['homepage'] = $theme->get( 'ThemeURI' );
$git_theme['author'] = $theme->get( 'Author' );
$git_theme['local_version'] = strtolower( $theme->get( 'Version' ) );
$git_theme['sections']['description'] = $theme->get( 'Description' );
$git_theme['local_path'] = get_theme_root() . '/' . $git_theme['slug'] . '/';
$git_theme['branch'] = $branch ?: 'master';
$git_theme['languages'] = $header['languages'];
$git_theme['ci_job'] = $header['ci_job'];
$git_theme['release_asset'] = $header['release_asset'];
$git_theme['broken'] = ( empty( $header['owner'] ) || empty( $header['repo'] ) );
break;
}
// Exit if not git hosted theme.
if ( empty( $git_theme ) ) {
continue;
}
$git_themes[ $git_theme['slug'] ] = (object) $git_theme;
}
return $git_themes;
}
/**
* Get remote theme meta to populate $config theme objects.
* Calls to remote APIs to get data.
*/
public function get_remote_theme_meta() {
$themes = [];
foreach ( (array) $this->config as $theme ) {
/**
* 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( $theme ) || static::is_wp_cli()
|| apply_filters( 'github_updater_disable_wpcron', false )
) {
$this->get_remote_repo_meta( $theme );
} else {
$themes[ $theme->slug ] = $theme;
}
/*
* Add update row to theme row, only in multisite.
*/
if ( is_multisite() ) {
add_action( 'after_theme_row', [ $this, 'remove_after_theme_row' ], 10, 2 );
if ( ! $this->tag ) {
add_action( "after_theme_row_{$theme->slug}", [ $this, 'wp_theme_update_row' ], 10, 2 );
if ( ! $theme->release_asset ) {
add_action( "after_theme_row_{$theme->slug}", [ $this, 'multisite_branch_switcher' ], 15, 2 );
}
}
}
}
$schedule_event = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? is_main_site() : true;
if ( $schedule_event ) {
if ( ! wp_next_scheduled( 'ghu_get_remote_theme' ) &&
! $this->is_duplicate_wp_cron_event( 'ghu_get_remote_theme' ) &&
! apply_filters( 'github_updater_disable_wpcron', false )
) {
wp_schedule_single_event( time(), 'ghu_get_remote_theme', [ $themes ] );
}
}
if ( ! static::is_wp_cli() ) {
$this->load_pre_filters();
}
}
/**
* Load pre-update filters.
*/
public function load_pre_filters() {
if ( ! is_multisite() ) {
add_filter( 'wp_prepare_themes_for_js', [ $this, 'customize_theme_update_html' ] );
}
add_filter( 'themes_api', [ $this, 'themes_api' ], 99, 3 );
add_filter( 'site_transient_update_themes', [ $this, 'update_site_transient' ], 15, 1 );
}
/**
* Put changelog in themes_api, return WP.org data as appropriate.
*
* @param bool $false
* @param string $action
* @param \stdClass $response
*
* @return mixed
*/
public function themes_api( $false, $action, $response ) {
if ( ! ( 'theme_information' === $action ) ) {
return $false;
}
$theme = isset( $this->config[ $response->slug ] ) ? $this->config[ $response->slug ] : false;
// Skip if waiting for background update.
if ( $this->waiting_for_background_update( $theme ) ) {
return $false;
}
// wp.org theme.
if ( ! $theme ) {
return $false;
}
$response->slug = $theme->slug;
$response->name = $theme->name;
$response->homepage = $theme->homepage;
$response->donate_link = $theme->donate_link;
$response->version = $theme->remote_version;
$response->sections = $theme->sections;
$response->description = implode( "\n", $theme->sections );
$response->author = $theme->author;
$response->preview_url = $theme->theme_uri;
$response->requires = $theme->requires;
$response->tested = $theme->tested;
$response->downloaded = $theme->downloaded;
$response->last_updated = $theme->last_updated;
$response->rating = $theme->rating;
$response->num_ratings = $theme->num_ratings;
return $response;
}
/**
* Add custom theme update row, from /wp-admin/includes/update.php
* Display update details or rollback links for multisite installation.
*
* @param string $theme_key
* @param array $theme
*
* @author Seth Carstens
*/
public function wp_theme_update_row( $theme_key, $theme ) {
$current = get_site_transient( 'update_themes' );
$themes_allowedtags = [
'a' => [
'href' => [],
'title' => [],
],
'abbr' => [ 'title' => [] ],
'acronym' => [ 'title' => [] ],
'code' => [],
'em' => [],
'strong' => [],
];
$theme_name = wp_kses( $theme['Name'], $themes_allowedtags );
// $wp_list_table = _get_list_table( 'WP_MS_Themes_List_Table' );
$details_url = esc_attr(
add_query_arg(
[
'tab' => 'theme-information',
'theme' => $theme_key,
'TB_iframe' => 'true',
'width' => 270,
'height' => 400,
],
self_admin_url( 'theme-install.php' )
)
);
$nonced_update_url = wp_nonce_url(
$this->get_update_url( 'theme', 'upgrade-theme', $theme_key ),
'upgrade-theme_' . $theme_key
);
$enclosure = $this->update_row_enclosure( $theme_key, 'theme' );
if ( isset( $current->response[ $theme_key ] ) ) {
$response = $current->response[ $theme_key ];
echo $enclosure['open'];
printf(
/* translators: %s: theme name */
esc_html__( 'There is a new version of %s available.', 'github-updater' ),
$theme_name
);
printf(
/* translators: %s: details URL, theme name */
' <a href="%s" class="thickbox" title="%s"> ',
$details_url,
$theme_name
);
if ( empty( $response['package'] ) ) {
printf(
/* translators: %s: theme version */
esc_html__( 'View version %s details.', 'github-updater' ),
$response['new_version']
);
echo '</a>&nbsp;<em>';
esc_html_e( 'Automatic update is unavailable for this theme.', 'github-updater' );
echo '</em>';
} else {
printf(
/* translators: 1: version number, 2: closing anchor tag, 3: update URL */
esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'github-updater' ),
$response['new_version'],
'</a>',
sprintf(
/* translators: %s: theme name */
'<a href="' . $nonced_update_url . '" class="update-link" aria-label="' . esc_html__( 'Update %s now', 'github-updater' ) . '">',
$theme_name
)
);
}
echo $enclosure['close'];
do_action( "in_theme_update_message-$theme_key", $theme, $response );
}
}
/**
* Create branch switcher row for multisite installation.
*
* @param string $theme_key
* @param array $theme
*
* @return bool
*/
public function multisite_branch_switcher( $theme_key, $theme ) {
if ( empty( static::$options['branch_switch'] ) ) {
return false;
}
$enclosure = $this->update_row_enclosure( $theme_key, 'theme', true );
$id = $theme_key . '-id';
$branches = isset( $this->config[ $theme_key ]->branches )
? $this->config[ $theme_key ]->branches
: null;
$nonced_update_url = wp_nonce_url(
$this->get_update_url( 'theme', 'upgrade-theme', $theme_key ),
'upgrade-theme_' . $theme_key
);
// Get current branch.
$repo = $this->config[ $theme_key ];
$branch = Singleton::get_instance( 'Branch', $this )->get_current_branch( $repo );
$branch_switch_data = [];
$branch_switch_data['slug'] = $theme_key;
$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_theme_row_
*/
echo $enclosure['open'];
$this->make_branch_switch_row( $branch_switch_data );
echo $enclosure['close'];
return true;
}
/**
* Remove default after_theme_row_$stylesheet.
*
* @author @grappler
*
* @param string $theme_key
* @param array $theme
*/
public function remove_after_theme_row( $theme_key, $theme ) {
$themes = $this->get_theme_configs();
foreach ( static::$git_servers as $server ) {
$repo_header = $server . ' Theme URI';
$repo_uri = $theme->get( $repo_header );
/**
* Filter to add themes not containing appropriate header line.
*
* @since 5.4.0
* @access public
*
* @param array $additions Listing of themes to add.
* Default null.
* @param array $themes Listing of all themes.
* @param string 'theme' Type being passed.
*/
$additions = apply_filters( 'github_updater_additions', null, $themes, 'theme' );
foreach ( (array) $additions as $addition ) {
if ( $theme_key === $addition['slug'] ) {
if ( ! empty( $addition[ $server . ' Theme URI' ] ) ) {
$repo_uri = $addition[ $server . ' Theme URI' ];
break;
}
}
}
if ( empty( $repo_uri ) ) {
continue;
}
break;
}
if ( array_key_exists( $theme_key, $themes ) ) {
remove_action( "after_theme_row_$theme_key", 'wp_theme_update_row' );
}
}
/**
* Call theme messaging for single site installation.
*
* @author Seth Carstens
*
* @param array $prepared_themes
*
* @return mixed
*/
public function customize_theme_update_html( $prepared_themes ) {
foreach ( (array) $this->config as $theme ) {
if ( empty( $prepared_themes[ $theme->slug ] ) ) {
continue;
}
if ( ! empty( $prepared_themes[ $theme->slug ]['hasUpdate'] ) ) {
$prepared_themes[ $theme->slug ]['update'] = $this->append_theme_actions_content( $theme );
} else {
$prepared_themes[ $theme->slug ]['description'] .= $this->append_theme_actions_content( $theme );
}
if ( ! $theme->release_asset ) {
$prepared_themes[ $theme->slug ]['description'] .= $this->single_install_switcher( $theme );
}
}
return $prepared_themes;
}
/**
* Create theme update messaging for single site installation.
*
* @author Seth Carstens
*
* @access protected
*
* @param \stdClass $theme
*
* @return string (content buffer)
*/
protected function append_theme_actions_content( $theme ) {
$details_url = esc_attr(
add_query_arg(
[
'tab' => 'theme-information',
'theme' => $theme->slug,
'TB_iframe' => 'true',
'width' => 270,
'height' => 400,
],
self_admin_url( 'theme-install.php' )
)
);
$nonced_update_url = wp_nonce_url(
$this->get_update_url( 'theme', 'upgrade-theme', $theme->slug ),
'upgrade-theme_' . $theme->slug
);
$current = get_site_transient( 'update_themes' );
/**
* Display theme update links.
*/
ob_start();
if ( isset( $current->response[ $theme->slug ] ) ) {
?>
<p>
<strong>
<?php
printf(
/* translators: %s: theme name */
esc_html__( 'There is a new version of %s available.', 'github-updater' ),
$theme->name
);
printf(
' <a href="%s" class="thickbox open-plugin-details-modal" title="%s">',
$details_url,
esc_attr( $theme->name )
);
printf(
/* translators: 1: version number, 2: closing anchor tag, 3: update URL */
esc_html__( 'View version %1$s details%2$s or %3$supdate now%2$s.', 'github-updater' ),
$theme->remote_version = isset( $theme->remote_version ) ? $theme->remote_version : null,
'</a>',
sprintf(
/* translators: %s: theme name */
'<a aria-label="' . esc_html__( 'Update %s now', 'github-updater' ) . '" id="update-theme" data-slug="' . $theme->slug . '" href="' . $nonced_update_url . '">',
$theme->name
)
);
?>
</strong>
</p>
<?php
}
return trim( ob_get_clean(), '1' );
}
/**
* Display rollback/branch switcher for single site installation.
*
* @access protected
*
* @param \stdClass $theme
*
* @return string
*/
protected function single_install_switcher( $theme ) {
$nonced_update_url = wp_nonce_url(
$this->get_update_url( 'theme', 'upgrade-theme', $theme->slug ),
'upgrade-theme_' . $theme->slug
);
$rollback_url = sprintf( '%s%s', $nonced_update_url, '&rollback=' );
if ( ! isset( static::$options['branch_switch'] ) ) {
return;
}
ob_start();
if ( '1' === static::$options['branch_switch'] ) {
printf(
/* translators: 1: branch name, 2: jQuery dropdown, 3: closing tag */
'<p>' . esc_html__( 'Current branch is `%1$s`, try %2$sanother version%3$s', 'github-updater' ),
$theme->branch,
'<a href="#" onclick="jQuery(\'#ghu_versions\').toggle();return false;">',
'</a>.</p>'
);
?>
<div id="ghu_versions" style="display:none; width: 100%;">
<label><select style="width: 60%;" onchange="if(jQuery(this).val() != '') { jQuery(this).parent().next().show(); jQuery(this).parent().next().attr('href','<?php echo esc_url( $rollback_url ); ?>'+jQuery(this).val()); } else jQuery(this).parent().next().hide();">
<option value=""><?php esc_html_e( 'Choose a Version', 'github-updater' ); ?>&#8230;</option>
<?php
if ( isset( $theme->branches ) ) {
foreach ( array_keys( $theme->branches ) as $branch ) {
echo '<option>' . $branch . '</option>';
}
}
if ( ! empty( $theme->rollback ) ) {
$rollback = array_keys( $theme->rollback );
usort( $rollback, 'version_compare' );
krsort( $rollback );
$rollback = array_splice( $rollback, 0, 4, true );
array_shift( $rollback ); // Dump current tag.
foreach ( $rollback as $tag ) {
echo '<option>' . $tag . '</option>';
}
}
if ( empty( $theme->rollback ) ) {
echo '<option>' . esc_html__( 'No previous tags to rollback to.', 'github-updater' ) . '</option></select></label>';
}
?>
</select></label>
<a style="display: none;" class="button-primary" href="?"><?php esc_html_e( 'Install', 'github-updater' ); ?></a>
</div>
<?php
}
return trim( ob_get_clean(), '1' );
}
/**
* Hook into site_transient_update_themes to update.
* Finds newest tag and compares to current tag.
*
* @param array $transient
*
* @return array|\stdClass
*/
public function update_site_transient( $transient ) {
foreach ( (array) $this->config as $theme ) {
if ( $this->can_update_repo( $theme ) ) {
$response = [
'theme' => $theme->slug,
'new_version' => $theme->remote_version,
'url' => $theme->uri,
'package' => $theme->download_link,
'branch' => $theme->branch,
'branches' => array_keys( $theme->branches ),
'type' => "{$theme->git}-{$theme->type}",
];
// Skip on RESTful updating.
if ( isset( $_GET['action'], $_GET['theme'] ) &&
'github-updater-update' === $_GET['action'] &&
$response['theme'] === $_GET['theme']
) {
continue;
}
// Pull update from dot org if not overriding.
if ( ! $this->override_dot_org( 'theme', $theme ) ) {
continue;
}
$transient->response[ $theme->slug ] = $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[ $theme->slug ] ) && in_array( $theme->slug, $overrides, true ) ) {
unset( $transient->response[ $theme->slug ] );
}
}
// Set transient for rollback.
if ( isset( $_GET['theme'], $_GET['rollback'] ) && $theme->slug === $_GET['theme']
) {
$transient->response[ $theme->slug ] = $this->set_rollback_transient( 'theme', $theme );
}
}
return $transient;
}
}

View File

@@ -0,0 +1,365 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\Traits;
use Fragen\GitHub_Updater\Readme_Parser as Readme_Parser;
/**
* Trait API_Common
*/
trait API_Common {
/**
* Holds loose class method name.
*
* @var null
*/
protected static $method;
/**
* Decode API responses that are base64 encoded.
*
* @param string $git (github|bitbucket|gitlab|gitea)
* @param mixed $response API response.
* @return mixed $response
*/
private function decode_response( $git, $response ) {
switch ( $git ) {
case 'github':
case 'gitlab':
$response = isset( $response->content ) ? base64_decode( $response->content ) : $response;
break;
case 'bbserver':
$response = isset( $response->lines ) ? $this->bbserver_recombine_response( $response ) : $response;
break;
}
return $response;
}
/**
* Parse API response that returns as stdClass.
*
* @param string $git (github|bitbucket|gitlab|gitea)
* @param mixed $response API response.
* @return mixed $response
*/
private function parse_response( $git, $response ) {
switch ( $git ) {
case 'bitbucket':
case 'bbserver':
$response = isset( $response->values ) ? $response->values : $response;
break;
}
return $response;
}
/**
* Parse API response to release asset URI.
*
* @param string $git (github|bitbucket|gitlab|gitea)
* @param string $request Query to API->api().
* @param mixed $response API response.
* @return string $response Release asset download link.
*/
private function parse_release_asset( $git, $request, $response ) {
switch ( $git ) {
case 'github':
$download_link = isset( $response->assets[0] ) && ! is_wp_error( $response ) ? $response->assets[0]->browser_download_url : null;
// Private repo.
$response = ( null !== $download_link && ( property_exists( $this->type, 'is_private' ) && $this->type->is_private ) ) ? $response->assets[0]->url : $download_link;
break;
case 'bitbucket':
$download_base = $this->get_api_url( $request, true );
$response = isset( $response->values[0] ) && ! is_wp_error( $response ) ? $download_base . '/' . $response->values[0]->name : null;
break;
case 'bbserver':
// TODO: make work.
break;
case 'gitlab':
$response = $this->get_api_url( $request );
break;
case 'gitea':
break;
}
return $response;
}
/**
* Read the remote file and parse headers.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $file Filename.
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_info( $git, $file, $request ) {
$response = isset( $this->response[ $file ] ) ? $this->response[ $file ] : false;
if ( ! $response ) {
self::$method = 'file';
$response = $this->api( $request );
$response = $this->decode_response( $git, $response );
}
if ( $response && is_string( $response ) && ! is_wp_error( $response ) ) {
$response = $this->get_file_headers( $response, $this->type->type );
$this->set_repo_cache( $file, $response );
$this->set_repo_cache( 'repo', $this->type->slug );
}
if ( ! is_array( $response ) || $this->validate_response( $response ) ) {
return false;
}
$response['dot_org'] = $this->get_dot_org_data();
$this->set_file_info( $response );
return true;
}
/**
* Get remote info for tags.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_tag( $git, $request ) {
$repo_type = $this->return_repo_type();
$response = isset( $this->response['tags'] ) ? $this->response['tags'] : false;
if ( ! $response ) {
self::$method = 'tags';
$response = $this->api( $request );
if ( ! $response ) {
$response = new \stdClass();
$response->message = 'No tags found';
}
if ( $response ) {
$response = $this->parse_tag_response( $response );
$this->set_repo_cache( 'tags', $response );
}
}
if ( $this->validate_response( $response ) ) {
return false;
}
$tags = $this->parse_tags( $response, $repo_type );
$this->sort_tags( $tags );
return true;
}
/**
* Read the remote CHANGES.md file.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $changes Changelog filename.
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_changes( $git, $changes, $request ) {
$response = isset( $this->response['changes'] ) ? $this->response['changes'] : false;
// Set $response from local file if no update available.
if ( ! $response && ! $this->can_update_repo( $this->type ) ) {
$response = $this->get_local_info( $this->type, $changes );
}
if ( ! $response ) {
self::$method = 'changes';
$response = $this->api( $request );
$response = $this->decode_response( $git, $response );
}
if ( ! $response && ! is_wp_error( $response ) ) {
$response = new \stdClass();
$response->message = 'No changelog found';
}
if ( $this->validate_response( $response ) ) {
return false;
}
if ( $response && ! isset( $this->response['changes'] ) ) {
$parser = new \Parsedown();
$response = $parser->text( $response );
$this->set_repo_cache( 'changes', $response );
}
$this->type->sections['changelog'] = $response;
return true;
}
/**
* Read and parse remote readme.txt.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_readme( $git, $request ) {
if ( ! $this->local_file_exists( 'readme.txt' ) ) {
return false;
}
$response = isset( $this->response['readme'] ) ? $this->response['readme'] : false;
// Set $response from local file if no update available.
if ( ! $response && ! $this->can_update_repo( $this->type ) ) {
$response = $this->get_local_info( $this->type, 'readme.txt' );
}
if ( ! $response ) {
self::$method = 'readme';
$response = $this->api( $request );
$response = $this->decode_response( $git, $response );
}
if ( ! $response && ! is_wp_error( $response ) ) {
$response = new \stdClass();
$response->message = 'No readme found';
}
if ( $this->validate_response( $response ) ) {
return false;
}
if ( $response && ! isset( $this->response['readme'] ) ) {
$parser = new Readme_Parser( $response );
$response = $parser->parse_data();
$this->set_repo_cache( 'readme', $response );
}
$this->set_readme_info( $response );
return true;
}
/**
* Read the repository meta from API.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_repo_meta( $git, $request ) {
$response = isset( $this->response['meta'] ) ? $this->response['meta'] : false;
if ( ! $response ) {
self::$method = 'meta';
$response = $this->api( $request );
if ( $response ) {
$response = $this->parse_meta_response( $response );
$this->set_repo_cache( 'meta', $response );
}
}
if ( $this->validate_response( $response ) ) {
return false;
}
$this->type->repo_meta = $response;
$this->add_meta_repo_object();
return true;
}
/**
* Create array of branches and download links as array.
*
* @param string $git github|bitbucket|gitlab|gitea)
* @param string $request API request.
*
* @return bool
*/
public function get_remote_api_branches( $git, $request ) {
$branches = [];
$response = isset( $this->response['branches'] ) ? $this->response['branches'] : false;
if ( $this->exit_no_update( $response, true ) ) {
return false;
}
if ( ! $response ) {
self::$method = 'branches';
$response = $this->api( $request );
$response = $this->parse_response( $git, $response );
if ( $this->validate_response( $response ) ) {
return false;
}
if ( $response ) {
$branches = $this->parse_branch_response( $response );
$this->type->branches = $branches;
$this->set_repo_cache( 'branches', $branches );
return true;
}
}
$this->type->branches = $response;
return true;
}
/**
* Get API release asset download link.
*
* @param string $git (github|bitbucket|gitlab|gitea)
* @param string $request Query for API->api().
* @return string $response Release asset URI.
*/
public function get_api_release_asset( $git, $request ) {
$response = isset( $this->response['release_asset'] ) ? $this->response['release_asset'] : false;
if ( $response && $this->exit_no_update( $response ) ) {
return false;
}
if ( ! $response ) {
self::$method = 'release_asset';
$response = $this->api( $request );
$response = $this->parse_release_asset( $git, $request, $response );
if ( ! $response && ! is_wp_error( $response ) ) {
$response = new \stdClass();
$response->message = 'No release asset found';
}
}
if ( $response && ! isset( $this->response['release_asset'] ) ) {
$this->set_repo_cache( 'release_asset', $response );
}
if ( $this->validate_response( $response ) ) {
return false;
}
return $response;
}
}

View File

@@ -0,0 +1,277 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\Traits;
use Fragen\Singleton;
use Fragen\GitHub_Updater\Install;
use Fragen\GitHub_Updater\API\Bitbucket_API;
use Fragen\GitHub_Updater\API\Bitbucket_Server_API;
/*
* Exit if called directly.
*/
if ( ! defined( 'WPINC' ) ) {
die;
}
/**
* Trait Basic_Auth_Loader
*/
trait Basic_Auth_Loader {
/**
* Stores array of git servers requiring Basic Authentication.
*
* @var array
*/
private static $basic_auth_required = [ 'Bitbucket' ];
/**
* Load hooks for Bitbucket authentication headers.
*
* @access public
*/
public function load_authentication_hooks() {
add_filter( 'http_request_args', [ $this, 'maybe_basic_authenticate_http' ], 5, 2 );
add_filter( 'http_request_args', [ $this, 'http_release_asset_auth' ], 15, 2 );
}
/**
* Remove hooks for Bitbucket authentication headers.
*
* @access public
*/
public function remove_authentication_hooks() {
remove_filter( 'http_request_args', [ $this, 'maybe_basic_authenticate_http' ] );
remove_filter( 'http_request_args', [ $this, 'http_release_asset_auth' ] );
}
/**
* Add Basic Authentication $args to http_request_args filter hook
* for private repositories only.
*
* @access public
*
* @param array $args Args passed to the URL.
* @param string $url The URL.
*
* @return array $args
*/
public function maybe_basic_authenticate_http( $args, $url ) {
$credentials = $this->get_credentials( $url );
if ( $credentials['private'] && $credentials['isset'] && ! $credentials['api.wordpress'] ) {
$username = $credentials['username'];
$password = $credentials['password'];
$args['headers']['Authorization'] = 'Basic ' . base64_encode( "$username:$password" );
}
return $args;
}
/**
* Get credentials (username/password) for Basic Authentication.
*
* @access private
*
* @param string $url The URL.
*
* @return array $credentials
*/
private function get_credentials( $url ) {
$headers = parse_url( $url );
$type = $this->get_class_vars( 'Base', 'caller' );
$username_key = null;
$password_key = null;
$credentials = [
'username' => null,
'password' => null,
'api.wordpress' => 'api.wordpress.org' === $headers['host'],
'isset' => false,
'private' => false,
];
$hosts = [ 'bitbucket.org', 'api.bitbucket.org' ];
$repos = array_merge(
Singleton::get_instance( 'Plugin', $this )->get_plugin_configs(),
Singleton::get_instance( 'Theme', $this )->get_theme_configs()
);
$slug = isset( $_REQUEST['slug'] ) ? $_REQUEST['slug'] : false;
$slug = ! $slug && isset( $_REQUEST['plugin'] ) ? $_REQUEST['plugin'] : $slug;
$slug = ! $slug && isset( $_REQUEST['theme'] ) ? $_REQUEST['theme'] : $slug;
// Set for bulk upgrade.
if ( ! $slug ) {
$plugins = isset( $_REQUEST['plugins'] )
? array_map( 'dirname', explode( ',', $_REQUEST['plugins'] ) )
: [];
$themes = isset( $_REQUEST['themes'] )
? explode( ',', $_REQUEST['themes'] )
: [];
$bulk_update = array_merge( $plugins, $themes );
if ( ! empty( $bulk_update ) ) {
$slug = array_filter(
$bulk_update,
function ( $e ) use ( $url ) {
return false !== strpos( $url, $e );
}
);
$slug = array_pop( $slug );
}
}
$type = $slug &&
isset( $repos[ $slug ] ) && property_exists( $repos[ $slug ], 'git' )
? $repos[ $slug ]->git
: $type;
// Set for WP-CLI.
if ( ! $slug ) {
foreach ( $repos as $repo ) {
if ( property_exists( $repo, 'download_link' ) && $url === $repo->download_link ) {
$type = $repo->git;
break;
}
}
}
// Set for Remote Install.
$type = isset( $_POST['github_updater_api'], $_POST['github_updater_repo'] ) &&
false !== strpos( $url, basename( $_POST['github_updater_repo'] ) )
? $_POST['github_updater_api']
: $type;
switch ( $type ) {
case 'bitbucket':
case $type instanceof Bitbucket_API:
case $type instanceof Bitbucket_Server_API:
$bitbucket_org = in_array( $headers['host'], $hosts, true );
$username_key = $bitbucket_org ? 'bitbucket_username' : 'bitbucket_server_username';
$password_key = $bitbucket_org ? 'bitbucket_password' : 'bitbucket_server_password';
break;
}
// TODO: can use `( $this->caller )::$options` in PHP7.
$caller = $this->get_class_vars( 'Base', 'caller' );
static::$options = $caller instanceof Install ? $caller::$options : static::$options;
if ( isset( static::$options[ $username_key ], static::$options[ $password_key ] ) ) {
$credentials['username'] = static::$options[ $username_key ];
$credentials['password'] = static::$options[ $password_key ];
$credentials['isset'] = true;
$credentials['private'] = $this->is_repo_private( $url );
}
return $credentials;
}
/**
* Determine if repo is private.
*
* @access private
*
* @param string $url The URL.
*
* @return bool true if private
*/
private function is_repo_private( $url ) {
// Used when updating.
$slug = isset( $_REQUEST['rollback'], $_REQUEST['plugin'] ) ? dirname( $_REQUEST['plugin'] ) : false;
$slug = isset( $_REQUEST['rollback'], $_REQUEST['theme'] ) ? $_REQUEST['theme'] : $slug;
$slug = isset( $_REQUEST['slug'] ) ? $_REQUEST['slug'] : $slug;
if ( $slug && array_key_exists( $slug, static::$options ) &&
1 === (int) static::$options[ $slug ] &&
false !== stripos( $url, $slug )
) {
return true;
}
// Used for remote install tab.
if ( isset( $_POST['option_page'], $_POST['is_private'] ) &&
'github_updater_install' === $_POST['option_page']
) {
return true;
}
// Used for refreshing cache.
foreach ( array_keys( static::$options ) as $option ) {
if ( 1 === (int) static::$options[ $option ] &&
false !== strpos( $url, $option )
) {
return true;
}
}
return false;
}
/**
* Removes Basic Authentication header for Bitbucket Release Assets.
* Storage in AmazonS3 buckets, uses Query String Request Authentication Alternative.
*
* @access public
* @link http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
*
* @param array $args The URL arguments passed.
* @param string $url The URL.
*
* @return array $args
*/
public function http_release_asset_auth( $args, $url ) {
$arr_url = parse_url( $url );
if ( isset( $arr_url['host'] ) && 'bbuseruploads.s3.amazonaws.com' === $arr_url['host'] ) {
unset( $args['headers']['Authorization'] );
}
return $args;
}
/**
* Loads authentication hooks when updating from update-core.php.
*
* @param bool $reply
* @param string $package Update package URL, unused.
* @param \Plugin_Upgrader|\Theme_Upgrader $class Upgrader object.
*
* @return mixed
*/
public function upgrader_pre_download( $reply, $package, $class ) {
if ( $class instanceof \Plugin_Upgrader &&
property_exists( $class->skin, 'plugin_info' )
) {
$headers = $class->skin->plugin_info;
foreach ( self::$basic_auth_required as $git_server ) {
$ghu_header = $headers[ $git_server . ' Plugin URI' ];
if ( ! empty( $ghu_header ) ) {
$this->load_authentication_hooks();
break;
}
}
}
if ( $class instanceof \Theme_Upgrader &&
property_exists( $class->skin, 'theme_info' )
) {
$theme = $class->skin->theme_info;
foreach ( self::$basic_auth_required as $git_server ) {
$ghu_header = $theme->get( $git_server . ' Theme URI' );
if ( ! empty( $ghu_header ) ) {
$this->load_authentication_hooks();
break;
}
}
}
remove_filter( 'upgrader_pre_download', [ $this, 'upgrader_pre_download' ] );
return $reply;
}
}

View File

@@ -0,0 +1,420 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\Traits;
use Fragen\Singleton;
/**
* Trait GHU_Trait
*/
trait GHU_Trait {
/**
* Checks to see if a heartbeat is resulting in activity.
*
* @return bool
*/
public static function is_heartbeat() {
return isset( $_POST['action'] ) && 'heartbeat' === $_POST['action'];
}
/**
* Checks to see if WP_CLI.
*
* @return bool
*/
public static function is_wp_cli() {
return defined( 'WP_CLI' ) && WP_CLI;
}
/**
* Checks to see if DOING_AJAX.
*
* @return bool
*/
public static function is_doing_ajax() {
return defined( 'DOING_AJAX' ) && DOING_AJAX;
}
/**
* Load site options.
*/
public function load_options() {
$base = Singleton::get_instance( 'Base', $this );
$base::$options = get_site_option( 'github_updater', [] );
}
/**
* Check current page.
*
* @param array $pages
* @return bool
*/
public function is_current_page( array $pages ) {
global $pagenow;
return in_array( $pagenow, $pages, true );
}
/**
* Returns repo cached data.
*
* @access protected
*
* @param string|bool $repo Repo name or false.
*
* @return array|bool The repo cache. False if expired.
*/
public function get_repo_cache( $repo = false ) {
if ( ! $repo ) {
$repo = isset( $this->type->slug ) ? $this->type->slug : 'ghu';
}
$cache_key = 'ghu-' . md5( $repo );
$cache = get_site_option( $cache_key );
if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
return false;
}
return $cache;
}
/**
* Sets repo data for cache in site option.
*
* @access protected
*
* @param string $id Data Identifier.
* @param mixed $response Data to be stored.
* @param string|bool $repo Repo name or false.
* @param string|bool $timeout Timeout for cache.
* Default is $hours (12 hours).
*
* @return bool
*/
public function set_repo_cache( $id, $response, $repo = false, $timeout = false ) {
if ( is_wp_error( $response ) ) {
return false;
}
$hours = $this->get_class_vars( 'API', 'hours' );
if ( ! $repo ) {
$repo = isset( $this->type->slug ) ? $this->type->slug : 'ghu';
}
$cache_key = 'ghu-' . md5( $repo );
$timeout = $timeout ? $timeout : '+' . $hours . ' hours';
/**
* Allow filtering of cache timeout for repo information.
*
* @since 8.7.1
*
* @param string $timeout Timeout value used with strtotime().
* @param string $id Data Identifier.
* @param mixed $response Data to be stored.
* @param string|bool $repo Repo name or false.
*/
$timeout = apply_filters( 'github_updater_repo_cache_timeout', $timeout, $id, $response, $repo );
$this->response['timeout'] = strtotime( $timeout );
$this->response[ $id ] = $response;
update_site_option( $cache_key, $this->response );
return true;
}
/**
* Getter for class variables.
*
* @param string $class_name Name of class.
* @param string $var Name of variable.
*
* @return mixed
*/
public function get_class_vars( $class_name, $var ) {
$class = Singleton::get_instance( $class_name, $this );
$reflection_obj = new \ReflectionObject( $class );
if ( ! $reflection_obj->hasProperty( $var ) ) {
return false;
}
$property = $reflection_obj->getProperty( $var );
$property->setAccessible( true );
return $property->getValue( $class );
}
/**
* Returns static class variable $error_code.
*
* @return array self::$error_code
*/
public function get_error_codes() {
return $this->get_class_vars( 'API', 'error_code' );
}
/**
* Function to check if plugin or theme object is able to be updated.
*
* @param \stdClass $type
*
* @return bool
*/
public function can_update_repo( $type ) {
$wp_version = get_bloginfo( 'version' );
$wp_version_ok = ! empty( $type->requires )
? version_compare( $wp_version, $type->requires, '>=' )
: true;
$php_version_ok = ! empty( $type->requires_php )
? version_compare( phpversion(), $type->requires_php, '>=' )
: true;
$remote_is_newer = isset( $type->remote_version )
? version_compare( $type->remote_version, $type->local_version, '>' )
: false;
/**
* Filter $remote_is_newer if you use another method to test for updates.
*
* @since 8.7.0
* @param bool $remote_is_newer
* @param \stdClass $type Plugin/Theme data.
*/
$remote_is_newer = apply_filters( 'github_updater_remote_is_newer', $remote_is_newer, $type );
return $remote_is_newer && $wp_version_ok && $php_version_ok;
}
/**
* Delete all `ghu-` prefixed data from options table.
*
* @return bool
*/
public function delete_all_cached_data() {
global $wpdb;
$table = is_multisite() ? $wpdb->base_prefix . 'sitemeta' : $wpdb->base_prefix . 'options';
$column = is_multisite() ? 'meta_key' : 'option_name';
$delete_string = 'DELETE FROM ' . $table . ' WHERE ' . $column . ' LIKE %s LIMIT 1000';
$wpdb->query( $wpdb->prepare( $delete_string, [ '%ghu-%' ] ) );
wp_cron();
return true;
}
/**
* Is this a private repo with a token/checked or needing token/checked?
* Test for whether remote_version is set ( default = 0.0.0 ) or
* a repo option is set/not empty.
*
* @param \stdClass $repo
*
* @return bool
*/
public function is_private( $repo ) {
if ( ! isset( $repo->remote_version ) && ! self::is_doing_ajax() ) {
return true;
}
if ( isset( $repo->remote_version ) && ! self::is_doing_ajax() ) {
return ( '0.0.0' === $repo->remote_version ) || ! empty( self::$options[ $repo->slug ] );
}
return false;
}
/**
* Do we override dot org updates?
*
* @param string $type (plugin|theme)
* @param \stdClass $repo Repository object.
*
* @return bool
*/
public function override_dot_org( $type, $repo ) {
// Correctly account for dashicon in Settings page.
$icon = is_array( $repo );
$repo = is_array( $repo ) ? (object) $repo : $repo;
$dot_org_master = ! $icon ? $repo->dot_org && 'master' === $repo->branch : true;
$transient_key = 'plugin' === $type ? $repo->file : null;
$transient_key = 'theme' === $type ? $repo->slug : $transient_key;
/**
* Filter update to override dot org.
*
* @since 8.5.0
*
* @return bool
*/
$override = in_array( $transient_key, apply_filters( 'github_updater_override_dot_org', [] ), true );
return ! $dot_org_master || $override || $this->deprecate_override_constant();
}
/**
* Deprecated dot org override constant.
*
* @return bool
*/
public function deprecate_override_constant() {
if ( defined( 'GITHUB_UPDATER_OVERRIDE_DOT_ORG' ) && GITHUB_UPDATER_OVERRIDE_DOT_ORG ) {
error_log( 'GITHUB_UPDATER_OVERRIDE_DOT_ORG constant deprecated. Use `github_updater_override_dot_org` filter hook.' );
return true;
}
return false;
}
/**
* Sanitize each setting field as needed.
*
* @param array $input Contains all settings fields as array keys.
*
* @return array
*/
public function sanitize( $input ) {
$new_input = [];
foreach ( array_keys( (array) $input ) as $id ) {
$new_input[ sanitize_file_name( $id ) ] = sanitize_text_field( $input[ $id ] );
}
return $new_input;
}
/**
* Return an array of the running git servers.
*
* @access public
* @return array $gits
*/
public function get_running_git_servers() {
$plugins = Singleton::get_instance( 'Plugin', $this )->get_plugin_configs();
$themes = Singleton::get_instance( 'Theme', $this )->get_theme_configs();
$repos = array_merge( $plugins, $themes );
$gits = array_map(
function ( $e ) {
if ( ! empty( $e->enterprise ) ) {
if ( 'bitbucket' === $e->git ) {
return 'bbserver';
}
if ( 'gitlab' === $e->git ) {
return 'gitlabce';
}
}
return $e->git;
},
$repos
);
return array_unique( array_values( $gits ) );
}
/**
* Parse URI param returning array of parts.
*
* @param string $repo_header
*
* @return array $header
*/
protected function parse_header_uri( $repo_header ) {
$header_parts = parse_url( $repo_header );
$header_path = pathinfo( $header_parts['path'] );
$header['original'] = $repo_header;
$header['scheme'] = isset( $header_parts['scheme'] ) ? $header_parts['scheme'] : null;
$header['host'] = isset( $header_parts['host'] ) ? $header_parts['host'] : null;
$header['owner'] = trim( $header_path['dirname'], '/' );
$header['repo'] = $header_path['filename'];
$header['owner_repo'] = implode( '/', [ $header['owner'], $header['repo'] ] );
$header['base_uri'] = str_replace( $header_parts['path'], '', $repo_header );
$header['uri'] = isset( $header['scheme'] ) ? trim( $repo_header, '/' ) : null;
$header = $this->sanitize( $header );
return $header;
}
/**
* Take remote file contents as string or array and parse and reduce headers.
*
* @param string|array $contents File contents or array of file headers.
* @param string $type plugin|theme.
*
* @return array $all_headers Reduced array of all headers.
*/
public function get_file_headers( $contents, $type ) {
$all_headers = [];
$default_plugin_headers = [
'Name' => 'Plugin Name',
'PluginURI' => 'Plugin URI',
'Version' => 'Version',
'Description' => 'Description',
'Author' => 'Author',
'AuthorURI' => 'Author URI',
'TextDomain' => 'Text Domain',
'DomainPath' => 'Domain Path',
'Network' => 'Network',
];
$default_theme_headers = [
'Name' => 'Theme Name',
'ThemeURI' => 'Theme URI',
'Description' => 'Description',
'Author' => 'Author',
'AuthorURI' => 'Author URI',
'Version' => 'Version',
'Template' => 'Template',
'Status' => 'Status',
'Tags' => 'Tags',
'TextDomain' => 'Text Domain',
'DomainPath' => 'Domain Path',
];
if ( 'plugin' === $type ) {
$all_headers = $default_plugin_headers;
}
if ( 'theme' === $type ) {
$all_headers = $default_theme_headers;
}
/*
* Merge extra headers and default headers.
*/
$all_headers = array_merge( self::$extra_headers, $all_headers );
$all_headers = array_unique( $all_headers );
/*
* Make sure we catch CR-only line endings.
*/
if ( is_string( $contents ) ) {
$file_data = str_replace( "\r", "\n", $contents );
foreach ( $all_headers as $field => $regex ) {
if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
$all_headers[ $field ] = _cleanup_header_comment( $match[1] );
} else {
$all_headers[ $field ] = '';
}
}
}
$all_headers = is_array( $contents ) ? $contents : $all_headers;
// Reduce array to only headers with data.
$all_headers = array_filter(
$all_headers,
function ( $e ) {
return ! empty( $e );
}
);
return $all_headers;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\WP_CLI;
use WP_CLI;
use WP_CLI_Command;
use Fragen\Singleton;
// Add WP-CLI commands.
WP_CLI::add_command( 'github-updater', 'Fragen\\GitHub_Updater\\WP_CLI\\CLI' );
/**
* Manage GitHub Updater commands.
*
* Class GitHub_Updater_CLI
*/
class CLI extends WP_CLI_Command {
/**
* Clear GitHub Updater cache.
*
* ## OPTIONS
*
* <delete>
* : delete the cache
*
* ## EXAMPLES
*
* wp github-updater cache delete
*
* @param array $args Array of arguments.
*
* @subcommand cache
*/
public function cache( $args ) {
list($action) = $args;
if ( 'delete' === $action ) {
Singleton::get_instance( 'CLI_Common', $this )->delete_all_cached_data();
WP_CLI::success( 'GitHub Updater cache has been cleared.' );
} else {
WP_CLI::error( sprintf( 'Incorrect command syntax, see %s for proper syntax.', '`wp help github-updater cache`' ) );
}
WP_CLI::success( 'WP-Cron is now running.' );
WP_CLI::runcommand( 'cron event run --due-now' );
}
/**
* Reset GitHub Updater REST API key.
*
* ## EXAMPLES
*
* wp github-updater reset-api-key
*
* @subcommand reset-api-key
*/
public function reset_api_key() {
delete_site_option( 'github_updater_api_key' );
Singleton::get_instance( 'Remote_Management', $this )->ensure_api_key_is_set();
$api_key = get_site_option( 'github_updater_api_key' );
$api_url = add_query_arg(
[
'action' => 'github-updater-update',
'key' => $api_key,
],
admin_url( 'admin-ajax.php' )
);
WP_CLI::success( 'GitHub Updater REST API key has been reset.' );
WP_CLI::success( sprintf( 'The current RESTful endpoint is `%s`', $api_url ) );
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\WP_CLI;
/**
* Class CLI_Common
*/
class CLI_Common {
/**
* Delete all `ghu-` prefixed data from options table.
*
* @return bool
*/
public function delete_all_cached_data() {
global $wpdb;
$table = is_multisite() ? $wpdb->base_prefix . 'sitemeta' : $wpdb->base_prefix . 'options';
$column = is_multisite() ? 'meta_key' : 'option_name';
$delete_string = 'DELETE FROM ' . $table . ' WHERE ' . $column . ' LIKE %s LIMIT 1000';
$wpdb->query( $wpdb->prepare( $delete_string, [ '%ghu-%' ] ) );
return true;
}
}

View File

@@ -0,0 +1,274 @@
<?php
/**
* GitHub Updater
*
* @author Andy Fragen
* @license GPL-2.0+
* @link https://github.com/afragen/github-updater
* @package github-updater
*/
namespace Fragen\GitHub_Updater\WP_CLI;
use WP_CLI;
use WP_CLI_Command;
use Fragen\Singleton;
// Add WP-CLI commands.
$class = new CLI_Integration();
WP_CLI::add_command( 'plugin install-git', [ $class, 'install_plugin' ] );
WP_CLI::add_command( 'theme install-git', [ $class, 'install_theme' ] );
/**
* Class CLI_Integration
*/
class CLI_Integration extends WP_CLI_Command {
/**
* CLI_Integration constructor.
*/
public function __construct() {
$this->run();
}
/**
* Off to the races.
*/
public function run() {
add_filter( 'site_transient_update_plugins', [ Singleton::get_instance( 'Plugin', $this ), 'update_site_transient' ], 10, 1 );
add_filter( 'site_transient_update_themes', [ Singleton::get_instance( 'Theme', $this ), 'update_site_transient' ], 10, 1 );
}
/**
* Install plugin from GitHub, Bitbucket, GitLab, or Gitea using GitHub Updater.
*
* ## OPTIONS
*
* <uri>
* : URI to the repo being installed
*
* [--branch=<branch_name>]
* : String indicating the branch name to be installed
* ---
* default: master
* ---
*
* [--token=<access_token>]
* : GitHub, GitLab, or Gitea access token if not already saved
*
* [--bitbucket-private]
* : Indicates a private Bitbucket repository
*
* [--slug=<slug>]
* : Optional string indicating the plugin slug
* [--github]
* : Optional to denote a GitHub repository
* Required when installing from a self-hosted GitHub installation
*
* [--bitbucket]
* : Optional switch to denote a Bitbucket repository
* Required when installing from a self-hosted Bitbucket installation
*
* [--gitlab]
* : Optional switch to denote a GitLab repository
* Required when installing from a self-hosted GitLab installation
*
* [--gitea]
* : Optional switch to denote a Gitea repository
* Required when installing from a Gitea installation
*
* [--zipfile]
* : Optional switch to denote a Zipfile
* Required when installing from a Zipfile
*
* ## EXAMPLES
*
* wp plugin install-git https://github.com/afragen/my-plugin
*
* wp plugin install-git https://github.com/afragen/my-plugin --branch=develop --github
*
* wp plugin install-git https://bitbucket.org/afragen/my-private-plugin --bitbucket-private
*
* wp plugin install-git https://github.com/afragen/my-private-plugin --token=lks9823evalki
*
* @param array $args An array of $uri.
* @param array $assoc_args Array of optional arguments.
*
* @subcommand install-git
*/
public function install_plugin( $args, $assoc_args ) {
list($uri) = $args;
$cli_config = $this->process_args( $uri, $assoc_args );
Singleton::get_instance( 'Install', $this )->install( 'plugin', $cli_config );
$headers = parse_url( $uri, PHP_URL_PATH );
$slug = basename( $headers );
$this->process_branch( $cli_config, $slug );
WP_CLI::success( sprintf( 'Plugin %s installed.', "'$slug'" ) );
}
/**
* Install theme from GitHub, Bitbucket, GitLab, or Gitea using GitHub Updater.
*
* ## OPTIONS
*
* <uri>
* : URI to the repo being installed
*
* [--branch=<branch_name>]
* : String indicating the branch name to be installed
* ---
* default: master
* ---
*
* [--token=<access_token>]
* : GitHub or GitLab access token if not already saved
*
* [--bitbucket-private]
* : Indicates a private Bitbucket repository
*
* [--slug=<slug>]
* : Optional string indicating the theme slug
*
* [--github]
* : Optional to denote a GitHub repository
* Required when installing from a self-hosted GitHub installation
*
* [--bitbucket]
* : Optional switch to denote a Bitbucket repository
* Required when installing from a self-hosted Bitbucket installation
*
* [--gitlab]
* : Optional switch to denote a GitLab repository
* Required when installing from a self-hosted GitLab installation
*
* [--gitea]
* : Optional switch to denote a Gitea repository
* Required when installing from a Gitea installation
*
* [--zipfile]
* : Optional switch to denote a Zipfile
* Required when installing from a Zipfile
*
* ## EXAMPLES
*
* wp theme install-git https://github.com/afragen/my-theme
*
* wp theme install-git https://bitbucket.org/afragen/my-theme --branch=develop --bitbucket
*
* wp theme install-git https://bitbucket.org/afragen/my-private-theme --bitbucket-private
*
* wp theme install-git https://github.com/afragen/my-private-theme --token=lks9823evalki
*
* @param array $args An array of $uri.
* @param array $assoc_args Array of optional arguments.
*
* @subcommand install-git
*/
public function install_theme( $args, $assoc_args ) {
list($uri) = $args;
$cli_config = $this->process_args( $uri, $assoc_args );
Singleton::get_instance( 'Install', $this )->install( 'theme', $cli_config );
$headers = parse_url( $uri, PHP_URL_PATH );
$slug = basename( $headers );
$this->process_branch( $cli_config, $slug );
WP_CLI::success( sprintf( 'Theme %s installed.', "'$slug'" ) );
}
/**
* Process WP-CLI config data.
*
* @param string $uri URI to process.
* @param array $assoc_args Args to process.
*
* @return array $cli_config
*/
private function process_args( $uri, $assoc_args ) {
$token = isset( $assoc_args['token'] ) ? $assoc_args['token'] : false;
$bitbucket_private = isset( $assoc_args['bitbucket-private'] ) ? $assoc_args['bitbucket-private'] : false;
$cli_config = [];
$cli_config['uri'] = $uri;
$cli_config['private'] = $token ?: $bitbucket_private;
$cli_config['branch'] = isset( $assoc_args['branch'] ) ? $assoc_args['branch'] : 'master';
$cli_config['slug'] = isset( $assoc_args['slug'] ) ? $assoc_args['slug'] : null;
switch ( $assoc_args ) {
case isset( $assoc_args['github'] ):
$cli_config['git'] = 'github';
break;
case isset( $assoc_args['bitbucket'] ):
$cli_config['git'] = 'bitbucket';
break;
case isset( $assoc_args['gitlab'] ):
$cli_config['git'] = 'gitlab';
break;
case isset( $assoc_args['gitea'] ):
$cli_config['git'] = 'gitea';
break;
case isset( $assoc_args['zipfile'] ):
$cli_config['git'] = 'zipfile';
break;
}
return $cli_config;
}
/**
* Process branch setting for WP-CLI.
*
* @param array $cli_config Config args.
* @param string $slug Repository slug.
*/
private function process_branch( $cli_config, $slug ) {
$branch_data['github_updater_branch'] = $cli_config['branch'];
$branch_data['repo'] = $slug;
Singleton::get_instance( 'Branch', $this )->set_branch_on_install( $branch_data );
}
}
/**
* Use custom installer skins to display error messages.
*/
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
/**
* Class GitHub_Upgrader_CLI_Plugin_Installer_Skin
*/
class CLI_Plugin_Installer_Skin extends \Plugin_Installer_Skin {
public function header() {
}
public function footer() {
}
public function error( $errors ) {
if ( is_wp_error( $errors ) ) {
WP_CLI::error( $errors->get_error_message() . "\n" . $errors->get_error_data() );
}
}
public function feedback( $string ) {
}
}
/**
* Class GitHub_Upgrader_CLI_Theme_Installer_Skin
*/
class CLI_Theme_Installer_Skin extends \Theme_Installer_Skin {
public function header() {
}
public function footer() {
}
public function error( $errors ) {
if ( is_wp_error( $errors ) ) {
WP_CLI::error( $errors->get_error_message() . "\n" . $errors->get_error_data() );
}
}
public function feedback( $string ) {
}
}