mirror of
https://gh.wpcy.net/https://github.com/fairpm/fair-plugin.git
synced 2026-06-10 01:04:28 +08:00
Signed-off-by: John Blackbourn <john@johnblackbourn.com> Signed-off-by: Andy Fragen <andy@thefragens.com> Signed-off-by: Carrie Dils <carriedils@gmail.com> Signed-off-by: Norcross <andrew.norcross@gmail.com> Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: joedolson <joedolson@users.noreply.github.com> Signed-off-by: Joe Dolson <design@joedolson.com> Signed-off-by: Shadi Sharaf <shady@sharaf.me> Signed-off-by: Chuck Adams <chaz@chaz.works> Signed-off-by: Mika Ipstenu Epstein <ipstenu@halfelf.org> Signed-off-by: Ipstenu (Mika Epstein) <Ipstenu@users.noreply.github.com> Signed-off-by: Mika <ipstenu@halfelf.org> Signed-off-by: Mika Epstein <ipstenu@halfelf.org> Signed-off-by: Marc Armengou <83702259+marcarmengou@users.noreply.github.com> Signed-off-by: Carrie Dils <cdils@users.noreply.github.com> Co-authored-by: John Blackbourn <john@johnblackbourn.com> Co-authored-by: Chuck Adams <chaz@chaz.works> Co-authored-by: Andy Fragen <andy@thefragens.com> Co-authored-by: Norcross <andrew.norcross@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: rmccue <21655+rmccue@users.noreply.github.com> Co-authored-by: cdils <3099408+cdils@users.noreply.github.com> Co-authored-by: joedolson <joedolson@users.noreply.github.com> Co-authored-by: Joe Dolson <design@joedolson.com> Co-authored-by: Shady Sharaf <shady@sharaf.me> Co-authored-by: Mika Ipstenu Epstein <ipstenu@halfelf.org> Co-authored-by: Ipstenu (Mika Epstein) <Ipstenu@users.noreply.github.com> Co-authored-by: Marc Armengou <83702259+marcarmengou@users.noreply.github.com> Co-authored-by: Namith Jawahar <48271037+namithj@users.noreply.github.com> Co-authored-by: Kaspars Dambis <hi@kaspars.net>
279 lines
7.6 KiB
PHP
279 lines
7.6 KiB
PHP
<?php
|
|
/**
|
|
* Update FAIR packages.
|
|
*
|
|
* @package FAIR
|
|
*/
|
|
|
|
namespace FAIR\Updater;
|
|
|
|
use const FAIR\CACHE_LIFETIME_FAILURE;
|
|
use const FAIR\Packages\CACHE_DID_FOR_INSTALL;
|
|
use const FAIR\Packages\CACHE_RELEASE_PACKAGES;
|
|
use const FAIR\Packages\CACHE_UPDATE_ERRORS;
|
|
use FAIR\DID\Crypto\DidCodec;
|
|
use FAIR\Packages;
|
|
use function FAIR\is_wp_cli;
|
|
use Plugin_Upgrader;
|
|
use Theme_Upgrader;
|
|
use WP_CLI;
|
|
use WP_Error;
|
|
use WP_Upgrader;
|
|
|
|
/**
|
|
* Bootstrap.
|
|
*/
|
|
function bootstrap() {
|
|
add_action( 'init', __NAMESPACE__ . '\\run' );
|
|
add_action( 'admin_init', __NAMESPACE__ . '\\register_plugin_row_hooks' );
|
|
}
|
|
|
|
/**
|
|
* Gather all plugins/themes with data in Update URI and DID header.
|
|
*
|
|
* @return array
|
|
*/
|
|
function get_packages() : array {
|
|
$packages = [];
|
|
|
|
// Seems to be required for PHPUnit testing on GitHub workflow.
|
|
if ( ! function_exists( 'get_plugins' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
|
|
$plugin_path = trailingslashit( WP_PLUGIN_DIR );
|
|
$plugins = get_plugins();
|
|
foreach ( $plugins as $file => $plugin ) {
|
|
$plugin_id = get_file_data( $plugin_path . $file, [ 'PluginID' => 'Plugin ID' ] )['PluginID'];
|
|
if ( ! empty( $plugin_id ) ) {
|
|
$packages['plugins'][ $plugin_id ] = $plugin_path . $file;
|
|
}
|
|
}
|
|
|
|
$themes = wp_get_themes();
|
|
foreach ( $themes as $theme ) {
|
|
$stylesheet_directory = $theme->get_stylesheet_directory();
|
|
if ( empty( $stylesheet_directory ) ) {
|
|
// The theme root is missing.
|
|
continue;
|
|
}
|
|
|
|
$stylesheet_file = trailingslashit( $stylesheet_directory ) . 'style.css';
|
|
$theme_id = get_file_data( $stylesheet_file, [ 'ThemeID' => 'Theme ID' ] )['ThemeID'];
|
|
if ( ! empty( $theme_id ) ) {
|
|
$packages['themes'][ $theme_id ] = $stylesheet_file;
|
|
}
|
|
}
|
|
|
|
return $packages;
|
|
}
|
|
|
|
/**
|
|
* Run FAIR\Updater\Updater for potential packages.
|
|
*
|
|
* @return void
|
|
*/
|
|
function run() {
|
|
if ( ! Updater::should_run_on_current_page() ) {
|
|
return;
|
|
}
|
|
|
|
$packages = get_packages();
|
|
$plugins = $packages['plugins'] ?? [];
|
|
$themes = $packages['themes'] ?? [];
|
|
|
|
foreach ( $plugins as $did => $filepath ) {
|
|
Updater::register_plugin( $did, $filepath );
|
|
}
|
|
|
|
foreach ( $themes as $did => $filepath ) {
|
|
Updater::register_theme( $did, $filepath );
|
|
}
|
|
|
|
// Load hooks once for all packages.
|
|
Updater::load_hooks();
|
|
}
|
|
|
|
/**
|
|
* Register hooks to display update errors below plugin rows.
|
|
*/
|
|
function register_plugin_row_hooks(): void {
|
|
$packages = get_packages();
|
|
$plugins = $packages['plugins'] ?? [];
|
|
|
|
foreach ( $plugins as $did => $path ) {
|
|
$plugin_file = plugin_basename( $path );
|
|
add_action(
|
|
"after_plugin_row_{$plugin_file}",
|
|
function ( $file, $plugin_data, $status ) use ( $did ) {
|
|
display_plugin_update_error( $file, $plugin_data, $status, $did );
|
|
},
|
|
10,
|
|
3
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display a cached update error below the plugin row.
|
|
*
|
|
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
|
|
* @param array $plugin_data An array of plugin data.
|
|
* @param string $status Status filter currently applied to the plugin list.
|
|
* @param string $did The DID of the plugin.
|
|
*/
|
|
function display_plugin_update_error( $plugin_file, $plugin_data, $status, $did ): void {
|
|
$error = get_site_transient( CACHE_UPDATE_ERRORS . $did );
|
|
if ( ! is_wp_error( $error ) ) {
|
|
return;
|
|
}
|
|
|
|
$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
|
|
$colspan = $wp_list_table->get_column_count();
|
|
|
|
// Calculate time remaining until retry.
|
|
$error_data = $error->get_error_data();
|
|
$timestamp = $error_data['timestamp'] ?? 0;
|
|
$retry_time = $timestamp + CACHE_LIFETIME_FAILURE;
|
|
$time_remaining = human_time_diff( time(), $retry_time );
|
|
|
|
$message = sprintf(
|
|
/* translators: %1$s: Error message, %2$s: Time period */
|
|
__( 'Error: %1$s. Update checks paused for %2$s.', 'fair' ),
|
|
$error->get_error_message(),
|
|
$time_remaining,
|
|
);
|
|
|
|
$active_class = is_plugin_active( $plugin_file ) ? ' active' : '';
|
|
|
|
printf(
|
|
'<tr class="plugin-update-tr%1$s" id="fair-error-%2$s">
|
|
<td colspan="%3$d" class="plugin-update colspanchange">
|
|
<div class="update-message notice inline notice-error notice-alt"><p>%4$s</p></div>
|
|
</td>
|
|
</tr>',
|
|
esc_attr( $active_class ),
|
|
esc_attr( sanitize_title( $plugin_file ) ),
|
|
esc_attr( $colspan ),
|
|
esc_html( $message ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Download a package with signature verification.
|
|
*
|
|
* @param bool|string|WP_Error $reply Whether to proceed with the download, the path to the downloaded package, or an existing WP_Error object.
|
|
* @param string $package The URI of the package. If this is the full path to an existing local file, it will be returned untouched.
|
|
* @param WP_Upgrader $upgrader The WP_Upgrader instance.
|
|
* @param array $hook_extra Extra hook data.
|
|
* @return string|WP_Error The package path if the signature is valid, otherwise WP_Error.
|
|
*/
|
|
function verify_signature_on_download( $reply, string $package, WP_Upgrader $upgrader, $hook_extra ) {
|
|
static $has_run = [];
|
|
|
|
if ( false !== $reply || ( ! $upgrader instanceof Plugin_Upgrader && ! $upgrader instanceof Theme_Upgrader ) ) {
|
|
return $reply;
|
|
}
|
|
|
|
$did = get_site_transient( CACHE_DID_FOR_INSTALL );
|
|
if ( ! $did ) {
|
|
return $reply;
|
|
}
|
|
|
|
// This method is hooked to 'upgrader_pre_download', which is used in WP_Upgrader::download_package().
|
|
// Bailing on subsequent runs for the same package URI prevents an infinite loop.
|
|
$key = sha1( $did . '_' . $package );
|
|
if ( isset( $has_run[ $key ] ) ) {
|
|
return $reply;
|
|
}
|
|
$has_run[ $key ] = true;
|
|
|
|
// Local files should be returned untouched.
|
|
if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) {
|
|
return $package;
|
|
}
|
|
|
|
$releases = get_site_transient( CACHE_RELEASE_PACKAGES ) ?? [];
|
|
if ( empty( $releases ) || ! isset( $releases[ $did ] ) ) {
|
|
return $reply;
|
|
}
|
|
|
|
$artifact = Packages\pick_artifact_by_lang( $releases[ $did ]->artifacts->package );
|
|
if ( ! $artifact || $package !== $artifact->url ) {
|
|
return $reply;
|
|
}
|
|
|
|
$path = $upgrader->download_package( $package, false, $hook_extra );
|
|
if ( is_wp_error( $path ) ) {
|
|
return $path;
|
|
}
|
|
|
|
add_filter( 'wp_trusted_keys', __NAMESPACE__ . '\\get_trusted_keys', 100 );
|
|
$decoded_base64url = sodium_base642bin( $artifact->signature, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING );
|
|
$result = verify_file_signature( $path, base64_encode( $decoded_base64url ) );
|
|
remove_filter( 'wp_trusted_keys', __NAMESPACE__ . '\\get_trusted_keys', 100 );
|
|
|
|
if ( $result === true ) {
|
|
if ( is_wp_cli() ) {
|
|
WP_CLI::success(
|
|
sprintf(
|
|
/* translators: %s: The DID of the package. */
|
|
__( 'Verified signature for %s', 'fair' ),
|
|
$did
|
|
)
|
|
);
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
return new WP_Error(
|
|
'fair.packages.signature_verification.failed',
|
|
sprintf(
|
|
/* translators: %s: The package's URL. */
|
|
__( 'Signature verification could not be performed for the package: %s', 'fair' ),
|
|
$package
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get trusted keys for signature verification.
|
|
*
|
|
* @return array
|
|
*/
|
|
function get_trusted_keys(): array {
|
|
$did = get_site_transient( CACHE_DID_FOR_INSTALL );
|
|
if ( ! $did ) {
|
|
return [];
|
|
}
|
|
|
|
$doc = Packages\get_did_document( $did );
|
|
if ( is_wp_error( $doc ) ) {
|
|
return [];
|
|
}
|
|
|
|
$keys = Packages\get_fair_signing_keys( $doc );
|
|
if ( empty( $keys ) ) {
|
|
return [];
|
|
}
|
|
|
|
// FAIR uses Base58BTC-encoded Ed25519 keys.
|
|
// Core expects base64-encoded keys.
|
|
$recoded_keys = [];
|
|
foreach ( $keys as $key ) {
|
|
$decoded = DidCodec::from_multibase_key( $key['publicKeyMultibase'] );
|
|
|
|
// Ed25519 keys only.
|
|
if ( $decoded['codec'] !== DidCodec::MULTICODEC_ED25519_PUB ) {
|
|
continue;
|
|
}
|
|
|
|
$recoded_keys[] = base64_encode( $decoded['key'] );
|
|
}
|
|
|
|
return $recoded_keys;
|
|
}
|