mirror of
https://github.com/Heretek-AI/heretek-control-core.git
synced 2026-10-12 02:03:55 +00:00
Sweeps shipped copy clean of em dashes: readme.txt, README.md, CHANGELOG.md, the bundled sample prompts, and user-facing PHP strings (tool descriptions, admin labels). PHP was swept on non-comment lines only, so developer docblocks and inline comments are untouched. Internal dev docs (CLAUDE.md, docs/) were deliberately left alone. Replacements are contextual: colons for label/definition patterns, commas for asides, en dashes for numeric ranges. The .mcpb bundle display_name now reads "EMCP Tools - <host>" with a plain hyphen; its test is updated to match. Fixing CHANGELOG.md also clears the em dashes from the website's generated changelog on the next site build, since that file is its source. Verified: php -l clean on every touched file, pro suite 1559 pass, public suite 102 pass. Bumps the pro submodule for the Pro-side sweep.
247 lines
7.5 KiB
PHP
247 lines
7.5 KiB
PHP
<?php
|
|
/**
|
|
* Content mirror — export Elementor page/template content to git-trackable JSON
|
|
* files under uploads/, so an external VCS (the user's git/CI) can version and
|
|
* diff page designs. The plugin never runs git itself.
|
|
*
|
|
* Complements AI-safe transactions: transactions are an in-DB recent-change
|
|
* ledger + rollback; the mirror is durable, diffable, file-based history.
|
|
*
|
|
* @package EMCP_Tools
|
|
* @since 3.3.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Writes/reads/restores the content mirror.
|
|
*
|
|
* @since 3.3.0
|
|
*/
|
|
class EMCP_Tools_Content_Mirror {
|
|
|
|
const OPTION_ENABLED = 'emcp_tools_content_mirror_enabled';
|
|
const MIRROR_DIR = 'emcp-content-mirror';
|
|
|
|
/**
|
|
* Whether the on-save auto-export is enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function enabled(): bool {
|
|
return '1' === (string) get_option( self::OPTION_ENABLED, '' );
|
|
}
|
|
|
|
/**
|
|
* Wire the on-save / on-delete hooks.
|
|
*/
|
|
public static function init(): void {
|
|
add_action( 'save_post', array( __CLASS__, 'on_save_post' ), 40, 2 );
|
|
add_action( 'before_delete_post', array( __CLASS__, 'on_delete_post' ), 10, 1 );
|
|
}
|
|
|
|
/**
|
|
* The mirror base directory (absolute).
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function dir(): string {
|
|
$uploads = wp_upload_dir();
|
|
return trailingslashit( $uploads['basedir'] ) . self::MIRROR_DIR;
|
|
}
|
|
|
|
/**
|
|
* Pure: build the export payload for a post.
|
|
*
|
|
* @param array $post { id, type, slug, title }.
|
|
* @param array $elements Elementor elements array.
|
|
* @return array
|
|
*/
|
|
public static function build_export( array $post, array $elements ): array {
|
|
return array(
|
|
'id' => (int) ( $post['id'] ?? 0 ),
|
|
'type' => (string) ( $post['type'] ?? '' ),
|
|
'slug' => (string) ( $post['slug'] ?? '' ),
|
|
'title' => (string) ( $post['title'] ?? '' ),
|
|
'elementor_data' => $elements,
|
|
'exported_at' => time(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pure: a deterministic mirror filename.
|
|
*
|
|
* @param string $type Object type.
|
|
* @param int $id Post ID.
|
|
* @param string $slug Human slug (sanitized).
|
|
* @return string
|
|
*/
|
|
public static function file_name( string $type, int $id, string $slug ): string {
|
|
$slug = strtolower( (string) preg_replace( '/[^A-Za-z0-9]+/', '-', $slug ) );
|
|
$slug = trim( $slug, '-' );
|
|
$base = $type . '-' . $id . ( '' !== $slug ? '-' . $slug : '' );
|
|
return $base . '.json';
|
|
}
|
|
|
|
/**
|
|
* Export one post to the mirror.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @return string|WP_Error Absolute file path, or error.
|
|
*/
|
|
public static function export_post( int $post_id ) {
|
|
$post = get_post( $post_id );
|
|
if ( ! $post ) {
|
|
return new WP_Error( 'not_found', __( 'Post not found.', 'emcp-tools' ) );
|
|
}
|
|
$type = ( 'elementor_library' === $post->post_type ) ? 'template' : 'page';
|
|
|
|
$elements = array();
|
|
if ( class_exists( 'EMCP_Tools_Data' ) ) {
|
|
try {
|
|
$data = new EMCP_Tools_Data();
|
|
$e = $data->get_page_data( $post_id );
|
|
if ( is_array( $e ) ) {
|
|
$elements = $e;
|
|
}
|
|
} catch ( \Throwable $ex ) {
|
|
$elements = array();
|
|
}
|
|
}
|
|
|
|
$payload = self::build_export(
|
|
array(
|
|
'id' => $post_id,
|
|
'type' => $type,
|
|
'slug' => $post->post_name,
|
|
'title' => get_the_title( $post_id ),
|
|
),
|
|
$elements
|
|
);
|
|
|
|
$dir = self::dir();
|
|
if ( ! wp_mkdir_p( $dir ) ) {
|
|
return new WP_Error( 'mkdir_failed', __( 'Could not create the mirror directory.', 'emcp-tools' ) );
|
|
}
|
|
if ( ! is_file( $dir . '/.gitignore' ) ) {
|
|
// Do NOT ignore json — this dir is meant to be committed. Keep a README hint.
|
|
@file_put_contents( $dir . '/README.txt', "EMCP content mirror, commit the .json files to version your page designs.\n" ); // phpcs:ignore
|
|
}
|
|
|
|
$path = $dir . '/' . self::file_name( $type, $post_id, (string) $post->post_name );
|
|
$bytes = file_put_contents( $path, wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
|
|
if ( false === $bytes ) {
|
|
return new WP_Error( 'write_failed', __( 'Could not write the mirror file.', 'emcp-tools' ) );
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Restore a post's Elementor content from its mirror file.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @return true|WP_Error
|
|
*/
|
|
public static function restore_post( int $post_id ) {
|
|
$post = get_post( $post_id );
|
|
if ( ! $post ) {
|
|
return new WP_Error( 'not_found', __( 'Post not found.', 'emcp-tools' ) );
|
|
}
|
|
$type = ( 'elementor_library' === $post->post_type ) ? 'template' : 'page';
|
|
$path = self::dir() . '/' . self::file_name( $type, $post_id, (string) $post->post_name );
|
|
if ( ! is_file( $path ) ) {
|
|
return new WP_Error( 'no_export', __( 'No mirror file exists for this post.', 'emcp-tools' ) );
|
|
}
|
|
$decoded = json_decode( (string) file_get_contents( $path ), true );
|
|
if ( ! is_array( $decoded ) || ! isset( $decoded['elementor_data'] ) || ! is_array( $decoded['elementor_data'] ) ) {
|
|
return new WP_Error( 'bad_export', __( 'The mirror file is invalid.', 'emcp-tools' ) );
|
|
}
|
|
if ( ! class_exists( 'EMCP_Tools_Data' ) ) {
|
|
return new WP_Error( 'no_data_layer', __( 'The Elementor data layer is unavailable.', 'emcp-tools' ) );
|
|
}
|
|
$data = new EMCP_Tools_Data();
|
|
$res = $data->save_page_data( $post_id, $decoded['elementor_data'] );
|
|
return is_wp_error( $res ) ? $res : true;
|
|
}
|
|
|
|
/**
|
|
* Export all Elementor pages + templates.
|
|
*
|
|
* @return array{pages:int,templates:int}
|
|
*/
|
|
public static function export_all(): array {
|
|
$counts = array( 'pages' => 0, 'templates' => 0 );
|
|
|
|
$pages = new WP_Query( array(
|
|
'post_type' => array( 'page', 'post' ),
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
'meta_query' => array( array( 'key' => '_elementor_edit_mode', 'value' => 'builder' ) ), // phpcs:ignore WordPress.DB.SlowDBQuery
|
|
) );
|
|
foreach ( (array) $pages->posts as $id ) {
|
|
if ( ! is_wp_error( self::export_post( (int) $id ) ) ) {
|
|
++$counts['pages'];
|
|
}
|
|
}
|
|
|
|
$templates = new WP_Query( array(
|
|
'post_type' => 'elementor_library',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
) );
|
|
foreach ( (array) $templates->posts as $id ) {
|
|
if ( ! is_wp_error( self::export_post( (int) $id ) ) ) {
|
|
++$counts['templates'];
|
|
}
|
|
}
|
|
|
|
return $counts;
|
|
}
|
|
|
|
/**
|
|
* save_post handler: auto-export when enabled.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param WP_Post|null $post Post.
|
|
*/
|
|
public static function on_save_post( $post_id, $post = null ): void {
|
|
if ( ! self::enabled() ) {
|
|
return;
|
|
}
|
|
$post_id = (int) $post_id;
|
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
|
return;
|
|
}
|
|
if ( function_exists( 'wp_is_post_revision' ) && wp_is_post_revision( $post_id ) ) {
|
|
return;
|
|
}
|
|
$ptype = $post && isset( $post->post_type ) ? $post->post_type : ( function_exists( 'get_post_type' ) ? get_post_type( $post_id ) : '' );
|
|
if ( 'elementor_library' === $ptype ) {
|
|
self::export_post( $post_id );
|
|
} elseif ( in_array( $ptype, array( 'page', 'post' ), true ) && 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) {
|
|
self::export_post( $post_id );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* before_delete_post handler: remove the mirror file.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
*/
|
|
public static function on_delete_post( $post_id ): void {
|
|
$post = get_post( $post_id );
|
|
if ( ! $post ) {
|
|
return;
|
|
}
|
|
$type = ( 'elementor_library' === $post->post_type ) ? 'template' : 'page';
|
|
$path = self::dir() . '/' . self::file_name( $type, (int) $post_id, (string) $post->post_name );
|
|
if ( is_file( $path ) ) {
|
|
wp_delete_file( $path );
|
|
}
|
|
}
|
|
}
|