Update bundled plugin-update-checker to v5.7 and remove broken setBranch() call
The bundled PUC was missing the setBranch() method on the main UpdateChecker class (it's only on Vcs\PluginUpdateChecker via the VcsCheckerMethods trait). When called with a non-VCS URL like the Gitea update URL, this caused a fatal error at plugin activation. Updating to PUC v5.7 (May 2026) which is the latest upstream release. The setBranch() call is removed because PUC doesn't recognize Gitea as a VCS host; the plugin falls back to PUC's plain JSON metadata mode, which is fine for a plugin hosted on a self-managed repo.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\PucFactory;
|
||||
|
||||
if ( !class_exists(Package::class, false) ):
|
||||
|
||||
class Package extends InstalledPackage {
|
||||
/**
|
||||
* @var UpdateChecker
|
||||
*/
|
||||
protected $updateChecker;
|
||||
|
||||
/**
|
||||
* @var string Full path of the main plugin file.
|
||||
*/
|
||||
protected $pluginAbsolutePath = '';
|
||||
|
||||
/**
|
||||
* @var string Plugin basename.
|
||||
*/
|
||||
private $pluginFile;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $cachedInstalledVersion = null;
|
||||
|
||||
public function __construct($pluginAbsolutePath, $updateChecker) {
|
||||
$this->pluginAbsolutePath = $pluginAbsolutePath;
|
||||
$this->pluginFile = plugin_basename($this->pluginAbsolutePath);
|
||||
|
||||
parent::__construct($updateChecker);
|
||||
|
||||
//Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
|
||||
add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
|
||||
add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
|
||||
}
|
||||
|
||||
public function getInstalledVersion() {
|
||||
if ( isset($this->cachedInstalledVersion) ) {
|
||||
return $this->cachedInstalledVersion;
|
||||
}
|
||||
|
||||
$pluginHeader = $this->getPluginHeader();
|
||||
if ( isset($pluginHeader['Version']) ) {
|
||||
$this->cachedInstalledVersion = $pluginHeader['Version'];
|
||||
return $pluginHeader['Version'];
|
||||
} else {
|
||||
//This can happen if the filename points to something that is not a plugin.
|
||||
$this->updateChecker->triggerError(
|
||||
sprintf(
|
||||
"Cannot read the Version header for '%s'. The filename is incorrect or is not a plugin.",
|
||||
$this->updateChecker->pluginFile
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cached plugin version. This method can be set up as a filter (hook) and will
|
||||
* return the filter argument unmodified.
|
||||
*
|
||||
* @param mixed $filterArgument
|
||||
* @return mixed
|
||||
*/
|
||||
public function clearCachedVersion($filterArgument = null) {
|
||||
$this->cachedInstalledVersion = null;
|
||||
return $filterArgument;
|
||||
}
|
||||
|
||||
public function getAbsoluteDirectoryPath() {
|
||||
return dirname($this->pluginAbsolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a specific plugin or theme header.
|
||||
*
|
||||
* @param string $headerName
|
||||
* @param string $defaultValue
|
||||
* @return string Either the value of the header, or $defaultValue if the header doesn't exist or is empty.
|
||||
*/
|
||||
public function getHeaderValue($headerName, $defaultValue = '') {
|
||||
$headers = $this->getPluginHeader();
|
||||
if ( isset($headers[$headerName]) && ($headers[$headerName] !== '') ) {
|
||||
return $headers[$headerName];
|
||||
}
|
||||
return $defaultValue;
|
||||
}
|
||||
|
||||
protected function getHeaderNames() {
|
||||
return array(
|
||||
'Name' => 'Plugin Name',
|
||||
'PluginURI' => 'Plugin URI',
|
||||
'Version' => 'Version',
|
||||
'Description' => 'Description',
|
||||
'Author' => 'Author',
|
||||
'AuthorURI' => 'Author URI',
|
||||
'TextDomain' => 'Text Domain',
|
||||
'DomainPath' => 'Domain Path',
|
||||
'Network' => 'Network',
|
||||
|
||||
//The newest WordPress version that this plugin requires or has been tested with.
|
||||
//We support several different formats for compatibility with other libraries.
|
||||
'Tested WP' => 'Tested WP',
|
||||
'Requires WP' => 'Requires WP',
|
||||
'Tested up to' => 'Tested up to',
|
||||
'Requires at least' => 'Requires at least',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated plugin title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginTitle() {
|
||||
$title = '';
|
||||
$header = $this->getPluginHeader();
|
||||
if ( $header && !empty($header['Name']) && isset($header['TextDomain']) ) {
|
||||
$title = translate($header['Name'], $header['TextDomain']);
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin's metadata from its file header.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPluginHeader() {
|
||||
if ( !is_file($this->pluginAbsolutePath) ) {
|
||||
//This can happen if the plugin filename is wrong.
|
||||
$this->updateChecker->triggerError(
|
||||
sprintf(
|
||||
"Can't to read the plugin header for '%s'. The file does not exist.",
|
||||
$this->updateChecker->pluginFile
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( !function_exists('get_plugin_data') ) {
|
||||
require_once(ABSPATH . '/wp-admin/includes/plugin.php');
|
||||
}
|
||||
return get_plugin_data($this->pluginAbsolutePath, false, false);
|
||||
}
|
||||
|
||||
public function removeHooks() {
|
||||
remove_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
|
||||
remove_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin file is inside the mu-plugins directory.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMuPlugin() {
|
||||
static $cachedResult = null;
|
||||
|
||||
if ( $cachedResult === null ) {
|
||||
if ( !defined('WPMU_PLUGIN_DIR') || !is_string(WPMU_PLUGIN_DIR) ) {
|
||||
$cachedResult = false;
|
||||
return $cachedResult;
|
||||
}
|
||||
|
||||
//Convert both paths to the canonical form before comparison.
|
||||
$muPluginDir = realpath(WPMU_PLUGIN_DIR);
|
||||
$pluginPath = realpath($this->pluginAbsolutePath);
|
||||
//If realpath() fails, just normalize the syntax instead.
|
||||
if (($muPluginDir === false) || ($pluginPath === false)) {
|
||||
$muPluginDir = PucFactory::normalizePath(WPMU_PLUGIN_DIR);
|
||||
$pluginPath = PucFactory::normalizePath($this->pluginAbsolutePath);
|
||||
}
|
||||
|
||||
$cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
|
||||
}
|
||||
|
||||
return $cachedResult;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Metadata;
|
||||
|
||||
if ( !class_exists(PluginInfo::class, false) ):
|
||||
|
||||
/**
|
||||
* A container class for holding and transforming various plugin metadata.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2016
|
||||
* @access public
|
||||
*/
|
||||
class PluginInfo extends Metadata {
|
||||
//Most fields map directly to the contents of the plugin's info.json file.
|
||||
//See the relevant docs for a description of their meaning.
|
||||
public $name;
|
||||
public $slug;
|
||||
public $version;
|
||||
public $homepage;
|
||||
public $sections = array();
|
||||
public $download_url;
|
||||
|
||||
public $banners;
|
||||
public $icons = array();
|
||||
public $translations = array();
|
||||
|
||||
public $author;
|
||||
public $author_homepage;
|
||||
|
||||
public $requires;
|
||||
public $tested;
|
||||
public $requires_php;
|
||||
public $upgrade_notice;
|
||||
|
||||
public $rating;
|
||||
public $num_ratings;
|
||||
public $downloaded;
|
||||
public $active_installs;
|
||||
public $last_updated;
|
||||
public $autoupdate = false;
|
||||
|
||||
public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.
|
||||
|
||||
public $filename; //Plugin filename relative to the plugins directory.
|
||||
|
||||
/**
|
||||
* Create a new instance of Plugin Info from JSON-encoded plugin info
|
||||
* returned by an external update API.
|
||||
*
|
||||
* @param string $json Valid JSON string representing plugin info.
|
||||
* @return self|null New instance of Plugin Info, or NULL on error.
|
||||
*/
|
||||
public static function fromJson($json){
|
||||
$instance = new self();
|
||||
|
||||
if ( !parent::createFromJson($json, $instance) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
//json_decode decodes assoc. arrays as objects. We want them as arrays.
|
||||
$instance->sections = (array)$instance->sections;
|
||||
$instance->icons = (array)$instance->icons;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Very, very basic validation.
|
||||
*
|
||||
* @param \StdClass $apiResponse
|
||||
* @return bool|\WP_Error
|
||||
*/
|
||||
protected function validateMetadata($apiResponse) {
|
||||
if (
|
||||
!isset($apiResponse->name, $apiResponse->version)
|
||||
|| empty($apiResponse->name)
|
||||
|| empty($apiResponse->version)
|
||||
) {
|
||||
return new \WP_Error(
|
||||
'puc-invalid-metadata',
|
||||
"The plugin metadata file does not contain the required 'name' and/or 'version' keys."
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Transform plugin info into the format used by the native WordPress.org API
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function toWpFormat(){
|
||||
$info = new \stdClass;
|
||||
|
||||
//The custom update API is built so that many fields have the same name and format
|
||||
//as those returned by the native WordPress.org API. These can be assigned directly.
|
||||
$sameFormat = array(
|
||||
'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',
|
||||
'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',
|
||||
'requires_php',
|
||||
);
|
||||
foreach($sameFormat as $field){
|
||||
if ( isset($this->$field) ) {
|
||||
$info->$field = $this->$field;
|
||||
} else {
|
||||
$info->$field = null;
|
||||
}
|
||||
}
|
||||
|
||||
//Other fields need to be renamed and/or transformed.
|
||||
$info->download_link = $this->download_url;
|
||||
$info->author = $this->getFormattedAuthor();
|
||||
$info->sections = array_merge(array('description' => ''), $this->sections);
|
||||
|
||||
if ( !empty($this->banners) ) {
|
||||
//WP expects an array with two keys: "high" and "low". Both are optional.
|
||||
//Docs: https://wordpress.org/plugins/about/faq/#banners
|
||||
$info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;
|
||||
$info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
protected function getFormattedAuthor() {
|
||||
if ( !empty($this->author_homepage) ){
|
||||
/** @noinspection HtmlUnknownTarget */
|
||||
return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);
|
||||
}
|
||||
return $this->author;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
if ( !class_exists(Ui::class, false) ):
|
||||
/**
|
||||
* Additional UI elements for plugins.
|
||||
*/
|
||||
class Ui {
|
||||
private $updateChecker;
|
||||
private $manualCheckErrorTransient = '';
|
||||
|
||||
/**
|
||||
* @param UpdateChecker $updateChecker
|
||||
*/
|
||||
public function __construct($updateChecker) {
|
||||
$this->updateChecker = $updateChecker;
|
||||
$this->manualCheckErrorTransient = $this->updateChecker->getUniqueName('manual_check_errors');
|
||||
|
||||
add_action('admin_init', array($this, 'onAdminInit'));
|
||||
}
|
||||
|
||||
public function onAdminInit() {
|
||||
if ( $this->updateChecker->userCanInstallUpdates() ) {
|
||||
$this->handleManualCheck();
|
||||
|
||||
add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
|
||||
add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
|
||||
add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "View Details" link to the plugin row in the "Plugins" page. By default,
|
||||
* the new link will appear before the "Visit plugin site" link (if present).
|
||||
*
|
||||
* You can change the link text by using the "puc_view_details_link-$slug" filter.
|
||||
* Returning an empty string from the filter will disable the link.
|
||||
*
|
||||
* You can change the position of the link using the
|
||||
* "puc_view_details_link_position-$slug" filter.
|
||||
* Returning 'before' or 'after' will place the link immediately before/after
|
||||
* the "Visit plugin site" link.
|
||||
* Returning 'append' places the link after any existing links at the time of the hook.
|
||||
* Returning 'replace' replaces the "Visit plugin site" link.
|
||||
* Returning anything else disables the link when there is a "Visit plugin site" link.
|
||||
*
|
||||
* If there is no "Visit plugin site" link 'append' is always used!
|
||||
*
|
||||
* @param array $pluginMeta Array of meta links.
|
||||
* @param string $pluginFile
|
||||
* @param array $pluginData Array of plugin header data.
|
||||
* @return array
|
||||
*/
|
||||
public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
|
||||
if ( $this->isMyPluginFile($pluginFile) && !isset($pluginData['slug']) ) {
|
||||
$linkText = apply_filters($this->updateChecker->getUniqueName('view_details_link'), __('View details'));
|
||||
if ( !empty($linkText) ) {
|
||||
$viewDetailsLinkPosition = 'append';
|
||||
|
||||
//Find the "Visit plugin site" link (if present).
|
||||
$visitPluginSiteLinkIndex = count($pluginMeta) - 1;
|
||||
if ( $pluginData['PluginURI'] ) {
|
||||
$escapedPluginUri = esc_url($pluginData['PluginURI']);
|
||||
foreach ($pluginMeta as $linkIndex => $existingLink) {
|
||||
if ( strpos($existingLink, $escapedPluginUri) !== false ) {
|
||||
$visitPluginSiteLinkIndex = $linkIndex;
|
||||
$viewDetailsLinkPosition = apply_filters(
|
||||
$this->updateChecker->getUniqueName('view_details_link_position'),
|
||||
'before'
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$viewDetailsLink = sprintf('<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
|
||||
esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->updateChecker->slug) .
|
||||
'&TB_iframe=true&width=600&height=550')),
|
||||
esc_attr(sprintf(__('More information about %s'), $pluginData['Name'])),
|
||||
esc_attr($pluginData['Name']),
|
||||
$linkText
|
||||
);
|
||||
switch ($viewDetailsLinkPosition) {
|
||||
case 'before':
|
||||
array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
|
||||
break;
|
||||
case 'after':
|
||||
array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
|
||||
break;
|
||||
case 'replace':
|
||||
$pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
|
||||
break;
|
||||
case 'append':
|
||||
default:
|
||||
$pluginMeta[] = $viewDetailsLink;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pluginMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
|
||||
* the new link will appear after the "Visit plugin site" link if present, otherwise
|
||||
* after the "View plugin details" link.
|
||||
*
|
||||
* You can change the link text by using the "puc_manual_check_link-$slug" filter.
|
||||
* Returning an empty string from the filter will disable the link.
|
||||
*
|
||||
* @param array $pluginMeta Array of meta links.
|
||||
* @param string $pluginFile
|
||||
* @return array
|
||||
*/
|
||||
public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
|
||||
if ( $this->isMyPluginFile($pluginFile) ) {
|
||||
$linkUrl = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'puc_check_for_updates' => 1,
|
||||
'puc_slug' => $this->updateChecker->slug,
|
||||
),
|
||||
self_admin_url('plugins.php')
|
||||
),
|
||||
'puc_check_for_updates'
|
||||
);
|
||||
|
||||
$linkText = apply_filters(
|
||||
$this->updateChecker->getUniqueName('manual_check_link'),
|
||||
__('Check for updates', 'plugin-update-checker')
|
||||
);
|
||||
if ( !empty($linkText) ) {
|
||||
/** @noinspection HtmlUnknownTarget */
|
||||
$pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
|
||||
}
|
||||
}
|
||||
return $pluginMeta;
|
||||
}
|
||||
|
||||
protected function isMyPluginFile($pluginFile) {
|
||||
return ($pluginFile == $this->updateChecker->pluginFile)
|
||||
|| (!empty($this->updateChecker->muPluginFile) && ($pluginFile == $this->updateChecker->muPluginFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates when the user clicks the "Check for updates" link.
|
||||
*
|
||||
* @see self::addCheckForUpdatesLink()
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handleManualCheck() {
|
||||
$shouldCheck =
|
||||
isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
|
||||
&& $_GET['puc_slug'] == $this->updateChecker->slug
|
||||
&& check_admin_referer('puc_check_for_updates');
|
||||
|
||||
if ( $shouldCheck ) {
|
||||
$update = $this->updateChecker->checkForUpdates();
|
||||
$status = ($update === null) ? 'no_update' : 'update_available';
|
||||
$lastRequestApiErrors = $this->updateChecker->getLastRequestApiErrors();
|
||||
|
||||
if ( ($update === null) && !empty($lastRequestApiErrors) ) {
|
||||
//Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
|
||||
//file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
|
||||
//from working. Maybe the plugin simply doesn't have a readme.
|
||||
//Let's only show important errors.
|
||||
$foundCriticalErrors = false;
|
||||
$questionableErrorCodes = array(
|
||||
'puc-github-http-error',
|
||||
'puc-gitlab-http-error',
|
||||
'puc-bitbucket-http-error',
|
||||
);
|
||||
|
||||
foreach ($lastRequestApiErrors as $item) {
|
||||
$wpError = $item['error'];
|
||||
/** @var \WP_Error $wpError */
|
||||
if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
|
||||
$foundCriticalErrors = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $foundCriticalErrors ) {
|
||||
$status = 'error';
|
||||
set_site_transient($this->manualCheckErrorTransient, $lastRequestApiErrors, 60);
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect(add_query_arg(
|
||||
array(
|
||||
'puc_update_check_result' => $status,
|
||||
'puc_slug' => $this->updateChecker->slug,
|
||||
),
|
||||
self_admin_url('plugins.php')
|
||||
));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the results of a manual update check.
|
||||
*
|
||||
* @see self::handleManualCheck()
|
||||
*
|
||||
* You can change the result message by using the "puc_manual_check_message-$slug" filter.
|
||||
*/
|
||||
public function displayManualCheckResult() {
|
||||
//phpcs:disable WordPress.Security.NonceVerification.Recommended -- Just displaying a message.
|
||||
if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->updateChecker->slug) ) {
|
||||
$status = sanitize_key($_GET['puc_update_check_result']);
|
||||
$title = $this->updateChecker->getInstalledPackage()->getPluginTitle();
|
||||
$noticeClass = 'updated notice-success';
|
||||
$details = '';
|
||||
|
||||
if ( $status == 'no_update' ) {
|
||||
$message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
|
||||
} else if ( $status == 'update_available' ) {
|
||||
$message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
|
||||
} else if ( $status === 'error' ) {
|
||||
$message = sprintf(_x('Could not determine if updates are available for %s.', 'the plugin title', 'plugin-update-checker'), $title);
|
||||
$noticeClass = 'error notice-error';
|
||||
|
||||
$details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
|
||||
delete_site_transient($this->manualCheckErrorTransient);
|
||||
} else {
|
||||
$message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), $status);
|
||||
$noticeClass = 'error notice-error';
|
||||
}
|
||||
|
||||
$message = esc_html($message);
|
||||
|
||||
//Plugins can replace the message with their own, including adding HTML.
|
||||
$message = apply_filters(
|
||||
$this->updateChecker->getUniqueName('manual_check_message'),
|
||||
$message,
|
||||
$status
|
||||
);
|
||||
|
||||
printf(
|
||||
'<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
|
||||
esc_attr($noticeClass),
|
||||
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Was escaped above, and plugins can add HTML.
|
||||
$message,
|
||||
//phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Contains HTML. Content should already be escaped.
|
||||
$details
|
||||
);
|
||||
}
|
||||
//phpcs:enable
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the list of errors that were thrown during an update check.
|
||||
*
|
||||
* @param array $errors
|
||||
* @return string
|
||||
*/
|
||||
protected function formatManualCheckErrors($errors) {
|
||||
if ( empty($errors) ) {
|
||||
return '';
|
||||
}
|
||||
$output = '';
|
||||
|
||||
$showAsList = count($errors) > 1;
|
||||
if ( $showAsList ) {
|
||||
$output .= '<ol>';
|
||||
$formatString = '<li>%1$s <code>%2$s</code></li>';
|
||||
} else {
|
||||
$formatString = '<p>%1$s <code>%2$s</code></p>';
|
||||
}
|
||||
foreach ($errors as $item) {
|
||||
$wpError = $item['error'];
|
||||
/** @var \WP_Error $wpError */
|
||||
$output .= sprintf(
|
||||
$formatString,
|
||||
esc_html($wpError->get_error_message()),
|
||||
esc_html($wpError->get_error_code())
|
||||
);
|
||||
}
|
||||
if ( $showAsList ) {
|
||||
$output .= '</ol>';
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function removeHooks() {
|
||||
remove_action('admin_init', array($this, 'onAdminInit'));
|
||||
remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
|
||||
remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
|
||||
remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
|
||||
}
|
||||
}
|
||||
endif;
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Update as BaseUpdate;
|
||||
|
||||
if ( !class_exists(Update::class, false) ):
|
||||
|
||||
/**
|
||||
* A simple container class for holding information about an available update.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2016
|
||||
* @access public
|
||||
*/
|
||||
class Update extends BaseUpdate {
|
||||
public $id = 0;
|
||||
public $homepage;
|
||||
public $upgrade_notice;
|
||||
public $tested;
|
||||
public $requires_php = false;
|
||||
public $icons = array();
|
||||
public $filename; //Plugin filename relative to the plugins directory.
|
||||
public $autoupdate = false;
|
||||
|
||||
protected static $extraFields = array(
|
||||
'id', 'homepage', 'tested', 'requires_php', 'upgrade_notice', 'icons', 'filename', 'autoupdate',
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new instance of PluginUpdate from its JSON-encoded representation.
|
||||
*
|
||||
* @param string $json
|
||||
* @return self|null
|
||||
*/
|
||||
public static function fromJson($json){
|
||||
//Since update-related information is simply a subset of the full plugin info,
|
||||
//we can parse the update JSON as if it was a plugin info string, then copy over
|
||||
//the parts that we care about.
|
||||
$pluginInfo = PluginInfo::fromJson($json);
|
||||
if ( $pluginInfo !== null ) {
|
||||
return self::fromPluginInfo($pluginInfo);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of PluginUpdate based on an instance of PluginInfo.
|
||||
* Basically, this just copies a subset of fields from one object to another.
|
||||
*
|
||||
* @param PluginInfo $info
|
||||
* @return static
|
||||
*/
|
||||
public static function fromPluginInfo($info){
|
||||
return static::fromObject($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance by copying the necessary fields from another object.
|
||||
*
|
||||
* @param \StdClass|PluginInfo|self $object The source object.
|
||||
* @return self The new copy.
|
||||
*/
|
||||
public static function fromObject($object) {
|
||||
$update = new self();
|
||||
$update->copyFields($object, $update);
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getFieldNames() {
|
||||
return array_merge(parent::getFieldNames(), self::$extraFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the update into the format used by WordPress native plugin API.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function toWpFormat() {
|
||||
$update = parent::toWpFormat();
|
||||
|
||||
$update->id = $this->id;
|
||||
$update->url = $this->homepage;
|
||||
$update->tested = $this->tested;
|
||||
$update->requires_php = $this->requires_php;
|
||||
$update->plugin = $this->filename;
|
||||
$update->autoupdate = $this->autoupdate;
|
||||
|
||||
if ( !empty($this->upgrade_notice) ) {
|
||||
$update->upgrade_notice = $this->upgrade_notice;
|
||||
}
|
||||
|
||||
if ( !empty($this->icons) && is_array($this->icons) ) {
|
||||
//This should be an array with up to 4 keys: 'svg', '1x', '2x' and 'default'.
|
||||
//Docs: https://developer.wordpress.org/plugins/wordpress-org/plugin-assets/#plugin-icons
|
||||
$icons = array_intersect_key(
|
||||
$this->icons,
|
||||
array('svg' => true, '1x' => true, '2x' => true, 'default' => true)
|
||||
);
|
||||
if ( !empty($icons) ) {
|
||||
$update->icons = $icons;
|
||||
|
||||
//It appears that the 'default' icon isn't used anywhere in WordPress 4.9,
|
||||
//but let's set it just in case a future release needs it.
|
||||
if ( !isset($update->icons['default']) ) {
|
||||
$update->icons['default'] = current($update->icons);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $update;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
namespace YahnisElsts\PluginUpdateChecker\v5p7\Plugin;
|
||||
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\InstalledPackage;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\UpdateChecker as BaseUpdateChecker;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\Scheduler;
|
||||
use YahnisElsts\PluginUpdateChecker\v5p7\DebugBar;
|
||||
|
||||
if ( !class_exists(UpdateChecker::class, false) ):
|
||||
|
||||
/**
|
||||
* A custom plugin update checker.
|
||||
*
|
||||
* @author Janis Elsts
|
||||
* @copyright 2018
|
||||
* @access public
|
||||
*/
|
||||
class UpdateChecker extends BaseUpdateChecker {
|
||||
protected $updateTransient = 'update_plugins';
|
||||
protected $componentType = 'plugin';
|
||||
|
||||
public $pluginAbsolutePath = ''; //Full path of the main plugin file.
|
||||
public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
|
||||
public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
|
||||
|
||||
/**
|
||||
* @var Package
|
||||
*/
|
||||
protected $package;
|
||||
|
||||
private $extraUi = null;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param string $metadataUrl The URL of the plugin's metadata file.
|
||||
* @param string $pluginFile Fully qualified path to the main plugin file.
|
||||
* @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
|
||||
* @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.
|
||||
* @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
|
||||
* @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
|
||||
*/
|
||||
public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
|
||||
$this->pluginAbsolutePath = $pluginFile;
|
||||
$this->pluginFile = plugin_basename($this->pluginAbsolutePath);
|
||||
$this->muPluginFile = $muPluginFile;
|
||||
|
||||
//If no slug is specified, use the name of the main plugin file as the slug.
|
||||
//For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
|
||||
if ( empty($slug) ){
|
||||
$slug = basename($this->pluginFile, '.php');
|
||||
}
|
||||
|
||||
//Plugin slugs must be unique.
|
||||
$slugCheckFilter = 'puc_is_slug_in_use-' . $slug;
|
||||
$slugUsedBy = apply_filters($slugCheckFilter, false);
|
||||
if ( $slugUsedBy ) {
|
||||
$this->triggerError(sprintf(
|
||||
'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
|
||||
$slug,
|
||||
$slugUsedBy
|
||||
), E_USER_ERROR);
|
||||
}
|
||||
add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
|
||||
|
||||
parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
|
||||
|
||||
//Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
|
||||
//it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
|
||||
if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
|
||||
$this->muPluginFile = $this->pluginFile;
|
||||
}
|
||||
|
||||
//To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
|
||||
//Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
|
||||
add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
|
||||
|
||||
$this->extraUi = new Ui($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the scheduler.
|
||||
*
|
||||
* @param int $checkPeriod
|
||||
* @return Scheduler
|
||||
*/
|
||||
protected function createScheduler($checkPeriod) {
|
||||
$scheduler = new Scheduler($this, $checkPeriod, array('load-plugins.php'));
|
||||
register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
|
||||
return $scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the hooks required to run periodic update checks and inject update info
|
||||
* into WP data structures.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function installHooks(){
|
||||
//Override requests for plugin information
|
||||
add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
|
||||
|
||||
parent::installHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove update checker hooks.
|
||||
*
|
||||
* The intent is to prevent a fatal error that can happen if the plugin has an uninstall
|
||||
* hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
|
||||
* the uninstall hook runs, WP deletes the plugin files and then updates some transients.
|
||||
* If PUC hooks are still around at this time, they could throw an error while trying to
|
||||
* autoload classes from files that no longer exist.
|
||||
*
|
||||
* The "site_transient_{$transient}" filter is the main problem here, but let's also remove
|
||||
* most other PUC hooks to be safe.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function removeHooks() {
|
||||
parent::removeHooks();
|
||||
$this->extraUi->removeHooks();
|
||||
$this->package->removeHooks();
|
||||
|
||||
remove_filter('plugins_api', array($this, 'injectInfo'), 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve plugin info from the configured API endpoint.
|
||||
*
|
||||
* @uses wp_remote_get()
|
||||
*
|
||||
* @param array $queryArgs Additional query arguments to append to the request. Optional.
|
||||
* @return PluginInfo
|
||||
*/
|
||||
public function requestInfo($queryArgs = array()) {
|
||||
list($pluginInfo, $result) = $this->requestMetadata(
|
||||
PluginInfo::class,
|
||||
'request_info',
|
||||
$queryArgs
|
||||
);
|
||||
|
||||
if ( $pluginInfo !== null ) {
|
||||
/** @var PluginInfo $pluginInfo */
|
||||
$pluginInfo->filename = $this->pluginFile;
|
||||
$pluginInfo->slug = $this->slug;
|
||||
}
|
||||
|
||||
$pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
|
||||
return $pluginInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the latest update (if any) from the configured API endpoint.
|
||||
*
|
||||
* @uses UpdateChecker::requestInfo()
|
||||
*
|
||||
* @return Update|null An instance of Plugin Update, or NULL when no updates are available.
|
||||
*/
|
||||
public function requestUpdate() {
|
||||
//For the sake of simplicity, this function just calls requestInfo()
|
||||
//and transforms the result accordingly.
|
||||
$pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
|
||||
if ( $pluginInfo === null ){
|
||||
return null;
|
||||
}
|
||||
$update = Update::fromPluginInfo($pluginInfo);
|
||||
|
||||
$update = $this->filterUpdateResult($update);
|
||||
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept plugins_api() calls that request information about our plugin and
|
||||
* use the configured API endpoint to satisfy them.
|
||||
*
|
||||
* @see plugins_api()
|
||||
*
|
||||
* @param mixed $result
|
||||
* @param string $action
|
||||
* @param array|object $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function injectInfo($result, $action = null, $args = null){
|
||||
$relevant = ($action == 'plugin_information') && isset($args->slug) && (
|
||||
($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
|
||||
);
|
||||
if ( !$relevant ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$pluginInfo = $this->requestInfo();
|
||||
$this->fixSupportedWordpressVersion($pluginInfo);
|
||||
|
||||
$pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
|
||||
if ( $pluginInfo ) {
|
||||
return $pluginInfo->toWpFormat();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function shouldShowUpdates() {
|
||||
//No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
|
||||
//is usually different from the main plugin file so the update wouldn't show up properly anyway.
|
||||
return !$this->isUnknownMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \stdClass|null $updates
|
||||
* @param \stdClass $updateToAdd
|
||||
* @return \stdClass
|
||||
*/
|
||||
protected function addUpdateToList($updates, $updateToAdd) {
|
||||
if ( $this->package->isMuPlugin() ) {
|
||||
//WP does not support automatic update installation for mu-plugins, but we can
|
||||
//still display a notice.
|
||||
$updateToAdd->package = null;
|
||||
}
|
||||
return parent::addUpdateToList($updates, $updateToAdd);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \stdClass|null $updates
|
||||
* @return \stdClass|null
|
||||
*/
|
||||
protected function removeUpdateFromList($updates) {
|
||||
$updates = parent::removeUpdateFromList($updates);
|
||||
if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
|
||||
unset($updates->response[$this->muPluginFile]);
|
||||
}
|
||||
return $updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* For plugins, the update array is indexed by the plugin filename relative to the "plugins"
|
||||
* directory. Example: "plugin-name/plugin.php".
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getUpdateListKey() {
|
||||
if ( $this->package->isMuPlugin() ) {
|
||||
return $this->muPluginFile;
|
||||
}
|
||||
return $this->pluginFile;
|
||||
}
|
||||
|
||||
protected function getNoUpdateItemFields() {
|
||||
return array_merge(
|
||||
parent::getNoUpdateItemFields(),
|
||||
array(
|
||||
'id' => $this->pluginFile,
|
||||
'slug' => $this->slug,
|
||||
'plugin' => $this->pluginFile,
|
||||
'icons' => array(),
|
||||
'banners' => array(),
|
||||
'banners_rtl' => array(),
|
||||
'tested' => '',
|
||||
'compatibility' => new \stdClass(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for isBeingUpgraded().
|
||||
*
|
||||
* @deprecated
|
||||
* @param \WP_Upgrader|null $upgrader The upgrader that's performing the current update.
|
||||
* @return bool
|
||||
*/
|
||||
public function isPluginBeingUpgraded($upgrader = null) {
|
||||
return $this->isBeingUpgraded($upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there an update being installed for this plugin, right now?
|
||||
*
|
||||
* @param \WP_Upgrader|null $upgrader
|
||||
* @return bool
|
||||
*/
|
||||
public function isBeingUpgraded($upgrader = null) {
|
||||
return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of the currently available update, if any.
|
||||
*
|
||||
* If no updates are available, or if the last known update version is below or equal
|
||||
* to the currently installed version, this method will return NULL.
|
||||
*
|
||||
* Uses cached update data. To retrieve update information straight from
|
||||
* the metadata URL, call requestUpdate() instead.
|
||||
*
|
||||
* @return Update|null
|
||||
*/
|
||||
public function getUpdate() {
|
||||
$update = parent::getUpdate();
|
||||
if ( isset($update) ) {
|
||||
/** @var Update $update */
|
||||
$update->filename = $this->pluginFile;
|
||||
}
|
||||
return $update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translated plugin title.
|
||||
*
|
||||
* @deprecated
|
||||
* @return string
|
||||
*/
|
||||
public function getPluginTitle() {
|
||||
return $this->package->getPluginTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has the required permissions to install updates.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function userCanInstallUpdates() {
|
||||
return current_user_can('update_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the plugin file is inside the mu-plugins directory.
|
||||
*
|
||||
* @deprecated
|
||||
* @return bool
|
||||
*/
|
||||
protected function isMuPlugin() {
|
||||
return $this->package->isMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* MU plugins are partially supported, but only when we know which file in mu-plugins
|
||||
* corresponds to this plugin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUnknownMuPlugin() {
|
||||
return empty($this->muPluginFile) && $this->package->isMuPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to the main plugin file.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAbsolutePath() {
|
||||
return $this->pluginAbsolutePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering query arguments.
|
||||
*
|
||||
* The callback function should take one argument - an associative array of query arguments.
|
||||
* It should return a modified array of query arguments.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addQueryArgFilter($callback){
|
||||
$this->addFilter('request_info_query_args', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering arguments passed to wp_remote_get().
|
||||
*
|
||||
* The callback function should take one argument - an associative array of arguments -
|
||||
* and return a modified array or arguments. See the WP documentation on wp_remote_get()
|
||||
* for details on what arguments are available and how they work.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addHttpRequestArgFilter($callback) {
|
||||
$this->addFilter('request_info_options', $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback for filtering the plugin info retrieved from the external API.
|
||||
*
|
||||
* The callback function should take two arguments. If the plugin info was retrieved
|
||||
* successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
|
||||
* it will be NULL. The second argument will be the corresponding return value of
|
||||
* wp_remote_get (see WP docs for details).
|
||||
*
|
||||
* The callback function should return a new or modified instance of PluginInfo or NULL.
|
||||
*
|
||||
* @uses add_filter() This method is a convenience wrapper for add_filter().
|
||||
*
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public function addResultFilter($callback) {
|
||||
$this->addFilter('request_info_result', $callback, 10, 2);
|
||||
}
|
||||
|
||||
protected function createDebugBarExtension() {
|
||||
return new DebugBar\PluginExtension($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a package instance that represents this plugin or theme.
|
||||
*
|
||||
* @return InstalledPackage
|
||||
*/
|
||||
protected function createInstalledPackage() {
|
||||
return new Package($this->pluginAbsolutePath, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Package
|
||||
*/
|
||||
public function getInstalledPackage() {
|
||||
return $this->package;
|
||||
}
|
||||
}
|
||||
|
||||
endif;
|
||||
Reference in New Issue
Block a user