wp-git-sync/includes/class-wpgs-diff.php
2026-02-09 21:54:39 +00:00

182 lines
4.6 KiB
PHP

<?php
/**
* Diff/check helpers.
*
* @package WPGitSync
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Helpers for comparing a local post to its remote representation in GitHub.
*/
final class WPGS_Diff {
/**
* Compute the deterministic repo-relative paths for a post.
*
* @param WP_Post $post Post.
* @return array{content_path:string,post_path:string,meta_path:string}
*/
public static function paths_for_post( WP_Post $post ): array {
return [
'content_path' => WPGS_Paths::content_relpath( (string) $post->post_type, (int) $post->ID ),
'post_path' => WPGS_Paths::post_data_relpath( (string) $post->post_type, (int) $post->ID ),
'meta_path' => WPGS_Paths::meta_relpath( (string) $post->post_type, (int) $post->ID ),
];
}
/**
* Build the local export content, post-data JSON, and meta JSON for a post.
*
* @param WP_Post $post Post.
* @return array{content:string,post_json:string,meta_json:string,post_data:array<string,mixed>,meta:array<string,mixed>}
*/
public static function build_local_payload( WP_Post $post ): array {
$content = (string) $post->post_content;
$post_data = get_post( $post->ID, ARRAY_A );
if ( ! is_array( $post_data ) ) {
$post_data = [];
}
unset( $post_data['post_content'] );
$all_meta = get_post_meta( $post->ID );
if ( ! is_array( $all_meta ) ) {
$all_meta = [];
}
// Avoid exporting our own internal meta.
foreach ( WPGS_Sync_Meta::internal_keys() as $k ) {
unset( $all_meta[ $k ] );
}
$meta_blacklist = self::meta_blacklist();
foreach ( array_keys( $all_meta ) as $meta_key ) {
$meta_key = (string) $meta_key;
if ( self::meta_key_matches_blacklist( $meta_key, $meta_blacklist ) ) {
unset( $all_meta[ $meta_key ] );
}
}
/**
* Filter exported post meta.
*
* @param array<string,mixed> $all_meta All meta.
* @param int $post_id Post ID.
*/
$all_meta = apply_filters( 'wpgs_export_postmeta', $all_meta, (int) $post->ID );
$post_json = self::stable_json( $post_data ) . "\n";
$meta_json = self::stable_json( $all_meta ) . "\n";
return [
'content' => $content,
'post_json' => $post_json,
'meta_json' => $meta_json,
'post_data' => $post_data,
'meta' => $all_meta,
];
}
/**
* Post-meta keys excluded from export by default.
*
* @return string[]
*/
public static function meta_blacklist(): array {
$blacklist = array_merge(
[ '_edit_lock' ],
WPGS_Settings::excluded_post_meta_keys()
);
/**
* Filter post-meta keys excluded from export.
*
* @param string[] $blacklist Meta keys to exclude.
*/
$blacklist = apply_filters( 'wpgs_export_postmeta_blacklist', $blacklist );
if ( ! is_array( $blacklist ) ) {
return [ '_edit_lock' ];
}
return array_values( array_unique( array_map( 'strval', $blacklist ) ) );
}
/**
* Determine whether a meta key matches any blacklist rule.
*
* Supports exact-key rules and simple wildcard patterns using `*`.
* Example: `_elementor*` matches `_elementor_data`.
*
* @param string $meta_key Meta key being evaluated.
* @param string[] $blacklist Blacklist rules.
* @return bool
*/
public static function meta_key_matches_blacklist( string $meta_key, array $blacklist ): bool {
$meta_key = (string) $meta_key;
if ( '' === $meta_key ) {
return false;
}
foreach ( $blacklist as $rule ) {
$rule = trim( (string) $rule );
if ( '' === $rule ) {
continue;
}
if ( false === strpos( $rule, '*' ) ) {
if ( $meta_key === $rule ) {
return true;
}
continue;
}
$regex = '/^' . str_replace( '\*', '.*', preg_quote( $rule, '/' ) ) . '$/';
if ( 1 === preg_match( $regex, $meta_key ) ) {
return true;
}
}
return false;
}
/**
* Normalize line endings to improve diff stability.
*
* @param string $text Text.
* @return string
*/
public static function normalize_newlines( string $text ): string {
return str_replace( "\r\n", "\n", $text );
}
/**
* Generate a stable JSON string with recursive key sorting.
*
* @param mixed $data Data.
* @return string
*/
public static function stable_json( $data ): string {
$data = self::ksort_recursive( $data );
return (string) wp_json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
}
/**
* Recursively sort associative arrays by key.
*
* @param mixed $value Value.
* @return mixed
*/
private static function ksort_recursive( $value ) {
if ( is_array( $value ) ) {
$is_assoc = array_keys( $value ) !== range( 0, count( $value ) - 1 );
if ( $is_assoc ) {
ksort( $value );
}
foreach ( $value as $k => $v ) {
$value[ $k ] = self::ksort_recursive( $v );
}
}
return $value;
}
}