Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-uploader): support new Kintone UI #2711

Merged
merged 14 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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