597 lines
25 KiB
PHP
597 lines
25 KiB
PHP
<?php
|
|
/**
|
|
* Oribi Sync — Push client.
|
|
*
|
|
* Writes page content back to the Git repository.
|
|
* On SHA conflict (remote file changed since last sync), creates a branch
|
|
* named oribi-sync/{slug}-{timestamp} and opens a pull request.
|
|
*
|
|
* Currently supports Gitea / Forgejo.
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Detect the actual casing of the Pages directory in the repo tree.
|
|
*
|
|
* Looks at existing synced pages for a stored repo path, extracts the
|
|
* directory prefix. Falls back to 'Pages/' if nothing found.
|
|
*/
|
|
function oribi_sync_detect_pages_prefix(): string {
|
|
// Check post meta of any previously-synced page for the real path
|
|
$existing = get_posts( [
|
|
'post_type' => 'page',
|
|
'meta_key' => '_oribi_sync_source',
|
|
'numberposts' => 1,
|
|
'fields' => 'ids',
|
|
] );
|
|
|
|
if ( ! empty( $existing ) ) {
|
|
$source = get_post_meta( $existing[0], '_oribi_sync_source', true );
|
|
// Extract the repo-path portion after the last colon (skip 'https:').
|
|
$colon = strrpos( $source, ':' );
|
|
if ( $colon !== false ) {
|
|
$path_part = substr( $source, $colon + 1 ); // e.g. 'pages/about.php'
|
|
// Validate it looks like a pages/ path before trusting it.
|
|
if ( strncasecmp( $path_part, 'pages/', 6 ) === 0 ) {
|
|
return substr( $path_part, 0, 6 ); // preserve original casing
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'pages/';
|
|
}
|
|
|
|
// ─── Generic authenticated request helpers ────────────────────────────────────
|
|
|
|
/**
|
|
* Perform an authenticated POST request to the Git API.
|
|
*
|
|
* @param string $url Full API URL.
|
|
* @param array $body Body payload (will be JSON-encoded).
|
|
* @param string $provider Provider key.
|
|
* @param string $pat Personal access token.
|
|
*
|
|
* @return array{code: int, body: array|string}|WP_Error
|
|
*/
|
|
function oribi_sync_api_post( string $url, array $body, string $provider, string $pat ) {
|
|
return oribi_sync_api_request( 'POST', $url, $body, $provider, $pat );
|
|
}
|
|
|
|
/**
|
|
* Perform an authenticated PUT request to the Git API.
|
|
*
|
|
* @return array{code: int, body: array|string}|WP_Error
|
|
*/
|
|
function oribi_sync_api_put( string $url, array $body, string $provider, string $pat ) {
|
|
return oribi_sync_api_request( 'PUT', $url, $body, $provider, $pat );
|
|
}
|
|
|
|
/**
|
|
* Perform an authenticated DELETE request to the Git API.
|
|
*
|
|
* @return array{code: int, body: array|string}|WP_Error
|
|
*/
|
|
function oribi_sync_api_delete( string $url, array $body, string $provider, string $pat ) {
|
|
return oribi_sync_api_request( 'DELETE', $url, $body, $provider, $pat );
|
|
}
|
|
|
|
/**
|
|
* Internal: send an authenticated JSON request.
|
|
*
|
|
* @return array{code: int, body: array|string}|WP_Error
|
|
*/
|
|
function oribi_sync_api_request( string $method, string $url, array $body, string $provider, string $pat ) {
|
|
$headers = array_merge(
|
|
oribi_sync_auth_headers( $provider, $pat ),
|
|
[
|
|
'Content-Type' => 'application/json; charset=utf-8',
|
|
'Accept' => 'application/json',
|
|
'User-Agent' => 'Oribi-Sync-WP/' . ORIBI_SYNC_VERSION,
|
|
]
|
|
);
|
|
|
|
// Ensure UTF-8 encoding of all body content
|
|
array_walk_recursive( $body, function ( &$item ) {
|
|
if ( is_string( $item ) && ! mb_check_encoding( $item, 'UTF-8' ) ) {
|
|
$item = mb_convert_encoding( $item, 'UTF-8' );
|
|
}
|
|
});
|
|
|
|
$args = [
|
|
'method' => $method,
|
|
'timeout' => 30,
|
|
'headers' => $headers,
|
|
'body' => wp_json_encode( $body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
|
|
];
|
|
|
|
$response = wp_remote_request( $url, $args );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
$code = wp_remote_retrieve_response_code( $response );
|
|
$raw_body = wp_remote_retrieve_body( $response );
|
|
$decoded = json_decode( $raw_body, true );
|
|
|
|
if ( json_last_error() !== JSON_ERROR_NONE ) {
|
|
$decoded = $raw_body;
|
|
}
|
|
|
|
return [ 'code' => $code, 'body' => $decoded ];
|
|
}
|
|
|
|
// ─── Gitea file metadata ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Get file metadata (including current SHA) from the Gitea API.
|
|
*
|
|
* @param string $api_base Gitea API base (e.g. https://host/api/v1/repos/owner/repo).
|
|
* @param string $branch Branch name.
|
|
* @param string $filepath Repo-relative file path.
|
|
* @param string $pat Personal access token.
|
|
*
|
|
* @return array{sha: string, content: string}|null|WP_Error
|
|
* null if file does not exist (404), WP_Error on failure.
|
|
*/
|
|
function oribi_sync_gitea_get_file_meta( string $api_base, string $branch, string $filepath, string $pat ) {
|
|
$encoded_path = implode( '/', array_map( 'rawurlencode', explode( '/', $filepath ) ) );
|
|
$url = $api_base . '/contents/' . $encoded_path . '?ref=' . rawurlencode( $branch );
|
|
|
|
$result = oribi_sync_api_get( $url, 'gitea', $pat );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
$msg = $result->get_error_message();
|
|
// 404 means file doesn't exist yet — not an error
|
|
if ( strpos( $msg, 'HTTP 404' ) !== false ) {
|
|
return null;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
// Gitea inserts \n every 60 chars in base64 — strip before decoding.
|
|
$raw_b64 = $result['content'] ?? '';
|
|
$content = ! empty( $raw_b64 )
|
|
? base64_decode( str_replace( [ "\r", "\n", " " ], '', $raw_b64 ), true )
|
|
: '';
|
|
|
|
return [
|
|
'sha' => $result['sha'] ?? '',
|
|
'content' => ( $content !== false ) ? $content : '',
|
|
];
|
|
}
|
|
|
|
// ─── Gitea file create / update ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Create or update a file in a Gitea repository.
|
|
*
|
|
* @param string $api_base API base URL.
|
|
* @param string $branch Target branch.
|
|
* @param string $filepath Repo-relative path.
|
|
* @param string $content Raw file content (will be base64-encoded).
|
|
* @param string $pat Personal access token.
|
|
* @param string|null $sha Current file SHA (required for updates; null for creates).
|
|
* @param string $message Commit message.
|
|
*
|
|
* @return array{code: int, body: array}|WP_Error
|
|
*/
|
|
function oribi_sync_gitea_put_file(
|
|
string $api_base,
|
|
string $branch,
|
|
string $filepath,
|
|
string $content,
|
|
string $pat,
|
|
?string $sha = null,
|
|
string $message = ''
|
|
) {
|
|
// Validate and fix UTF-8 encoding before base64-encoding
|
|
if ( ! mb_check_encoding( $content, 'UTF-8' ) ) {
|
|
$content = mb_convert_encoding( $content, 'UTF-8', 'UTF-8, ISO-8859-1, Windows-1252' );
|
|
}
|
|
|
|
$encoded_path = implode( '/', array_map( 'rawurlencode', explode( '/', $filepath ) ) );
|
|
$url = $api_base . '/contents/' . $encoded_path;
|
|
|
|
$body = [
|
|
'content' => base64_encode( $content ),
|
|
'branch' => $branch,
|
|
'message' => $message ?: 'Update ' . basename( $filepath ),
|
|
];
|
|
|
|
if ( $sha !== null ) {
|
|
$body['sha'] = $sha;
|
|
return oribi_sync_api_put( $url, $body, 'gitea', $pat );
|
|
}
|
|
|
|
return oribi_sync_api_post( $url, $body, 'gitea', $pat );
|
|
}
|
|
|
|
// ─── Gitea branch creation ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Create a new branch in a Gitea repository.
|
|
*
|
|
* @return array{code: int, body: array}|WP_Error
|
|
*/
|
|
function oribi_sync_gitea_create_branch( string $api_base, string $new_branch, string $base_branch, string $pat ) {
|
|
$url = $api_base . '/branches';
|
|
|
|
return oribi_sync_api_post( $url, [
|
|
'new_branch_name' => $new_branch,
|
|
'old_branch_name' => $base_branch,
|
|
], 'gitea', $pat );
|
|
}
|
|
|
|
// ─── Gitea pull request ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Open a pull request in a Gitea repository.
|
|
*
|
|
* @return array{code: int, body: array}|WP_Error
|
|
*/
|
|
function oribi_sync_gitea_create_pr( string $api_base, string $head, string $base, string $title, string $body_text, string $pat ) {
|
|
$url = $api_base . '/pulls';
|
|
|
|
return oribi_sync_api_post( $url, [
|
|
'title' => $title,
|
|
'body' => $body_text,
|
|
'head' => $head,
|
|
'base' => $base,
|
|
], 'gitea', $pat );
|
|
}
|
|
|
|
// ─── PHP page-data wrapper generation ─────────────────────────────────────────
|
|
|
|
/**
|
|
* Generate a PHP page-data wrapper suitable for the repo's page-data convention.
|
|
*
|
|
* The wrapper uses a nowdoc return so the file can be `include`d by
|
|
* oribi_sync_execute_php() and produce the Gutenberg block HTML.
|
|
*
|
|
* @param string $content Gutenberg block HTML (from post_content).
|
|
* @param string $slug Page slug.
|
|
* @param string $title Page title.
|
|
*
|
|
* @return string PHP source code.
|
|
*/
|
|
function oribi_sync_generate_php_wrapper( string $content, string $slug, string $title = '' ): string {
|
|
if ( empty( $title ) ) {
|
|
$title = oribi_sync_slug_to_title( $slug );
|
|
}
|
|
|
|
// Use a nowdoc so the content is treated as a literal string (no interpolation).
|
|
$delimiter = 'ORIBI_SYNC_CONTENT';
|
|
$safe_content = str_replace( "\n" . $delimiter, "\n " . $delimiter, $content );
|
|
|
|
$php = "<?php\n";
|
|
$php .= "/*\n";
|
|
$php .= " * Title: {$title}\n";
|
|
$php .= " * Slug: {$slug}\n";
|
|
$php .= " * Post Type: page\n";
|
|
$php .= " */\n\n";
|
|
$php .= "return <<<'{$delimiter}'\n";
|
|
$php .= $safe_content . "\n";
|
|
$php .= $delimiter . ";\n";
|
|
|
|
return $php;
|
|
}
|
|
|
|
/**
|
|
* Replace the content body in an existing PHP page-data file.
|
|
*
|
|
* Preserves the original header (everything before the `return` statement)
|
|
* and only replaces the body between the heredoc / nowdoc delimiters.
|
|
* If the file format can't be parsed, falls back to generating a new wrapper.
|
|
*
|
|
* @param string $existing_source Current PHP source from the repo.
|
|
* @param string $new_content New Gutenberg block HTML.
|
|
* @param string $slug Page slug (used for fallback wrapper).
|
|
* @param string $title Page title (used for fallback wrapper).
|
|
*
|
|
* @return string Updated PHP source code.
|
|
*/
|
|
function oribi_sync_replace_php_body( string $existing_source, string $new_content, string $slug, string $title ): string {
|
|
// Match: return <<<'DELIMITER' or return <<<DELIMITER (heredoc / nowdoc)
|
|
if ( preg_match( '/^(.*?return\s+<<<\'?)(\w+)(\'?\s*\n)(.*)(\n\2;?\s*)$/s', $existing_source, $m ) ) {
|
|
$header = $m[1]; // everything up to and including "return <<<"
|
|
$delimiter = $m[2]; // e.g. ORIBI_SYNC_CONTENT
|
|
$quote_end = $m[3]; // closing quote + newline
|
|
$suffix = $m[5]; // closing delimiter + semicolon
|
|
|
|
// Escape content if it contains the delimiter string on its own line
|
|
$safe_content = str_replace( "\n" . $delimiter, "\n " . $delimiter, $new_content );
|
|
|
|
return $header . $delimiter . $quote_end . $safe_content . $suffix;
|
|
}
|
|
|
|
// Couldn't parse the existing file — fall back to a fresh wrapper.
|
|
return oribi_sync_generate_php_wrapper( $new_content, $slug, $title );
|
|
}
|
|
|
|
// ─── Push orchestrator ───────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Push a single WordPress page back to the Git repository.
|
|
*
|
|
* Flow:
|
|
* 1. Derive repo path from post meta (_oribi_sync_source) or settings.
|
|
* 2. Generate PHP wrapper from page content.
|
|
* 3. Fetch remote file SHA from the Gitea contents API.
|
|
* 4. If remote SHA matches stored SHA → direct update (no conflict).
|
|
* 5. If remote SHA differs (file changed externally) → create branch + PR.
|
|
* 6. If file doesn't exist → create it on the target branch.
|
|
* 7. Update post meta on success.
|
|
*
|
|
* @param int $post_id WP post ID.
|
|
* @param array $opts Optional overrides: 'message' (commit message).
|
|
*
|
|
* @return array{ok: bool, action: string, message: string, pr_url?: string}
|
|
*/
|
|
function oribi_sync_push_page( int $post_id, array $opts = [] ): array {
|
|
$post = get_post( $post_id );
|
|
|
|
if ( ! $post || $post->post_type !== 'page' ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Post not found or not a page.' ];
|
|
}
|
|
|
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
$repo_url = get_option( 'oribi_sync_repo', '' );
|
|
$branch = get_option( 'oribi_sync_branch', 'main' ) ?: 'main';
|
|
$pat = oribi_sync_get_pat();
|
|
$provider = oribi_sync_get_provider();
|
|
|
|
if ( empty( $repo_url ) || empty( $pat ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Repository URL or PAT not configured.' ];
|
|
}
|
|
|
|
if ( $provider !== 'gitea' ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Push is currently supported for Gitea / Forgejo only.' ];
|
|
}
|
|
|
|
$parsed = oribi_sync_parse_repo_url( $repo_url );
|
|
if ( is_wp_error( $parsed ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => $parsed->get_error_message() ];
|
|
}
|
|
|
|
$api_base = oribi_sync_api_base( $provider, $parsed );
|
|
|
|
// ── Determine repo path ───────────────────────────────────────────────
|
|
$source_meta = get_post_meta( $post_id, '_oribi_sync_source', true );
|
|
$repo_path = '';
|
|
|
|
if ( ! empty( $source_meta ) ) {
|
|
// Format: {repo_url}@{branch}:{path}
|
|
// Use strrpos to find the LAST colon (skips the one in 'https:').
|
|
$colon_pos = strrpos( $source_meta, ':' );
|
|
if ( $colon_pos !== false ) {
|
|
$at_pos = strrpos( substr( $source_meta, 0, $colon_pos ), '@' );
|
|
if ( $at_pos !== false ) {
|
|
$candidate = substr( $source_meta, $colon_pos + 1 );
|
|
// Validate: path must start with 'pages/' (case-insensitive).
|
|
// Discard corrupted values left by earlier bugs.
|
|
if ( strncasecmp( $candidate, 'pages/', 6 ) === 0 ) {
|
|
$repo_path = $candidate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $repo_path ) ) {
|
|
// Derive from slug — files live under pages/
|
|
$repo_path = 'pages/' . $post->post_name . '.php';
|
|
}
|
|
|
|
// ── Generate content ──────────────────────────────────────────────────
|
|
$slug = $post->post_name;
|
|
$title = $post->post_title;
|
|
|
|
// Read post_content directly from the DB — bypassing every get_post()
|
|
// filter — so we get exactly what oribi_sync_save_post() wrote.
|
|
global $wpdb;
|
|
$wp_content = (string) $wpdb->get_var(
|
|
$wpdb->prepare( 'SELECT post_content FROM ' . $wpdb->posts . ' WHERE ID = %d', $post_id )
|
|
);
|
|
|
|
// Clean any corruption baked in by previous syncs (e.g. \u0026amp; artefacts)
|
|
$wp_content = oribi_sync_clean_block_content( $wp_content );
|
|
|
|
$commit_msg = $opts['message'] ?? "Sync: update {$slug} from WordPress";
|
|
|
|
// ── Fetch remote file metadata ────────────────────────────────────────
|
|
$remote = oribi_sync_gitea_get_file_meta( $api_base, $branch, $repo_path, $pat );
|
|
|
|
if ( is_wp_error( $remote ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Failed to check remote file: ' . $remote->get_error_message() ];
|
|
}
|
|
|
|
// Build PHP source: preserve original header for existing files, fresh wrapper for new ones.
|
|
if ( $remote !== null && ! empty( $remote['content'] ) ) {
|
|
$php_source = oribi_sync_replace_php_body( $remote['content'], $wp_content, $slug, $title );
|
|
} else {
|
|
$php_source = oribi_sync_generate_php_wrapper( $wp_content, $slug, $title );
|
|
}
|
|
$new_checksum = hash( 'sha256', $php_source );
|
|
|
|
$stored_sha = get_post_meta( $post_id, '_oribi_sync_git_sha', true );
|
|
|
|
// ── Decide strategy ───────────────────────────────────────────────────
|
|
if ( $remote === null ) {
|
|
// File doesn't exist in repo → create it directly
|
|
$result = oribi_sync_gitea_put_file( $api_base, $branch, $repo_path, $php_source, $pat, null, $commit_msg );
|
|
if ( is_wp_error( $result ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Create failed: ' . $result->get_error_message() ];
|
|
}
|
|
if ( $result['code'] < 200 || $result['code'] >= 300 ) {
|
|
$err = is_array( $result['body'] ) ? ( $result['body']['message'] ?? wp_json_encode( $result['body'] ) ) : $result['body'];
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => "Create failed (HTTP {$result['code']}): {$err}" ];
|
|
}
|
|
|
|
// Update post meta
|
|
$new_sha = $result['body']['content']['sha'] ?? '';
|
|
oribi_sync_update_push_meta( $post_id, $new_sha, $new_checksum, $repo_url, $branch, $repo_path );
|
|
oribi_sync_log_push( $slug, 'created', $branch );
|
|
|
|
return [ 'ok' => true, 'action' => 'created', 'message' => "Created {$repo_path} on branch {$branch}." ];
|
|
}
|
|
|
|
$remote_sha = $remote['sha'];
|
|
|
|
// Check for conflict: remote SHA differs from what we last synced
|
|
$has_conflict = ! empty( $stored_sha ) && $remote_sha !== $stored_sha;
|
|
|
|
if ( $has_conflict ) {
|
|
// ── Conflict path: branch + PR ────────────────────────────────────
|
|
$timestamp = gmdate( 'Ymd-His' );
|
|
$new_branch = 'oribi-sync/' . $slug . '-' . $timestamp;
|
|
|
|
// Create branch
|
|
$branch_result = oribi_sync_gitea_create_branch( $api_base, $new_branch, $branch, $pat );
|
|
if ( is_wp_error( $branch_result ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Branch creation failed: ' . $branch_result->get_error_message() ];
|
|
}
|
|
if ( $branch_result['code'] < 200 || $branch_result['code'] >= 300 ) {
|
|
$err = is_array( $branch_result['body'] ) ? ( $branch_result['body']['message'] ?? '' ) : $branch_result['body'];
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => "Branch creation failed (HTTP {$branch_result['code']}): {$err}" ];
|
|
}
|
|
|
|
// Fetch file meta on the new branch (same SHA as base branch initially)
|
|
$branch_remote = oribi_sync_gitea_get_file_meta( $api_base, $new_branch, $repo_path, $pat );
|
|
$branch_sha = null;
|
|
if ( ! is_wp_error( $branch_remote ) && $branch_remote !== null ) {
|
|
$branch_sha = $branch_remote['sha'];
|
|
}
|
|
|
|
// Commit to the new branch
|
|
$put_result = oribi_sync_gitea_put_file( $api_base, $new_branch, $repo_path, $php_source, $pat, $branch_sha, $commit_msg );
|
|
if ( is_wp_error( $put_result ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Commit to branch failed: ' . $put_result->get_error_message() ];
|
|
}
|
|
if ( $put_result['code'] < 200 || $put_result['code'] >= 300 ) {
|
|
$err = is_array( $put_result['body'] ) ? ( $put_result['body']['message'] ?? '' ) : $put_result['body'];
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => "Commit to branch failed (HTTP {$put_result['code']}): {$err}" ];
|
|
}
|
|
|
|
// Open PR
|
|
$pr_title = "Sync: {$slug}";
|
|
$pr_body = "Automatic push from WordPress (Oribi Tech Sync).\n\n";
|
|
$pr_body .= "**Page:** {$title} (`{$slug}`)\n";
|
|
$pr_body .= "**Commit:** {$commit_msg}\n\n";
|
|
$pr_body .= "The target branch `{$branch}` has been modified since the last sync, ";
|
|
$pr_body .= "so this change was pushed to `{$new_branch}` for review.\n\n";
|
|
$pr_body .= "Stored SHA: `{$stored_sha}`\n";
|
|
$pr_body .= "Remote SHA: `{$remote_sha}`\n";
|
|
|
|
$pr_result = oribi_sync_gitea_create_pr( $api_base, $new_branch, $branch, $pr_title, $pr_body, $pat );
|
|
if ( is_wp_error( $pr_result ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'PR creation failed: ' . $pr_result->get_error_message() ];
|
|
}
|
|
|
|
$pr_url = '';
|
|
if ( $pr_result['code'] >= 200 && $pr_result['code'] < 300 && is_array( $pr_result['body'] ) ) {
|
|
$pr_url = $pr_result['body']['html_url'] ?? '';
|
|
}
|
|
|
|
// Save PR URL on the post
|
|
if ( ! empty( $pr_url ) ) {
|
|
update_post_meta( $post_id, '_oribi_sync_pr_url', $pr_url );
|
|
}
|
|
|
|
oribi_sync_log_push( $slug, 'pr_created', $new_branch, $pr_url );
|
|
|
|
return [
|
|
'ok' => true,
|
|
'action' => 'pr_created',
|
|
'message' => "Conflict detected — created PR on branch {$new_branch}.",
|
|
'pr_url' => $pr_url,
|
|
];
|
|
}
|
|
|
|
// ── No conflict: direct update ────────────────────────────────────────
|
|
$result = oribi_sync_gitea_put_file( $api_base, $branch, $repo_path, $php_source, $pat, $remote_sha, $commit_msg );
|
|
if ( is_wp_error( $result ) ) {
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => 'Update failed: ' . $result->get_error_message() ];
|
|
}
|
|
if ( $result['code'] < 200 || $result['code'] >= 300 ) {
|
|
$err = is_array( $result['body'] ) ? ( $result['body']['message'] ?? wp_json_encode( $result['body'] ) ) : $result['body'];
|
|
return [ 'ok' => false, 'action' => 'error', 'message' => "Update failed (HTTP {$result['code']}): {$err}" ];
|
|
}
|
|
|
|
// Update post meta
|
|
$new_sha = $result['body']['content']['sha'] ?? '';
|
|
oribi_sync_update_push_meta( $post_id, $new_sha, $new_checksum, $repo_url, $branch, $repo_path );
|
|
oribi_sync_log_push( $slug, 'updated', $branch );
|
|
|
|
return [ 'ok' => true, 'action' => 'updated', 'message' => "Updated {$repo_path} on branch {$branch}." ];
|
|
}
|
|
|
|
/**
|
|
* Bulk-push all synced pages.
|
|
*
|
|
* @return array{ok: bool, results: array}
|
|
*/
|
|
function oribi_sync_push_all(): array {
|
|
$query = new WP_Query( [
|
|
'post_type' => 'page',
|
|
'post_status' => 'publish',
|
|
'meta_key' => '_oribi_sync_checksum',
|
|
'posts_per_page' => -1,
|
|
'fields' => 'ids',
|
|
] );
|
|
|
|
$results = [];
|
|
|
|
foreach ( $query->posts as $post_id ) {
|
|
$page = get_post( $post_id );
|
|
$slug = $page ? $page->post_name : "#{$post_id}";
|
|
$result = oribi_sync_push_page( (int) $post_id );
|
|
$results[] = array_merge( $result, [ 'slug' => $slug, 'post_id' => $post_id ] );
|
|
}
|
|
|
|
$all_ok = ! empty( $results ) && count( array_filter( $results, fn( $r ) => ! $r['ok'] ) ) === 0;
|
|
|
|
return [ 'ok' => $all_ok, 'results' => $results ];
|
|
}
|
|
|
|
// ─── Post meta helpers ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Update post meta after a successful push.
|
|
*/
|
|
function oribi_sync_update_push_meta( int $post_id, string $sha, string $checksum, string $repo_url, string $branch, string $path ): void {
|
|
if ( ! empty( $sha ) ) {
|
|
update_post_meta( $post_id, '_oribi_sync_git_sha', $sha );
|
|
}
|
|
update_post_meta( $post_id, '_oribi_sync_checksum', $checksum );
|
|
update_post_meta( $post_id, '_oribi_sync_source', $repo_url . '@' . $branch . ':' . $path );
|
|
update_post_meta( $post_id, '_oribi_sync_last_push', current_time( 'mysql' ) );
|
|
|
|
// Clear any stale PR URL on successful direct push
|
|
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
|
|
}
|
|
|
|
// ─── Push log ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Append an entry to the push log.
|
|
*/
|
|
function oribi_sync_log_push( string $slug, string $action, string $branch, string $pr_url = '' ): void {
|
|
$log = get_option( 'oribi_sync_push_log', [] );
|
|
if ( ! is_array( $log ) ) $log = [];
|
|
|
|
array_unshift( $log, [
|
|
'time' => current_time( 'mysql' ),
|
|
'slug' => $slug,
|
|
'action' => $action,
|
|
'branch' => $branch,
|
|
'pr_url' => $pr_url,
|
|
] );
|
|
|
|
// Keep last 50 entries
|
|
$log = array_slice( $log, 0, 50 );
|
|
update_option( 'oribi_sync_push_log', $log, 'no' );
|
|
}
|