mirror of
https://hk.gh-proxy.com/https://github.com/CaptainCore/do.git
synced 2025-10-03 23:34:10 +08:00
237 lines
No EOL
9.5 KiB
Bash
237 lines
No EOL
9.5 KiB
Bash
# ----------------------------------------------------
|
|
# Remotely installs the Disembark plugin, connects, and initiates a backup using an embedded Playwright script.
|
|
# ----------------------------------------------------
|
|
function run_disembark() {
|
|
local target_url="$1"
|
|
|
|
echo "🚀 Starting Disembark process for ${target_url}..."
|
|
|
|
# --- 1. Pre-flight Checks for disembark-cli ---
|
|
if [ -z "$target_url" ];then
|
|
echo "❌ Error: Missing required URL argument." >&2
|
|
show_command_help "disembark"
|
|
return 1
|
|
fi
|
|
|
|
if ! setup_disembark; then return 1; fi
|
|
|
|
# --- 2. Attempt backup with a potentially stored token ---
|
|
echo "✅ Attempting backup using a stored token..."
|
|
if "$DISEMBARK_CMD" backup "$target_url"; then
|
|
echo "✨ Backup successful using a pre-existing token."
|
|
return 0
|
|
fi
|
|
|
|
# --- 3. If backup fails, proceed to full browser authentication ---
|
|
echo "⚠️ Backup with stored token failed. A new connection token is likely required."
|
|
echo "Proceeding with browser authentication..."
|
|
|
|
# --- Pre-flight Checks for browser automation ---
|
|
if ! setup_playwright; then return 1; fi
|
|
if ! setup_gum; then return 1; fi
|
|
|
|
# --- Get Credentials Interactively ---
|
|
echo "Please provide WordPress administrator credentials:"
|
|
local username
|
|
username=$(gum input --placeholder="Enter WordPress username...")
|
|
if [ -z "$username" ]; then
|
|
echo "No username provided. Aborting." >&2
|
|
return 1
|
|
fi
|
|
|
|
local password
|
|
password=$(gum input --placeholder="Enter WordPress password..." --password)
|
|
if [ -z "$password" ]; then
|
|
echo "No password provided. Aborting." >&2
|
|
return 1
|
|
fi
|
|
|
|
# --- Define the Playwright script using a Heredoc ---
|
|
local PLAYWRIGHT_SCRIPT
|
|
PLAYWRIGHT_SCRIPT=$(cat <<'EOF'
|
|
// --- Embedded Playwright Script ---
|
|
const { chromium } = require('playwright');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const PLUGIN_ZIP_URL = 'https://github.com/DisembarkHost/disembark-connector/releases/latest/download/disembark-connector.zip';
|
|
|
|
async function main() {
|
|
const [, , targetUrl, username, password] = process.argv;
|
|
|
|
if (!targetUrl || !username || !password) {
|
|
console.error('Usage: node disembark-browser.js <url> <username> <password>');
|
|
process.exit(1);
|
|
}
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'
|
|
});
|
|
const page = await context.newPage();
|
|
try {
|
|
// 1. LOGIN
|
|
process.stdout.write(' - Step 1/5: Authenticating with WordPress...');
|
|
await page.goto(`${targetUrl}/wp-login.php`, { waitUntil: 'domcontentloaded' });
|
|
await page.fill('#user_login', username);
|
|
await page.fill('#user_pass', password);
|
|
// Click first, then wait for a specific element on the next page. This is more reliable.
|
|
await page.click('#wp-submit');
|
|
await page.waitForSelector('#wpadminbar', { timeout: 60000 }); // Wait 60s for admin bar
|
|
|
|
// Verify login by checking for the admin bar.
|
|
if (!(await page.isVisible('#wpadminbar'))) {
|
|
throw new Error('Authentication failed. Please check credentials or for 2FA/CAPTCHA.');
|
|
}
|
|
console.log(' Success!');
|
|
|
|
// 2. CHECK IF PLUGIN EXISTS & ACTIVATE IF NEEDED
|
|
process.stdout.write(' - Step 2/5: Checking plugin status...');
|
|
await page.goto(`${targetUrl}/wp-admin/plugins.php`, { waitUntil: 'networkidle' });
|
|
|
|
const pluginRow = page.locator('tr[data-slug="disembark-connector"]');
|
|
if (await pluginRow.count() > 0) {
|
|
console.log(' Plugin found.');
|
|
// More robustly check if the 'Activate' link exists.
|
|
const activateLink = pluginRow.locator('a.edit:has-text("Activate")');
|
|
if (await activateLink.count() > 0) {
|
|
process.stdout.write(' - Activating existing plugin...');
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
|
activateLink.click()
|
|
]);
|
|
console.log(' Activated!');
|
|
} else {
|
|
console.log(' - Plugin already active.');
|
|
}
|
|
} else {
|
|
// 3. UPLOAD AND INSTALL PLUGIN
|
|
console.log(' Plugin not found, proceeding with installation.');
|
|
process.stdout.write(' - Step 3/5: Downloading plugin...');
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'disembark-'));
|
|
const pluginZipPath = path.join(tempDir, 'disembark-connector.zip');
|
|
// This promise-based downloader now correctly handles HTTP redirects.
|
|
await new Promise((resolve, reject) => {
|
|
const file = fs.createWriteStream(pluginZipPath);
|
|
const request = (url) => {
|
|
https.get(url, (response) => {
|
|
if (response.statusCode > 300 && response.statusCode < 400 && response.headers.location) {
|
|
request(response.headers.location);
|
|
} else {
|
|
response.pipe(file);
|
|
file.on('finish', () => file.close(resolve));
|
|
}
|
|
}).on('error', (err) => {
|
|
fs.unlinkSync(pluginZipPath);
|
|
reject(err);
|
|
});
|
|
};
|
|
request(PLUGIN_ZIP_URL);
|
|
});
|
|
|
|
console.log(' Download complete.');
|
|
process.stdout.write(' - Uploading and installing...');
|
|
await page.goto(`${targetUrl}/wp-admin/plugin-install.php?tab=upload`);
|
|
await page.setInputFiles('input#pluginzip', pluginZipPath);
|
|
await page.waitForSelector('input#install-plugin-submit:not([disabled])', { timeout: 10000 });
|
|
|
|
await page.click('input#install-plugin-submit');
|
|
|
|
// Define selectors for possible outcomes after installation attempt.
|
|
const activationLinkSelector = 'a:has-text("Activate Plugin"), a.activate-now, .button.activate-now';
|
|
const alreadyInstalledSelector = 'body:has-text("Destination folder already exists.")';
|
|
const genericErrorSelector = '.wrap > .error, .wrap > #message.error'; // Common WP error notice selectors
|
|
|
|
try {
|
|
// Wait for any of the outcomes to appear on the page.
|
|
await page.waitForSelector(
|
|
`${activationLinkSelector}, ${alreadyInstalledSelector}, ${genericErrorSelector}`,
|
|
{ timeout: 90000 }
|
|
);
|
|
} catch (e) {
|
|
// If none of the expected outcomes appear, throw a specific timeout error.
|
|
throw new Error('Timed out waiting for a response after clicking "Install Now". The page may have hung or produced an unexpected result.');
|
|
}
|
|
|
|
// Now, check which outcome occurred and act accordingly.
|
|
if (await page.locator(activationLinkSelector).count() > 0) {
|
|
// Outcome 1: Success, the plugin was installed and needs activation.
|
|
const activateButton = page.locator(activationLinkSelector);
|
|
await Promise.all([
|
|
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
|
activateButton.first().click(),
|
|
]);
|
|
console.log(' Installed & Activated!');
|
|
} else if (await page.locator(alreadyInstalledSelector).count() > 0) {
|
|
// Outcome 2: The plugin was already installed.
|
|
console.log(' Plugin already installed.');
|
|
// No action needed here, the script will proceed to the token retrieval step.
|
|
} else {
|
|
// Outcome 3: A generic WordPress error occurred.
|
|
const errorText = await page.locator(genericErrorSelector).first().textContent();
|
|
throw new Error(`Plugin installation failed with a WordPress error: ${errorText.trim()}`);
|
|
}
|
|
|
|
fs.unlinkSync(pluginZipPath);
|
|
fs.rmdirSync(tempDir);
|
|
}
|
|
|
|
// 4. RETRIEVE TOKEN
|
|
process.stdout.write(' - Step 4/5: Retrieving connection token...');
|
|
const tokenPageUrl = `${targetUrl}/wp-admin/plugin-install.php?tab=plugin-information&plugin=disembark-connector`;
|
|
await page.goto(tokenPageUrl, { waitUntil: 'networkidle' });
|
|
|
|
const tokenElement = page.locator('div#section-description > code');
|
|
if (await tokenElement.count() === 0) {
|
|
throw new Error('Could not find the connection token element on the page.');
|
|
}
|
|
const token = await tokenElement.first().textContent();
|
|
console.log(' Token found!');
|
|
// 5. OUTPUT TOKEN FOR BASH SCRIPT
|
|
process.stdout.write(' - Step 5/5: Sending token back to script...');
|
|
console.log(token.trim());
|
|
|
|
} catch (error) {
|
|
console.error(`\nError: ${error.message}`);
|
|
process.exit(1);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
main();
|
|
EOF
|
|
)
|
|
|
|
# --- 4. Run Browser Automation ---
|
|
echo "🤖 Launching browser to automate login and plugin setup..."
|
|
|
|
# Enable pipefail to catch errors from the node script before the pipe to tee
|
|
set -o pipefail
|
|
local full_output
|
|
full_output=$(echo "$PLAYWRIGHT_SCRIPT" | node - "$target_url" "$username" "$password" | tee /dev/tty)
|
|
local exit_code=$?
|
|
|
|
# Disable pipefail after the command to avoid affecting other parts of the script
|
|
set +o pipefail
|
|
|
|
if [ $exit_code -ne 0 ]; then
|
|
# The error message from the script has already been displayed by tee
|
|
echo "❌ Browser automation failed." >&2
|
|
return 1
|
|
fi
|
|
|
|
local token
|
|
token=$(echo "$full_output" | tail -n 1 | tr -d '[:space:]')
|
|
|
|
echo "✅ Browser automation successful. Token retrieved: ${token}"
|
|
|
|
# --- 5. Connect and Backup ---
|
|
echo "📞 Connecting and starting backup with disembark-cli..."
|
|
"$DISEMBARK_CMD" connect "${target_url}" "${token}"
|
|
"$DISEMBARK_CMD" backup "${target_url}"
|
|
|
|
echo "✨ Disembark process complete!"
|
|
} |