v0.5.0
This commit is contained in:
parent
38fbcf80a5
commit
90ac22a845
8 changed files with 379 additions and 5 deletions
337
includes/class-robotstxt-manager-updater.php
Normal file
337
includes/class-robotstxt-manager-updater.php
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
<?php
|
||||
/**
|
||||
* Native WordPress update integration for ROBOTSTXT catalog plugins.
|
||||
*
|
||||
* @package Robotstxt_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class Robotstxt_Manager_Updater
|
||||
*
|
||||
* Feeds catalog data into WordPress's native update system so catalog plugins
|
||||
* show the standard "Update available" badge and update through the regular
|
||||
* wp-admin flow, with the store's download proxy as the package source.
|
||||
*
|
||||
* - `pre_set_site_transient_update_plugins`: adds entries to ->response for
|
||||
* installed catalog plugins with a newer remote version, and to ->no_update
|
||||
* for up-to-date ones (prevents false WordPress.org matches).
|
||||
* - `plugins_api`: serves the "View details" modal from catalog data.
|
||||
*
|
||||
* Plugins that bundle their own update SDK already inject their own entries;
|
||||
* Manager never overwrites an existing response entry.
|
||||
*/
|
||||
class Robotstxt_Manager_Updater {
|
||||
|
||||
/**
|
||||
* Registers all hooks via the loader.
|
||||
*
|
||||
* @param Robotstxt_Manager_Loader $loader The plugin hook loader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register( Robotstxt_Manager_Loader $loader ): void {
|
||||
$loader->add_filter( 'pre_set_site_transient_update_plugins', $this, 'inject_updates' );
|
||||
$loader->add_filter( 'plugins_api', $this, 'plugins_api_filter', 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects catalog update data into the WordPress update transient.
|
||||
*
|
||||
* @param mixed $transient The update_plugins transient object.
|
||||
*
|
||||
* @return mixed Modified transient.
|
||||
*/
|
||||
public function inject_updates( mixed $transient ): mixed {
|
||||
if ( ! is_object( $transient )
|
||||
|| ! property_exists( $transient, 'checked' )
|
||||
|| ! is_array( $transient->checked )
|
||||
|| empty( $transient->checked )
|
||||
) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||
|
||||
if ( ! $client->is_configured() ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$catalog = $client->get_catalog();
|
||||
|
||||
if ( empty( $catalog ) ) {
|
||||
return $transient;
|
||||
}
|
||||
|
||||
$entries = $this->catalog_by_slug( $catalog );
|
||||
|
||||
foreach ( $transient->checked as $plugin_file => $raw_version ) {
|
||||
$version = is_string( $raw_version ) ? $raw_version : '';
|
||||
$slug = $this->slug_from_file( (string) $plugin_file );
|
||||
$entry = $entries[ $slug ] ?? null;
|
||||
|
||||
if ( null === $entry || '' === $version ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_version = $entry['new_version'];
|
||||
|
||||
if ( '' === $new_version ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Never overwrite an entry injected by the plugin's own SDK.
|
||||
if ( property_exists( $transient, 'response' )
|
||||
&& is_array( $transient->response )
|
||||
&& isset( $transient->response[ $plugin_file ] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( version_compare( $version, $new_version, '<' ) ) {
|
||||
if ( property_exists( $transient, 'response' ) && is_array( $transient->response ) ) {
|
||||
$transient->response[ $plugin_file ] = $this->build_update_object( $slug, (string) $plugin_file, $entry );
|
||||
}
|
||||
} elseif ( property_exists( $transient, 'no_update' ) && is_array( $transient->no_update ) ) {
|
||||
$transient->no_update[ $plugin_file ] = $this->build_update_object( $slug, (string) $plugin_file, $entry, $version );
|
||||
}
|
||||
}
|
||||
|
||||
return $transient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves the "View details" modal for catalog plugins from catalog data.
|
||||
*
|
||||
* @param mixed $result Current filter result.
|
||||
* @param string $action API action (only 'plugin_information' is handled).
|
||||
* @param mixed $args Request arguments; expected object with 'slug'.
|
||||
*
|
||||
* @return mixed Plugin info object, or the original $result.
|
||||
*/
|
||||
public function plugins_api_filter( mixed $result, string $action, mixed $args ): mixed {
|
||||
if ( 'plugin_information' !== $action || ! is_object( $args ) || empty( $args->slug ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||
|
||||
if ( ! $client->is_configured() ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$catalog = $client->get_catalog();
|
||||
|
||||
if ( empty( $catalog ) ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$slug = sanitize_key( (string) $args->slug );
|
||||
$entries = $this->catalog_by_slug( $catalog );
|
||||
$entry = $entries[ $slug ] ?? null;
|
||||
|
||||
if ( null === $entry ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$sections = array();
|
||||
|
||||
if ( '' !== $entry['description'] ) {
|
||||
$sections['description'] = wp_kses_post( wpautop( $entry['description'] ) );
|
||||
}
|
||||
|
||||
if ( empty( $sections ) ) {
|
||||
$sections['description'] = '';
|
||||
}
|
||||
|
||||
$info = array(
|
||||
'name' => $entry['name'],
|
||||
'slug' => $slug,
|
||||
'version' => $entry['new_version'],
|
||||
'requires' => $entry['requires_wp'],
|
||||
'requires_php' => $entry['requires_php'],
|
||||
'tested' => $entry['tested_up_to'],
|
||||
'last_updated' => '',
|
||||
'homepage' => $entry['page_url'],
|
||||
'sections' => $sections,
|
||||
'download_link' => '',
|
||||
);
|
||||
|
||||
if ( '' !== $entry['icon_url'] ) {
|
||||
$info['icons'] = array(
|
||||
'1x' => $entry['icon_url'],
|
||||
'2x' => $entry['icon_url'],
|
||||
);
|
||||
}
|
||||
|
||||
if ( '' !== $entry['banner_url'] ) {
|
||||
$info['banners'] = array(
|
||||
'low' => $entry['banner_url'],
|
||||
'high' => $entry['banner_url'],
|
||||
);
|
||||
}
|
||||
|
||||
return (object) $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes catalog entries by slug with normalised fields.
|
||||
*
|
||||
* @param list<array<string,mixed>> $catalog Catalog entries from Core.
|
||||
*
|
||||
* @return array<string, array<string,string>> Normalised entries keyed by slug.
|
||||
*/
|
||||
private function catalog_by_slug( array $catalog ): array {
|
||||
$entries = array();
|
||||
|
||||
foreach ( $catalog as $row ) {
|
||||
$raw_slug = $row['slug'] ?? '';
|
||||
$slug = is_string( $raw_slug ) ? sanitize_key( $raw_slug ) : '';
|
||||
|
||||
if ( '' === $slug ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$entries[ $slug ] = array(
|
||||
'name' => $this->str( $row, 'name', $slug ),
|
||||
'type' => $this->str( $row, 'type', 'free' ),
|
||||
'new_version' => $this->str( $row, 'current_version', '' ),
|
||||
'requires_wp' => $this->str( $row, 'requires_wp', '' ),
|
||||
'requires_php' => $this->str( $row, 'requires_php', '' ),
|
||||
'tested_up_to' => $this->str( $row, 'tested_up_to', '' ),
|
||||
'page_url' => $this->str( $row, 'page_url', '' ),
|
||||
'description' => $this->str( $row, 'description', '' ),
|
||||
'icon_url' => $this->str( $row, 'icon_url', '' ),
|
||||
'banner_url' => $this->str( $row, 'banner_url', '' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the update/no-update object for the WordPress transient.
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @param string $plugin_file Plugin basename.
|
||||
* @param array<string, string> $entry Normalised catalog entry.
|
||||
* @param string|null $current_version Installed version; when null an
|
||||
* update entry is built, otherwise a
|
||||
* no-update entry pinned to this version.
|
||||
*
|
||||
* @return object stdClass for the transient bucket.
|
||||
*/
|
||||
private function build_update_object( string $slug, string $plugin_file, array $entry, ?string $current_version = null ): object {
|
||||
$is_no_update = null !== $current_version;
|
||||
|
||||
$data = array(
|
||||
'id' => $plugin_file,
|
||||
'slug' => $slug,
|
||||
'plugin' => $plugin_file,
|
||||
'new_version' => $is_no_update ? $current_version : $entry['new_version'],
|
||||
'url' => $entry['page_url'],
|
||||
'package' => $is_no_update ? '' : $this->package_url( $slug, $entry['type'] ),
|
||||
'requires' => $entry['requires_wp'],
|
||||
'requires_php' => $entry['requires_php'],
|
||||
'tested' => $entry['tested_up_to'],
|
||||
'icons' => array(),
|
||||
'banners' => array(),
|
||||
);
|
||||
|
||||
if ( ! $is_no_update ) {
|
||||
if ( '' !== $entry['icon_url'] ) {
|
||||
$data['icons'] = array(
|
||||
'1x' => $entry['icon_url'],
|
||||
'2x' => $entry['icon_url'],
|
||||
);
|
||||
}
|
||||
|
||||
if ( '' !== $entry['banner_url'] ) {
|
||||
$data['banners'] = array(
|
||||
'low' => $entry['banner_url'],
|
||||
'high' => $entry['banner_url'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (object) $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the download package URL for the native upgrader.
|
||||
*
|
||||
* Free plugins stream through the proxy without auth (Core 1.4.0+).
|
||||
* Premium plugins append the account API key as an api_key query parameter
|
||||
* (accepted by Core 1.5.0+) — the native upgrader cannot send headers.
|
||||
*
|
||||
* @param string $slug Plugin slug.
|
||||
* @param string $type Plugin type ('free' or 'premium').
|
||||
*
|
||||
* @return string Package URL.
|
||||
*/
|
||||
private function package_url( string $slug, string $type ): string {
|
||||
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||
|
||||
$url = $client->get_store_url() . '/wp-json/robotstxt-core/v1/plugins/' . rawurlencode( $slug ) . '/download';
|
||||
$args = array(
|
||||
'domain' => $this->site_domain(),
|
||||
);
|
||||
|
||||
if ( 'premium' === $type ) {
|
||||
$api_key_raw = get_option( 'robotstxt_manager_api_key', '' );
|
||||
$api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : '';
|
||||
|
||||
if ( '' !== $api_key ) {
|
||||
$args['api_key'] = $api_key;
|
||||
}
|
||||
}
|
||||
|
||||
return add_query_arg( $args, $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalised domain of the current site.
|
||||
*
|
||||
* @return string Domain (e.g. 'example.com').
|
||||
*/
|
||||
private function site_domain(): string {
|
||||
$host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
|
||||
return strtolower( ltrim( $host, 'www.' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a plugin basename to its directory slug.
|
||||
*
|
||||
* @param string $plugin_file Plugin basename (e.g. 'slug/file.php').
|
||||
*
|
||||
* @return string Slug.
|
||||
*/
|
||||
private function slug_from_file( string $plugin_file ): string {
|
||||
$slug = dirname( $plugin_file );
|
||||
|
||||
if ( '.' === $slug ) {
|
||||
$slug = basename( $plugin_file, '.php' );
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a string value from a mixed-value row.
|
||||
*
|
||||
* @param array<string, mixed> $row Catalog row.
|
||||
* @param string $key Key to read.
|
||||
* @param string $fallback Default when absent or non-string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function str( array $row, string $key, string $fallback ): string {
|
||||
$value = $row[ $key ] ?? $fallback;
|
||||
|
||||
return is_string( $value ) ? $value : $fallback;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue