Add clean-room WordPress connector plugin
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"dev": "electron-vite dev",
|
||||
"build": "npm run typecheck && electron-vite build",
|
||||
"build:modules": "node scripts/package-modules.cjs",
|
||||
"build:connector": "node scripts/package-connector.cjs",
|
||||
"module:new": "node scripts/create-module.cjs",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Aurora Dockside Connector
|
||||
* Description: Securely connects a WordPress site to Aurora Dockside for local development workflows.
|
||||
* Version: 0.1.0
|
||||
* Author: Aurora Dockside
|
||||
* Requires at least: 6.5
|
||||
* Requires PHP: 8.0
|
||||
* Text Domain: aurora-dockside-connector
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define('AURORA_DOCKSIDE_CONNECTOR_VERSION', '0.1.0');
|
||||
define('AURORA_DOCKSIDE_CONNECTOR_FILE', __FILE__);
|
||||
define('AURORA_DOCKSIDE_CONNECTOR_DIR', plugin_dir_path(__FILE__));
|
||||
|
||||
require_once AURORA_DOCKSIDE_CONNECTOR_DIR . 'includes/class-aurora-dockside-manifest.php';
|
||||
require_once AURORA_DOCKSIDE_CONNECTOR_DIR . 'includes/class-aurora-dockside-rest.php';
|
||||
|
||||
add_action('rest_api_init', array('Aurora_Dockside_REST', 'register_routes'));
|
||||
add_action('admin_menu', function () {
|
||||
add_management_page(
|
||||
__('Aurora Dockside', 'aurora-dockside-connector'),
|
||||
__('Aurora Dockside', 'aurora-dockside-connector'),
|
||||
'manage_options',
|
||||
'aurora-dockside-connector',
|
||||
'aurora_dockside_connector_admin_page'
|
||||
);
|
||||
});
|
||||
|
||||
function aurora_dockside_connector_admin_page(): void
|
||||
{
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
$endpoint = rest_url('aurora-dockside/v1/status');
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Aurora Dockside Connector', 'aurora-dockside-connector'); ?></h1>
|
||||
<p><?php esc_html_e('This plugin gives Aurora Dockside an authenticated, read-only view of this site. Transfers are authorized with a WordPress Application Password.', 'aurora-dockside-connector'); ?></p>
|
||||
<table class="widefat striped" style="max-width: 820px">
|
||||
<tbody>
|
||||
<tr><th><?php esc_html_e('Connector version', 'aurora-dockside-connector'); ?></th><td><?php echo esc_html(AURORA_DOCKSIDE_CONNECTOR_VERSION); ?></td></tr>
|
||||
<tr><th><?php esc_html_e('REST endpoint', 'aurora-dockside-connector'); ?></th><td><code><?php echo esc_html($endpoint); ?></code></td></tr>
|
||||
<tr><th><?php esc_html_e('Authentication', 'aurora-dockside-connector'); ?></th><td><?php esc_html_e('WordPress user + Application Password', 'aurora-dockside-connector'); ?></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2><?php esc_html_e('Connect from Dockside', 'aurora-dockside-connector'); ?></h2>
|
||||
<ol>
|
||||
<li><?php esc_html_e('Open Users → Profile and create an Application Password named “Aurora Dockside”.', 'aurora-dockside-connector'); ?></li>
|
||||
<li><?php esc_html_e('In Dockside, open Remote Site and enter this site URL, your username, and the generated password.', 'aurora-dockside-connector'); ?></li>
|
||||
<li><?php esc_html_e('Use Test Connection before starting a pull.', 'aurora-dockside-connector'); ?></li>
|
||||
</ol>
|
||||
<p><strong><?php esc_html_e('Safety:', 'aurora-dockside-connector'); ?></strong> <?php esc_html_e('Version 0.1 is read-only. It cannot modify files or import a database.', 'aurora-dockside-connector'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class Aurora_Dockside_Manifest
|
||||
{
|
||||
private const EXCLUDED_PARTS = array('.git', 'cache', 'upgrade', 'backup', 'backups', 'ai1wm-backups', 'wflogs');
|
||||
|
||||
public static function page(string $scope, int $cursor, int $limit): array
|
||||
{
|
||||
$uploads = wp_get_upload_dir();
|
||||
$root = $scope === 'uploads' ? (string) $uploads['basedir'] : WP_CONTENT_DIR;
|
||||
$root = realpath($root) ?: $root;
|
||||
$files = self::scan($root);
|
||||
$slice = array_slice($files, $cursor, $limit);
|
||||
return array(
|
||||
'scope' => $scope,
|
||||
'cursor' => $cursor,
|
||||
'next_cursor' => $cursor + count($slice) < count($files) ? $cursor + count($slice) : null,
|
||||
'total' => count($files),
|
||||
'files' => $slice,
|
||||
);
|
||||
}
|
||||
|
||||
private static function scan(string $root): array
|
||||
{
|
||||
if (!is_dir($root) || !is_readable($root)) {
|
||||
return array();
|
||||
}
|
||||
$result = array();
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveCallbackFilterIterator(
|
||||
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
|
||||
function (SplFileInfo $current): bool {
|
||||
return !in_array($current->getFilename(), self::EXCLUDED_PARTS, true) && !$current->isLink();
|
||||
}
|
||||
),
|
||||
RecursiveIteratorIterator::LEAVES_ONLY
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if (!$file->isFile() || !$file->isReadable()) {
|
||||
continue;
|
||||
}
|
||||
$absolute = $file->getPathname();
|
||||
$relative = ltrim(str_replace('\\', '/', substr($absolute, strlen($root))), '/');
|
||||
$result[] = array(
|
||||
'path' => $relative,
|
||||
'bytes' => $file->getSize(),
|
||||
'modified' => $file->getMTime(),
|
||||
'sha256' => hash_file('sha256', $absolute),
|
||||
);
|
||||
}
|
||||
usort($result, fn(array $a, array $b): int => strcmp($a['path'], $b['path']));
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class Aurora_Dockside_REST
|
||||
{
|
||||
private const NAMESPACE = 'aurora-dockside/v1';
|
||||
|
||||
public static function register_routes(): void
|
||||
{
|
||||
register_rest_route(self::NAMESPACE, '/status', array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array(self::class, 'status'),
|
||||
'permission_callback' => array(self::class, 'authorize'),
|
||||
));
|
||||
register_rest_route(self::NAMESPACE, '/manifest', array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array(self::class, 'manifest'),
|
||||
'permission_callback' => array(self::class, 'authorize'),
|
||||
'args' => array(
|
||||
'scope' => array('type' => 'string', 'enum' => array('content', 'uploads'), 'default' => 'content'),
|
||||
'cursor' => array('type' => 'integer', 'minimum' => 0, 'default' => 0),
|
||||
'limit' => array('type' => 'integer', 'minimum' => 1, 'maximum' => 1000, 'default' => 250),
|
||||
),
|
||||
));
|
||||
register_rest_route(self::NAMESPACE, '/database', array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array(self::class, 'database'),
|
||||
'permission_callback' => array(self::class, 'authorize'),
|
||||
));
|
||||
}
|
||||
|
||||
public static function authorize(): bool
|
||||
{
|
||||
return is_user_logged_in() && current_user_can('manage_options');
|
||||
}
|
||||
|
||||
public static function status(): WP_REST_Response
|
||||
{
|
||||
global $wp_version, $wpdb;
|
||||
$theme = wp_get_theme();
|
||||
return new WP_REST_Response(array(
|
||||
'connector' => array('name' => 'Aurora Dockside Connector', 'version' => AURORA_DOCKSIDE_CONNECTOR_VERSION, 'mode' => 'read-only'),
|
||||
'site' => array(
|
||||
'name' => get_bloginfo('name'),
|
||||
'url' => home_url('/'),
|
||||
'admin_url' => admin_url('/'),
|
||||
'multisite' => is_multisite(),
|
||||
'language' => get_locale(),
|
||||
),
|
||||
'runtime' => array('wordpress' => $wp_version, 'php' => PHP_VERSION, 'database' => $wpdb->db_version()),
|
||||
'theme' => array('stylesheet' => $theme->get_stylesheet(), 'version' => $theme->get('Version')),
|
||||
'capabilities' => array('status' => true, 'file_manifest' => true, 'database_inventory' => true, 'pull' => false, 'push' => false),
|
||||
));
|
||||
}
|
||||
|
||||
public static function manifest(WP_REST_Request $request): WP_REST_Response
|
||||
{
|
||||
$result = Aurora_Dockside_Manifest::page(
|
||||
(string) $request->get_param('scope'),
|
||||
(int) $request->get_param('cursor'),
|
||||
(int) $request->get_param('limit')
|
||||
);
|
||||
return new WP_REST_Response($result);
|
||||
}
|
||||
|
||||
public static function database(): WP_REST_Response
|
||||
{
|
||||
global $wpdb;
|
||||
$tables = $wpdb->get_col('SHOW TABLES');
|
||||
$inventory = array();
|
||||
foreach ($tables as $table) {
|
||||
$safe_table = esc_sql($table);
|
||||
$rows = (int) $wpdb->get_var("SELECT COUNT(*) FROM `{$safe_table}`"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$size = $wpdb->get_row($wpdb->prepare(
|
||||
'SELECT DATA_LENGTH, INDEX_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s',
|
||||
DB_NAME,
|
||||
$table
|
||||
), ARRAY_A);
|
||||
$inventory[] = array(
|
||||
'name' => $table,
|
||||
'rows' => $rows,
|
||||
'bytes' => (int) (($size['DATA_LENGTH'] ?? 0) + ($size['INDEX_LENGTH'] ?? 0)),
|
||||
);
|
||||
}
|
||||
return new WP_REST_Response(array('engine_version' => $wpdb->db_version(), 'prefix' => $wpdb->prefix, 'tables' => $inventory));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
=== Aurora Dockside Connector ===
|
||||
Contributors: auroradockside
|
||||
Tags: local development, migration, development
|
||||
Requires at least: 6.5
|
||||
Tested up to: 6.8
|
||||
Requires PHP: 8.0
|
||||
Stable tag: 0.1.0
|
||||
License: GPLv2 or later
|
||||
|
||||
An original, secure connector between WordPress and Aurora Dockside.
|
||||
|
||||
== Description ==
|
||||
|
||||
Aurora Dockside Connector exposes authenticated site metadata, a paginated file manifest, and a database inventory to Aurora Dockside. Authentication uses WordPress Application Passwords and requires an administrator account.
|
||||
|
||||
Version 0.1 is deliberately read-only. It does not modify the production database or production files.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the plugin ZIP in Plugins → Add New → Upload Plugin.
|
||||
2. Activate Aurora Dockside Connector.
|
||||
3. Open Tools → Aurora Dockside.
|
||||
4. Create an Application Password from Users → Profile.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.1.0 =
|
||||
* Initial read-only connection API.
|
||||
* Site and runtime discovery.
|
||||
* Paginated content and uploads manifests.
|
||||
* Database table inventory.
|
||||
@@ -0,0 +1,19 @@
|
||||
'use strict'
|
||||
|
||||
const { mkdirSync, rmSync } = require('fs')
|
||||
const { join, resolve } = require('path')
|
||||
const { spawnSync } = require('child_process')
|
||||
|
||||
const root = resolve(__dirname, '..')
|
||||
const source = join(root, 'packages', 'aurora-dockside-connector')
|
||||
const outputDir = join(root, 'dist', 'connectors')
|
||||
const output = join(outputDir, 'aurora-dockside-connector-0.1.0.zip')
|
||||
mkdirSync(outputDir, { recursive: true })
|
||||
rmSync(output, { force: true })
|
||||
const result = spawnSync('zip', ['-qr', output, 'aurora-dockside-connector'], {
|
||||
cwd: join(root, 'packages'),
|
||||
stdio: 'inherit'
|
||||
})
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) throw new Error(`zip failed with exit code ${result.status}`)
|
||||
process.stdout.write(`${output}\n`)
|
||||
Reference in New Issue
Block a user