Skip to content

Commit

Permalink
feat(plugin-uploader): support new Kintone UI (#2711)
Browse files Browse the repository at this point in the history
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: hung-nguyen <hung-nguyen@cybozu.vn>
  • Loading branch information
3 people authored May 8, 2024
1 parent e884597 commit ca6ca58
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 128 deletions.
103 changes: 103 additions & 0 deletions packages/plugin-uploader/src/controllers/ControllerBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Browser, Page } from "puppeteer";
import puppeteer from "puppeteer";
import chalk from "chalk";
import type { BoundMessage } from "../messages";

const TIMEOUT_MS = 10000;

export interface BasicAuth {
username: string;
password: string;
}

export abstract class ControllerBase {
private _browser?: Browser;
private _page?: Page;

public get browser() {
if (this._browser === undefined) {
throw new Error(
"Browser is not launched yet. Please call launchBrowser() first.",
);
}
return this._browser;
}

public set browser(value) {
this._browser = value;
}

public get page() {
if (this._page === undefined) {
throw new Error(
"Page is not opened yet. Please call openNewPage() first.",
);
}
return this._page;
}

public set page(value) {
this._page = value;
}

public async launchBrowser(
options: {
proxy?: string;
ignoreDefaultArgs?: string[];
} = {},
) {
const args = options.proxy ? [`--proxy-server=${options.proxy}`] : [];
this._browser = await puppeteer.launch({
args,
ignoreDefaultArgs: options.ignoreDefaultArgs,
headless: "shell",
});
}

public async closeBrowser() {
return this.browser.close();
}

public async openNewPage() {
this._page = await this.browser.newPage();
}

protected async login(options: {
baseUrl: string;
userName: string;
password: string;
boundMessage: BoundMessage;
basicAuth?: BasicAuth;
}): Promise<void> {
const { baseUrl, userName, password, boundMessage, basicAuth } = options;
const loginUrl = `${baseUrl}/login?saml=off`;

if (basicAuth) {
await this.page.authenticate(basicAuth);
}

console.log(`Open ${loginUrl}`);
await this.page.goto(loginUrl);
try {
await this.page.waitForSelector(".form-username-slash", {
timeout: TIMEOUT_MS,
});
} catch (e) {
throw chalk.red(boundMessage("Error_cannotOpenLogin"));
}

console.log("Trying to log in...");
await this.page.type(".form-username-slash > input.form-text", userName);
await this.page.type(".form-password-slash > input.form-text", password);
await this.page.click(".login-button");

try {
await this.page.waitForNavigation({
timeout: TIMEOUT_MS,
waitUntil: "domcontentloaded",
});
} catch (e) {
throw chalk.red(boundMessage("Error_failedLogin"));
}
}
}
86 changes: 86 additions & 0 deletions packages/plugin-uploader/src/controllers/PluginSystemController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { Lang } from "../lang";
import chalk from "chalk";
import { getBoundMessage } from "../messages";
import { ReactPluginSystemPage } from "../pages/ReactPluginSystemPage";
import type { PluginSystemPageInterface } from "../pages/PluginSystemPageInterface";
import {
IMPORT_BUTTON_SELECTOR,
OldPluginSystemPage,
} from "../pages/OldPluginSystemPage";
import { ControllerBase } from "./ControllerBase";

const DETECT_PAGE_TIMEOUT_MS = 10000;
const NO_PRIVILEGE_STATUS_CODE = "CB_NO02";

export interface BasicAuth {
username: string;
password: string;
}

export default class PluginSystemController extends ControllerBase {
private ui?: PluginSystemPageInterface;

private async getUI() {
if (this.ui) {
return this.ui;
}

const isReactPage = await this.isReactPage();
this.ui = isReactPage
? new ReactPluginSystemPage()
: new OldPluginSystemPage();

return this.ui;
}

private async isReactPage(): Promise<boolean> {
try {
await this.page.waitForSelector(IMPORT_BUTTON_SELECTOR, {
timeout: DETECT_PAGE_TIMEOUT_MS,
});
return false;
} catch (e) {
return true;
}
}

protected async goToPluginSystemPage(baseUrl: string) {
const pluginSystemPageUri = `${baseUrl}/k/admin/system/plugin/`;
console.log(`Navigate to ${pluginSystemPageUri}`);
return this.page.goto(pluginSystemPageUri);
}

public async readyForUpload(options: {
baseUrl: string;
userName: string;
password: string;
lang: Lang;
basicAuth?: BasicAuth;
}): Promise<void> {
const { baseUrl, userName, password, lang, basicAuth } = options;
const boundMessage = getBoundMessage(lang);

await this.login({
baseUrl,
userName,
password,
boundMessage,
basicAuth,
});

const response = await this.goToPluginSystemPage(baseUrl);
if (
!response ||
(response.headers()["x-cybozu-error"] &&
response.headers()["x-cybozu-error"] === NO_PRIVILEGE_STATUS_CODE)
) {
throw chalk.red(boundMessage("Error_adminPrivilege"));
}

await (await this.getUI()).readyForImportButton(this.page, boundMessage);
}

public async upload(pluginPath: string, lang: Lang): Promise<void> {
await (await this.getUI()).upload(this.page, pluginPath, lang);
}
}
152 changes: 27 additions & 125 deletions packages/plugin-uploader/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,109 +1,8 @@
import chalk from "chalk";
import fs from "fs";
import puppeteer from "puppeteer";
import type { Browser, Page } from "puppeteer";

import type { Lang } from "./lang";
import { getBoundMessage } from "./messages";

const TIMEOUT_MS = 10000;
const UPLOAD_TIMEOUT_MS = 60000;

interface BasicAuth {
username: string;
password: string;
}

const launchBrowser = (
proxy?: string,
ignoreDefaultArgs?: string[],
): Promise<Browser> => {
const args = proxy ? [`--proxy-server=${proxy}`] : [];
return puppeteer.launch({ args, ignoreDefaultArgs, headless: "shell" });
};

const readyForUpload = async (
browser: Browser,
baseUrl: string,
userName: string,
password: string,
lang: Lang,
basicAuth?: BasicAuth,
): Promise<Page> => {
const m = getBoundMessage(lang);

const page = await browser.newPage();
const loginUrl = `${baseUrl}/login?saml=off`;

if (basicAuth) {
await page.authenticate(basicAuth);
}

console.log(`Open ${loginUrl}`);
await page.goto(loginUrl);
try {
await page.waitForSelector(".form-username-slash", { timeout: TIMEOUT_MS });
} catch (e) {
throw chalk.red(m("Error_cannotOpenLogin"));
}
console.log("Trying to log in...");
await page.type(".form-username-slash > input.form-text", userName);
await page.type(".form-password-slash > input.form-text", password);
await page.click(".login-button");
try {
await page.waitForNavigation({
timeout: TIMEOUT_MS,
waitUntil: "domcontentloaded",
});
} catch (e) {
throw chalk.red(m("Error_failedLogin"));
}

const pluginUrl = `${baseUrl}/k/admin/system/plugin/`;
console.log(`Navigate to ${pluginUrl}`);
await page.goto(pluginUrl);

try {
await page.waitForSelector("#page-admin-system-plugin-index-addplugin", {
timeout: TIMEOUT_MS,
});
} catch (e) {
throw chalk.red(m("Error_adminPrivilege"));
}
return page;
};

const upload = async (
page: Page,
pluginPath: string,
lang: Lang,
): Promise<void> => {
const m = getBoundMessage(lang);
console.log(`Trying to upload ${pluginPath}`);
await page.click("#page-admin-system-plugin-index-addplugin");

const file = await page.$('.plupload > input[type="file"]');
if (file == null) {
throw new Error('input[type="file"] is not found');
}
await file.uploadFile(pluginPath);
// HACK: `page.click` does not work as expected, so we use `page.evaluate` instead.
// ref: https://github.com/puppeteer/puppeteer/pull/7097#issuecomment-850348366
await page.evaluate(() => {
const button =
document.querySelector<HTMLButtonElement>('button[name="ok"]');
if (button) {
button.click();
} else {
throw new Error('button[name="ok"] is not found');
}
});
await page.waitForSelector(".ocean-ui-dialog", {
hidden: true,
timeout: UPLOAD_TIMEOUT_MS,
});
console.log(`${pluginPath} ${m("Uploaded")}`);
};
import type { BasicAuth } from "./controllers/ControllerBase";
import PluginSystemController from "./controllers/PluginSystemController";
import type { Lang } from "./lang";

interface Option {
proxyServer?: string;
Expand All @@ -120,23 +19,26 @@ export const run = async (
pluginPath: string,
options: Option,
): Promise<void> => {
let browser = await launchBrowser(
options.proxyServer,
options.puppeteerIgnoreDefaultArgs,
);
let page: Page;
const { lang, basicAuth } = options;
const m = getBoundMessage(lang);
const boundMessage = getBoundMessage(lang);

const browserOptions = {
proxy: options.proxyServer,
ignoreDefaultArgs: options.puppeteerIgnoreDefaultArgs,
};
const pluginSystemController = new PluginSystemController();
await pluginSystemController.launchBrowser(browserOptions);

try {
page = await readyForUpload(
browser,
await pluginSystemController.openNewPage();
await pluginSystemController.readyForUpload({
baseUrl,
userName,
password,
lang,
basicAuth,
);
await upload(page, pluginPath, lang);
});
await pluginSystemController.upload(pluginPath, lang);
if (options.watch) {
let uploading = false;
fs.watch(pluginPath, async () => {
Expand All @@ -145,30 +47,30 @@ export const run = async (
}
try {
uploading = true;
await upload(page, pluginPath, lang);
await pluginSystemController.upload(pluginPath, lang);
} catch (e) {
console.log(e);
console.log(m("Error_retry"));
await browser.close();
browser = await launchBrowser(options.proxyServer);
page = await readyForUpload(
browser,
console.log(boundMessage("Error_retry"));
await pluginSystemController.closeBrowser();
await pluginSystemController.launchBrowser(browserOptions);
await pluginSystemController.openNewPage();
await pluginSystemController.readyForUpload({
baseUrl,
userName,
password,
lang,
basicAuth,
);
await upload(page, pluginPath, lang);
});
await pluginSystemController.upload(pluginPath, lang);
} finally {
uploading = false;
}
});
} else {
await browser.close();
await pluginSystemController.closeBrowser();
}
} catch (e) {
console.error(m("Error"), e);
await browser.close();
console.error(boundMessage("Error"), e);
await pluginSystemController.closeBrowser();
}
};
Loading

0 comments on commit ca6ca58

Please sign in to comment.