Skip to content

Commit c6ddc34

Browse files
committed
👌 IMPROVE: Disembark
1 parent 971bcdf commit c6ddc34

File tree

1 file changed

+189
-68
lines changed

1 file changed

+189
-68
lines changed

commands/disembark

Lines changed: 189 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,75 @@
22
# Remotely installs the Disembark plugin, connects, and initiates a backup using an embedded Playwright script.
33
# ----------------------------------------------------
44
function run_disembark() {
5-
local target_url="$1"
5+
local target_url_input="$1"
66
local debug_flag="$2"
77

8-
echo "🚀 Starting Disembark process for ${target_url}..."
8+
echo "🚀 Starting Disembark process for ${target_url_input}..."
99

1010
# --- 1. Pre-flight Checks for disembark-cli ---
11-
if [ -z "$target_url" ];then
11+
if [ -z "$target_url_input" ];then
1212
echo "❌ Error: Missing required URL argument." >&2
1313
show_command_help "disembark"
1414
return 1
1515
fi
1616

1717
if ! setup_disembark; then return 1; fi
1818

19-
# --- 2. Attempt backup with a potentially stored token ---
19+
# --- 2. Smart URL Parsing ---
20+
local base_url
21+
local login_path
22+
23+
# Check if the input URL contains /wp-admin or /wp-login.php
24+
if [[ "$target_url_input" == *"/wp-admin"* || "$target_url_input" == *"/wp-login.php"* ]]; then
25+
# If it's a backend URL, extract the base and the path
26+
base_url=$(echo "$target_url_input" | sed -E 's#(https?://[^/]+).*#\1#')
27+
login_path=$(echo "$target_url_input" | sed -E "s#$base_url##")
28+
echo " - Detected backend URL. Base: '$base_url', Path: '$login_path'"
29+
# Handle URLs with a path component that don't end in a slash (potential custom login)
30+
elif [[ "$target_url_input" == *"/"* && "${target_url_input: -1}" != "/" ]]; then
31+
local path_part
32+
# Use sed -n with 'p' to ensure it only outputs on a successful match
33+
path_part=$(echo "$target_url_input" | sed -n -E 's#https?://[^/]+(/.*)#\1#p')
34+
35+
# If path_part is empty, it means there was no path after the domain (e.g., https://example.com)
36+
if [ -z "$path_part" ]; then
37+
base_url="${target_url_input%/}"
38+
login_path="/wp-login.php"
39+
echo " - Homepage URL detected. Assuming default login path: '$login_path'"
40+
# Check for deep links inside WordPress content directories
41+
elif [[ "$path_part" == *"/wp-content"* || "$path_part" == *"/wp-includes"* ]]; then
42+
base_url="$target_url_input"
43+
login_path="/wp-login.php"
44+
echo " - Deep link detected. Assuming default login for base URL."
45+
# Otherwise, assume it's a custom login path
46+
else
47+
base_url=$(echo "$target_url_input" | sed -E 's#(https?://[^/]+).*#\1#')
48+
login_path="$path_part"
49+
echo " - Custom login path detected. Base: '$base_url', Path: '$login_path'"
50+
fi
51+
# Handle homepage URLs that might end with a slash
52+
else
53+
base_url="${target_url_input%/}" # Remove trailing slash if present
54+
login_path="/wp-login.php"
55+
echo " - Homepage URL detected. Assuming default login path: '$login_path'"
56+
fi
57+
58+
59+
# --- 3. Attempt backup with a potentially stored token ---
2060
echo "✅ Attempting backup using a stored token..."
21-
if "$DISEMBARK_CMD" backup "$target_url"; then
61+
if "$DISEMBARK_CMD" backup "$base_url"; then
2262
echo "✨ Backup successful using a pre-existing token."
2363
return 0
2464
fi
2565

26-
# --- 3. If backup fails, proceed to full browser authentication ---
66+
# --- 4. If backup fails, proceed to full browser authentication ---
2767
echo "⚠️ Backup with stored token failed. A new connection token is likely required."
2868
echo "Proceeding with browser authentication..."
2969

3070
# --- Pre-flight Checks for browser automation ---
3171
if ! setup_playwright; then return 1; fi
3272
if ! setup_gum; then return 1; fi
3373

34-
# --- Get Credentials Interactively ---
35-
echo "Please provide WordPress administrator credentials:"
36-
local username
37-
username=$(gum input --placeholder="Enter WordPress username...")
38-
if [ -z "$username" ]; then
39-
echo "No username provided. Aborting." >&2
40-
return 1
41-
fi
42-
43-
local password
44-
password=$(gum input --placeholder="Enter WordPress password..." --password)
45-
if [ -z "$password" ]; then
46-
echo "No password provided. Aborting." >&2
47-
return 1
48-
fi
49-
5074
# --- Define the Playwright script using a Heredoc ---
5175
local PLAYWRIGHT_SCRIPT
5276
PLAYWRIGHT_SCRIPT=$(cat <<'EOF'
@@ -60,12 +84,15 @@ const path = require('path');
6084
const PLUGIN_ZIP_URL = 'https://github.com/DisembarkHost/disembark-connector/releases/latest/download/disembark-connector.zip';
6185

6286
async function main() {
63-
const [, , targetUrl, username, password, debugFlag] = process.argv;
87+
const [, , baseUrl, loginPath, username, password, debugFlag] = process.argv;
6488

65-
if (!targetUrl || !username || !password) {
66-
console.error('Usage: node disembark-browser.js <url> <username> <password> [debug]');
89+
if (!baseUrl || !loginPath || !username || !password) {
90+
console.error('Usage: node disembark-browser.js <baseUrl> <loginPath> <username> <password> [debug]');
6791
process.exit(1);
6892
}
93+
94+
const loginUrl = baseUrl + loginPath;
95+
const adminUrl = baseUrl + '/wp-admin/';
6996

7097
const isHeadless = debugFlag !== 'true';
7198
const browser = await chromium.launch({ headless: isHeadless });
@@ -76,24 +103,54 @@ async function main() {
76103
const page = await context.newPage();
77104
try {
78105
// 1. LOGIN
79-
process.stdout.write(' - Step 1/5: Authenticating with WordPress...');
80-
await page.goto(`${targetUrl}/wp-login.php`, { waitUntil: 'domcontentloaded' });
81-
await page.waitForSelector('#user_login', { state: 'visible', timeout: 30000 });
106+
process.stdout.write(` - Step 1/5: Authenticating with WordPress at ${loginUrl}...`);
107+
await page.goto(loginUrl, { waitUntil: 'domcontentloaded' });
108+
109+
if (!(await page.isVisible('#user_login'))) {
110+
console.log(' Failed.');
111+
console.error('LOGIN_URL_INVALID');
112+
process.exit(2);
113+
}
114+
82115
await page.fill('#user_login', username);
83-
await page.waitForSelector('#user_pass', { state: 'visible', timeout: 30000 });
84116
await page.fill('#user_pass', password);
85-
await page.waitForSelector('#wp-submit', { state: 'visible', timeout: 30000 });
86117
await page.click('#wp-submit');
87-
await page.waitForSelector('#wpadminbar', { timeout: 60000 });
118+
119+
try {
120+
await page.waitForSelector('#wpadminbar, #login_error, #correct-admin-email', { timeout: 60000 });
121+
} catch (e) {
122+
throw new Error('Authentication timed out. The page did not load the admin bar, a login error, or the admin email confirmation screen.');
123+
}
124+
125+
if (await page.isVisible('#login_error')) {
126+
const errorText = await page.locator('#login_error').textContent();
127+
console.error(`LOGIN_FAILED: ${errorText.trim()}`);
128+
process.exit(3);
129+
}
130+
131+
if (await page.isVisible('#correct-admin-email')) {
132+
process.stdout.write(' Admin email confirmation required. Submitting...');
133+
await page.click('#correct-admin-email');
134+
await page.waitForSelector('#wpadminbar', { timeout: 60000 });
135+
}
88136

89137
if (!(await page.isVisible('#wpadminbar'))) {
90-
throw new Error('Authentication failed. Please check credentials or for 2FA/CAPTCHA.');
138+
throw new Error('Authentication failed. Admin bar not found after login.');
91139
}
140+
141+
// --- Recovery Step: Navigate to the main dashboard to bypass any welcome/update screens ---
142+
if (!page.url().startsWith(adminUrl)) {
143+
process.stdout.write(' Navigating to main dashboard to bypass intermediate pages...');
144+
await page.goto(adminUrl, { waitUntil: 'networkidle' });
145+
await page.waitForSelector('#wpadminbar'); // Re-confirm we are in the admin area
146+
process.stdout.write(' Done.');
147+
}
148+
92149
console.log(' Success!');
93150

94151
// 2. CHECK IF PLUGIN EXISTS & ACTIVATE IF NEEDED
95152
process.stdout.write(' - Step 2/5: Checking plugin status...');
96-
await page.goto(`${targetUrl}/wp-admin/plugins.php`, { waitUntil: 'networkidle' });
153+
await page.goto(`${adminUrl}plugins.php`, { waitUntil: 'networkidle' });
97154

98155
const pluginRow = page.locator('tr[data-slug="disembark-connector"]');
99156
if (await pluginRow.count() > 0) {
@@ -135,45 +192,38 @@ async function main() {
135192

136193
console.log(' Download complete.');
137194
process.stdout.write(' - Uploading and installing...');
138-
await page.goto(`${targetUrl}/wp-admin/plugin-install.php?tab=upload`);
195+
await page.goto(`${adminUrl}plugin-install.php?tab=upload`);
139196
await page.setInputFiles('input#pluginzip', pluginZipPath);
140197
await page.waitForSelector('input#install-plugin-submit:not([disabled])', { timeout: 10000 });
141198

142199
await page.click('input#install-plugin-submit');
143200

144-
// Define selectors for all possible outcomes after installation attempt.
145201
const activationLinkSelector = 'a:has-text("Activate Plugin"), a.activate-now, .button.activate-now';
146202
const alreadyInstalledSelector = 'body:has-text("Destination folder already exists.")';
147-
const mixedSuccessSelector = 'body:has-text("Plugin installed successfully.")'; // For your specific error case
203+
const mixedSuccessSelector = 'body:has-text("Plugin installed successfully.")';
148204
const genericErrorSelector = '.wrap > .error, .wrap > #message.error';
149205

150206
try {
151-
// Wait for ANY of the outcomes to appear on the page.
152207
await page.waitForSelector(
153208
`${activationLinkSelector}, ${alreadyInstalledSelector}, ${mixedSuccessSelector}, ${genericErrorSelector}`,
154209
{ timeout: 90000 }
155210
);
156211
} catch (e) {
157-
// If none of the expected outcomes appear, throw a specific timeout error.
158-
throw new Error('Timed out waiting for a response after clicking "Install Now". The page may have hung or produced an unexpected result.');
212+
throw new Error('Timed out waiting for a response after clicking "Install Now".');
159213
}
160214

161-
// Now, check which outcome occurred and act accordingly.
162215
if (await page.locator(activationLinkSelector).count() > 0) {
163-
// Outcome 1: Success, the plugin was installed and needs activation.
164216
const activateButton = page.locator(activationLinkSelector);
165217
await Promise.all([
166218
page.waitForNavigation({ waitUntil: 'networkidle' }),
167219
activateButton.first().click(),
168220
]);
169221
console.log(' Installed & Activated!');
170222
} else if (await page.locator(alreadyInstalledSelector).count() > 0) {
171-
// Outcome 2: The plugin was already installed.
172223
console.log(' Plugin already installed.');
173224
} else if (await page.locator(mixedSuccessSelector).count() > 0) {
174-
// Outcome 3: Install was successful, but the page crashed.
175225
console.log(' Install succeeded, but page reported an error. Navigating to plugins page to activate...');
176-
await page.goto(`${targetUrl}/wp-admin/plugins.php`, { waitUntil: 'networkidle' });
226+
await page.goto(`${adminUrl}plugins.php`, { waitUntil: 'networkidle' });
177227
const pluginRow = page.locator('tr[data-slug="disembark-connector"]');
178228
const activateLink = pluginRow.locator('a.edit:has-text("Activate")');
179229
if (await activateLink.count() > 0) {
@@ -186,7 +236,6 @@ async function main() {
186236
console.log(' - Plugin was already active on the plugins page.');
187237
}
188238
} else {
189-
// Outcome 4: A generic WordPress error occurred.
190239
const errorText = await page.locator(genericErrorSelector).first().textContent();
191240
throw new Error(`Plugin installation failed with a WordPress error: ${errorText.trim()}`);
192241
}
@@ -197,7 +246,7 @@ async function main() {
197246

198247
// 4. RETRIEVE TOKEN
199248
process.stdout.write(' - Step 4/5: Retrieving connection token...');
200-
const tokenPageUrl = `${targetUrl}/wp-admin/plugin-install.php?tab=plugin-information&plugin=disembark-connector`;
249+
const tokenPageUrl = `${adminUrl}plugin-install.php?tab=plugin-information&plugin=disembark-connector`;
201250
await page.goto(tokenPageUrl, { waitUntil: 'networkidle' });
202251

203252
const tokenElement = page.locator('div#section-description > code');
@@ -221,34 +270,106 @@ async function main() {
221270
main();
222271
EOF
223272
)
273+
# --- Helper function for getting credentials ---
274+
_get_credentials() {
275+
echo "Please provide WordPress administrator credentials:"
276+
username=$("$GUM_CMD" input --placeholder="Enter WordPress username...")
277+
if [ -z "$username" ]; then
278+
echo "No username provided. Aborting." >&2
279+
return 1
280+
fi
281+
282+
password=$("$GUM_CMD" input --placeholder="Enter WordPress password..." --password)
283+
if [ -z "$password" ]; then
284+
echo "No password provided. Aborting." >&2
285+
return 1
286+
fi
287+
return 0
288+
}
224289

225-
# --- 4. Run Browser Automation ---
226-
echo "🤖 Launching browser to automate login and plugin setup..."
227-
228-
# Enable pipefail to catch errors from the node script before the pipe to tee
229-
set -o pipefail
230-
local full_output
231-
full_output=$(echo "$PLAYWRIGHT_SCRIPT" | node - "$target_url" "$username" "$password" "$debug_flag" | tee /dev/tty)
232-
local exit_code=$?
233-
234-
# Disable pipefail after the command to avoid affecting other parts of the script
235-
set +o pipefail
290+
# --- Helper for running playwright ---
291+
run_playwright_and_get_token() {
292+
local url_to_run="$1"
293+
local path_to_run="$2"
294+
local user_to_run="$3"
295+
local pass_to_run="$4"
296+
297+
echo "🤖 Launching browser to automate login and plugin setup..."
298+
echo " - Attempting login at: ${url_to_run}${path_to_run}"
299+
300+
set -o pipefail
301+
local full_output
302+
full_output=$(echo "$PLAYWRIGHT_SCRIPT" | node - "$url_to_run" "$path_to_run" "$user_to_run" "$pass_to_run" "$debug_flag" | tee /dev/tty)
303+
local exit_code=$?
304+
set +o pipefail
305+
306+
if [ $exit_code -eq 3 ]; then
307+
return 3 # Bad credentials
308+
elif [ $exit_code -eq 2 ]; then
309+
return 2 # Invalid URL
310+
elif [ $exit_code -ne 0 ]; then
311+
return 1 # General failure
312+
fi
313+
314+
echo "$full_output" | tail -n 1 | tr -d '[:space:]'
315+
return 0
316+
}
236317

237-
if [ $exit_code -ne 0 ]; then
238-
# The error message from the script has already been displayed by tee
239-
echo "❌ Browser automation failed." >&2
318+
# --- Authentication Loop ---
319+
local token
320+
local playwright_exit_code
321+
322+
# Initial credential prompt
323+
if ! _get_credentials; then return 1; fi
324+
325+
while true; do
326+
token=$(run_playwright_and_get_token "$base_url" "$login_path" "$username" "$password")
327+
playwright_exit_code=$?
328+
329+
if [ $playwright_exit_code -eq 0 ]; then
330+
break # Success
331+
elif [ $playwright_exit_code -eq 2 ]; then # Invalid URL
332+
echo "⚠️ The login URL '${base_url}${login_path}' appears to be incorrect."
333+
local new_login_url
334+
new_login_url=$("$GUM_CMD" input --placeholder="Enter the full, correct WordPress Admin URL...")
335+
336+
if [ -z "$new_login_url" ]; then
337+
echo "No URL provided. Aborting." >&2; return 1;
338+
fi
339+
base_url=$(echo "$new_login_url" | sed -E 's#(https?://[^/]+).*#\1#')
340+
login_path=$(echo "$new_login_url" | sed -E "s#$base_url##")
341+
continue # Retry loop with new URL
342+
elif [ $playwright_exit_code -eq 3 ]; then # Bad credentials
343+
echo "⚠️ Login failed. The credentials may be incorrect."
344+
if "$GUM_CMD" confirm "Re-enter credentials and try again?"; then
345+
if ! _get_credentials; then return 1; fi
346+
continue # Retry loop with new credentials
347+
else
348+
echo "Authentication cancelled." >&2; return 1;
349+
fi
350+
else # General failure
351+
echo "❌ Browser automation failed. Please check errors above." >&2
352+
return 1
353+
fi
354+
done
355+
356+
# --- Final Check and Backup ---
357+
if [ $playwright_exit_code -ne 0 ] || [ -z "$token" ]; then
358+
echo "❌ Could not retrieve a token. Aborting." >&2
240359
return 1
241360
fi
242361

243-
local token
244-
token=$(echo "$full_output" | tail -n 1 | tr -d '[:space:]')
245-
246-
echo "✅ Browser automation successful. Token retrieved: ${token}"
247-
248-
# --- 5. Connect and Backup ---
362+
echo "✅ Browser automation successful. Token retrieved."
249363
echo "📞 Connecting and starting backup with disembark-cli..."
250-
"$DISEMBARK_CMD" connect "${target_url}" "${token}"
251-
"$DISEMBARK_CMD" backup "${target_url}"
364+
if ! "$DISEMBARK_CMD" connect "${base_url}" "${token}"; then
365+
echo "❌ Error: Failed to connect using the retrieved token." >&2
366+
return 1
367+
fi
368+
369+
if ! "$DISEMBARK_CMD" backup "${base_url}"; then
370+
echo "❌ Error: Backup command failed after connecting." >&2
371+
return 1
372+
fi
252373

253374
echo "✨ Disembark process complete!"
254375
}

0 commit comments

Comments
 (0)