Add Oribi Sync plugin for syncing WordPress pages and theme files from a Git repository

- Implement encryption helpers for storing and retrieving the Personal Access Token (PAT).
- Create REST API endpoints for triggering sync, checking sync status, and handling webhooks.
- Develop the sync engine to fetch pages from the Git repository, create/update WordPress pages, and trash removed pages.
- Add functionality for previewing and applying theme files from the repository.
- Set up plugin activation and deactivation hooks to manage default options and scheduled tasks.
- Implement uninstall routine to clean up plugin options and metadata from posts.
This commit is contained in:
Matt Batchelder
2026-02-19 16:05:43 -05:00
commit f17b9ccb98
12 changed files with 1784 additions and 0 deletions

233
includes/theme-preview.php Normal file
View File

@@ -0,0 +1,233 @@
<?php
/**
* Oribi Sync — Theme file preview & apply.
*
* Provides an admin screen that fetches files from the repo's `theme/`
* directory, shows a preview diff against the active theme, and lets the
* admin selectively apply files.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
// ─── Handle theme preview request ────────────────────────────────────────────
add_action( 'admin_post_oribi_sync_theme_preview', function () {
if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Permission denied.' );
check_admin_referer( 'oribi_sync_theme_preview' );
$theme_data = oribi_sync_fetch_theme_files();
set_transient( 'oribi_sync_theme_preview', $theme_data, 300 );
wp_redirect( add_query_arg( 'oribi_sync_tab', 'theme', admin_url( 'options-general.php?page=oribi-sync' ) ) );
exit;
} );
// ─── Handle theme file apply ─────────────────────────────────────────────────
add_action( 'admin_post_oribi_sync_theme_apply', function () {
if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Permission denied.' );
check_admin_referer( 'oribi_sync_theme_apply' );
$selected = $_POST['oribi_sync_theme_files'] ?? [];
if ( ! is_array( $selected ) || empty( $selected ) ) {
wp_redirect( add_query_arg( [
'oribi_sync_tab' => 'theme',
'oribi_sync_saved' => 'no_files',
], admin_url( 'options-general.php?page=oribi-sync' ) ) );
exit;
}
$theme_data = get_transient( 'oribi_sync_theme_preview' );
if ( ! $theme_data || empty( $theme_data['files'] ) ) {
wp_redirect( add_query_arg( [
'oribi_sync_tab' => 'theme',
'oribi_sync_saved' => 'expired',
], admin_url( 'options-general.php?page=oribi-sync' ) ) );
exit;
}
$applied = [];
$errors = [];
$theme_dir = get_template_directory();
foreach ( $theme_data['files'] as $file ) {
$relative = $file['relative'];
if ( ! in_array( $relative, $selected, true ) ) continue;
$dest = $theme_dir . '/' . $relative;
// Safety: only allow CSS, JS, JSON, PHP, HTML extensions
$ext = strtolower( pathinfo( $relative, PATHINFO_EXTENSION ) );
$allowed = [ 'css', 'js', 'json', 'php', 'html', 'htm', 'svg', 'txt' ];
if ( ! in_array( $ext, $allowed, true ) ) {
$errors[] = $relative . ' — file type not allowed.';
continue;
}
// Create subdirectory if needed
$dir = dirname( $dest );
if ( ! is_dir( $dir ) ) {
if ( ! wp_mkdir_p( $dir ) ) {
$errors[] = $relative . ' — could not create directory.';
continue;
}
}
// Write file
$written = file_put_contents( $dest, $file['content'] );
if ( $written === false ) {
$errors[] = $relative . ' — write failed (check permissions).';
} else {
$applied[] = $relative;
}
}
// Record
update_option( 'oribi_sync_theme_applied', [
'time' => current_time( 'mysql' ),
'applied' => $applied,
'errors' => $errors,
], 'no' );
set_transient( 'oribi_sync_theme_result', [
'applied' => $applied,
'errors' => $errors,
], 60 );
wp_redirect( add_query_arg( [
'oribi_sync_tab' => 'theme',
'oribi_sync_saved' => 'theme_applied',
], admin_url( 'options-general.php?page=oribi-sync' ) ) );
exit;
} );
// ─── Render theme preview panel (called from settings page) ──────────────────
/**
* Render the theme preview panel within the settings page.
*/
function oribi_sync_render_theme_preview(): void {
$theme_data = get_transient( 'oribi_sync_theme_preview' );
$theme_result = get_transient( 'oribi_sync_theme_result' );
if ( $theme_result ) delete_transient( 'oribi_sync_theme_result' );
$saved = $_GET['oribi_sync_saved'] ?? '';
?>
<?php if ( $saved === 'theme_applied' && $theme_result ): ?>
<div class="notice notice-success">
<p><strong>Theme files applied:</strong></p>
<?php if ( ! empty( $theme_result['applied'] ) ): ?>
<ul style="list-style:disc;padding-left:1.5rem;">
<?php foreach ( $theme_result['applied'] as $f ): ?>
<li><?php echo esc_html( $f ); ?> ✓</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ( ! empty( $theme_result['errors'] ) ): ?>
<ul style="list-style:disc;padding-left:1.5rem;color:#d63638;">
<?php foreach ( $theme_result['errors'] as $e ): ?>
<li><?php echo esc_html( $e ); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php elseif ( $saved === 'no_files' ): ?>
<div class="notice notice-warning is-dismissible"><p>No theme files were selected.</p></div>
<?php elseif ( $saved === 'expired' ): ?>
<div class="notice notice-warning is-dismissible"><p>Theme preview data expired. Please preview again.</p></div>
<?php endif; ?>
<?php if ( ! $theme_data ): ?>
<p>Click <strong>Preview Theme Files</strong> above to fetch files from the repo's <code>theme/</code> directory.</p>
<?php return; ?>
<?php endif; ?>
<?php if ( ! empty( $theme_data['errors'] ) ): ?>
<div class="notice notice-error">
<p><strong>Errors fetching theme files:</strong></p>
<ul style="list-style:disc;padding-left:1.5rem;">
<?php foreach ( $theme_data['errors'] as $err ): ?>
<li><?php echo esc_html( $err ); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<?php if ( empty( $theme_data['files'] ) ): ?>
<p>No files found under <code>theme/</code> in the repository.</p>
<?php return; ?>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<input type="hidden" name="action" value="oribi_sync_theme_apply" />
<?php wp_nonce_field( 'oribi_sync_theme_apply' ); ?>
<table class="widefat fixed striped">
<thead><tr>
<th style="width:30px;"><input type="checkbox" id="oribi-sync-check-all" /></th>
<th>File</th>
<th style="width:100px;">Status</th>
<th>Preview</th>
</tr></thead>
<tbody>
<?php foreach ( $theme_data['files'] as $file ): ?>
<tr>
<td>
<input type="checkbox" name="oribi_sync_theme_files[]"
value="<?php echo esc_attr( $file['relative'] ); ?>"
<?php echo $file['changed'] ? '' : 'disabled'; ?> />
</td>
<td><code><?php echo esc_html( $file['relative'] ); ?></code></td>
<td>
<?php if ( ! $file['local_exists'] ): ?>
<span style="color:#00a32a;font-weight:bold;">New</span>
<?php elseif ( $file['changed'] ): ?>
<span style="color:#dba617;font-weight:bold;">Changed</span>
<?php else: ?>
<span style="color:#999;">Unchanged</span>
<?php endif; ?>
</td>
<td>
<?php if ( $file['changed'] ): ?>
<details>
<summary>View content</summary>
<pre style="max-height:300px;overflow:auto;background:#f6f7f7;padding:8px;font-size:12px;border:1px solid #ddd;"><?php echo esc_html( $file['content'] ); ?></pre>
<?php if ( $file['local_exists'] && $file['local_content'] !== null ): ?>
<details style="margin-top:4px;">
<summary>Current local content</summary>
<pre style="max-height:300px;overflow:auto;background:#fef7f1;padding:8px;font-size:12px;border:1px solid #ddd;"><?php echo esc_html( $file['local_content'] ); ?></pre>
</details>
<?php endif; ?>
</details>
<?php else: ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p style="margin-top:12px;">
<button type="submit" class="button button-primary"
onclick="return confirm('Apply selected files to the active theme directory?');">
Apply Selected Files
</button>
<span class="description" style="margin-left:8px;">
Files will be written directly into the active theme: <code><?php echo esc_html( basename( get_template_directory() ) ); ?></code>
</span>
</p>
</form>
<script>
document.getElementById('oribi-sync-check-all')?.addEventListener('change', function() {
document.querySelectorAll('input[name="oribi_sync_theme_files[]"]:not(:disabled)')
.forEach(cb => cb.checked = this.checked);
});
</script>
<?php
}
// ─── Hook the theme preview into the settings page ───────────────────────────
// We append it via a late-bound action so the admin page can call it.
// The settings page checks for the 'oribi_sync_tab' parameter and renders this.