diff --git a/includes/admin.php b/includes/admin.php
index 3b303d0..c9eee8e 100644
--- a/includes/admin.php
+++ b/includes/admin.php
@@ -39,6 +39,13 @@ add_action( 'admin_post_oribi_sync_save_settings', function () {
update_option( 'oribi_sync_branch', $branch, 'no' );
update_option( 'oribi_sync_provider', $provider, 'no' );
+ // Posts sync settings
+ $posts_enabled = isset( $_POST['oribi_sync_posts_enabled'] ) ? '1' : '0';
+ $posts_folder = sanitize_text_field( wp_unslash( $_POST['oribi_sync_posts_folder'] ?? 'posts' ) );
+ $posts_folder = trim( $posts_folder, '/' ) ?: 'posts';
+ update_option( 'oribi_sync_posts_enabled', $posts_enabled, 'no' );
+ update_option( 'oribi_sync_posts_folder', $posts_folder, 'no' );
+
// Only update PAT if a new one was provided (non-empty)
if ( ! empty( $pat ) ) {
oribi_sync_save_pat( $pat );
@@ -106,7 +113,10 @@ add_action( 'admin_post_oribi_sync_push', function () {
$post_id = (int) ( $_POST['oribi_sync_push_post_id'] ?? 0 );
if ( $post_id > 0 ) {
- $result = oribi_sync_push_page( $post_id );
+ $post = get_post( $post_id );
+ $result = ( $post && $post->post_type === 'post' )
+ ? oribi_sync_push_post( $post_id )
+ : oribi_sync_push_page( $post_id );
set_transient( 'oribi_sync_push_result', $result, 60 );
}
@@ -249,6 +259,36 @@ function oribi_sync_settings_page() {
+
Posts Sync
+ Import WordPress posts from Markdown files with YAML front-matter stored in a repo subfolder.
+
+
@@ -333,6 +373,61 @@ function oribi_sync_settings_page() {
+ 'post',
+ 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
+ 'meta_key' => '_oribi_sync_checksum',
+ 'posts_per_page' => -1,
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ ] );
+
+ if ( $synced_posts->have_posts() ):
+ ?>
+ Synced Posts
+
+
+ | Post |
+ Push |
+
+
+ have_posts() ): $synced_posts->the_post();
+ $pid = get_the_ID();
+ $last_push = get_post_meta( $pid, '_oribi_sync_last_push', true );
+ $pr_url = get_post_meta( $pid, '_oribi_sync_pr_url', true );
+ ?>
+
+
+
+
+ —
+
+ PR
+
+
+ pushed
+
+ |
+
+
+ |
+
+
+
+
+
+
@@ -374,12 +469,15 @@ function oribi_sync_settings_page() {
*/
function oribi_sync_render_result_list( array $r ): void {
$items = [];
- if ( ! empty( $r['created'] ) ) $items[] = 'Created: ' . implode( ', ', $r['created'] );
- if ( ! empty( $r['updated'] ) ) $items[] = 'Updated: ' . implode( ', ', $r['updated'] );
- if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
- if ( ! empty( $r['trashed'] ) ) $items[] = 'Trashed: ' . implode( ', ', $r['trashed'] );
- if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
- if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
+ if ( ! empty( $r['created'] ) ) $items[] = 'Pages created: ' . implode( ', ', $r['created'] );
+ if ( ! empty( $r['updated'] ) ) $items[] = 'Pages updated: ' . implode( ', ', $r['updated'] );
+ if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
+ if ( ! empty( $r['trashed'] ) ) $items[] = 'Pages trashed: ' . implode( ', ', $r['trashed'] );
+ if ( ! empty( $r['posts_created'] ) ) $items[] = 'Posts created: ' . implode( ', ', $r['posts_created'] );
+ if ( ! empty( $r['posts_updated'] ) ) $items[] = 'Posts updated: ' . implode( ', ', $r['posts_updated'] );
+ if ( ! empty( $r['posts_trashed'] ) ) $items[] = 'Posts trashed: ' . implode( ', ', $r['posts_trashed'] );
+ if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
+ if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
if ( empty( $items ) ) { echo 'No changes.
'; return; }
diff --git a/includes/post-sync.php b/includes/post-sync.php
new file mode 100644
index 0000000..186b938
--- /dev/null
+++ b/includes/post-sync.php
@@ -0,0 +1,1065 @@
+ [], 'body' => $raw ];
+
+ $raw = ltrim( $raw );
+ if ( strncmp( $raw, '---', 3 ) !== 0 ) {
+ return $empty;
+ }
+
+ // Find the closing --- (must start at beginning of a line)
+ $after_open = substr( $raw, 3 );
+ $close_pos = strpos( $after_open, "\n---" );
+ if ( $close_pos === false ) {
+ return $empty;
+ }
+
+ $yaml_part = substr( $after_open, 0, $close_pos );
+ // Body begins after closing --- and optional newline
+ $body_raw = substr( $after_open, $close_pos + 4 );
+ $body = ltrim( $body_raw, "\r\n" );
+
+ $fm = [];
+ $current_list_key = null;
+
+ foreach ( explode( "\n", $yaml_part ) as $line ) {
+ // Block list item (leading spaces + dash)
+ if ( $current_list_key !== null && preg_match( '/^\s+-\s+(.+)$/', $line, $m ) ) {
+ $fm[ $current_list_key ][] = trim( $m[1], '"\' ' );
+ continue;
+ }
+
+ $current_list_key = null;
+
+ // key: value
+ if ( ! preg_match( '/^([\w][\w-]*):\s*(.*)$/', $line, $m ) ) {
+ continue;
+ }
+
+ $key = strtolower( $m[1] );
+ $value = trim( $m[2] );
+
+ if ( $value === 'true' ) {
+ $fm[ $key ] = true;
+ } elseif ( $value === 'false' ) {
+ $fm[ $key ] = false;
+ } elseif ( $value === '' ) {
+ // Block list follows
+ $fm[ $key ] = [];
+ $current_list_key = $key;
+ } elseif ( $value[0] === '[' && substr( $value, -1 ) === ']' ) {
+ // Inline array [a, b, c]
+ $inner = substr( $value, 1, -1 );
+ $fm[ $key ] = array_map( function ( $v ) {
+ return trim( $v, '"\' ' );
+ }, explode( ',', $inner ) );
+ } else {
+ $fm[ $key ] = trim( $value, '"\' ' );
+ }
+ }
+
+ return [ 'front_matter' => $fm, 'body' => $body ];
+}
+
+// ─── Markdown to HTML ─────────────────────────────────────────────────────────
+
+/**
+ * Process inline Markdown elements on a text fragment.
+ *
+ * Applied to every run of text that is NOT inside a protected code block.
+ *
+ * @param string $text
+ * @return string HTML
+ */
+function oribi_sync_md_inline( string $text ): string {
+ // Images first (must come before links to avoid mismatching alt text)
+ $text = preg_replace(
+ '/!\[([^\]]*)\]\(([^\s\)]+)(?:\s+"[^"]*")?\)/',
+ '
',
+ $text
+ );
+
+ // Links [text](url "optional title")
+ $text = preg_replace(
+ '/\[([^\]]+)\]\(([^\s\)]+)(?:\s+"[^"]*")?\)/',
+ '$1',
+ $text
+ );
+
+ // Bold + italic ***…*** or ___…___
+ $text = preg_replace( '/\*{3}(.+?)\*{3}/s', '$1', $text );
+ $text = preg_replace( '/_{3}(.+?)_{3}/s', '$1', $text );
+
+ // Bold **…** or __…__
+ $text = preg_replace( '/\*{2}(.+?)\*{2}/s', '$1', $text );
+ $text = preg_replace( '/_{2}(.+?)_{2}/s', '$1', $text );
+
+ // Italic *…* (not inside a word)
+ $text = preg_replace( '/(?$1', $text );
+ $text = preg_replace( '/(?$1', $text );
+
+ // Strikethrough ~~…~~
+ $text = preg_replace( '/~~(.+?)~~/s', '$1', $text );
+
+ // Hard line break: 2+ spaces before newline
+ $text = preg_replace( '/ +\n/', "
\n", $text );
+
+ return $text;
+}
+
+/**
+ * Convert a Markdown string to HTML.
+ *
+ * Handles: fenced code blocks, ATX headings, setext headings, blockquotes,
+ * horizontal rules, unordered and ordered lists, images, paragraphs,
+ * and all inline formatting.
+ *
+ * @param string $md Markdown source.
+ * @return string HTML output.
+ */
+function oribi_sync_markdown_to_html( string $md ): string {
+ // Normalize line endings
+ $md = str_replace( [ "\r\n", "\r" ], "\n", $md );
+
+ // ── Protect fenced code blocks ──────────────────────────────────────────
+ $fenced = [];
+ $md = preg_replace_callback(
+ '/^(`{3,}|~{3,})([\w-]*)\n(.*?)\n\1\h*$/ms',
+ function ( $m ) use ( &$fenced ) {
+ $tok = "\x02FENCE" . count( $fenced ) . "\x03";
+ $lang = $m[2] !== '' ? ' class="language-' . htmlspecialchars( $m[2], ENT_QUOTES ) . '"' : '';
+ $fenced[ $tok ] = '' . htmlspecialchars( $m[3] ) . '
';
+ return $tok;
+ },
+ $md
+ );
+
+ // ── Protect inline code ─────────────────────────────────────────────────
+ $icodes = [];
+ $md = preg_replace_callback(
+ '/``(.+?)``|`([^`\n]+)`/s',
+ function ( $m ) use ( &$icodes ) {
+ $tok = "\x02ICODE" . count( $icodes ) . "\x03";
+ $content = $m[1] !== '' ? $m[1] : $m[2];
+ $icodes[ $tok ] = '' . htmlspecialchars( $content ) . '';
+ return $tok;
+ },
+ $md
+ );
+
+ $lines = explode( "\n", $md );
+ $out = []; // accumulated output blocks (strings)
+ $para = []; // accumulated paragraph lines
+ $in_list = null; // 'ul' | 'ol' | null
+ $list_buf = []; // items for current list
+
+ $flush_para = function () use ( &$para, &$out ) {
+ if ( empty( $para ) ) return;
+ $text = implode( "\n", $para );
+ $out[] = '' . oribi_sync_md_inline( $text ) . '
';
+ $para = [];
+ };
+
+ $flush_list = function () use ( &$in_list, &$list_buf, &$out ) {
+ if ( $in_list === null ) return;
+ $tag = $in_list;
+ $items = implode( '', $list_buf );
+ $out[] = "<{$tag}>{$items}{$tag}>";
+ $in_list = null;
+ $list_buf = [];
+ };
+
+ $n = count( $lines );
+ for ( $i = 0; $i < $n; $i++ ) {
+ $line = $lines[ $i ];
+
+ // ── Fenced block placeholder ────────────────────────────────────────
+ if ( isset( $fenced[ trim( $line ) ] ) ) {
+ $flush_para();
+ $flush_list();
+ $out[] = $fenced[ trim( $line ) ];
+ continue;
+ }
+
+ // ── Blank line ──────────────────────────────────────────────────────
+ if ( trim( $line ) === '' ) {
+ $flush_para();
+ $flush_list();
+ continue;
+ }
+
+ // ── ATX heading #…###### ───────────────────────────────────────────
+ if ( preg_match( '/^(#{1,6})\s+(.+?)(?:\s+#+\s*)?$/', $line, $m ) ) {
+ $flush_para();
+ $flush_list();
+ $lvl = strlen( $m[1] );
+ $out[] = "" . oribi_sync_md_inline( trim( $m[2] ) ) . "";
+ continue;
+ }
+
+ // ── Setext heading (text followed by === or ---) ────────────────────
+ if ( ! empty( $para ) && isset( $lines[ $i + 1 ] ) ) {
+ $next = $lines[ $i + 1 ];
+ if ( preg_match( '/^=+\s*$/', $next ) ) {
+ $flush_list();
+ $text = implode( "\n", $para ) . "\n" . $line;
+ $out[] = '' . oribi_sync_md_inline( trim( $text ) ) . '
';
+ $para = [];
+ $i++;
+ continue;
+ }
+ if ( preg_match( '/^-{2,}\s*$/', $next ) ) {
+ $flush_list();
+ $text = implode( "\n", $para ) . "\n" . $line;
+ $out[] = '' . oribi_sync_md_inline( trim( $text ) ) . '
';
+ $para = [];
+ $i++;
+ continue;
+ }
+ }
+
+ // ── Horizontal rule ─────────────────────────────────────────────────
+ if ( preg_match( '/^(?:\*\s*){3,}$|^(?:-\s*){3,}$|^(?:_\s*){3,}$/', trim( $line ) ) ) {
+ $flush_para();
+ $flush_list();
+ $out[] = '
';
+ continue;
+ }
+
+ // ── Blockquote ──────────────────────────────────────────────────────
+ if ( preg_match( '/^>\s?(.*)$/', $line, $m ) ) {
+ $flush_para();
+ $flush_list();
+ $out[] = '' . oribi_sync_md_inline( $m[1] ) . '
';
+ continue;
+ }
+
+ // ── Unordered list item ─────────────────────────────────────────────
+ if ( preg_match( '/^[-*+]\s+(.+)$/', $line, $m ) ) {
+ $flush_para();
+ if ( $in_list !== 'ul' ) {
+ $flush_list();
+ $in_list = 'ul';
+ }
+ $list_buf[] = '' . oribi_sync_md_inline( $m[1] ) . '';
+ continue;
+ }
+
+ // ── Ordered list item ───────────────────────────────────────────────
+ if ( preg_match( '/^\d+[.)]\s+(.+)$/', $line, $m ) ) {
+ $flush_para();
+ if ( $in_list !== 'ol' ) {
+ $flush_list();
+ $in_list = 'ol';
+ }
+ $list_buf[] = '' . oribi_sync_md_inline( $m[1] ) . '';
+ continue;
+ }
+
+ // ── Regular text → paragraph ────────────────────────────────────────
+ $flush_list();
+ $para[] = $line;
+ }
+
+ $flush_para();
+ $flush_list();
+
+ $html = implode( "\n", $out );
+
+ // Restore protected tokens
+ $html = strtr( $html, $fenced );
+ $html = strtr( $html, $icodes );
+
+ return trim( $html );
+}
+
+// ─── Author resolution ────────────────────────────────────────────────────────
+
+/**
+ * Resolve a front-matter author value to a WP user ID.
+ *
+ * Tries (in order): user_login, user_email, display_name.
+ * Falls back to the current user ID, or 1 if no current user.
+ *
+ * @param string $identifier Value from front-matter `author` field.
+ * @return int WP user ID.
+ */
+function oribi_sync_resolve_author( string $identifier ): int {
+ if ( empty( $identifier ) ) {
+ $uid = get_current_user_id();
+ return $uid > 0 ? $uid : 1;
+ }
+
+ // Try login
+ $user = get_user_by( 'login', $identifier );
+ if ( $user ) return $user->ID;
+
+ // Try email
+ $user = get_user_by( 'email', $identifier );
+ if ( $user ) return $user->ID;
+
+ // Try display_name (slower, via meta query)
+ $users = get_users( [ 'search' => $identifier, 'search_columns' => [ 'display_name' ], 'number' => 1 ] );
+ if ( ! empty( $users ) ) return $users[0]->ID;
+
+ $uid = get_current_user_id();
+ return $uid > 0 ? $uid : 1;
+}
+
+// ─── Post slug helpers ────────────────────────────────────────────────────────
+
+/**
+ * Derive a URL-safe slug from a post filename.
+ *
+ * Strips the extension, then strips a leading YYYY-MM-DD- date prefix if present.
+ * Examples:
+ * 2026-02-21-my-great-post.md → my-great-post
+ * hello-world.markdown → hello-world
+ */
+function oribi_sync_post_filename_to_slug( string $filename ): string {
+ $base = pathinfo( $filename, PATHINFO_FILENAME );
+ // Remove leading date prefix YYYY-MM-DD-
+ $base = preg_replace( '/^\d{4}-\d{2}-\d{2}-/', '', $base );
+ return sanitize_title( $base );
+}
+
+/**
+ * Find a WP `post` record that was previously synced and has the given slug.
+ */
+function oribi_sync_get_synced_post_by_slug( string $slug ): ?WP_Post {
+ $posts = get_posts( [
+ 'name' => $slug,
+ 'post_type' => 'post',
+ 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
+ 'meta_key' => '_oribi_sync_checksum',
+ 'posts_per_page' => 1,
+ ] );
+
+ return ! empty( $posts ) ? $posts[0] : null;
+}
+
+// ─── Media import helpers ─────────────────────────────────────────────────────
+
+/**
+ * Resolve a relative image path (from a post file) to an absolute repo path.
+ *
+ * @param string $src Image src (relative or absolute URL).
+ * @param string $post_file_path Repo path of the Markdown file (e.g. posts/2026-hello.md).
+ * @return string Resolved repo-relative path, or empty string if $src is an absolute URL.
+ */
+function oribi_sync_resolve_repo_image_path( string $src, string $post_file_path ): string {
+ if ( preg_match( '#^https?://#i', $src ) ) {
+ return ''; // Absolute URL — caller handles directly
+ }
+
+ $post_dir = trim( dirname( $post_file_path ), '.' . DIRECTORY_SEPARATOR . '/' );
+
+ if ( $post_dir !== '' ) {
+ $combined = $post_dir . '/' . ltrim( $src, '/' );
+ } else {
+ $combined = ltrim( $src, '/' );
+ }
+
+ // Normalize ./ and ../
+ $parts = explode( '/', $combined );
+ $normalized = [];
+ foreach ( $parts as $part ) {
+ if ( $part === '..' ) {
+ array_pop( $normalized );
+ } elseif ( $part !== '.' && $part !== '' ) {
+ $normalized[] = $part;
+ }
+ }
+
+ return implode( '/', $normalized );
+}
+
+/**
+ * Sideload an image file into the WP media library.
+ *
+ * For absolute URLs, the file is downloaded directly.
+ * For relative repo paths, the file is fetched via the authenticated API.
+ *
+ * Deduplicates by tracking the original src in `_oribi_sync_origin_src` post meta.
+ *
+ * @param string $src Original src attribute value.
+ * @param int $post_id Parent post to attach to.
+ * @param string $api_base Provider API base URL.
+ * @param string $branch Branch name.
+ * @param string $provider Provider key.
+ * @param string $pat Personal access token.
+ * @param string $post_file_path Repo path of the Markdown file.
+ * @return int Attachment post ID, or 0 on failure.
+ */
+function oribi_sync_sideload_attachment(
+ string $src,
+ int $post_id,
+ string $api_base,
+ string $branch,
+ string $provider,
+ string $pat,
+ string $post_file_path
+): int {
+ if ( ! function_exists( 'media_handle_sideload' ) ) {
+ require_once ABSPATH . 'wp-admin/includes/file.php';
+ require_once ABSPATH . 'wp-admin/includes/media.php';
+ require_once ABSPATH . 'wp-admin/includes/image.php';
+ }
+
+ // Deduplication: check if this src was already imported
+ global $wpdb;
+ $existing_id = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
+ "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_oribi_sync_origin_src' AND meta_value = %s LIMIT 1",
+ $src
+ ) );
+ if ( $existing_id ) {
+ return (int) $existing_id;
+ }
+
+ $filename = basename( strtok( $src, '?' ) );
+ $tmp_path = null;
+
+ if ( preg_match( '#^https?://#i', $src ) ) {
+ // Absolute public URL — use WP's built-in download
+ $tmp = download_url( $src, 30 );
+ if ( is_wp_error( $tmp ) ) {
+ return 0;
+ }
+ $tmp_path = $tmp;
+ } else {
+ // Relative repo path — fetch via authenticated API
+ $repo_path = oribi_sync_resolve_repo_image_path( $src, $post_file_path );
+ if ( empty( $repo_path ) ) {
+ return 0;
+ }
+
+ $content = oribi_sync_fetch_file( $api_base, $branch, $repo_path, $provider, $pat );
+ if ( is_wp_error( $content ) ) {
+ return 0;
+ }
+
+ $tmp_path = wp_tempnam( $filename );
+ if ( ! $tmp_path ) {
+ return 0;
+ }
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
+ file_put_contents( $tmp_path, $content );
+ }
+
+ $file_array = [
+ 'name' => $filename,
+ 'tmp_name' => $tmp_path,
+ ];
+
+ $att_id = media_handle_sideload( $file_array, $post_id );
+ // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
+ @unlink( $tmp_path );
+
+ if ( is_wp_error( $att_id ) ) {
+ return 0;
+ }
+
+ // Store src for deduplication on future syncs
+ add_post_meta( $att_id, '_oribi_sync_origin_src', $src, true );
+
+ return $att_id;
+}
+
+/**
+ * Scan HTML content for
tags, sideload each image, and rewrite src attributes.
+ *
+ * @return string HTML with src attributes pointing to local media library URLs.
+ */
+function oribi_sync_import_media_in_content(
+ int $post_id,
+ string $html,
+ string $api_base,
+ string $branch,
+ string $provider,
+ string $pat,
+ string $post_file_path
+): string {
+ if ( empty( $html ) ) {
+ return $html;
+ }
+
+ $changed = false;
+
+ $html = preg_replace_callback(
+ '/
]+)>/i',
+ function ( $tag_match ) use ( $post_id, $api_base, $branch, $provider, $pat, $post_file_path, &$changed ) {
+ $tag = $tag_match[0];
+ $attrs = $tag_match[1];
+
+ // Extract src
+ if ( ! preg_match( '/src=["\']([^"\']+)["\']/i', $attrs, $src_match ) ) {
+ return $tag;
+ }
+ $original_src = $src_match[1];
+
+ // Skip data URIs and WP-hosted images (already local)
+ if ( strncmp( $original_src, 'data:', 5 ) === 0 ) {
+ return $tag;
+ }
+ $upload_dir = wp_upload_dir();
+ if ( strpos( $original_src, $upload_dir['baseurl'] ) === 0 ) {
+ return $tag;
+ }
+
+ $att_id = oribi_sync_sideload_attachment(
+ $original_src, $post_id, $api_base, $branch, $provider, $pat, $post_file_path
+ );
+
+ if ( ! $att_id ) {
+ return $tag;
+ }
+
+ $local_url = wp_get_attachment_url( $att_id );
+ if ( ! $local_url ) {
+ return $tag;
+ }
+
+ $changed = true;
+ $new_attrs = preg_replace(
+ '/src=["\'][^"\']+["\']/i',
+ 'src="' . esc_url( $local_url ) . '"',
+ $attrs
+ );
+ return '
';
+ },
+ $html
+ );
+
+ return $html !== null ? $html : $html;
+}
+
+// ─── Posts sync pipeline ──────────────────────────────────────────────────────
+
+/**
+ * Sync WordPress posts from the configured posts folder in the repository.
+ *
+ * Called from oribi_sync_run() after fetching the tree.
+ *
+ * @param string $api_base Provider API base URL.
+ * @param string $branch Branch name.
+ * @param string $provider Provider key.
+ * @param string $pat Personal access token.
+ * @param array $tree Full repo tree (from oribi_sync_fetch_tree).
+ * @param bool $dry_run If true, report changes without writing.
+ * @return array{created: string[], updated: string[], trashed: string[], skipped: string[], errors: string[]}
+ */
+function oribi_sync_run_posts(
+ string $api_base,
+ string $branch,
+ string $provider,
+ string $pat,
+ array $tree,
+ bool $dry_run = false
+): array {
+ $result = [
+ 'created' => [],
+ 'updated' => [],
+ 'trashed' => [],
+ 'skipped' => [],
+ 'errors' => [],
+ ];
+
+ // Feature gate
+ if ( ! get_option( 'oribi_sync_posts_enabled', '' ) ) {
+ return $result;
+ }
+
+ $posts_folder = get_option( 'oribi_sync_posts_folder', 'posts' ) ?: 'posts';
+ $repo_url = get_option( 'oribi_sync_repo', '' );
+
+ // Filter tree to posts folder (allow recursive subdirectories)
+ $post_files = oribi_sync_filter_tree( $tree, $posts_folder, true );
+
+ if ( empty( $post_files ) ) {
+ $result['skipped'][] = 'No Markdown files found under ' . $posts_folder . '/ in the repository.';
+ return $result;
+ }
+
+ $synced_slugs = [];
+
+ foreach ( $post_files as $entry ) {
+ $filename = basename( $entry['path'] );
+ $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
+
+ // Only process Markdown files
+ if ( ! in_array( $ext, [ 'md', 'markdown' ], true ) ) {
+ continue;
+ }
+
+ // Derive slug before fetching content (for fast-path dedup)
+ $slug = oribi_sync_post_filename_to_slug( $filename );
+ if ( empty( $slug ) ) {
+ $result['skipped'][] = $entry['path'] . ' (could not derive slug)';
+ continue;
+ }
+
+ $synced_slugs[] = $slug;
+
+ $existing = oribi_sync_get_synced_post_by_slug( $slug );
+
+ // ── Fast-path: skip identical git SHA ─────────────────────────────
+ $git_sha = $entry['sha'] ?? '';
+ $stored_git_sha = $existing ? get_post_meta( $existing->ID, '_oribi_sync_git_sha', true ) : '';
+
+ if ( $existing && ! empty( $git_sha ) && $git_sha === $stored_git_sha ) {
+ $result['skipped'][] = $slug . ' (unchanged)';
+ if ( ! $dry_run ) {
+ update_post_meta( $existing->ID, '_oribi_sync_last_run', current_time( 'mysql' ) );
+ }
+ continue;
+ }
+
+ // ── Fetch raw file ─────────────────────────────────────────────────
+ $raw = oribi_sync_fetch_file( $api_base, $branch, $entry['path'], $provider, $pat );
+ if ( is_wp_error( $raw ) ) {
+ $result['errors'][] = $entry['path'] . ': ' . $raw->get_error_message();
+ continue;
+ }
+ $raw = (string) $raw;
+
+ // ── Parse front-matter + Markdown ──────────────────────────────────
+ $parsed = oribi_sync_parse_front_matter( $raw );
+ $fm = $parsed['front_matter'];
+ $body = $parsed['body'];
+
+ // ── Map front-matter to WP post fields ─────────────────────────────
+ $post_title = ! empty( $fm['title'] ) ? $fm['title'] : oribi_sync_slug_to_title( $slug );
+ $post_name = ! empty( $fm['slug'] ) ? sanitize_title( $fm['slug'] ) : $slug;
+ $post_status = ! empty( $fm['status'] ) ? $fm['status'] : 'publish';
+ $post_excerpt = ! empty( $fm['excerpt'] ) ? $fm['excerpt'] : '';
+ $author_id = oribi_sync_resolve_author( $fm['author'] ?? '' );
+
+ // Resolve date: front-matter takes priority, then filename prefix
+ $post_date = '';
+ if ( ! empty( $fm['date'] ) ) {
+ $ts = strtotime( $fm['date'] );
+ if ( $ts ) {
+ $post_date = gmdate( 'Y-m-d H:i:s', $ts );
+ }
+ } elseif ( preg_match( '/^(\d{4}-\d{2}-\d{2})-/', $filename, $dpm ) ) {
+ $ts = strtotime( $dpm[1] );
+ if ( $ts ) {
+ $post_date = gmdate( 'Y-m-d H:i:s', $ts );
+ }
+ }
+
+ $categories = isset( $fm['categories'] ) ? (array) $fm['categories'] : [];
+ $tags = isset( $fm['tags'] ) ? (array) $fm['tags'] : [];
+ $featured_img = $fm['featured_image'] ?? '';
+
+ // ── Convert Markdown body to HTML ──────────────────────────────────
+ $html_content = oribi_sync_markdown_to_html( $body );
+
+ $checksum = hash( 'sha256', $raw );
+
+ // ── Dry-run ────────────────────────────────────────────────────────
+ if ( $dry_run ) {
+ if ( $existing ) {
+ $old_checksum = get_post_meta( $existing->ID, '_oribi_sync_checksum', true );
+ if ( empty( $git_sha ) && $old_checksum === $checksum ) {
+ $result['skipped'][] = $slug . ' (unchanged)';
+ } else {
+ $result['updated'][] = $slug;
+ }
+ } else {
+ $result['created'][] = $slug;
+ }
+ continue;
+ }
+
+ // ── Build WP post array ────────────────────────────────────────────
+ $post_arr = [
+ 'post_title' => $post_title,
+ 'post_name' => $post_name,
+ 'post_status' => $post_status,
+ 'post_type' => 'post',
+ 'post_content' => $html_content,
+ 'post_author' => $author_id,
+ 'post_excerpt' => $post_excerpt,
+ ];
+
+ if ( $post_date ) {
+ $post_arr['post_date'] = $post_date;
+ $post_arr['post_date_gmt'] = get_gmt_from_date( $post_date );
+ }
+
+ // ── Create or update ───────────────────────────────────────────────
+ if ( $existing ) {
+ // Checksum fallback for providers without tree SHA
+ if ( empty( $git_sha ) ) {
+ $old_checksum = get_post_meta( $existing->ID, '_oribi_sync_checksum', true );
+ if ( $old_checksum === $checksum ) {
+ $result['skipped'][] = $slug . ' (unchanged)';
+ update_post_meta( $existing->ID, '_oribi_sync_last_run', current_time( 'mysql' ) );
+ continue;
+ }
+ }
+
+ $post_arr['ID'] = $existing->ID;
+ $post_id = wp_update_post( $post_arr, true );
+ if ( is_wp_error( $post_id ) ) {
+ $result['errors'][] = $slug . ': ' . $post_id->get_error_message();
+ continue;
+ }
+ $result['updated'][] = $slug;
+ } else {
+ $post_id = wp_insert_post( $post_arr, true );
+ if ( is_wp_error( $post_id ) ) {
+ $result['errors'][] = $slug . ': ' . $post_id->get_error_message();
+ continue;
+ }
+ $result['created'][] = $slug;
+ }
+
+ // ── Save sync meta ──────────────────────────────────────────────────
+ update_post_meta( $post_id, '_oribi_sync_checksum', $checksum );
+ update_post_meta( $post_id, '_oribi_sync_git_sha', $git_sha );
+ update_post_meta( $post_id, '_oribi_sync_source', $repo_url . '@' . $branch . ':' . $entry['path'] );
+ update_post_meta( $post_id, '_oribi_sync_last_run', current_time( 'mysql' ) );
+
+ // ── Taxonomies ──────────────────────────────────────────────────────
+ if ( ! empty( $categories ) ) {
+ $cat_ids = [];
+ foreach ( $categories as $cat_name ) {
+ $cat_name = trim( $cat_name );
+ if ( empty( $cat_name ) ) continue;
+
+ $term = term_exists( $cat_name, 'category' );
+ if ( ! $term ) {
+ $term = wp_insert_term( $cat_name, 'category' );
+ }
+ if ( ! is_wp_error( $term ) ) {
+ $cat_ids[] = (int) ( is_array( $term ) ? $term['term_id'] : $term );
+ }
+ }
+ if ( ! empty( $cat_ids ) ) {
+ wp_set_post_categories( $post_id, $cat_ids );
+ }
+ }
+
+ if ( ! empty( $tags ) ) {
+ $tag_list = array_filter( array_map( 'trim', $tags ) );
+ if ( ! empty( $tag_list ) ) {
+ wp_set_post_tags( $post_id, $tag_list );
+ }
+ }
+
+ // ── Media import & URL rewriting ────────────────────────────────────
+ $rewritten = oribi_sync_import_media_in_content(
+ $post_id, $html_content,
+ $api_base, $branch, $provider, $pat,
+ $entry['path']
+ );
+ if ( $rewritten !== $html_content ) {
+ wp_update_post( [ 'ID' => $post_id, 'post_content' => $rewritten ] );
+ }
+
+ // ── Featured image ──────────────────────────────────────────────────
+ if ( ! empty( $featured_img ) ) {
+ $att_id = oribi_sync_sideload_attachment(
+ $featured_img, $post_id,
+ $api_base, $branch, $provider, $pat,
+ $entry['path']
+ );
+ if ( $att_id > 0 ) {
+ set_post_thumbnail( $post_id, $att_id );
+ }
+ }
+ }
+
+ // ── Trash posts removed from repo ──────────────────────────────────────
+ if ( ! $dry_run && ! empty( $synced_slugs ) ) {
+ $result['trashed'] = oribi_sync_trash_removed_posts( $synced_slugs );
+ }
+
+ return $result;
+}
+
+// ─── Trash removed posts ──────────────────────────────────────────────────────
+
+/**
+ * Trash WP posts that were previously synced but are no longer in the repo.
+ *
+ * @param string[] $current_slugs Slugs present in the current repo tree.
+ * @return string[]
+ */
+function oribi_sync_trash_removed_posts( array $current_slugs ): array {
+ $trashed = [];
+
+ $query = new WP_Query( [
+ 'post_type' => 'post',
+ 'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
+ 'meta_key' => '_oribi_sync_checksum',
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ ] );
+
+ foreach ( $query->posts as $post_id ) {
+ $post = get_post( $post_id );
+ if ( ! $post ) continue;
+
+ if ( ! in_array( $post->post_name, $current_slugs, true ) ) {
+ wp_trash_post( $post->ID );
+ $trashed[] = $post->post_name;
+ }
+ }
+
+ return $trashed;
+}
+
+// ─── Markdown generation (for push) ──────────────────────────────────────────
+
+/**
+ * Generate a Markdown file (with YAML front-matter) from a WP post.
+ *
+ * The post_content (HTML) is stored as the body — raw HTML is valid in
+ * most Markdown flavours and renders correctly when re-imported.
+ *
+ * @param WP_Post $post
+ * @return string Markdown source.
+ */
+function oribi_sync_generate_post_markdown( WP_Post $post ): string {
+ $fm = "---\n";
+
+ // Title (escape newlines)
+ $fm .= 'title: ' . str_replace( [ "\r", "\n" ], ' ', $post->post_title ) . "\n";
+ $fm .= 'slug: ' . $post->post_name . "\n";
+ $fm .= 'status: ' . $post->post_status . "\n";
+
+ // Date
+ if ( ! empty( $post->post_date ) && '0000-00-00 00:00:00' !== $post->post_date ) {
+ $fm .= 'date: ' . substr( $post->post_date, 0, 10 ) . "\n";
+ }
+
+ // Author
+ $author = get_user_by( 'id', $post->post_author );
+ if ( $author ) {
+ $fm .= 'author: ' . $author->user_login . "\n";
+ }
+
+ // Categories
+ $cats = get_the_category( $post->ID );
+ if ( ! empty( $cats ) ) {
+ $fm .= "categories:\n";
+ foreach ( $cats as $cat ) {
+ $fm .= ' - ' . $cat->name . "\n";
+ }
+ }
+
+ // Tags
+ $post_tags = get_the_tags( $post->ID );
+ if ( ! empty( $post_tags ) ) {
+ $fm .= "tags:\n";
+ foreach ( $post_tags as $tag ) {
+ $fm .= ' - ' . $tag->name . "\n";
+ }
+ }
+
+ // Excerpt
+ if ( ! empty( $post->post_excerpt ) ) {
+ $fm .= 'excerpt: ' . str_replace( [ "\r", "\n" ], ' ', $post->post_excerpt ) . "\n";
+ }
+
+ // Featured image (absolute URL so it round-trips cleanly)
+ $thumb_id = get_post_thumbnail_id( $post->ID );
+ if ( $thumb_id ) {
+ $thumb_url = wp_get_attachment_url( $thumb_id );
+ if ( $thumb_url ) {
+ $fm .= 'featured_image: ' . $thumb_url . "\n";
+ }
+ }
+
+ $fm .= "---\n\n";
+
+ return $fm . $post->post_content;
+}
+
+// ─── Push post to repo ────────────────────────────────────────────────────────
+
+/**
+ * Push a single WordPress post back to the Git repository as a Markdown file.
+ *
+ * Follows the same Gitea-only flow as oribi_sync_push_page():
+ * - Direct commit if remote SHA matches stored SHA.
+ * - Branch + PR if a conflict is detected.
+ * - Create file if it does not yet exist.
+ *
+ * @param int $post_id WP post ID (post_type must be 'post').
+ * @param array $opts Optional: 'message' (commit message).
+ * @return array{ok: bool, action: string, message: string, pr_url?: string}
+ */
+function oribi_sync_push_post( int $post_id, array $opts = [] ): array {
+ $post = get_post( $post_id );
+
+ if ( ! $post || $post->post_type !== 'post' ) {
+ return [ 'ok' => false, 'action' => 'error', 'message' => 'Post not found or not of type "post".' ];
+ }
+
+ $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 ───────────────────────────────────────────────────
+ $repo_path = '';
+ $source_meta = get_post_meta( $post_id, '_oribi_sync_source', true );
+
+ if ( ! empty( $source_meta ) ) {
+ $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 );
+ $posts_folder = get_option( 'oribi_sync_posts_folder', 'posts' ) ?: 'posts';
+ if ( strncasecmp( $candidate, $posts_folder . '/', strlen( $posts_folder ) + 1 ) === 0 ) {
+ $repo_path = $candidate;
+ }
+ }
+ }
+ }
+
+ if ( empty( $repo_path ) ) {
+ $posts_folder = get_option( 'oribi_sync_posts_folder', 'posts' ) ?: 'posts';
+ $repo_path = rtrim( $posts_folder, '/' ) . '/' . $post->post_name . '.md';
+ }
+
+ $markdown_content = oribi_sync_generate_post_markdown( $post );
+ $commit_msg = $opts['message'] ?? 'Sync: update post ' . $post->post_name . ' from WordPress';
+ $new_checksum = hash( 'sha256', $markdown_content );
+
+ $stored_sha = get_post_meta( $post_id, '_oribi_sync_git_sha', true );
+
+ // ── 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() ];
+ }
+
+ // ── File doesn't exist: create ────────────────────────────────────────────
+ if ( $remote === null ) {
+ $result = oribi_sync_gitea_put_file( $api_base, $branch, $repo_path, $markdown_content, $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}" ];
+ }
+ $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( $post->post_name, 'created', $branch );
+ return [ 'ok' => true, 'action' => 'created', 'message' => "Created {$repo_path} on branch {$branch}." ];
+ }
+
+ $remote_sha = $remote['sha'];
+ $has_conflict = ! empty( $stored_sha ) && $remote_sha !== $stored_sha;
+
+ // ── Conflict: create branch + PR ─────────────────────────────────────────
+ if ( $has_conflict ) {
+ $timestamp = gmdate( 'Ymd-His' );
+ $new_branch = 'oribi-sync/' . $post->post_name . '-' . $timestamp;
+
+ $branch_result = oribi_sync_gitea_create_branch( $api_base, $new_branch, $branch, $pat );
+ if ( is_wp_error( $branch_result ) || $branch_result['code'] < 200 || $branch_result['code'] >= 300 ) {
+ $msg = is_wp_error( $branch_result )
+ ? $branch_result->get_error_message()
+ : ( $branch_result['body']['message'] ?? "HTTP {$branch_result['code']}" );
+ return [ 'ok' => false, 'action' => 'error', 'message' => 'Branch creation failed: ' . $msg ];
+ }
+
+ $branch_remote = oribi_sync_gitea_get_file_meta( $api_base, $new_branch, $repo_path, $pat );
+ $branch_sha = ( ! is_wp_error( $branch_remote ) && $branch_remote !== null ) ? $branch_remote['sha'] : null;
+
+ $put_result = oribi_sync_gitea_put_file( $api_base, $new_branch, $repo_path, $markdown_content, $pat, $branch_sha, $commit_msg );
+ if ( is_wp_error( $put_result ) || $put_result['code'] < 200 || $put_result['code'] >= 300 ) {
+ $msg = is_wp_error( $put_result )
+ ? $put_result->get_error_message()
+ : ( $put_result['body']['message'] ?? "HTTP {$put_result['code']}" );
+ return [ 'ok' => false, 'action' => 'error', 'message' => 'Commit to branch failed: ' . $msg ];
+ }
+
+ $pr_title = 'Sync: post ' . $post->post_name;
+ $pr_body = "Automatic push from WordPress (Oribi Tech Sync).\n\n";
+ $pr_body .= "**Post:** {$post->post_title} (`{$post->post_name}`)\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";
+
+ $pr_result = oribi_sync_gitea_create_pr( $api_base, $new_branch, $branch, $pr_title, $pr_body, $pat );
+ $pr_url = '';
+ if ( ! is_wp_error( $pr_result ) && $pr_result['code'] >= 200 && $pr_result['code'] < 300 ) {
+ $pr_url = $pr_result['body']['html_url'] ?? '';
+ }
+ if ( $pr_url ) {
+ update_post_meta( $post_id, '_oribi_sync_pr_url', $pr_url );
+ }
+ oribi_sync_log_push( $post->post_name, 'pr_created', $new_branch, $pr_url );
+ return [ 'ok' => true, 'action' => 'pr_created', 'message' => "Conflict — 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, $markdown_content, $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}" ];
+ }
+
+ $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( $post->post_name, 'updated', $branch );
+ return [ 'ok' => true, 'action' => 'updated', 'message' => "Updated {$repo_path} on branch {$branch}." ];
+}
diff --git a/includes/push-client.php b/includes/push-client.php
index 5d7fe5e..6207692 100644
--- a/includes/push-client.php
+++ b/includes/push-client.php
@@ -44,36 +44,6 @@ function oribi_sync_detect_pages_prefix(): string {
return 'pages/';
}
-// ─── Auto-push on page save ──────────────────────────────────────────────────
-add_action( 'save_post_page', 'oribi_sync_maybe_push_on_save', 20, 3 );
-
-/**
- * Push a synced page to the repo whenever it is saved.
- *
- * Only fires for pages that were previously pulled (have _oribi_sync_checksum
- * meta), skips autosaves, revisions, and non-publish statuses.
- * Uses a static guard to prevent re-entry when we update post meta after push.
- */
-function oribi_sync_maybe_push_on_save( int $post_id, WP_Post $post, bool $update ): void {
- // Guard: only on genuine content saves
- if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
- if ( wp_is_post_revision( $post_id ) ) return;
- if ( $post->post_status !== 'publish' ) return;
-
- // Guard: only pages that came from a sync (have checksum meta)
- $checksum = get_post_meta( $post_id, '_oribi_sync_checksum', true );
- if ( empty( $checksum ) ) return;
-
- // Guard: prevent re-entry when push updates meta on the same post
- static $pushing = [];
- if ( isset( $pushing[ $post_id ] ) ) return;
- $pushing[ $post_id ] = true;
-
- oribi_sync_push_page( $post_id );
-
- unset( $pushing[ $post_id ] );
-}
-
// ─── Generic authenticated request helpers ────────────────────────────────────
/**
diff --git a/includes/sync-engine.php b/includes/sync-engine.php
index 2f9ce88..9d995c4 100644
--- a/includes/sync-engine.php
+++ b/includes/sync-engine.php
@@ -105,13 +105,16 @@ function oribi_sync_execute_php( string $php_source, string $slug ) {
*/
function oribi_sync_run( bool $dry_run = false ): array {
$result = [
- 'ok' => true,
- 'created' => [],
- 'updated' => [],
- 'trashed' => [],
- 'skipped' => [],
- 'errors' => [],
+ 'ok' => true,
+ 'created' => [],
+ 'updated' => [],
+ 'trashed' => [],
+ 'skipped' => [],
+ 'errors' => [],
'theme_updated' => [],
+ 'posts_created' => [],
+ 'posts_updated' => [],
+ 'posts_trashed' => [],
];
// ── Gather settings ────────────────────────────────────────────────────
@@ -296,6 +299,18 @@ function oribi_sync_run( bool $dry_run = false ): array {
$result['errors'][] = '[theme] ' . $err;
}
+ // ── Sync posts from repo posts folder ─────────────────────────────────
+ $posts_sync = oribi_sync_run_posts( $api_base, $branch, $provider, $pat, $tree, $dry_run );
+ $result['posts_created'] = $posts_sync['created'];
+ $result['posts_updated'] = $posts_sync['updated'];
+ $result['posts_trashed'] = $posts_sync['trashed'];
+ foreach ( $posts_sync['skipped'] as $sk ) {
+ $result['skipped'][] = '[post] ' . $sk;
+ }
+ foreach ( $posts_sync['errors'] as $err ) {
+ $result['errors'][] = '[post] ' . $err;
+ }
+
// ── Record run ─────────────────────────────────────────────────────────
if ( ! $dry_run ) {
oribi_sync_record_run( $result );
@@ -387,6 +402,9 @@ function oribi_sync_record_run( array $result ): void {
'skipped' => $result['skipped'],
'errors' => $result['errors'],
'theme_updated' => $result['theme_updated'] ?? [],
+ 'posts_created' => $result['posts_created'] ?? [],
+ 'posts_updated' => $result['posts_updated'] ?? [],
+ 'posts_trashed' => $result['posts_trashed'] ?? [],
] );
// Keep last 20 entries
diff --git a/oribi-tech-sync.php b/oribi-tech-sync.php
index 40db151..b0e535c 100644
--- a/oribi-tech-sync.php
+++ b/oribi-tech-sync.php
@@ -20,6 +20,7 @@ require_once ORIBI_SYNC_DIR . 'includes/crypto.php';
require_once ORIBI_SYNC_DIR . 'includes/api-client.php';
require_once ORIBI_SYNC_DIR . 'includes/sync-engine.php';
require_once ORIBI_SYNC_DIR . 'includes/push-client.php';
+require_once ORIBI_SYNC_DIR . 'includes/post-sync.php';
require_once ORIBI_SYNC_DIR . 'includes/admin.php';
require_once ORIBI_SYNC_DIR . 'includes/rest.php';
require_once ORIBI_SYNC_DIR . 'includes/theme-preview.php';
@@ -30,12 +31,14 @@ register_deactivation_hook( __FILE__, 'oribi_sync_deactivate' );
function oribi_sync_activate() {
// Ensure default options exist
- add_option( 'oribi_sync_repo', '', '', 'no' );
- add_option( 'oribi_sync_branch', 'main', '', 'no' );
- add_option( 'oribi_sync_provider', '', '', 'no' );
- add_option( 'oribi_sync_pat', '', '', 'no' );
- add_option( 'oribi_sync_last_run', '', '', 'no' );
- add_option( 'oribi_sync_log', [], '', 'no' );
+ add_option( 'oribi_sync_repo', '', '', 'no' );
+ add_option( 'oribi_sync_branch', 'main', '', 'no' );
+ add_option( 'oribi_sync_provider', '', '', 'no' );
+ add_option( 'oribi_sync_pat', '', '', 'no' );
+ add_option( 'oribi_sync_last_run', '', '', 'no' );
+ add_option( 'oribi_sync_log', [], '', 'no' );
+ add_option( 'oribi_sync_posts_enabled', '', '', 'no' );
+ add_option( 'oribi_sync_posts_folder', 'posts', '', 'no' );
}
function oribi_sync_deactivate() {
diff --git a/uninstall.php b/uninstall.php
index f2fa7dd..6c518dd 100644
--- a/uninstall.php
+++ b/uninstall.php
@@ -17,10 +17,12 @@ delete_option( 'oribi_sync_log' );
delete_option( 'oribi_sync_webhook_secret' );
delete_option( 'oribi_sync_theme_applied' );
delete_option( 'oribi_sync_push_log' );
+delete_option( 'oribi_sync_posts_enabled' );
+delete_option( 'oribi_sync_posts_folder' );
-// Remove sync metadata from posts
+// Remove sync metadata from pages and posts
$posts = get_posts( [
- 'post_type' => 'page',
+ 'post_type' => [ 'page', 'post' ],
'post_status' => 'any',
'posts_per_page' => -1,
'meta_key' => '_oribi_sync_checksum',
@@ -36,5 +38,17 @@ foreach ( $posts as $post_id ) {
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
}
+// Remove origin src meta from media attachments
+$attachments = get_posts( [
+ 'post_type' => 'attachment',
+ 'post_status' => 'any',
+ 'posts_per_page' => -1,
+ 'meta_key' => '_oribi_sync_origin_src',
+ 'fields' => 'ids',
+] );
+foreach ( $attachments as $att_id ) {
+ delete_post_meta( $att_id, '_oribi_sync_origin_src' );
+}
+
// Clear any scheduled cron
wp_clear_scheduled_hook( 'oribi_sync_cron_run' );