mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
- Neutralize Freemius update hooks in class-freemius-abstract.php and class-fs-plugin-updater.php - Add admin_init cache purge for stale Freemius update notices on update-core.php - Rebrand plugin to Heretek Control Core: Unlocked Machine Rites - Update admin menu to Heretek AI and transform submenus (Cogitator Deck, Machine Rites, Noosphere Uplink, Cyber-Theurgy, STC Archives, Engram Memory, Canticle Library, Adept Doctrines) - Update app bar header to HERETEK CONTROL CORE with ARCH-HERETEK UNLOCKED badge - Update help menu to Omnissiah Archives and drawer to Vox Transmissions
507 lines
17 KiB
PHP
507 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* GitHub-release updater for the FREE build.
|
|
*
|
|
* The free plugin is distributed via GitHub releases (not wordpress.org and not
|
|
* Freemius — the auto-generated Freemius free build is kept unreleased). Without
|
|
* this, free users would never see an in-dashboard "update available" prompt.
|
|
* This class bridges GitHub releases into WordPress's native plugin-update flow
|
|
* so free users update straight from Dashboard → Updates / the Plugins screen,
|
|
* exactly like a wordpress.org plugin.
|
|
*
|
|
* Premium builds ship the `.emcp-pro` marker and let Freemius handle updates —
|
|
* this updater self-disables there (see init()).
|
|
*
|
|
* @package EMCP_Tools
|
|
* @since 3.1.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Serves free-tier updates from the public GitHub repo's latest release.
|
|
*
|
|
* @since 3.1.0
|
|
*/
|
|
class EMCP_Tools_GitHub_Updater {
|
|
|
|
/** GitHub `owner/repo` the free releases live on. */
|
|
const REPO = 'Heretek-AI/elementor-mcp';
|
|
|
|
/** Transient caching the parsed latest-release payload (limits API hits). */
|
|
const TRANSIENT = 'emcp_tools_github_release';
|
|
|
|
/** How long to cache a successful release lookup. */
|
|
const CACHE_TTL = 6 * HOUR_IN_SECONDS;
|
|
|
|
/** Shorter cache after a failed/empty lookup so we recover quickly. */
|
|
const CACHE_TTL_FAIL = 2 * HOUR_IN_SECONDS;
|
|
|
|
/** Matches the release ZIP asset (elementor-mcp or emcp-tools). */
|
|
const ASSET_PATTERN = '/^.*(elementor-mcp|emcp-tools).*\.zip$/i';
|
|
|
|
/**
|
|
* The installed plugin folder slug (e.g. `emcp-tools` or `elementor-mcp`),
|
|
* used as the `plugins_api` slug and the target extraction directory.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $slug;
|
|
|
|
public function __construct() {
|
|
$this->slug = dirname( EMCP_TOOLS_BASENAME );
|
|
}
|
|
|
|
/**
|
|
* Register the update hooks for the unlocked fork.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init(): void {
|
|
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update' ) );
|
|
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
|
add_filter( 'upgrader_source_selection', array( $this, 'rename_source_dir' ), 10, 4 );
|
|
add_action( 'upgrader_process_complete', array( $this, 'clear_cache_after_update' ), 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* The current version/update status for the plugin, read from WordPress's own
|
|
* `update_plugins` transient. Build-agnostic: on free installs that transient
|
|
* is populated by this updater, on premium installs by Freemius — so the same
|
|
* call powers the Dashboard indicator either way. Does NOT hit the network.
|
|
*
|
|
* @since 3.1.0
|
|
* @return array{current:string,latest:string,update_available:bool,update_url:string}
|
|
*/
|
|
public static function current_update_status(): array {
|
|
$current = EMCP_TOOLS_VERSION;
|
|
$latest = $current;
|
|
$update = false;
|
|
|
|
$transient = get_site_transient( 'update_plugins' );
|
|
if ( is_object( $transient ) && ! empty( $transient->response[ EMCP_TOOLS_BASENAME ]->new_version ) ) {
|
|
$candidate = (string) $transient->response[ EMCP_TOOLS_BASENAME ]->new_version;
|
|
if ( version_compare( $candidate, $current, '>' ) ) {
|
|
$latest = $candidate;
|
|
$update = true;
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'current' => $current,
|
|
'latest' => $latest,
|
|
'update_available' => $update,
|
|
'update_url' => self_admin_url( 'update-core.php' ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Whether this is an external premium build. Unlocked fork updates via GitHub.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_premium_build(): bool {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Inject an update entry into the plugin-update transient when GitHub's latest
|
|
* release is newer than the installed version; otherwise record a no_update
|
|
* entry so WordPress shows the plugin as up to date.
|
|
*
|
|
* @param mixed $transient The `update_plugins` site transient (object or empty).
|
|
* @return mixed
|
|
*/
|
|
public function inject_update( $transient ) {
|
|
if ( ! is_object( $transient ) ) {
|
|
$transient = new stdClass();
|
|
}
|
|
|
|
$release = $this->get_latest_release();
|
|
if ( ! $release ) {
|
|
return $transient;
|
|
}
|
|
|
|
// Compare against the version WordPress freshly parsed from the plugin
|
|
// header ($transient->checked), NOT the EMCP_TOOLS_VERSION constant. Right
|
|
// after a self-update the old plugin file is still loaded in this request
|
|
// (and may be OPcache-cached), so the constant still reads the OLD version
|
|
// and we'd wrongly keep offering the update the user just installed. The
|
|
// `checked` map is rebuilt from the on-disk headers after the update, so
|
|
// it reflects the true installed version. Fall back to the constant when
|
|
// WordPress hasn't populated `checked` (e.g. a bare transient).
|
|
$installed = ( isset( $transient->checked ) && is_array( $transient->checked ) && ! empty( $transient->checked[ EMCP_TOOLS_BASENAME ] ) )
|
|
? (string) $transient->checked[ EMCP_TOOLS_BASENAME ]
|
|
: EMCP_TOOLS_VERSION;
|
|
|
|
$item = (object) array(
|
|
'id' => 'github.com/' . self::REPO,
|
|
'slug' => $this->slug,
|
|
'plugin' => EMCP_TOOLS_BASENAME,
|
|
'new_version' => $release['version'],
|
|
'url' => 'https://github.com/' . self::REPO,
|
|
'package' => $release['package'],
|
|
'icons' => $this->plugin_icons(),
|
|
'banners' => array(),
|
|
'tested' => $release['tested'],
|
|
'requires' => $release['requires'],
|
|
'requires_php' => $release['requires_php'],
|
|
'compatibility' => new stdClass(),
|
|
);
|
|
|
|
if (
|
|
'' !== $release['package']
|
|
&& version_compare( $release['version'], $installed, '>' )
|
|
) {
|
|
if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
|
|
$transient->response = array();
|
|
}
|
|
$transient->response[ EMCP_TOOLS_BASENAME ] = $item;
|
|
unset( $transient->no_update[ EMCP_TOOLS_BASENAME ] );
|
|
} else {
|
|
// Up to date — populate no_update so the Plugins screen reflects it.
|
|
if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) {
|
|
$transient->no_update = array();
|
|
}
|
|
$transient->no_update[ EMCP_TOOLS_BASENAME ] = $item;
|
|
}
|
|
|
|
return $transient;
|
|
}
|
|
|
|
/**
|
|
* Supply the "View details" modal for our plugin from the GitHub release.
|
|
*
|
|
* @param mixed $result The value being filtered (false by default).
|
|
* @param string $action The requested plugins_api action.
|
|
* @param object $args Args, including the requested `slug`.
|
|
* @return mixed
|
|
*/
|
|
public function plugin_info( $result, $action, $args ) {
|
|
if ( 'plugin_information' !== $action ) {
|
|
return $result;
|
|
}
|
|
if ( empty( $args->slug ) || $args->slug !== $this->slug ) {
|
|
return $result;
|
|
}
|
|
|
|
$release = $this->get_latest_release();
|
|
if ( ! $release ) {
|
|
return $result;
|
|
}
|
|
|
|
$info = new stdClass();
|
|
$info->name = 'Heretek Control Core: Unlocked Machine Rites';
|
|
$info->slug = $this->slug;
|
|
$info->version = $release['version'];
|
|
$info->author = '<a href="https://github.com/Heretek-AI">Heretek AI</a>';
|
|
$info->homepage = 'https://github.com/' . self::REPO;
|
|
$info->download_link = $release['package'];
|
|
$info->trunk = $release['package'];
|
|
$info->requires = $release['requires'];
|
|
$info->requires_php = $release['requires_php'];
|
|
$info->tested = $release['tested'];
|
|
$info->last_updated = $release['published'];
|
|
$info->icons = $this->plugin_icons();
|
|
$info->sections = array(
|
|
'changelog' => $this->render_changelog( $release['changelog'], $release['html_url'] ),
|
|
);
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Ensure the extracted release folder is renamed to the installed plugin
|
|
* folder before WordPress moves it into place. The free ZIP unpacks to
|
|
* `emcp-tools/`; a site whose folder differs (e.g. a `git clone` named
|
|
* `elementor-mcp`) would otherwise get a second, orphaned plugin copy.
|
|
*
|
|
* @param string $source Extracted source directory.
|
|
* @param string $remote_source Parent of $source.
|
|
* @param WP_Upgrader $upgrader The upgrader instance.
|
|
* @param array $args Hook extras (includes `plugin` on plugin updates).
|
|
* @return string|WP_Error
|
|
*/
|
|
public function rename_source_dir( $source, $remote_source, $upgrader, $args = array() ) {
|
|
global $wp_filesystem;
|
|
|
|
// Only touch OUR update.
|
|
if ( empty( $args['plugin'] ) || EMCP_TOOLS_BASENAME !== $args['plugin'] ) {
|
|
return $source;
|
|
}
|
|
if ( ! $wp_filesystem ) {
|
|
return $source;
|
|
}
|
|
|
|
$desired = trailingslashit( $remote_source ) . $this->slug . '/';
|
|
if ( untrailingslashit( $source ) === untrailingslashit( $desired ) ) {
|
|
return $source;
|
|
}
|
|
|
|
if ( $wp_filesystem->move( $source, $desired, true ) ) {
|
|
return $desired;
|
|
}
|
|
|
|
return new WP_Error(
|
|
'emcp_tools_rename_failed',
|
|
__( 'Could not rename the downloaded EMCP Tools folder during the update.', 'emcp-tools' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clear the cached release after our plugin finishes updating, so the next
|
|
* check reflects the freshly installed version.
|
|
*
|
|
* @param WP_Upgrader $upgrader The upgrader instance.
|
|
* @param array $data Process data (`action`, `type`, `plugins`).
|
|
* @return void
|
|
*/
|
|
public function clear_cache_after_update( $upgrader, $data ): void {
|
|
if ( ! is_array( $data ) || ( $data['action'] ?? '' ) !== 'update' || ( $data['type'] ?? '' ) !== 'plugin' ) {
|
|
return;
|
|
}
|
|
$plugins = (array) ( $data['plugins'] ?? array() );
|
|
if ( ! in_array( EMCP_TOOLS_BASENAME, $plugins, true ) ) {
|
|
return;
|
|
}
|
|
|
|
// Drop our cached GitHub release so the next check re-fetches.
|
|
delete_transient( self::TRANSIENT );
|
|
|
|
// Also remove our now-satisfied entry from the update_plugins transient so
|
|
// the "update available" notice clears immediately, without waiting for the
|
|
// next scheduled check. (WP rebuilds the rest on the next request.)
|
|
$updates = get_site_transient( 'update_plugins' );
|
|
if ( is_object( $updates ) ) {
|
|
unset( $updates->response[ EMCP_TOOLS_BASENAME ] );
|
|
set_site_transient( 'update_plugins', $updates );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch + parse the latest GitHub release, cached in a transient to respect
|
|
* GitHub's unauthenticated rate limit (60/hr per IP). Returns null on any
|
|
* failure (network, non-200, no matching asset), caching the miss briefly.
|
|
*
|
|
* @return array{version:string,package:string,changelog:string,published:string,html_url:string,tested:string,requires:string,requires_php:string}|null
|
|
*/
|
|
private function get_latest_release(): ?array {
|
|
$cached = get_transient( self::TRANSIENT );
|
|
if ( is_array( $cached ) ) {
|
|
return isset( $cached['version'] ) ? $cached : null;
|
|
}
|
|
|
|
$response = wp_remote_get(
|
|
'https://api.github.com/repos/' . self::REPO . '/releases/latest',
|
|
array(
|
|
'timeout' => 15,
|
|
'headers' => array(
|
|
'Accept' => 'application/vnd.github+json',
|
|
'User-Agent' => 'Heretek-AI-Elementor-MCP-Updater/' . EMCP_TOOLS_VERSION,
|
|
),
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
set_transient( self::TRANSIENT, array(), self::CACHE_TTL_FAIL );
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
if ( ! is_array( $data ) || empty( $data['tag_name'] ) ) {
|
|
set_transient( self::TRANSIENT, array(), self::CACHE_TTL_FAIL );
|
|
return null;
|
|
}
|
|
|
|
// Skip drafts / pre-releases — users should only get stable builds.
|
|
if ( ! empty( $data['draft'] ) || ! empty( $data['prerelease'] ) ) {
|
|
set_transient( self::TRANSIENT, array(), self::CACHE_TTL_FAIL );
|
|
return null;
|
|
}
|
|
|
|
$version = ltrim( (string) $data['tag_name'], 'vV' );
|
|
$package = '';
|
|
foreach ( (array) ( $data['assets'] ?? array() ) as $asset ) {
|
|
if ( ! empty( $asset['name'] ) && preg_match( self::ASSET_PATTERN, $asset['name'] ) ) {
|
|
$url = (string) ( $asset['browser_download_url'] ?? '' );
|
|
if ( self::is_release_asset_url( $url ) ) {
|
|
$package = $url;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback to GitHub source zipball if no explicit release asset binary was uploaded.
|
|
if ( '' === $package && ! empty( $data['zipball_url'] ) ) {
|
|
$zipball = (string) $data['zipball_url'];
|
|
if ( self::is_release_asset_url( $zipball ) ) {
|
|
$package = $zipball;
|
|
}
|
|
}
|
|
|
|
if ( '' === $version || '' === $package ) {
|
|
set_transient( self::TRANSIENT, array(), self::CACHE_TTL_FAIL );
|
|
return null;
|
|
}
|
|
|
|
$release = array(
|
|
'version' => $version,
|
|
'package' => $package,
|
|
'changelog' => (string) ( $data['body'] ?? '' ),
|
|
'published' => (string) ( $data['published_at'] ?? '' ),
|
|
'html_url' => (string) ( $data['html_url'] ?? ( 'https://github.com/' . self::REPO . '/releases' ) ),
|
|
'tested' => $this->plugin_header( 'Tested up to', '6.9' ),
|
|
'requires' => $this->plugin_header( 'Requires at least', '6.9' ),
|
|
'requires_php' => $this->plugin_header( 'Requires PHP', '8.1' ),
|
|
);
|
|
|
|
set_transient( self::TRANSIENT, $release, self::CACHE_TTL );
|
|
return $release;
|
|
}
|
|
|
|
/**
|
|
* Read a value from the installed plugin's header (cheap, cached by core).
|
|
*
|
|
* @param string $field Header field label.
|
|
* @param string $fallback Default when unavailable.
|
|
* @return string
|
|
*/
|
|
private function plugin_header( string $field, string $fallback ): string {
|
|
if ( ! function_exists( 'get_plugin_data' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
}
|
|
$data = get_plugin_data( EMCP_TOOLS_DIR . basename( EMCP_TOOLS_BASENAME ), false, false );
|
|
$map = array(
|
|
'Tested up to' => 'Tested up to',
|
|
'Requires at least' => 'RequiresWP',
|
|
'Requires PHP' => 'RequiresPHP',
|
|
);
|
|
$key = $map[ $field ] ?? $field;
|
|
$val = isset( $data[ $key ] ) ? trim( (string) $data[ $key ] ) : '';
|
|
return '' !== $val ? $val : $fallback;
|
|
}
|
|
|
|
/**
|
|
* The plugin's own bundled icons for the update UI.
|
|
*
|
|
* @return array<string,string>
|
|
*/
|
|
private function plugin_icons(): array {
|
|
return array(
|
|
'1x' => EMCP_TOOLS_URL . 'assets/img/icon-sm.png',
|
|
'default' => EMCP_TOOLS_URL . 'assets/img/icon-sm.png',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Turn the release's markdown body into safe HTML for the details modal, with
|
|
* a link to the full notes on GitHub.
|
|
*
|
|
* @param string $markdown Release body.
|
|
* @param string $html_url GitHub release URL.
|
|
* @return string
|
|
*/
|
|
private function render_changelog( string $markdown, string $html_url ): string {
|
|
$markdown = trim( $markdown );
|
|
if ( '' === $markdown ) {
|
|
$body = '<p>' . esc_html__( 'See the full release notes on GitHub.', 'emcp-tools' ) . '</p>';
|
|
} else {
|
|
// Light markdown → HTML: headings, list bullets, paragraphs. Escaped first.
|
|
$lines = preg_split( '/\r\n|\r|\n/', $markdown );
|
|
$out = array();
|
|
$in_ul = false;
|
|
foreach ( $lines as $line ) {
|
|
$line = rtrim( $line );
|
|
if ( preg_match( '/^#{1,6}\s+(.*)$/', $line, $m ) ) {
|
|
if ( $in_ul ) {
|
|
$out[] = '</ul>';
|
|
$in_ul = false;
|
|
}
|
|
$out[] = '<h4>' . esc_html( $m[1] ) . '</h4>';
|
|
} elseif ( preg_match( '/^[\-\*]\s+(.*)$/', $line, $m ) ) {
|
|
if ( ! $in_ul ) {
|
|
$out[] = '<ul>';
|
|
$in_ul = true;
|
|
}
|
|
$out[] = '<li>' . esc_html( $m[1] ) . '</li>';
|
|
} elseif ( '' === trim( $line ) ) {
|
|
if ( $in_ul ) {
|
|
$out[] = '</ul>';
|
|
$in_ul = false;
|
|
}
|
|
} else {
|
|
if ( $in_ul ) {
|
|
$out[] = '</ul>';
|
|
$in_ul = false;
|
|
}
|
|
$out[] = '<p>' . esc_html( $line ) . '</p>';
|
|
}
|
|
}
|
|
if ( $in_ul ) {
|
|
$out[] = '</ul>';
|
|
}
|
|
$body = implode( "\n", $out );
|
|
}
|
|
|
|
$body .= '<p><a href="' . esc_url( $html_url ) . '" target="_blank" rel="noopener noreferrer">'
|
|
. esc_html__( 'View full release notes on GitHub →', 'emcp-tools' ) . '</a></p>';
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Is this URL one of THIS repository's release assets?
|
|
*
|
|
* GitHub serves release downloads from github.com and redirects to its own
|
|
* object storage, so both hosts are accepted; anything else is not a release
|
|
* of ours and is refused.
|
|
*
|
|
* @since 3.14.1
|
|
* @param string $url Candidate download URL.
|
|
* @return bool
|
|
*/
|
|
public static function is_release_asset_url( string $url ): bool {
|
|
$parts = wp_parse_url( $url );
|
|
if ( ! is_array( $parts ) || 'https' !== strtolower( (string) ( $parts['scheme'] ?? '' ) ) ) {
|
|
return false;
|
|
}
|
|
|
|
$host = strtolower( (string) ( $parts['host'] ?? '' ) );
|
|
$path = (string) ( $parts['path'] ?? '' );
|
|
|
|
if ( 'github.com' === $host ) {
|
|
return 0 === strpos( $path, '/' . self::REPO . '/releases/' )
|
|
|| 0 === strpos( $path, '/' . self::REPO . '/archive/' );
|
|
}
|
|
|
|
if ( 'api.github.com' === $host ) {
|
|
return 0 === strpos( $path, '/repos/' . self::REPO . '/zipball/' );
|
|
}
|
|
|
|
if ( 'codeload.github.com' === $host ) {
|
|
return 0 === strpos( $path, '/' . self::REPO . '/' );
|
|
}
|
|
|
|
return 'objects.githubusercontent.com' === $host || 'release-assets.githubusercontent.com' === $host;
|
|
}
|
|
|
|
/**
|
|
* Force an immediate release check by purging transients.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function check_now(): bool {
|
|
delete_transient( self::TRANSIENT );
|
|
$updates = get_site_transient( 'update_plugins' );
|
|
if ( is_object( $updates ) ) {
|
|
unset( $updates->response[ EMCP_TOOLS_BASENAME ] );
|
|
unset( $updates->no_update[ EMCP_TOOLS_BASENAME ] );
|
|
set_site_transient( 'update_plugins', $updates );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
}
|