casdoor 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,127 +12,328 @@
12
12
  // See the License for the specific language governing permissions and
13
13
  // limitations under the License.
14
14
 
15
- /* eslint-disable no-undef */
16
-
17
- const updateTime = 300;
18
-
19
- function waitForElementToDisplay(xpath, time, func) {
20
- if (document.evaluate(xpath, document).iterateNext() != null) {
21
- func();
22
- } else {
23
- setTimeout(function() {
24
- waitForElementToDisplay(xpath, time, func);
25
- }, time);
26
- }
27
- }
28
-
29
- function autoLogin(userName, password) {
30
- const usernameXpath = UsernameXpaths.join(" | ");
31
- waitForElementToDisplay(
32
- usernameXpath,
33
- updateTime,
34
- () => {
35
- const usernameElement = document.evaluate(usernameXpath, document).iterateNext();
36
- usernameElement.value = userName;
37
- const changeEvent = new Event("change", {
38
- bubbles: true,
39
- cancelable: true,
40
- });
41
- usernameElement.dispatchEvent(changeEvent);
42
-
43
- const passwordXpath = PasswordXpaths.join(" | ");
44
- waitForElementToDisplay(
45
- passwordXpath,
46
- updateTime,
47
- () => {
48
- const passwordElement = document.evaluate(passwordXpath, document).iterateNext();
49
- passwordElement.value = password;
50
- const changeEvent = new Event("change", {
51
- bubbles: true,
52
- cancelable: true,
53
- });
54
- passwordElement.dispatchEvent(changeEvent);
55
-
56
- const submitXpath = SubmitXpaths.join(" | ");
57
- waitForElementToDisplay(
58
- submitXpath,
59
- updateTime,
60
- () => {
61
- document
62
- .evaluate(submitXpath, document)
63
- .iterateNext()
64
- .click();
65
- }
66
- );
67
- }
68
- );
69
- }
70
- );
71
- }
72
-
73
- function autoFillCasdoorConfig() {
74
- const applicationNameXpath = "//div[@id='CasdoorApplicationName']";
75
- waitForElementToDisplay(
76
- applicationNameXpath,
77
- updateTime,
78
- () => {
79
- const applicationNameDom = document.evaluate(applicationNameXpath, document).iterateNext();
80
- if (applicationNameDom) {
81
- const applicationName = applicationNameDom.getAttribute("value");
82
- if (applicationName) {
83
- chrome.storage.sync.set({applicationName});
84
- chrome.storage.sync.set({endpoint: window.location.origin});
85
- }
15
+ // Fills the sign-in form of a legacy system with the user's managed account, and offers to save the account that the
16
+ // user signs in with by hand. The page never sees the account: it is asked for only when the form is filled.
17
+
18
+ (() => {
19
+ if (window.casdoorFormFillLoaded) {
20
+ return;
21
+ }
22
+ window.casdoorFormFillLoaded = true;
23
+
24
+ const isZh = (navigator.language || "").toLowerCase().startsWith("zh");
25
+ const strings = isZh
26
+ ? {
27
+ filled: "Casdoor 已填入 {app} 的账号 {user}",
28
+ switchAccount: "换成",
29
+ retry: "上次自动登录后又回到了登录页,这次没有自动提交,请检查账号是否正确",
30
+ failed: "Casdoor 代填失败:{error}",
31
+ askSave: "把 {app} 的账号 {user} 保存到 Casdoor,下次自动登录?",
32
+ save: "保存",
33
+ skip: "不用了",
34
+ saved: "已保存到 Casdoor",
35
+ close: "关闭",
36
+ }
37
+ : {
38
+ filled: "Casdoor filled in the account {user} of {app}",
39
+ switchAccount: "Use",
40
+ retry: "The last automatic sign-in came back to the sign-in page, so it was not submitted this time. Please check the account",
41
+ failed: "Casdoor could not fill the form: {error}",
42
+ askSave: "Save the account {user} of {app} to Casdoor and sign in automatically next time?",
43
+ save: "Save",
44
+ skip: "No thanks",
45
+ saved: "Saved to Casdoor",
46
+ close: "Close",
47
+ };
48
+
49
+ function format(text, values) {
50
+ return text.replace(/\{(\w+)\}/g, (_, key) => values[key] ?? "");
51
+ }
52
+
53
+ function send(message) {
54
+ return new Promise((resolve) => {
55
+ try {
56
+ chrome.runtime.sendMessage(message, (response) => {
57
+ resolve(chrome.runtime.lastError ? null : response);
58
+ });
59
+ } catch (error) {
60
+ // the extension was reloaded and this script is orphaned
61
+ resolve(null);
86
62
  }
63
+ });
64
+ }
65
+
66
+ function sleep(ms) {
67
+ return new Promise((resolve) => setTimeout(resolve, ms));
68
+ }
69
+
70
+ function isVisible(element) {
71
+ if (!element || element.disabled || element.readOnly) {
72
+ return false;
73
+ }
74
+ const style = window.getComputedStyle(element);
75
+ if (style.visibility === "hidden" || style.display === "none") {
76
+ return false;
77
+ }
78
+ return element.getClientRects().length > 0;
79
+ }
80
+
81
+ function query(selector) {
82
+ if (!selector) {
83
+ return null;
84
+ }
85
+ try {
86
+ return document.querySelector(selector);
87
+ } catch (error) {
88
+ return null;
87
89
  }
88
- );
89
- }
90
+ }
91
+
92
+ const usernameHints = /user|login|account|email|mail|phone|mobile|tel|name|uid|uname|loginid|工号|账号|帳號|用户|用戶|手机|邮箱/i;
93
+ const submitHints = /^\s*(登\s*录|登\s*入|登\s*陆|log\s*in|sign\s*in|login|signin|submit|确\s*定|提\s*交|进入系统)\s*$/i;
90
94
 
91
- window.onload = () => {
92
- chrome.storage.sync.get("accessToken", ({accessToken}) => {
93
- if (!accessToken) {
94
- autoFillCasdoorConfig();
95
+ function findPassword(site) {
96
+ const configured = query(site.passwordSelector);
97
+ if (configured) {
98
+ return configured;
95
99
  }
96
- });
97
- };
100
+ return [...document.querySelectorAll("input[type=password]")].find(isVisible) || null;
101
+ }
98
102
 
99
- chrome.runtime.onMessage.addListener((message) => {
100
- if (message.action === "autoLogin") {
101
- const managedAccount = message.managedAccount;
102
- autoLogin(managedAccount.username, managedAccount.password);
103
+ function isTextInput(input) {
104
+ const type = (input.getAttribute("type") || "text").toLowerCase();
105
+ return ["text", "email", "tel", "number", ""].includes(type);
103
106
  }
104
- });
105
107
 
106
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
107
- if (message.action === "isUserLoggedIn") {
108
- chrome.storage.sync.get("accessToken", ({accessToken}) => {
109
- if(accessToken){
110
- sendResponse(true);
111
- } else{
112
- sendResponse(false);
108
+ function findUsername(site, password) {
109
+ const configured = query(site.usernameSelector);
110
+ if (configured) {
111
+ return configured;
112
+ }
113
+ const scope = (password && password.form) || document;
114
+ const inputs = [...scope.querySelectorAll("input")].filter((input) => isTextInput(input) && isVisible(input));
115
+ const before = password
116
+ ? inputs.filter((input) => input.compareDocumentPosition(password) & Node.DOCUMENT_POSITION_FOLLOWING)
117
+ : inputs;
118
+ const candidates = before.length > 0 ? before : inputs;
119
+ const hinted = candidates.filter((input) => usernameHints.test(`${input.name} ${input.id} ${input.placeholder} ${input.getAttribute("autocomplete") || ""} ${input.getAttribute("aria-label") || ""}`));
120
+ const list = hinted.length > 0 ? hinted : candidates;
121
+ return list.length > 0 ? list[list.length - 1] : null;
122
+ }
123
+
124
+ function findSubmit(site, password) {
125
+ const configured = query(site.submitSelector);
126
+ if (configured) {
127
+ return configured;
128
+ }
129
+ const form = password && password.form;
130
+ if (form) {
131
+ const button = [...form.querySelectorAll("button, input[type=submit], input[type=image]")].find((element) => {
132
+ const type = (element.getAttribute("type") || "submit").toLowerCase();
133
+ return isVisible(element) && (type === "submit" || type === "image");
134
+ });
135
+ if (button) {
136
+ return button;
113
137
  }
114
- });
115
- return true;
138
+ }
139
+ const clickable = [...document.querySelectorAll("button, input[type=button], input[type=submit], a, [role=button], div[onclick], span[onclick]")];
140
+ return clickable.find((element) => isVisible(element) && submitHints.test(element.value || element.textContent || "")) || null;
116
141
  }
117
- });
118
142
 
119
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
120
- if (message.action === "isDisableAutoLogin") {
121
- chrome.storage.sync.get("disableAutoLogin", ({ disableAutoLogin }) => {
122
- sendResponse(disableAutoLogin);
123
- });
124
- return true;
143
+ function setValue(input, value) {
144
+ input.focus();
145
+ const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set;
146
+ setter.call(input, value);
147
+ input.dispatchEvent(new Event("input", {bubbles: true}));
148
+ input.dispatchEvent(new Event("change", {bubbles: true}));
149
+ input.dispatchEvent(new KeyboardEvent("keyup", {bubbles: true}));
150
+ input.blur();
151
+ }
152
+
153
+ function pressEnter(input) {
154
+ for (const type of ["keydown", "keypress", "keyup"]) {
155
+ input.dispatchEvent(new KeyboardEvent(type, {key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true}));
156
+ }
157
+ if (input.form) {
158
+ input.form.requestSubmit();
159
+ }
160
+ }
161
+
162
+ async function waitForPassword(site, timeout) {
163
+ const start = Date.now();
164
+ while (Date.now() - start < timeout) {
165
+ const password = findPassword(site);
166
+ if (password && isVisible(password)) {
167
+ return password;
168
+ }
169
+ await sleep(300);
170
+ }
171
+ return null;
125
172
  }
126
- });
127
173
 
128
- chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
129
- if (message.action === "getManagedAccounts") {
130
- chrome.storage.sync.get("accessToken", ({accessToken}) => {
131
- sdk.getAccount(accessToken).then((account) => {
132
- const managedAccounts = account.data.managedAccounts;
133
- sendResponse(managedAccounts);
174
+ // the notices live in a shadow root so that the page styles leave them alone
175
+ let noticeHost = null;
176
+
177
+ function showNotice(text, buttons = [], timeout = 8000) {
178
+ if (noticeHost) {
179
+ noticeHost.remove();
180
+ }
181
+ noticeHost = document.createElement("div");
182
+ noticeHost.style.cssText = "position:fixed;top:16px;right:16px;z-index:2147483647;";
183
+ const root = noticeHost.attachShadow({mode: "open"});
184
+ const box = document.createElement("div");
185
+ box.style.cssText = "max-width:360px;padding:12px 14px;border-radius:10px;background:#fff;color:#1f2328;box-shadow:0 6px 24px rgba(0,0,0,.18);font:14px/1.5 -apple-system,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;border:1px solid #e5e7eb;";
186
+ const title = document.createElement("div");
187
+ title.style.cssText = "font-weight:600;font-size:12px;color:#5c6ac4;margin-bottom:4px;";
188
+ title.textContent = "Casdoor";
189
+ const body = document.createElement("div");
190
+ body.textContent = text;
191
+ box.append(title, body);
192
+ if (buttons.length > 0) {
193
+ const row = document.createElement("div");
194
+ row.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;";
195
+ buttons.forEach((item, index) => {
196
+ const button = document.createElement("button");
197
+ button.textContent = item.label;
198
+ button.style.cssText = index === 0
199
+ ? "border:0;border-radius:6px;padding:4px 12px;background:#5c6ac4;color:#fff;cursor:pointer;font:inherit;"
200
+ : "border:1px solid #d0d7de;border-radius:6px;padding:4px 12px;background:#fff;color:#1f2328;cursor:pointer;font:inherit;";
201
+ button.addEventListener("click", () => {
202
+ noticeHost.remove();
203
+ noticeHost = null;
204
+ item.onClick();
205
+ });
206
+ row.append(button);
134
207
  });
135
- });
136
- return true;
208
+ box.append(row);
209
+ }
210
+ root.append(box);
211
+ document.documentElement.append(noticeHost);
212
+ if (timeout > 0) {
213
+ const host = noticeHost;
214
+ setTimeout(() => host.remove(), timeout);
215
+ }
216
+ }
217
+
218
+ async function fill(info, username) {
219
+ const site = info.site;
220
+ // a page of the system without a sign-in form, e.g. when the URLs end with "*"
221
+ const password = await waitForPassword(site, 15000);
222
+ if (!password) {
223
+ return;
224
+ }
225
+ const usernameInput = findUsername(site, password);
226
+ const account = await send({type: "getAccount", application: site.application, username, autoSubmit: site.autoSubmit});
227
+ if (!account || account.error) {
228
+ showNotice(format(strings.failed, {error: (account && account.error) || ""}));
229
+ return;
230
+ }
231
+ if (usernameInput) {
232
+ setValue(usernameInput, account.username);
233
+ }
234
+ setValue(password, account.password);
235
+
236
+ const others = info.accounts.filter((item) => item !== account.username);
237
+ const buttons = others.slice(0, 3).map((item) => ({label: `${strings.switchAccount} ${item}`, onClick: () => fill(info, item)}));
238
+ if (info.recentlySubmitted && info.accounts.length > 0) {
239
+ showNotice(strings.retry, buttons, 0);
240
+ return;
241
+ }
242
+ showNotice(format(strings.filled, {app: site.displayName, user: account.username}), buttons);
243
+
244
+ if (site.autoSubmit && !username) {
245
+ await sleep(400);
246
+ const submit = findSubmit(site, password);
247
+ if (submit) {
248
+ submit.click();
249
+ } else {
250
+ pressEnter(password);
251
+ }
252
+ }
253
+ }
254
+
255
+ // remembers the account typed into the sign-in form, to offer saving it once the user is signed in
256
+ function watchCapture(site) {
257
+ if (window.casdoorCaptureSite === site.application) {
258
+ return;
259
+ }
260
+ window.casdoorCaptureSite = site.application;
261
+ const grab = () => {
262
+ const password = findPassword(site);
263
+ if (!password || !password.value) {
264
+ return;
265
+ }
266
+ const usernameInput = findUsername(site, password);
267
+ const username = usernameInput ? usernameInput.value.trim() : "";
268
+ if (!username) {
269
+ return;
270
+ }
271
+ send({type: "capture", application: site.application, username, password: password.value});
272
+ // a single-page app signs in without leaving the page
273
+ setTimeout(checkPending, 3000);
274
+ };
275
+ document.addEventListener("submit", grab, true);
276
+ document.addEventListener("click", (event) => {
277
+ const target = event.target instanceof Element ? event.target.closest("button, input[type=submit], input[type=button], a, [role=button]") : null;
278
+ if (target) {
279
+ grab();
280
+ }
281
+ }, true);
282
+ document.addEventListener("keydown", (event) => {
283
+ if (event.key === "Enter" && event.target instanceof HTMLInputElement) {
284
+ grab();
285
+ }
286
+ }, true);
287
+ }
288
+
289
+ let pendingShown = false;
290
+
291
+ async function checkPending() {
292
+ if (pendingShown) {
293
+ return;
294
+ }
295
+ const pending = await send({type: "getPending"});
296
+ if (!pending) {
297
+ return;
298
+ }
299
+ // still on a sign-in form: the password was wrong, or the page has not moved on yet
300
+ const password = [...document.querySelectorAll("input[type=password]")].find(isVisible);
301
+ if (password) {
302
+ return;
303
+ }
304
+ pendingShown = true;
305
+ showNotice(format(strings.askSave, {app: pending.displayName, user: pending.username}), [
306
+ {
307
+ label: strings.save,
308
+ onClick: async() => {
309
+ const res = await send({type: "resolvePending", save: true});
310
+ showNotice(res && res.saved ? strings.saved : format(strings.failed, {error: (res && res.error) || ""}));
311
+ },
312
+ },
313
+ {label: strings.skip, onClick: () => send({type: "resolvePending", save: false})},
314
+ ], 0);
137
315
  }
138
- });
316
+
317
+ let checkedUrl = null;
318
+
319
+ async function check() {
320
+ if (checkedUrl === location.href) {
321
+ return;
322
+ }
323
+ checkedUrl = location.href;
324
+ checkPending();
325
+ const info = await send({type: "getFill"});
326
+ if (!info || !info.site) {
327
+ return;
328
+ }
329
+ if (info.accounts.length > 0) {
330
+ fill(info);
331
+ } else if (info.canSave) {
332
+ watchCapture(info.site);
333
+ }
334
+ }
335
+
336
+ check();
337
+ // single-page apps change the address without loading a page
338
+ setInterval(check, 1000);
339
+ })();
package/src/match.js ADDED
@@ -0,0 +1,92 @@
1
+ // Copyright 2026 The Casdoor Authors. All Rights Reserved.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ // The sign-in page patterns of form autofill, matched the same way as Casdoor matches them (object/form_fill.go):
16
+ // the host has to be the same, "*.example.com" covers the subdomains, and a path without "*" is a prefix.
17
+
18
+ function splitFormFillUrl(value) {
19
+ value = (value || "").trim();
20
+ let scheme = "";
21
+ const schemeMatch = value.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//);
22
+ if (schemeMatch) {
23
+ scheme = schemeMatch[0].slice(0, -3).toLowerCase();
24
+ value = value.slice(schemeMatch[0].length);
25
+ }
26
+ let host = value;
27
+ let rest = "/";
28
+ const index = value.search(/[/?#]/);
29
+ if (index >= 0) {
30
+ host = value.slice(0, index);
31
+ rest = value.slice(index);
32
+ if (!rest.startsWith("/")) {
33
+ rest = "/" + rest;
34
+ }
35
+ }
36
+ return [scheme, host.toLowerCase(), rest];
37
+ }
38
+
39
+ function matchFormFillHost(pattern, host) {
40
+ if (pattern === host) {
41
+ return true;
42
+ }
43
+ if (pattern.startsWith("*.")) {
44
+ return host.endsWith(pattern.slice(1));
45
+ }
46
+ return false;
47
+ }
48
+
49
+ function escapeRegex(value) {
50
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
51
+ }
52
+
53
+ function matchFormFillUrl(pattern, pageUrl) {
54
+ pattern = (pattern || "").trim();
55
+ if (!pattern) {
56
+ return false;
57
+ }
58
+ const [patternScheme, patternHost, patternPath] = splitFormFillUrl(pattern);
59
+ const [pageScheme, pageHost, fullPagePath] = splitFormFillUrl(pageUrl);
60
+ if (pageScheme !== "http" && pageScheme !== "https") {
61
+ return false;
62
+ }
63
+ if (patternScheme && patternScheme !== pageScheme) {
64
+ return false;
65
+ }
66
+ if (!matchFormFillHost(patternHost, pageHost)) {
67
+ return false;
68
+ }
69
+ const pagePath = fullPagePath.split("#")[0];
70
+ if (!patternPath.includes("*")) {
71
+ return pagePath.startsWith(patternPath);
72
+ }
73
+ const expression = "^" + escapeRegex(patternPath).replace(/\\\*/g, ".*") + "$";
74
+ return new RegExp(expression).test(pagePath);
75
+ }
76
+
77
+ function matchFormFillSite(site, pageUrl) {
78
+ return (site.urls || []).some((pattern) => matchFormFillUrl(pattern, pageUrl));
79
+ }
80
+
81
+ // the sign-in URL of a managed account before form autofill, a regular expression
82
+ function matchLegacySigninUrl(pattern, pageUrl) {
83
+ try {
84
+ return new RegExp(pattern).test(pageUrl);
85
+ } catch (error) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ if (typeof module !== "undefined") {
91
+ module.exports = {matchFormFillUrl, matchFormFillSite, matchLegacySigninUrl};
92
+ }