@falkenslab/moodle-driver 0.0.0-stage → 0.3.1
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.
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/dist/areas/activities/build.d.ts +98 -0
- package/dist/areas/activities/build.js +246 -0
- package/dist/areas/activities/build.js.map +1 -0
- package/dist/areas/activities/content.d.ts +53 -0
- package/dist/areas/activities/content.js +113 -0
- package/dist/areas/activities/content.js.map +1 -0
- package/dist/areas/assignments/grader.d.ts +68 -0
- package/dist/areas/assignments/grader.js +169 -0
- package/dist/areas/assignments/grader.js.map +1 -0
- package/dist/areas/assignments/grading.d.ts +86 -0
- package/dist/areas/assignments/grading.js +162 -0
- package/dist/areas/assignments/grading.js.map +1 -0
- package/dist/areas/assignments/student.d.ts +57 -0
- package/dist/areas/assignments/student.js +115 -0
- package/dist/areas/assignments/student.js.map +1 -0
- package/dist/areas/calendar/calendar.d.ts +43 -0
- package/dist/areas/calendar/calendar.js +84 -0
- package/dist/areas/calendar/calendar.js.map +1 -0
- package/dist/areas/completion/completion.d.ts +50 -0
- package/dist/areas/completion/completion.js +112 -0
- package/dist/areas/completion/completion.js.map +1 -0
- package/dist/areas/courses/list.d.ts +59 -0
- package/dist/areas/courses/list.js +74 -0
- package/dist/areas/courses/list.js.map +1 -0
- package/dist/areas/courses/overview.d.ts +83 -0
- package/dist/areas/courses/overview.js +137 -0
- package/dist/areas/courses/overview.js.map +1 -0
- package/dist/areas/courses/state.d.ts +51 -0
- package/dist/areas/courses/state.js +45 -0
- package/dist/areas/courses/state.js.map +1 -0
- package/dist/areas/courses/structure.d.ts +89 -0
- package/dist/areas/courses/structure.js +104 -0
- package/dist/areas/courses/structure.js.map +1 -0
- package/dist/areas/forums/discussions.d.ts +57 -0
- package/dist/areas/forums/discussions.js +119 -0
- package/dist/areas/forums/discussions.js.map +1 -0
- package/dist/areas/forums/posts.d.ts +83 -0
- package/dist/areas/forums/posts.js +114 -0
- package/dist/areas/forums/posts.js.map +1 -0
- package/dist/areas/grades/gradebook.d.ts +104 -0
- package/dist/areas/grades/gradebook.js +250 -0
- package/dist/areas/grades/gradebook.js.map +1 -0
- package/dist/areas/grades/user-report.d.ts +26 -0
- package/dist/areas/grades/user-report.js +71 -0
- package/dist/areas/grades/user-report.js.map +1 -0
- package/dist/areas/h5p/h5p.d.ts +33 -0
- package/dist/areas/h5p/h5p.js +104 -0
- package/dist/areas/h5p/h5p.js.map +1 -0
- package/dist/areas/lessons/lessons.d.ts +60 -0
- package/dist/areas/lessons/lessons.js +145 -0
- package/dist/areas/lessons/lessons.js.map +1 -0
- package/dist/areas/messages/messages.d.ts +68 -0
- package/dist/areas/messages/messages.js +84 -0
- package/dist/areas/messages/messages.js.map +1 -0
- package/dist/areas/participants/participants.d.ts +57 -0
- package/dist/areas/participants/participants.js +135 -0
- package/dist/areas/participants/participants.js.map +1 -0
- package/dist/areas/questions/banks.d.ts +83 -0
- package/dist/areas/questions/banks.js +239 -0
- package/dist/areas/questions/banks.js.map +1 -0
- package/dist/areas/quizzes/attempts.d.ts +123 -0
- package/dist/areas/quizzes/attempts.js +300 -0
- package/dist/areas/quizzes/attempts.js.map +1 -0
- package/dist/areas/quizzes/results.d.ts +36 -0
- package/dist/areas/quizzes/results.js +93 -0
- package/dist/areas/quizzes/results.js.map +1 -0
- package/dist/areas/quizzes/structure.d.ts +42 -0
- package/dist/areas/quizzes/structure.js +100 -0
- package/dist/areas/quizzes/structure.js.map +1 -0
- package/dist/areas/site/info.d.ts +25 -0
- package/dist/areas/site/info.js +45 -0
- package/dist/areas/site/info.js.map +1 -0
- package/dist/areas/site/users.d.ts +56 -0
- package/dist/areas/site/users.js +89 -0
- package/dist/areas/site/users.js.map +1 -0
- package/dist/areas/surveys/choice.d.ts +38 -0
- package/dist/areas/surveys/choice.js +105 -0
- package/dist/areas/surveys/choice.js.map +1 -0
- package/dist/areas/surveys/surveys.d.ts +51 -0
- package/dist/areas/surveys/surveys.js +201 -0
- package/dist/areas/surveys/surveys.js.map +1 -0
- package/dist/browser/chromium.d.ts +15 -0
- package/dist/browser/chromium.js +53 -0
- package/dist/browser/chromium.js.map +1 -0
- package/dist/browser/cookies.d.ts +11 -0
- package/dist/browser/cookies.js +31 -0
- package/dist/browser/cookies.js.map +1 -0
- package/dist/browser/manager.d.ts +57 -0
- package/dist/browser/manager.js +177 -0
- package/dist/browser/manager.js.map +1 -0
- package/dist/browser.d.ts +53 -0
- package/dist/browser.js +167 -0
- package/dist/browser.js.map +1 -0
- package/dist/categories.d.ts +28 -0
- package/dist/categories.js +222 -0
- package/dist/categories.js.map +1 -0
- package/dist/chromiumInstaller.d.ts +15 -0
- package/dist/chromiumInstaller.js +52 -0
- package/dist/chromiumInstaller.js.map +1 -0
- package/dist/cli/args.d.ts +33 -0
- package/dist/cli/args.js +194 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/commands.generated.d.ts +4 -0
- package/dist/cli/commands.generated.js +2203 -0
- package/dist/cli/commands.generated.js.map +1 -0
- package/dist/cli/config.d.ts +46 -0
- package/dist/cli/config.js +107 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +71 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/run.d.ts +15 -0
- package/dist/cli/run.js +194 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/spec.d.ts +42 -0
- package/dist/cli/spec.js +4 -0
- package/dist/cli/spec.js.map +1 -0
- package/dist/client.d.ts +217 -0
- package/dist/client.js +179 -0
- package/dist/client.js.map +1 -0
- package/dist/core/ajax.d.ts +25 -0
- package/dist/core/ajax.js +85 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/editor.d.ts +18 -0
- package/dist/core/editor.js +51 -0
- package/dist/core/editor.js.map +1 -0
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/files.d.ts +16 -0
- package/dist/core/files.js +58 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/forms.d.ts +45 -0
- package/dist/core/forms.js +200 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/http.d.ts +18 -0
- package/dist/core/http.js +22 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/inPage.d.ts +8 -0
- package/dist/core/inPage.js +10 -0
- package/dist/core/inPage.js.map +1 -0
- package/dist/core/login.d.ts +40 -0
- package/dist/core/login.js +83 -0
- package/dist/core/login.js.map +1 -0
- package/dist/core/pace.d.ts +4 -0
- package/dist/core/pace.js +21 -0
- package/dist/core/pace.js.map +1 -0
- package/dist/core/session.d.ts +55 -0
- package/dist/core/session.js +157 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/site.d.ts +8 -0
- package/dist/core/site.js +36 -0
- package/dist/core/site.js.map +1 -0
- package/dist/core/text.d.ts +21 -0
- package/dist/core/text.js +113 -0
- package/dist/core/text.js.map +1 -0
- package/dist/home.d.ts +19 -0
- package/dist/home.js +54 -0
- package/dist/home.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +11 -0
- package/dist/internals.js +15 -0
- package/dist/internals.js.map +1 -0
- package/dist/moodle/activities.d.ts +94 -0
- package/dist/moodle/activities.js +240 -0
- package/dist/moodle/activities.js.map +1 -0
- package/dist/moodle/ajax.d.ts +22 -0
- package/dist/moodle/ajax.js +91 -0
- package/dist/moodle/ajax.js.map +1 -0
- package/dist/moodle/assign.d.ts +65 -0
- package/dist/moodle/assign.js +162 -0
- package/dist/moodle/assign.js.map +1 -0
- package/dist/moodle/calendar.d.ts +41 -0
- package/dist/moodle/calendar.js +80 -0
- package/dist/moodle/calendar.js.map +1 -0
- package/dist/moodle/choice.d.ts +33 -0
- package/dist/moodle/choice.js +100 -0
- package/dist/moodle/choice.js.map +1 -0
- package/dist/moodle/completion.d.ts +48 -0
- package/dist/moodle/completion.js +108 -0
- package/dist/moodle/completion.js.map +1 -0
- package/dist/moodle/content.d.ts +43 -0
- package/dist/moodle/content.js +105 -0
- package/dist/moodle/content.js.map +1 -0
- package/dist/moodle/course.d.ts +51 -0
- package/dist/moodle/course.js +45 -0
- package/dist/moodle/course.js.map +1 -0
- package/dist/moodle/courses.d.ts +57 -0
- package/dist/moodle/courses.js +70 -0
- package/dist/moodle/courses.js.map +1 -0
- package/dist/moodle/editor.d.ts +18 -0
- package/dist/moodle/editor.js +50 -0
- package/dist/moodle/editor.js.map +1 -0
- package/dist/moodle/files.d.ts +16 -0
- package/dist/moodle/files.js +57 -0
- package/dist/moodle/files.js.map +1 -0
- package/dist/moodle/forms.d.ts +44 -0
- package/dist/moodle/forms.js +199 -0
- package/dist/moodle/forms.js.map +1 -0
- package/dist/moodle/forum.d.ts +55 -0
- package/dist/moodle/forum.js +115 -0
- package/dist/moodle/forum.js.map +1 -0
- package/dist/moodle/forumPosts.d.ts +83 -0
- package/dist/moodle/forumPosts.js +113 -0
- package/dist/moodle/forumPosts.js.map +1 -0
- package/dist/moodle/gradebook.d.ts +102 -0
- package/dist/moodle/gradebook.js +246 -0
- package/dist/moodle/gradebook.js.map +1 -0
- package/dist/moodle/grades.d.ts +24 -0
- package/dist/moodle/grades.js +67 -0
- package/dist/moodle/grades.js.map +1 -0
- package/dist/moodle/grading.d.ts +92 -0
- package/dist/moodle/grading.js +161 -0
- package/dist/moodle/grading.js.map +1 -0
- package/dist/moodle/h5p.d.ts +31 -0
- package/dist/moodle/h5p.js +100 -0
- package/dist/moodle/h5p.js.map +1 -0
- package/dist/moodle/inPage.d.ts +8 -0
- package/dist/moodle/inPage.js +10 -0
- package/dist/moodle/inPage.js.map +1 -0
- package/dist/moodle/lesson.d.ts +57 -0
- package/dist/moodle/lesson.js +140 -0
- package/dist/moodle/lesson.js.map +1 -0
- package/dist/moodle/login.d.ts +37 -0
- package/dist/moodle/login.js +77 -0
- package/dist/moodle/login.js.map +1 -0
- package/dist/moodle/messages.d.ts +66 -0
- package/dist/moodle/messages.js +80 -0
- package/dist/moodle/messages.js.map +1 -0
- package/dist/moodle/overview.d.ts +83 -0
- package/dist/moodle/overview.js +137 -0
- package/dist/moodle/overview.js.map +1 -0
- package/dist/moodle/pace.d.ts +4 -0
- package/dist/moodle/pace.js +19 -0
- package/dist/moodle/pace.js.map +1 -0
- package/dist/moodle/participants.d.ts +55 -0
- package/dist/moodle/participants.js +131 -0
- package/dist/moodle/participants.js.map +1 -0
- package/dist/moodle/questionBank.d.ts +81 -0
- package/dist/moodle/questionBank.js +235 -0
- package/dist/moodle/questionBank.js.map +1 -0
- package/dist/moodle/quiz.d.ts +117 -0
- package/dist/moodle/quiz.js +291 -0
- package/dist/moodle/quiz.js.map +1 -0
- package/dist/moodle/quizResults.d.ts +36 -0
- package/dist/moodle/quizResults.js +93 -0
- package/dist/moodle/quizResults.js.map +1 -0
- package/dist/moodle/quizStructure.d.ts +42 -0
- package/dist/moodle/quizStructure.js +100 -0
- package/dist/moodle/quizStructure.js.map +1 -0
- package/dist/moodle/session.d.ts +39 -0
- package/dist/moodle/session.js +99 -0
- package/dist/moodle/session.js.map +1 -0
- package/dist/moodle/site.d.ts +8 -0
- package/dist/moodle/site.js +36 -0
- package/dist/moodle/site.js.map +1 -0
- package/dist/moodle/siteInfo.d.ts +25 -0
- package/dist/moodle/siteInfo.js +45 -0
- package/dist/moodle/siteInfo.js.map +1 -0
- package/dist/moodle/structure.d.ts +94 -0
- package/dist/moodle/structure.js +103 -0
- package/dist/moodle/structure.js.map +1 -0
- package/dist/moodle/submission.d.ts +48 -0
- package/dist/moodle/submission.js +109 -0
- package/dist/moodle/submission.js.map +1 -0
- package/dist/moodle/survey.d.ts +47 -0
- package/dist/moodle/survey.js +197 -0
- package/dist/moodle/survey.js.map +1 -0
- package/dist/moodle/text.d.ts +21 -0
- package/dist/moodle/text.js +113 -0
- package/dist/moodle/text.js.map +1 -0
- package/dist/moodle/user.d.ts +54 -0
- package/dist/moodle/user.js +78 -0
- package/dist/moodle/user.js.map +1 -0
- package/dist/options.d.ts +30 -0
- package/dist/options.js +35 -0
- package/dist/options.js.map +1 -0
- package/dist/sessionCookies.d.ts +11 -0
- package/dist/sessionCookies.js +31 -0
- package/dist/sessionCookies.js.map +1 -0
- package/dist/testing/fakeMoodle.d.ts +55 -0
- package/dist/testing/fakeMoodle.js +103 -0
- package/dist/testing/fakeMoodle.js.map +1 -0
- package/dist/testing/gradingPanel.d.ts +6 -0
- package/dist/testing/gradingPanel.js +22 -0
- package/dist/testing/gradingPanel.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +18 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +77 -4
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
import type { Trail } from "../options.js";
|
|
3
|
+
import { type MediaItem } from "./text.js";
|
|
4
|
+
export declare const CONTENT_TYPES: readonly ["page", "book", "label", "url", "resource", "folder"];
|
|
5
|
+
export type ContentType = (typeof CONTENT_TYPES)[number];
|
|
6
|
+
/** The activity isn't of the given type, or the person can't view it. */
|
|
7
|
+
export declare class NotViewableError extends Error {
|
|
8
|
+
name: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ActivityContent {
|
|
11
|
+
activity: {
|
|
12
|
+
id: number;
|
|
13
|
+
type: ContentType;
|
|
14
|
+
};
|
|
15
|
+
/** Page and label content, as plain text with paragraphs and lists kept. */
|
|
16
|
+
text?: string;
|
|
17
|
+
/** A book's chapters, in order. */
|
|
18
|
+
chapters?: {
|
|
19
|
+
title: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Videos, audio and players in a page's, label's or book's content. */
|
|
23
|
+
media?: MediaItem[];
|
|
24
|
+
/** A URL resource's target. */
|
|
25
|
+
url?: string;
|
|
26
|
+
/** A file resource's or a folder's files. */
|
|
27
|
+
files?: {
|
|
28
|
+
name: string;
|
|
29
|
+
url: string;
|
|
30
|
+
}[];
|
|
31
|
+
trail: Trail;
|
|
32
|
+
/** With the realistic trail, the time spent reading. */
|
|
33
|
+
readingSeconds?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads a page's or label's text, a book's chapters, a URL resource's target, or a file resource's
|
|
37
|
+
* or folder's files. With the realistic trail, page by page at a reader's pace, calling `onProgress`
|
|
38
|
+
* with the seconds read so far while it pauses.
|
|
39
|
+
*/
|
|
40
|
+
export declare function activityContent(context: BrowserContext, siteUrl: string, cmid: number, type: ContentType, { trail, onProgress }?: {
|
|
41
|
+
trail?: Trail;
|
|
42
|
+
onProgress?: (secondsRead: number) => void;
|
|
43
|
+
}): Promise<ActivityContent>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { pause, readingSeconds } from "./pace.js";
|
|
2
|
+
import { openSitePage } from "./session.js";
|
|
3
|
+
import { readMedia, readTexts } from "./text.js";
|
|
4
|
+
export const CONTENT_TYPES = ["page", "book", "label", "url", "resource", "folder"];
|
|
5
|
+
// A book has a reasonable number of chapters; the bound stops a loop on a broken "next" link.
|
|
6
|
+
const MAX_CHAPTERS = 200;
|
|
7
|
+
/** The activity isn't of the given type, or the person can't view it. */
|
|
8
|
+
export class NotViewableError extends Error {
|
|
9
|
+
name = "NotViewableError";
|
|
10
|
+
}
|
|
11
|
+
async function links(page, selector) {
|
|
12
|
+
return page.evaluate((selector) => {
|
|
13
|
+
const anchors = globalThis.document.querySelectorAll(selector);
|
|
14
|
+
return Array.from(anchors).map((a) => ({ name: (a.textContent ?? "").replace(/\s+/g, " ").trim(), url: a.href }));
|
|
15
|
+
}, selector);
|
|
16
|
+
}
|
|
17
|
+
/** A book chapter's title and text, the title taken off the text's start. */
|
|
18
|
+
function chapter(title, body) {
|
|
19
|
+
return { title, text: body.startsWith(title) ? body.slice(title.length).trim() : body };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Reads a page's or label's text, a book's chapters, a URL resource's target, or a file resource's
|
|
23
|
+
* or folder's files. With the realistic trail, page by page at a reader's pace, calling `onProgress`
|
|
24
|
+
* with the seconds read so far while it pauses.
|
|
25
|
+
*/
|
|
26
|
+
export async function activityContent(context, siteUrl, cmid, type, { trail = "realistic", onProgress } = {}) {
|
|
27
|
+
const realistic = trail === "realistic";
|
|
28
|
+
const output = { activity: { id: cmid, type }, trail };
|
|
29
|
+
let secondsRead = 0;
|
|
30
|
+
const readAsAPerson = async (text) => {
|
|
31
|
+
if (!realistic)
|
|
32
|
+
return;
|
|
33
|
+
const seconds = readingSeconds(text);
|
|
34
|
+
await pause(seconds, (elapsed) => onProgress?.(secondsRead + elapsed));
|
|
35
|
+
secondsRead += seconds;
|
|
36
|
+
};
|
|
37
|
+
// Opens the activity's page and checks it shows what this type should.
|
|
38
|
+
const view = async (path, ready) => {
|
|
39
|
+
const page = await openSitePage(context, siteUrl, path);
|
|
40
|
+
if ((await page.locator(ready).count()) === 0) {
|
|
41
|
+
await page.close().catch(() => undefined);
|
|
42
|
+
throw new NotViewableError(`Activity ${cmid} isn't a ${type} you can view. Take its id and module from the course structure.`);
|
|
43
|
+
}
|
|
44
|
+
return page;
|
|
45
|
+
};
|
|
46
|
+
if (type === "page" || type === "label") {
|
|
47
|
+
const path = type === "page" ? `mod/page/view.php?id=${cmid}` : `mod/label/view.php?id=${cmid}`;
|
|
48
|
+
const selector = type === "page" ? "#region-main .generalbox" : `[data-for="cmitem"][data-id="${cmid}"] .activity-altcontent`;
|
|
49
|
+
const page = await view(path, selector);
|
|
50
|
+
output.text = (await readTexts(page, selector))[0] ?? "";
|
|
51
|
+
output.media = await readMedia(page, selector);
|
|
52
|
+
await page.close();
|
|
53
|
+
await readAsAPerson(output.text);
|
|
54
|
+
}
|
|
55
|
+
else if (type === "book") {
|
|
56
|
+
output.chapters = [];
|
|
57
|
+
const media = [];
|
|
58
|
+
if (realistic) {
|
|
59
|
+
// Chapter by chapter, following "next", as a reader does.
|
|
60
|
+
const page = await view(`mod/book/view.php?id=${cmid}`, "#region-main .book_content");
|
|
61
|
+
try {
|
|
62
|
+
for (let i = 0; i < MAX_CHAPTERS; i++) {
|
|
63
|
+
const title = (await readTexts(page, "#region-main .book_content h3, #region-main .book_content h2"))[0] ?? "";
|
|
64
|
+
const read = chapter(title, (await readTexts(page, "#region-main .book_content"))[0] ?? "");
|
|
65
|
+
output.chapters.push(read);
|
|
66
|
+
media.push(...(await readMedia(page, "#region-main .book_content")));
|
|
67
|
+
await readAsAPerson(read.text);
|
|
68
|
+
const next = page.locator("#region-main a.btn-next").first();
|
|
69
|
+
if ((await next.count()) === 0)
|
|
70
|
+
break;
|
|
71
|
+
await page.goto((await next.getAttribute("href")), { waitUntil: "domcontentloaded" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await page.close().catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const page = await view(`mod/book/tool/print/index.php?id=${cmid}`, ".book_chapter");
|
|
80
|
+
const titles = await readTexts(page, ".book_chapter h2, .book_chapter h3");
|
|
81
|
+
const bodies = await readTexts(page, ".book_chapter");
|
|
82
|
+
output.chapters = bodies.map((body, i) => chapter(titles[i] ?? "", body));
|
|
83
|
+
media.push(...(await readMedia(page, ".book_chapter")));
|
|
84
|
+
await page.close();
|
|
85
|
+
}
|
|
86
|
+
// The same video can appear in several chapters: once each.
|
|
87
|
+
output.media = [...new Map(media.map((item) => [item.url, item])).values()];
|
|
88
|
+
}
|
|
89
|
+
else if (type === "url") {
|
|
90
|
+
// forceview logs the view and shows the link instead of leaving the site.
|
|
91
|
+
const page = await view(`mod/url/view.php?id=${cmid}&forceview=1`, ".urlworkaround a");
|
|
92
|
+
output.url = (await links(page, ".urlworkaround a"))[0]?.url;
|
|
93
|
+
await page.close();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const [path, selector] = type === "resource" ? [`mod/resource/view.php?id=${cmid}&forceview=1`, ".resourceworkaround a"] : [`mod/folder/view.php?id=${cmid}`, '.foldertree a[href*="pluginfile.php"]'];
|
|
97
|
+
const page = await view(path, selector);
|
|
98
|
+
output.files = await links(page, selector);
|
|
99
|
+
await page.close();
|
|
100
|
+
}
|
|
101
|
+
if (secondsRead > 0)
|
|
102
|
+
output.readingSeconds = secondsRead;
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/moodle/content.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
/** `core_courseformat_get_state` as Moodle 4.0–5.2 returns it, the fields read. */
|
|
3
|
+
export interface CourseState {
|
|
4
|
+
course?: {
|
|
5
|
+
id: string;
|
|
6
|
+
baseurl?: string;
|
|
7
|
+
};
|
|
8
|
+
section: {
|
|
9
|
+
id: string;
|
|
10
|
+
number: number;
|
|
11
|
+
title: string;
|
|
12
|
+
visible?: boolean;
|
|
13
|
+
hasrestrictions?: boolean;
|
|
14
|
+
sectionurl?: string;
|
|
15
|
+
component?: string | null;
|
|
16
|
+
parentsectionid?: string | null;
|
|
17
|
+
cmlist?: string[];
|
|
18
|
+
}[];
|
|
19
|
+
cm: {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
module: string;
|
|
23
|
+
url?: string;
|
|
24
|
+
visible: boolean;
|
|
25
|
+
uservisible?: boolean;
|
|
26
|
+
accessvisible?: boolean;
|
|
27
|
+
hascmrestrictions?: boolean;
|
|
28
|
+
sectionid: string;
|
|
29
|
+
istrackeduser?: boolean;
|
|
30
|
+
completionstate?: number;
|
|
31
|
+
isoverallcomplete?: boolean;
|
|
32
|
+
}[];
|
|
33
|
+
}
|
|
34
|
+
/** The course page's own state: sections and activities, as a JSON string (ADR-004). */
|
|
35
|
+
export declare function courseState(context: BrowserContext, siteUrl: string, courseId: number): Promise<CourseState>;
|
|
36
|
+
/** What the course page shows of an activity, in the site's language. */
|
|
37
|
+
export interface ActivityDetails {
|
|
38
|
+
id: number;
|
|
39
|
+
dates: {
|
|
40
|
+
label: string;
|
|
41
|
+
value: string;
|
|
42
|
+
}[];
|
|
43
|
+
restriction?: string;
|
|
44
|
+
}
|
|
45
|
+
/** The activities on a course or section page: their dates and restriction texts. */
|
|
46
|
+
export declare function readActivityDetails(page: Page): Promise<ActivityDetails[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Details for the given activities: from the course page, then from the section pages of those it
|
|
49
|
+
* doesn't show (courses with one section per page).
|
|
50
|
+
*/
|
|
51
|
+
export declare function activityDetails(context: BrowserContext, siteUrl: string, courseId: number, state: CourseState): Promise<Map<number, ActivityDetails>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { callService } from "./ajax.js";
|
|
2
|
+
import { openSitePage } from "./session.js";
|
|
3
|
+
/** The course page's own state: sections and activities, as a JSON string (ADR-004). */
|
|
4
|
+
export async function courseState(context, siteUrl, courseId) {
|
|
5
|
+
return JSON.parse(await callService(context, siteUrl, "core_courseformat_get_state", { courseid: courseId }));
|
|
6
|
+
}
|
|
7
|
+
/** The activities on a course or section page: their dates and restriction texts. */
|
|
8
|
+
export function readActivityDetails(page) {
|
|
9
|
+
return page.evaluate(() => {
|
|
10
|
+
const doc = globalThis.document;
|
|
11
|
+
// No helper functions in here: see the conventions on page.evaluate.
|
|
12
|
+
return Array.from(doc.querySelectorAll('[data-for="cmitem"][data-id], li.activity[data-id]')).map((item) => {
|
|
13
|
+
const dates = Array.from(item.querySelectorAll('[data-region="activity-dates"] .date-item')).map((date) => {
|
|
14
|
+
const label = (date.querySelector(".date-item-label")?.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
15
|
+
const all = (date.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
16
|
+
return { label: label.replace(/:$/, ""), value: all.slice(label.length).trim() };
|
|
17
|
+
});
|
|
18
|
+
const restriction = (item.querySelector(".availabilityinfo")?.textContent ?? "").replace(/\s+/g, " ").trim() || undefined;
|
|
19
|
+
return { id: Number(item.getAttribute("data-id")), dates, restriction };
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Details for the given activities: from the course page, then from the section pages of those it
|
|
25
|
+
* doesn't show (courses with one section per page).
|
|
26
|
+
*/
|
|
27
|
+
export async function activityDetails(context, siteUrl, courseId, state) {
|
|
28
|
+
const details = new Map();
|
|
29
|
+
const read = async (path) => {
|
|
30
|
+
const page = await openSitePage(context, siteUrl, path);
|
|
31
|
+
try {
|
|
32
|
+
for (const item of await readActivityDetails(page))
|
|
33
|
+
details.set(item.id, item);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await page.close().catch(() => undefined);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
await read(`course/view.php?id=${courseId}`);
|
|
40
|
+
const missingSections = new Set(state.cm.filter((cm) => !details.has(Number(cm.id))).map((cm) => cm.sectionid));
|
|
41
|
+
for (const sectionId of missingSections)
|
|
42
|
+
await read(`course/section.php?id=${sectionId}`);
|
|
43
|
+
return details;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=course.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"course.js","sourceRoot":"","sources":["../../src/moodle/course.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
import { type FieldValue } from "./forms.js";
|
|
3
|
+
/** Which courses by their dates; "all" includes those the person hid from their dashboard. */
|
|
4
|
+
export declare const COURSE_CLASSIFICATIONS: readonly ["all", "inprogress", "future", "past"];
|
|
5
|
+
export type CourseClassification = (typeof COURSE_CLASSIFICATIONS)[number];
|
|
6
|
+
export interface Course {
|
|
7
|
+
id: number;
|
|
8
|
+
shortname: string;
|
|
9
|
+
fullname: string;
|
|
10
|
+
category?: string;
|
|
11
|
+
url: string;
|
|
12
|
+
/** Whether students can see the course. */
|
|
13
|
+
visible: boolean;
|
|
14
|
+
startDate?: string;
|
|
15
|
+
endDate?: string;
|
|
16
|
+
/** The person's completion percentage, when the course tracks it. */
|
|
17
|
+
progress?: number;
|
|
18
|
+
/** When the person last opened the course. */
|
|
19
|
+
lastAccess?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A course as `core_course_get_enrolled_courses_by_timeline_classification` returns it (Moodle 5.2). */
|
|
22
|
+
export interface MoodleCourse {
|
|
23
|
+
id: number;
|
|
24
|
+
shortname: string;
|
|
25
|
+
fullname: string;
|
|
26
|
+
coursecategory?: string;
|
|
27
|
+
viewurl: string;
|
|
28
|
+
visible: boolean;
|
|
29
|
+
startdate?: number;
|
|
30
|
+
enddate?: number;
|
|
31
|
+
progress?: number | null;
|
|
32
|
+
hasprogress?: boolean;
|
|
33
|
+
timeaccess?: number | null;
|
|
34
|
+
}
|
|
35
|
+
/** Unix seconds as ISO 8601; Moodle uses 0 for "none". */
|
|
36
|
+
export declare function isoDate(seconds: number | null | undefined): string | undefined;
|
|
37
|
+
export declare function toCourse(raw: MoodleCourse): Course;
|
|
38
|
+
/** The courses the person is enrolled in, as teacher or student. */
|
|
39
|
+
export declare function listCourses(context: BrowserContext, siteUrl: string, classification?: CourseClassification): Promise<Course[]>;
|
|
40
|
+
/** A course that isn't there, or the person can't change. */
|
|
41
|
+
export declare class CourseError extends Error {
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Changes a course's full name, short name, summary (HTML) or any other setting by its form field
|
|
46
|
+
* name, through the course's settings form; returns the course as `listCourses` gives it. Moodle's
|
|
47
|
+
* refusals (a short name already taken) come back as `FormError` by field.
|
|
48
|
+
*/
|
|
49
|
+
export declare function updateCourse(context: BrowserContext, siteUrl: string, courseId: number, { fullName, shortName, summary, settings }: {
|
|
50
|
+
fullName?: string;
|
|
51
|
+
shortName?: string;
|
|
52
|
+
summary?: string;
|
|
53
|
+
settings?: Record<string, FieldValue>;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
changed: string;
|
|
56
|
+
course?: Course;
|
|
57
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { callService } from "./ajax.js";
|
|
2
|
+
import { openForm, setFields, submitForm } from "./forms.js";
|
|
3
|
+
import { NotLoggedInError } from "./session.js";
|
|
4
|
+
import { decodeEntities } from "./text.js";
|
|
5
|
+
/** Which courses by their dates; "all" includes those the person hid from their dashboard. */
|
|
6
|
+
export const COURSE_CLASSIFICATIONS = ["all", "inprogress", "future", "past"];
|
|
7
|
+
const MOODLE_CLASSIFICATIONS = { all: "allincludinghidden", inprogress: "inprogress", future: "future", past: "past" };
|
|
8
|
+
/** Unix seconds as ISO 8601; Moodle uses 0 for "none". */
|
|
9
|
+
export function isoDate(seconds) {
|
|
10
|
+
return seconds ? new Date(seconds * 1000).toISOString() : undefined;
|
|
11
|
+
}
|
|
12
|
+
export function toCourse(raw) {
|
|
13
|
+
return {
|
|
14
|
+
id: raw.id,
|
|
15
|
+
shortname: decodeEntities(raw.shortname),
|
|
16
|
+
fullname: decodeEntities(raw.fullname),
|
|
17
|
+
category: raw.coursecategory ? decodeEntities(raw.coursecategory) : undefined,
|
|
18
|
+
url: raw.viewurl,
|
|
19
|
+
visible: raw.visible,
|
|
20
|
+
startDate: isoDate(raw.startdate),
|
|
21
|
+
endDate: isoDate(raw.enddate),
|
|
22
|
+
progress: raw.hasprogress && typeof raw.progress === "number" ? raw.progress : undefined,
|
|
23
|
+
lastAccess: isoDate(raw.timeaccess),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The courses the person is enrolled in, as teacher or student. */
|
|
27
|
+
export async function listCourses(context, siteUrl, classification = "all") {
|
|
28
|
+
const { courses } = await callService(context, siteUrl, "core_course_get_enrolled_courses_by_timeline_classification", {
|
|
29
|
+
classification: MOODLE_CLASSIFICATIONS[classification],
|
|
30
|
+
limit: 0,
|
|
31
|
+
offset: 0,
|
|
32
|
+
sort: "fullname",
|
|
33
|
+
});
|
|
34
|
+
return courses.map(toCourse);
|
|
35
|
+
}
|
|
36
|
+
/** A course that isn't there, or the person can't change. */
|
|
37
|
+
export class CourseError extends Error {
|
|
38
|
+
name = "CourseError";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Changes a course's full name, short name, summary (HTML) or any other setting by its form field
|
|
42
|
+
* name, through the course's settings form; returns the course as `listCourses` gives it. Moodle's
|
|
43
|
+
* refusals (a short name already taken) come back as `FormError` by field.
|
|
44
|
+
*/
|
|
45
|
+
export async function updateCourse(context, siteUrl, courseId, { fullName, shortName, summary, settings }) {
|
|
46
|
+
const values = { ...settings };
|
|
47
|
+
if (fullName !== undefined)
|
|
48
|
+
values.fullname = fullName;
|
|
49
|
+
if (shortName !== undefined)
|
|
50
|
+
values.shortname = shortName;
|
|
51
|
+
if (summary !== undefined)
|
|
52
|
+
values["summary_editor[text]"] = summary;
|
|
53
|
+
if (Object.keys(values).length === 0)
|
|
54
|
+
return { changed: "Nothing to change.", course: (await listCourses(context, siteUrl)).find((c) => c.id === courseId) };
|
|
55
|
+
const page = await openForm(context, siteUrl, `course/edit.php?id=${courseId}`).catch((error) => {
|
|
56
|
+
if (error instanceof NotLoggedInError)
|
|
57
|
+
throw error;
|
|
58
|
+
throw new CourseError(`You can't change course ${courseId}'s settings: it doesn't exist, or you aren't allowed.`);
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
await setFields(page, values);
|
|
62
|
+
await submitForm(page, "saveanddisplay");
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
await page.close().catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
const course = (await listCourses(context, siteUrl)).find((c) => c.id === courseId);
|
|
68
|
+
return { changed: `Course ${courseId} updated (${Object.keys(values).join(", ")}).`, course };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=courses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"courses.js","sourceRoot":"","sources":["../../src/moodle/courses.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
/** The course editor's actions this server uses (`core_courseformat\stateactions`, Moodle 4.0–5.2). */
|
|
3
|
+
export type EditorAction = "cm_show" | "cm_hide" | "cm_stealth" | "cm_move" | "cm_duplicate" | "cm_delete" | "section_add" | "section_show" | "section_hide" | "section_move_after" | "section_delete";
|
|
4
|
+
/**
|
|
5
|
+
* Runs one of the course editor's actions through its own service (ADR-004), as the editor does:
|
|
6
|
+
* ids of activities or sections, and a target section (`cm_move`: at its end; `section_add`,
|
|
7
|
+
* `section_move_after`: after it) or a target activity (`cm_move`: before it).
|
|
8
|
+
*/
|
|
9
|
+
export declare function editCourse(context: BrowserContext, siteUrl: string, courseId: number, action: EditorAction, ids: number[], target?: {
|
|
10
|
+
sectionId?: number;
|
|
11
|
+
activityId?: number;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
/** The course format (topics, weeks…), a class of the course page's body: no service tells teachers. */
|
|
14
|
+
export declare function courseFormat(context: BrowserContext, siteUrl: string, courseId: number): Promise<string>;
|
|
15
|
+
/** Renames an activity, as the course page's in-place editor does. */
|
|
16
|
+
export declare function renameActivity(context: BrowserContext, siteUrl: string, activityId: number, name: string): Promise<void>;
|
|
17
|
+
/** Renames a section through its course format's in-place editor. */
|
|
18
|
+
export declare function renameSection(context: BrowserContext, siteUrl: string, courseId: number, sectionId: number, name: string): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { callService } from "./ajax.js";
|
|
2
|
+
import { openSitePage } from "./session.js";
|
|
3
|
+
/**
|
|
4
|
+
* Runs one of the course editor's actions through its own service (ADR-004), as the editor does:
|
|
5
|
+
* ids of activities or sections, and a target section (`cm_move`: at its end; `section_add`,
|
|
6
|
+
* `section_move_after`: after it) or a target activity (`cm_move`: before it).
|
|
7
|
+
*/
|
|
8
|
+
export async function editCourse(context, siteUrl, courseId, action, ids, target = {}) {
|
|
9
|
+
await callService(context, siteUrl, "core_courseformat_update_course", {
|
|
10
|
+
action,
|
|
11
|
+
courseid: courseId,
|
|
12
|
+
ids,
|
|
13
|
+
...(target.sectionId ? { targetsectionid: target.sectionId } : {}),
|
|
14
|
+
...(target.activityId ? { targetcmid: target.activityId } : {}),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
// A course's format doesn't change while a program runs.
|
|
18
|
+
const formats = new Map();
|
|
19
|
+
/** The course format (topics, weeks…), a class of the course page's body: no service tells teachers. */
|
|
20
|
+
export async function courseFormat(context, siteUrl, courseId) {
|
|
21
|
+
const key = `${siteUrl}|${courseId}`;
|
|
22
|
+
const cached = formats.get(key);
|
|
23
|
+
if (cached)
|
|
24
|
+
return cached;
|
|
25
|
+
const page = await openSitePage(context, siteUrl, `course/view.php?id=${courseId}`);
|
|
26
|
+
try {
|
|
27
|
+
const classes = await page.evaluate(() => globalThis.document.body.className);
|
|
28
|
+
const format = classes
|
|
29
|
+
.split(" ")
|
|
30
|
+
.find((name) => name.startsWith("format-"))
|
|
31
|
+
?.slice("format-".length);
|
|
32
|
+
if (!format)
|
|
33
|
+
throw new Error(`Course ${courseId}'s format isn't on its page.`);
|
|
34
|
+
formats.set(key, format);
|
|
35
|
+
return format;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await page.close().catch(() => undefined);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Renames an activity, as the course page's in-place editor does. */
|
|
42
|
+
export async function renameActivity(context, siteUrl, activityId, name) {
|
|
43
|
+
await callService(context, siteUrl, "core_update_inplace_editable", { component: "core_course", itemtype: "activityname", itemid: activityId, value: name });
|
|
44
|
+
}
|
|
45
|
+
/** Renames a section through its course format's in-place editor. */
|
|
46
|
+
export async function renameSection(context, siteUrl, courseId, sectionId, name) {
|
|
47
|
+
const format = await courseFormat(context, siteUrl, courseId);
|
|
48
|
+
await callService(context, siteUrl, "core_update_inplace_editable", { component: `format_${format}`, itemtype: "sectionname", itemid: sectionId, value: name });
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.js","sourceRoot":"","sources":["../../src/moodle/editor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BrowserContext } from "playwright-core";
|
|
2
|
+
/** Local files given to upload (a submission, an attachment, a resource): absolute, and there. */
|
|
3
|
+
export declare function checkLocalFiles(files: string[]): Promise<void>;
|
|
4
|
+
/** A file name from Content-Disposition, else the URL's last segment; never a path. */
|
|
5
|
+
export declare function fileNameFor(url: URL, disposition: string | undefined): string;
|
|
6
|
+
export interface DownloadedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
name: string;
|
|
9
|
+
size: number;
|
|
10
|
+
contentType?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Saves a file of the site into `destDir` with the person's session, never overwriting. Only URLs of
|
|
14
|
+
* the site's origin: the session's cookies go nowhere else (its media outside Moodle's path included).
|
|
15
|
+
*/
|
|
16
|
+
export declare function downloadFile(context: BrowserContext, siteUrl: string, url: string, destDir: string): Promise<DownloadedFile>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NotLoggedInError } from "./session.js";
|
|
4
|
+
/** Local files given to upload (a submission, an attachment, a resource): absolute, and there. */
|
|
5
|
+
export async function checkLocalFiles(files) {
|
|
6
|
+
for (const file of files) {
|
|
7
|
+
if (!path.isAbsolute(file))
|
|
8
|
+
throw new Error(`Give files as absolute paths: "${file}".`);
|
|
9
|
+
await access(file).catch(() => {
|
|
10
|
+
throw new Error(`There's no file at ${file}.`);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** A file name from Content-Disposition, else the URL's last segment; never a path. */
|
|
15
|
+
export function fileNameFor(url, disposition) {
|
|
16
|
+
const encoded = disposition && /filename\*=UTF-8''([^;]+)/i.exec(disposition)?.[1];
|
|
17
|
+
const plain = disposition && /filename="?([^";]+)"?/i.exec(disposition)?.[1];
|
|
18
|
+
const raw = encoded ? decodeURIComponent(encoded) : (plain ?? decodeURIComponent(url.pathname.split("/").pop() || "download"));
|
|
19
|
+
// The last segment of whatever path it names, then without characters a file name can't have.
|
|
20
|
+
const name = (raw.split(/[\\/]/).pop() ?? "").replace(/[:*?"<>|]+/g, "_").trim();
|
|
21
|
+
return name === "" || name === "." || name === ".." ? "download" : name;
|
|
22
|
+
}
|
|
23
|
+
/** `dir/name`, or `dir/name (2).ext`… when it exists: a download never overwrites. */
|
|
24
|
+
async function freePath(dir, name) {
|
|
25
|
+
const { name: stem, ext } = path.parse(name);
|
|
26
|
+
for (let n = 1;; n++) {
|
|
27
|
+
const candidate = path.join(dir, n === 1 ? name : `${stem} (${n})${ext}`);
|
|
28
|
+
if (!(await access(candidate).then(() => true, () => false)))
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Saves a file of the site into `destDir` with the person's session, never overwriting. Only URLs of
|
|
34
|
+
* the site's origin: the session's cookies go nowhere else (its media outside Moodle's path included).
|
|
35
|
+
*/
|
|
36
|
+
export async function downloadFile(context, siteUrl, url, destDir) {
|
|
37
|
+
const target = new URL(url);
|
|
38
|
+
const origin = new URL(siteUrl).origin;
|
|
39
|
+
if (target.origin !== origin)
|
|
40
|
+
throw new Error(`Only files of ${origin} can be downloaded with the session; ${target.origin} isn't the site.`);
|
|
41
|
+
if (!path.isAbsolute(destDir))
|
|
42
|
+
throw new Error(`Give the destination folder as an absolute path: "${destDir}".`);
|
|
43
|
+
// The context's request API carries its cookies without opening a tab.
|
|
44
|
+
const response = await context.request.get(target.href, { maxRedirects: 5 });
|
|
45
|
+
const contentType = response.headers()["content-type"]?.split(";")[0]?.trim();
|
|
46
|
+
// A file the session can't reach comes back as the login page.
|
|
47
|
+
if (contentType === "text/html" && new URL(response.url()).pathname.includes("/login/"))
|
|
48
|
+
throw new NotLoggedInError();
|
|
49
|
+
if (!response.ok())
|
|
50
|
+
throw new Error(`Moodle answered ${response.status()} for ${target.pathname}: the file doesn't exist or you can't see it.`);
|
|
51
|
+
await mkdir(destDir, { recursive: true });
|
|
52
|
+
const body = await response.body();
|
|
53
|
+
const saved = await freePath(destDir, fileNameFor(target, response.headers()["content-disposition"]));
|
|
54
|
+
await writeFile(saved, body);
|
|
55
|
+
return { path: saved, name: path.basename(saved), size: body.length, contentType };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/moodle/files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BrowserContext, Page } from "playwright-core";
|
|
2
|
+
/** A form field's value: text, a number, a checkbox, or a multiple select's values. */
|
|
3
|
+
export type FieldValue = string | number | boolean | string[];
|
|
4
|
+
/** Moodle refused the form, or a field to set isn't in it. */
|
|
5
|
+
export declare class FormError extends Error {
|
|
6
|
+
readonly fields: {
|
|
7
|
+
field: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}[];
|
|
10
|
+
name: string;
|
|
11
|
+
constructor(message: string, fields: {
|
|
12
|
+
field: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}[]);
|
|
15
|
+
}
|
|
16
|
+
/** Opens a Moodle form page and waits for its editors to be ready. */
|
|
17
|
+
export declare function openForm(context: BrowserContext, siteUrl: string, pathAndQuery: string): Promise<Page>;
|
|
18
|
+
/** A hidden field's value in the form (a draft area id, the course id…). */
|
|
19
|
+
export declare function formValue(page: Page, name: string): Promise<string | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Sets fields by their form name: text inputs and textareas (through TinyMCE when it edits them),
|
|
22
|
+
* selects (several values for a multiple one), checkboxes and radios, in the order given (a field can
|
|
23
|
+
* enable the next one). Throws `FormError` naming the fields the form doesn't have, and those it has
|
|
24
|
+
* disabled: Moodle wouldn't send them, and the change would be lost unnoticed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function setFields(page: Page, values: Record<string, FieldValue>, formSelector?: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Uploads local files to a form's draft area, as the file picker's "Upload a file" does: the
|
|
29
|
+
* upload repository's id comes from the page's file picker settings, the context from `M.cfg`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function uploadToDraft(page: Page, draftItemId: string, filePaths: string[]): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Submits the form with one of its buttons and waits for the next page. Throws `FormError` with
|
|
34
|
+
* Moodle's messages by field (`#id_error_<field>`) when the form comes back with errors.
|
|
35
|
+
*/
|
|
36
|
+
export declare function submitForm(page: Page, button: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* The values of a Moodle date selector's parts (`name[day]`, `[month]`, `[year]`, `[hour]`,
|
|
39
|
+
* `[minute]`) for `date`, in the person's time zone (`M.cfg.usertimezone`), which is how Moodle
|
|
40
|
+
* reads them. Its "enable" checkbox, when it has one, is the caller's to set first.
|
|
41
|
+
*/
|
|
42
|
+
export declare function dateSelectorValues(page: Page, name: string, date: Date): Promise<Record<string, FieldValue>>;
|
|
43
|
+
/** The date a Moodle date selector holds (`name[day]`… in the person's time zone), or undefined when it's off. */
|
|
44
|
+
export declare function dateSelectorDate(page: Page, name: string): Promise<Date | undefined>;
|