@mcpher/gas-fakes 1.0.16 → 1.0.18
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/gasmess/tanaike/for_simple_test.js +3 -8
- package/gasmess/tanaike/sample5.js +63 -0
- package/ghissues/image-size-inconsistency-issue.sh +46 -0
- package/ghissues/review-sandbox-listing-issue.sh +45 -0
- package/package.json +3 -3
- package/src/services/advdocs/fakeadvdocs.js +6 -1
- package/src/services/advdocs/fakeadvdocuments.js +7 -16
- package/src/services/advdrive/fakeadvdrivefiles.js +13 -3
- package/src/services/advdrive/fakeadvdrivepermissions.js +6 -3
- package/src/services/advsheets/fakeadvsheets.js +0 -7
- package/src/services/advsheets/fakeadvsheetsdevelopermetadata.js +4 -2
- package/src/services/advsheets/fakeadvsheetsspreadsheets.js +18 -5
- package/src/services/advsheets/fakeadvsheetsvalues.js +4 -2
- package/src/services/advslides/fakeadvslides.js +10 -12
- package/src/services/documentapp/app.js +25 -3
- package/src/services/documentapp/elementhelpers.js +6 -0
- package/src/services/documentapp/fakedocumentapp.js +11 -3
- package/src/services/documentapp/shadowdocument.js +2 -0
- package/src/services/driveapp/fakedriveapp.js +0 -6
- package/src/services/driveapp/fakedrivefile.js +2 -3
- package/src/services/driveapp/fakefolderapp.js +1 -1
- package/src/services/scriptapp/behavior.js +290 -11
- package/src/services/slidesapp/app.js +25 -4
- package/src/services/slidesapp/fakepresentation.js +3 -1
- package/src/services/slidesapp/fakeslidesapp.js +5 -1
- package/src/services/spreadsheetapp/app.js +26 -9
- package/src/services/spreadsheetapp/fakeprotection.js +447 -0
- package/src/services/spreadsheetapp/fakesheet.js +62 -6
- package/src/services/spreadsheetapp/fakesheetrange.js +25 -0
- package/src/services/spreadsheetapp/fakespreadsheet.js +41 -2
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +9 -1
- package/src/support/proxies.js +14 -3
- package/fakepositionedimage.js +0 -0
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import "../../main.js";
|
|
2
|
-
import { FakeSpreadsheetApp } from "../../src/services/spreadsheetapp/fakespreadsheetapp.js";
|
|
3
|
-
import { moveToTempFolder, deleteTempFile } from "./tempfolder.js";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
ScriptApp.__behavior.sandBoxMode = true;
|
|
4
|
+
const spreadsheet = SpreadsheetApp.create("sample1");
|
|
6
5
|
const sheet = spreadsheet.getSheets()[0];
|
|
7
6
|
sheet.setName("sample");
|
|
8
|
-
|
|
9
|
-
const r = spreadsheet.getRangeByName("test111");
|
|
10
|
-
if (r) {
|
|
11
|
-
console.log(r.getA1Notation());
|
|
12
|
-
}
|
|
7
|
+
ScriptApp.__behavior.trash();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "../../main.js";
|
|
2
|
+
|
|
3
|
+
ScriptApp.__behavior.sandBoxMode = true;
|
|
4
|
+
sample();
|
|
5
|
+
ScriptApp.__behavior.trash();
|
|
6
|
+
|
|
7
|
+
function sample() {
|
|
8
|
+
const spreadsheet = SpreadsheetApp.create("sample");
|
|
9
|
+
const sheet = spreadsheet.getSheets()[0];
|
|
10
|
+
|
|
11
|
+
// Create protected sheet.
|
|
12
|
+
// For Class Sheet
|
|
13
|
+
const unprotectedRanges = [sheet.getRange("A1:B1")];
|
|
14
|
+
const p1 = sheet
|
|
15
|
+
.protect()
|
|
16
|
+
.setDescription("sample1")
|
|
17
|
+
.setWarningOnly(true)
|
|
18
|
+
.setUnprotectedRanges(unprotectedRanges);
|
|
19
|
+
console.log(p1.getDescription()); // "sample1"
|
|
20
|
+
console.log(p1.getUnprotectedRanges()[0].getA1Notation()); // "A1:B1"
|
|
21
|
+
console.log(p1.canEdit()); // true
|
|
22
|
+
console.log(p1.getEditors().length); // 1
|
|
23
|
+
console.log(p1.getProtectionType().toString()); // SHEET
|
|
24
|
+
console.log(p1.isWarningOnly()); // true
|
|
25
|
+
|
|
26
|
+
// For Class Spreadsheet
|
|
27
|
+
const protections = spreadsheet.getProtections(
|
|
28
|
+
SpreadsheetApp.ProtectionType.SHEET
|
|
29
|
+
);
|
|
30
|
+
console.log(protections.some((p) => p.getDescription() == "sample1")); // true
|
|
31
|
+
|
|
32
|
+
p1.remove();
|
|
33
|
+
|
|
34
|
+
// Create protected range with a range.
|
|
35
|
+
// For Class Range
|
|
36
|
+
const p2 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
37
|
+
p2.setRange(sheet.getRange("B2:E6"));
|
|
38
|
+
console.log(p2.getRange().getA1Notation()); // "B2:E6"
|
|
39
|
+
console.log(p2.getProtectionType().toString()); // RANGE
|
|
40
|
+
p2.remove();
|
|
41
|
+
|
|
42
|
+
// Create named range.
|
|
43
|
+
const rangeName = "sampleNamedRange1";
|
|
44
|
+
const range1 = sheet.getRange("C3:F7");
|
|
45
|
+
spreadsheet.setNamedRange(rangeName, range1);
|
|
46
|
+
const namedRange = spreadsheet
|
|
47
|
+
.getNamedRanges()
|
|
48
|
+
.find((e) => e.getName() == rangeName);
|
|
49
|
+
|
|
50
|
+
// Create protected range with named range 1.
|
|
51
|
+
const p3 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
52
|
+
p3.setRangeName(rangeName);
|
|
53
|
+
console.log(p3.getRangeName()); // sampleNamedRange1
|
|
54
|
+
p3.remove();
|
|
55
|
+
|
|
56
|
+
// Create protected range with named range 2.
|
|
57
|
+
const p4 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
58
|
+
p4.setNamedRange(namedRange);
|
|
59
|
+
console.log(p4.getRangeName()); // sampleNamedRange1
|
|
60
|
+
p4.remove();
|
|
61
|
+
|
|
62
|
+
namedRange.remove();
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for the image sizing inconsistency.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Investigate and Align Image Insertion Sizing Behavior"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
There is a significant and inconsistent discrepancy in how the live `DocumentApp` service and the `gas-fakes` emulation (via the Docs API) handle the dimensions of an image when it is inserted using `body.insertImage(image.copy())` or `body.appendImage(image.copy())`.
|
|
13
|
+
|
|
14
|
+
The `testdocsimages.js` test suite has repeatedly failed and required adjustments because the expected dimensions of the inserted image are unpredictable in the live environment.
|
|
15
|
+
|
|
16
|
+
## Observed Behaviors in Live Apps Script
|
|
17
|
+
|
|
18
|
+
Our testing has revealed several conflicting behaviors from the live API:
|
|
19
|
+
|
|
20
|
+
1. **Intrinsic Size:** Sometimes, the API ignores the dimensions of the copied `InlineImage` object and re-fetches the image from its source URI, using its original intrinsic dimensions (e.g., `544x184` for the Google logo).
|
|
21
|
+
2. **Copied Object Size:** At other times, the API correctly respects the dimensions of the copied `InlineImage` object (e.g., `61x181` in our tests).
|
|
22
|
+
3. **Default/Fixed Size:** On at least one occasion, the API appeared to resize the image to a seemingly arbitrary fixed size (e.g., `240x80`).
|
|
23
|
+
4. **State-Dependent:** The behavior seems to be dependent on the state of the document. A brand-new document might exhibit one behavior, while a reused document (even after `doc.clear()`) exhibits another.
|
|
24
|
+
|
|
25
|
+
## Current Workaround
|
|
26
|
+
|
|
27
|
+
The current test (`insertImage behavior on new documents` in `testdocsimages.js`) has been made more robust by:
|
|
28
|
+
- Forcing the creation of a new document for the test using `maketdoc(..., { forceNew: true })`.
|
|
29
|
+
- Abandoning exact dimension checks in the live environment and instead verifying the image's **aspect ratio** within a tolerance.
|
|
30
|
+
|
|
31
|
+
While this makes the test pass, it highlights a core emulation inaccuracy.
|
|
32
|
+
|
|
33
|
+
## TODO
|
|
34
|
+
|
|
35
|
+
1. **Investigate Further:** Conduct a definitive set of tests to determine if there is *any* predictable pattern to the live API's behavior.
|
|
36
|
+
2. **Decide on Emulation Strategy:**
|
|
37
|
+
- Should `gas-fakes` emulate the most common or most "correct" behavior (i.e., respecting the copied object's dimensions)?
|
|
38
|
+
- Or should it attempt to mimic the most recently observed "new document" behavior (using intrinsic size)?
|
|
39
|
+
3. **Document the Oddity:** Regardless of the chosen strategy, this inconsistency is a significant "oddity" and should be thoroughly documented in `oddities.md` to inform users who might encounter similar issues when working with images in `DocumentApp`.
|
|
40
|
+
|
|
41
|
+
This will improve the accuracy of the fake environment and provide valuable documentation for the community.
|
|
42
|
+
EOF
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create the issue using GitHub CLI
|
|
46
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "bug" --label "emulation-accuracy" --label "document-app"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for reviewing sandbox listing behavior.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Review Sandbox Strategy for File Listing Operations"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Currently, tests that perform broad file/folder listing operations (e.g., `DriveApp.getFiles()`, `someFolder.getFolders()`) fail when `strictSandbox` is `true`. This is because the underlying iterators may need to access metadata of parent folders that are not explicitly whitelisted, causing the sandbox to correctly deny access.
|
|
13
|
+
|
|
14
|
+
The current workaround, as implemented in `testdrive.js`, is to temporarily disable `strictSandbox` for the duration of these tests:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
const wasStrict = behavior.strictSandbox;
|
|
18
|
+
behavior.strictSandbox = false;
|
|
19
|
+
try {
|
|
20
|
+
// ... listing operations ...
|
|
21
|
+
} finally {
|
|
22
|
+
behavior.strictSandbox = wasStrict;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
While this works, it feels like a patch rather than a fundamental solution. We should review if there's a more robust and elegant way to handle this.
|
|
27
|
+
|
|
28
|
+
## TODO
|
|
29
|
+
|
|
30
|
+
Investigate and decide on the best long-term strategy for handling file listings in strict sandbox mode.
|
|
31
|
+
|
|
32
|
+
### Options to Consider:
|
|
33
|
+
|
|
34
|
+
1. **Implicit Parent Whitelisting:** Could the file/folder iterators be enhanced to automatically (and temporarily) whitelist parent folders as they are encountered? This would keep the sandbox strict for all other operations but allow listings to complete. This seems like the most robust solution if feasible.
|
|
35
|
+
|
|
36
|
+
2. **Refined Iterator Logic:** Can the iterators be modified to avoid the specific checks that cause the failure when in strict mode? This might involve fetching less metadata for items outside the session/whitelist scope.
|
|
37
|
+
|
|
38
|
+
3. **Accept and Document:** If the current `try...finally` workaround is deemed the most practical approach, we should formally document it in `sandbox.md` as the recommended pattern for this scenario.
|
|
39
|
+
|
|
40
|
+
This review will help ensure the sandbox feature is both as secure and as user-friendly as possible.
|
|
41
|
+
EOF
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Create the issue using GitHub CLI
|
|
45
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "enhancement"
|
package/package.json
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@mcpher/fake-gasenum": "^1.0.2",
|
|
7
|
-
"@mcpher/gas-fakes": "^1.0.14",
|
|
8
7
|
"@mcpher/unit": "^1.1.11",
|
|
9
8
|
"@sindresorhus/is": "^7.0.1",
|
|
10
9
|
"archiver": "^7.0.1",
|
|
@@ -54,13 +53,14 @@
|
|
|
54
53
|
"testdocsfooters": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfooters.js execute",
|
|
55
54
|
"testdocsfootnotes": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfootnotes.js execute",
|
|
56
55
|
"testdocsimages": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsimages.js execute",
|
|
56
|
+
"testsandbox": "cp mainlocal.js main.js && node --env-file=.env ./test/testsandbox.js execute",
|
|
57
57
|
"pub": "cp mainlocal.js main.js && npm publish --access public"
|
|
58
58
|
},
|
|
59
59
|
"name": "@mcpher/gas-fakes",
|
|
60
|
-
"version": "1.0.
|
|
60
|
+
"version": "1.0.18",
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"main": "main.js",
|
|
63
63
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
64
64
|
"repository": "github:brucemcpherson/gas-fakes",
|
|
65
65
|
"homepage": "https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/"
|
|
66
|
-
}
|
|
66
|
+
}
|
|
@@ -628,7 +628,12 @@ class FakeAdvDocs {
|
|
|
628
628
|
__getDocsPerformance() {
|
|
629
629
|
return docsCacher.getPerformance()
|
|
630
630
|
}
|
|
631
|
+
__addAllowed(id) {
|
|
632
|
+
if (ScriptApp.__behavior.sandBoxMode) {
|
|
633
|
+
ScriptApp.__behavior.addFile(id);
|
|
634
|
+
}
|
|
635
|
+
return id
|
|
636
|
+
}
|
|
631
637
|
}
|
|
632
638
|
|
|
633
639
|
export const newFakeAdvDocs = (...args) => Proxies.guard(new FakeAdvDocs(...args))
|
|
634
|
-
|
|
@@ -17,19 +17,7 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
17
17
|
constructor(docs) {
|
|
18
18
|
super(docs, 'documents', Syncit.fxDocs);
|
|
19
19
|
this.__fakeObjectType = "Docs.Documents";
|
|
20
|
-
|
|
21
|
-
// in sandbox mode only files created in this instance are
|
|
22
|
-
__allowed(id) {
|
|
23
|
-
if (!ScriptApp.__behavior.isAccessible(id)) {
|
|
24
|
-
throw new Error(`Access to document ${id} is not allowed in sandbox mode`);
|
|
25
|
-
}
|
|
26
|
-
return id
|
|
27
|
-
}
|
|
28
|
-
__addAllowed(id) {
|
|
29
|
-
if (ScriptApp.__behavior.sandBoxMode) {
|
|
30
|
-
ScriptApp.__behavior.addFile(id);
|
|
31
|
-
}
|
|
32
|
-
return id
|
|
20
|
+
this.docs = docs;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
23
|
create(resource, options) {
|
|
@@ -44,7 +32,7 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
44
32
|
|
|
45
33
|
// maybe we need to throw an error
|
|
46
34
|
gError(response, 'docs.documents', "create")
|
|
47
|
-
this.__addAllowed(data.documentId);
|
|
35
|
+
this.docs.__addAllowed(data.documentId);
|
|
48
36
|
return data
|
|
49
37
|
}
|
|
50
38
|
|
|
@@ -55,12 +43,14 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
55
43
|
matchThrow("API call to docs.documents.batchUpdate failed with error: Invalid JSON payload received.");
|
|
56
44
|
}
|
|
57
45
|
|
|
46
|
+
// any update to a doc is a write
|
|
47
|
+
ScriptApp.__behavior.isAccessible(documentId, 'Docs', 'write');
|
|
58
48
|
|
|
59
49
|
// Invalidate the cache for this document since we are updating it.
|
|
60
50
|
docsCacher.clear(documentId);
|
|
61
51
|
// console.log (JSON.stringify(requests))
|
|
62
52
|
const { response, data } = this._call("batchUpdate", {
|
|
63
|
-
documentId
|
|
53
|
+
documentId,
|
|
64
54
|
requestBody: requests
|
|
65
55
|
});
|
|
66
56
|
|
|
@@ -101,7 +91,8 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
101
91
|
|
|
102
92
|
if (is.nonEmptyObject(options) && !Reflect.ownKeys(options || {}).every(f => optionsSet.has(f))) matchThrow();
|
|
103
93
|
|
|
104
|
-
|
|
94
|
+
ScriptApp.__behavior.isAccessible(documentId, 'Docs', 'read');
|
|
95
|
+
const params = {documentId, ...(options || {}) };
|
|
105
96
|
|
|
106
97
|
const { response, data } = this._call("get", params);
|
|
107
98
|
gError(response, 'docs.documents', 'get');
|
|
@@ -115,7 +115,8 @@ class FakeAdvDriveFiles {
|
|
|
115
115
|
* @returns {Drive.File}
|
|
116
116
|
*/
|
|
117
117
|
get(id, params = {}, { allow404 = true } = {}) {
|
|
118
|
-
|
|
118
|
+
ScriptApp.__behavior.isAccessible(id, 'Drive', 'read');
|
|
119
|
+
const {data} = Syncit.fxDriveGet ({ id, prop: apiProp, method: 'get', params, allow404, allowCache: true });
|
|
119
120
|
return data
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -156,7 +157,15 @@ class FakeAdvDriveFiles {
|
|
|
156
157
|
if (!is.nonEmptyString(fileId)) {
|
|
157
158
|
throw new Error(`API call to drive.files.update failed with error: Required`)
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
// sandbox checks
|
|
161
|
+
const isMetadataWrite = file && Object.keys(file).some(k => k !== 'trashed');
|
|
162
|
+
if (blob || isMetadataWrite) {
|
|
163
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'write');
|
|
164
|
+
}
|
|
165
|
+
if (file && typeof file.trashed === 'boolean') {
|
|
166
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'trash');
|
|
167
|
+
}
|
|
168
|
+
return updateOrCreate ({method: 'update', file, blob, fileId, fields, params})
|
|
160
169
|
}
|
|
161
170
|
/**
|
|
162
171
|
* ceate a file and optionally upload some data
|
|
@@ -169,10 +178,11 @@ class FakeAdvDriveFiles {
|
|
|
169
178
|
if (!is.nonEmptyString(fileId)) {
|
|
170
179
|
throw new Error(`API call to drive.files.copy failed with error: Required`)
|
|
171
180
|
}
|
|
181
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'read');
|
|
172
182
|
const fields = mergeParamStrings(options.fields || "",minFields)
|
|
173
183
|
const params = {
|
|
174
184
|
fields,
|
|
175
|
-
fileId
|
|
185
|
+
fileId,
|
|
176
186
|
resource: file
|
|
177
187
|
}
|
|
178
188
|
|
|
@@ -21,9 +21,10 @@ class FakeAdvDrivePermissions extends FakeAdvResource {
|
|
|
21
21
|
const { nargs, matchThrow } = signatureArgs(arguments, "Drive.Permissions.create");
|
|
22
22
|
if (nargs < 2 || nargs > 3) matchThrow();
|
|
23
23
|
|
|
24
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'write');
|
|
24
25
|
const params = {
|
|
25
26
|
resource,
|
|
26
|
-
fileId
|
|
27
|
+
fileId,
|
|
27
28
|
...(optionalArgs || {})
|
|
28
29
|
};
|
|
29
30
|
const { data } = this._call('create', params);
|
|
@@ -34,8 +35,9 @@ class FakeAdvDrivePermissions extends FakeAdvResource {
|
|
|
34
35
|
const { nargs, matchThrow } = signatureArgs(arguments, "Drive.Permissions.delete");
|
|
35
36
|
if (nargs < 2 || nargs > 3) matchThrow();
|
|
36
37
|
|
|
38
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'write');
|
|
37
39
|
const params = {
|
|
38
|
-
fileId
|
|
40
|
+
fileId,
|
|
39
41
|
permissionId,
|
|
40
42
|
...(optionalArgs || {})
|
|
41
43
|
};
|
|
@@ -47,7 +49,8 @@ class FakeAdvDrivePermissions extends FakeAdvResource {
|
|
|
47
49
|
const { nargs, matchThrow } = signatureArgs(arguments, "Drive.Permissions.list");
|
|
48
50
|
if (nargs < 1 || nargs > 2) matchThrow();
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
ScriptApp.__behavior.isAccessible(fileId, 'Drive', 'read');
|
|
53
|
+
const params = { fileId, ...(optionalArgs || {}) };
|
|
51
54
|
const { data } = this._call('list', params);
|
|
52
55
|
return data;
|
|
53
56
|
}
|
|
@@ -877,13 +877,6 @@ class FakeAdvSheets {
|
|
|
877
877
|
// exposes cache performance to tests
|
|
878
878
|
__getSheetsPerformance() {
|
|
879
879
|
return sheetsCacher.getPerformance()
|
|
880
|
-
}
|
|
881
|
-
// in sandbox mode only files created in this instance are
|
|
882
|
-
__allowed(id) {
|
|
883
|
-
if (!ScriptApp.__behavior.isAccessible(id)) {
|
|
884
|
-
throw new Error(`Access to document ${id} is not allowed in sandbox mode`);
|
|
885
|
-
}
|
|
886
|
-
return id
|
|
887
880
|
}
|
|
888
881
|
__addAllowed(id) {
|
|
889
882
|
if (ScriptApp.__behavior.sandBoxMode) {
|
|
@@ -32,8 +32,9 @@ class FakeAdvSheetsDeveloperMetadata extends FakeAdvResource {
|
|
|
32
32
|
* @returns {object} DeveloperMetadata
|
|
33
33
|
*/
|
|
34
34
|
get(spreadsheetId, metadataId) {
|
|
35
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'read');
|
|
35
36
|
const { response, data } = this._call("get", {
|
|
36
|
-
spreadsheetId
|
|
37
|
+
spreadsheetId,
|
|
37
38
|
metadataId,
|
|
38
39
|
}, null, 'developerMetadata');
|
|
39
40
|
ssError(response, `spreadsheets.developerMetadata.get`);
|
|
@@ -41,8 +42,9 @@ class FakeAdvSheetsDeveloperMetadata extends FakeAdvResource {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
search(requestBody, spreadsheetId) {
|
|
45
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'read');
|
|
44
46
|
const { response, data } = this._call("search", {
|
|
45
|
-
spreadsheetId
|
|
47
|
+
spreadsheetId,
|
|
46
48
|
requestBody
|
|
47
49
|
}, null, 'developerMetadata');
|
|
48
50
|
|
|
@@ -18,10 +18,9 @@ class FakeAdvSheetsSpreadsheets extends FakeAdvResource {
|
|
|
18
18
|
super(sheets, 'spreadsheets', Syncit.fxSheets);
|
|
19
19
|
this.sheets = sheets
|
|
20
20
|
this.__fakeObjectType = "Sheets.Spreadsheets";
|
|
21
|
-
this.sheets = sheets
|
|
22
21
|
const props = [
|
|
23
22
|
'getByDataFilter',
|
|
24
|
-
|
|
23
|
+
]
|
|
25
24
|
|
|
26
25
|
props.forEach(f => {
|
|
27
26
|
this[f] = () => {
|
|
@@ -56,11 +55,24 @@ class FakeAdvSheetsSpreadsheets extends FakeAdvResource {
|
|
|
56
55
|
*/
|
|
57
56
|
__batchUpdate(requests, spreadsheetId, options, { ss = true } = {}) {
|
|
58
57
|
|
|
58
|
+
// sandbox check
|
|
59
|
+
if (requests && requests.requests) {
|
|
60
|
+
const hasWriteRequest = requests.requests.some(r => !r.deleteSheet);
|
|
61
|
+
const hasTrashRequest = requests.requests.some(r => r.deleteSheet);
|
|
62
|
+
|
|
63
|
+
if (hasWriteRequest) {
|
|
64
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'write');
|
|
65
|
+
}
|
|
66
|
+
if (hasTrashRequest) {
|
|
67
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'trash');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
// this is wrapper for batchupdate so we can alter the behavior depending on whether we're being called by spreadsheetapp
|
|
60
72
|
// note that in GAS adv sheet service doesnt take the requestBody parameter - it just sends requests as the arg
|
|
61
73
|
// so we need to wrap that in requestbody for the Node API
|
|
62
74
|
const { response, data } = this._call("batchUpdate", {
|
|
63
|
-
spreadsheetId
|
|
75
|
+
spreadsheetId,
|
|
64
76
|
requestBody: requests
|
|
65
77
|
}, options);
|
|
66
78
|
|
|
@@ -76,7 +88,8 @@ class FakeAdvSheetsSpreadsheets extends FakeAdvResource {
|
|
|
76
88
|
* @param {object} options
|
|
77
89
|
*/
|
|
78
90
|
get(id, options, { ss = false } = {}) {
|
|
79
|
-
|
|
91
|
+
ScriptApp.__behavior.isAccessible(id, 'Sheets', 'read');
|
|
92
|
+
const params = { spreadsheetId: id, ...options };
|
|
80
93
|
const { response, data } = this._call("get", params);
|
|
81
94
|
|
|
82
95
|
// maybe we need to throw an error
|
|
@@ -96,7 +109,7 @@ class FakeAdvSheetsSpreadsheets extends FakeAdvResource {
|
|
|
96
109
|
|
|
97
110
|
// maybe we need to throw an error
|
|
98
111
|
ssError(response, "create", ss)
|
|
99
|
-
this.sheets.__addAllowed
|
|
112
|
+
this.sheets.__addAllowed(data.spreadsheetId);
|
|
100
113
|
return data
|
|
101
114
|
}
|
|
102
115
|
}
|
|
@@ -48,7 +48,8 @@ class FakeAdvSheetsValues extends FakeAdvResource {
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
get(spreadsheetId, range, options = {}) {
|
|
51
|
-
|
|
51
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'read');
|
|
52
|
+
const params = { spreadsheetId, range, ...options };
|
|
52
53
|
const { response, data } = this._call("get", params, null, 'values');
|
|
53
54
|
// maybe we need to throw an error
|
|
54
55
|
ssError(response, "get")
|
|
@@ -56,9 +57,10 @@ class FakeAdvSheetsValues extends FakeAdvResource {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
batchUpdate(requests, spreadsheetId, { ss = false } = {}) {
|
|
60
|
+
ScriptApp.__behavior.isAccessible(spreadsheetId, 'Sheets', 'write');
|
|
59
61
|
const requestBody = requests
|
|
60
62
|
const { response, data } = this._call("batchUpdate", {
|
|
61
|
-
spreadsheetId
|
|
63
|
+
spreadsheetId,
|
|
62
64
|
requestBody
|
|
63
65
|
}, null, 'values');
|
|
64
66
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FakeAdvResource } from '../common/fakeadvresource.js';
|
|
2
2
|
import { Syncit } from '../../support/syncit.js';
|
|
3
|
+
import { gError } from '../../support/helpers.js';
|
|
3
4
|
import { slidesCacher } from '../../support/slidescacher.js';
|
|
4
5
|
import { Proxies } from '../../support/proxies.js';
|
|
5
6
|
|
|
@@ -13,9 +14,11 @@ class FakeAdvSlidesPresentations extends FakeAdvResource {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// Override 'get' to use the caching-enabled function fxSlidesGet.
|
|
16
|
-
get(presentationId) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
get(presentationId, options) {
|
|
18
|
+
ScriptApp.__behavior.isAccessible(presentationId, 'Slides', 'read');
|
|
19
|
+
const { response, data } = this._call('get', { presentationId, ...options }, Syncit.fxSlidesGet);
|
|
20
|
+
gError(response, 'slides.presentations', 'get');
|
|
21
|
+
return data;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
// Signature matches Apps Script advanced service.
|
|
@@ -30,10 +33,13 @@ class FakeAdvSlidesPresentations extends FakeAdvResource {
|
|
|
30
33
|
|
|
31
34
|
// Signature matches Apps Script advanced service.
|
|
32
35
|
batchUpdate(requests, presentationId) {
|
|
36
|
+
// for slides, any batch update is a write
|
|
37
|
+
ScriptApp.__behavior.isAccessible(presentationId, 'Slides', 'write');
|
|
33
38
|
const result = this._call('batchUpdate', {
|
|
34
|
-
presentationId
|
|
39
|
+
presentationId,
|
|
35
40
|
resource: { requests },
|
|
36
41
|
});
|
|
42
|
+
gError(result.response, 'slides.presentations', 'batchUpdate');
|
|
37
43
|
|
|
38
44
|
// Any update should invalidate the cache for that presentation.
|
|
39
45
|
if (presentationId) {
|
|
@@ -53,14 +59,6 @@ class FakeAdvSlides {
|
|
|
53
59
|
this.Presentations = Proxies.guard(new FakeAdvSlidesPresentations(this));
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
// in sandbox mode only files created in this instance are
|
|
57
|
-
__allowed(id) {
|
|
58
|
-
|
|
59
|
-
if (!ScriptApp.__behavior.isAccessible(id)) {
|
|
60
|
-
throw new Error(`Access to slides ${id} is not allowed in sandbox mode`);
|
|
61
|
-
}
|
|
62
|
-
return id
|
|
63
|
-
}
|
|
64
62
|
__addAllowed(id) {
|
|
65
63
|
if (ScriptApp.__behavior.sandBoxMode) {
|
|
66
64
|
ScriptApp.__behavior.addFile(id);
|
|
@@ -11,13 +11,35 @@ import './elements.js'; // This ensures all element types register themselves be
|
|
|
11
11
|
|
|
12
12
|
let _app = null;
|
|
13
13
|
|
|
14
|
-
const name =
|
|
14
|
+
const name = 'DocumentApp';
|
|
15
|
+
const serviceName = 'DocumentApp';
|
|
16
|
+
|
|
15
17
|
if (typeof globalThis[name] === typeof undefined) {
|
|
16
18
|
// By importing this, we ensure all element types register themselves.
|
|
17
19
|
const getApp = () => {
|
|
18
20
|
if (!_app) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const realApp = newFakeDocumentApp();
|
|
22
|
+
|
|
23
|
+
_app = new Proxy(realApp, {
|
|
24
|
+
get(target, prop, receiver) {
|
|
25
|
+
if (prop === 'toString') {
|
|
26
|
+
return () => name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const serviceBehavior = ScriptApp.__behavior.sandboxService[serviceName];
|
|
30
|
+
|
|
31
|
+
if (!serviceBehavior.enabled) {
|
|
32
|
+
throw new Error(`${name} service is disabled by sandbox settings.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const allowedMethods = serviceBehavior.methods;
|
|
36
|
+
if (allowedMethods && typeof target[prop] === 'function' && !allowedMethods.includes(prop)) {
|
|
37
|
+
throw new Error(`Method ${name}.${prop} is not allowed by sandbox settings.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Reflect.get(...arguments);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
21
43
|
}
|
|
22
44
|
return _app;
|
|
23
45
|
};
|
|
@@ -102,6 +102,12 @@ export const findItem = (elementMap, type, startIndex, segmentId) => {
|
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
// For container elements that don't have a startIndex (like Header, Footer, Footnote),
|
|
106
|
+
// we can find them by type and segmentId alone.
|
|
107
|
+
if (is.undefined(startIndex)) {
|
|
108
|
+
return f.__type === type;
|
|
109
|
+
}
|
|
110
|
+
|
|
105
111
|
// A ListItem is a specialized Paragraph. A search for a PARAGRAPH should also find a LIST_ITEM
|
|
106
112
|
// at the given location.
|
|
107
113
|
if (type === 'PARAGRAPH') {
|
|
@@ -40,21 +40,29 @@ class FakeDocumentApp {
|
|
|
40
40
|
title: name,
|
|
41
41
|
};
|
|
42
42
|
const doc = Docs.Documents.create(resource);
|
|
43
|
+
ScriptApp.__behavior.addFile(doc.documentId);
|
|
43
44
|
return newFakeDocument(doc.documentId);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
openById(id) {
|
|
47
48
|
const { nargs, matchThrow } = signatureArgs(arguments, "DocumentApp.openById");
|
|
48
49
|
if (nargs !== 1 || !is.string(id)) matchThrow();
|
|
50
|
+
|
|
51
|
+
if (!ScriptApp.__behavior.isAccessible(id, 'DocumentApp')) {
|
|
52
|
+
throw new Error(`Access to document "${id}" is denied by sandbox rules.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
return newFakeDocument(id);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
openByUrl(url) {
|
|
53
59
|
const { nargs, matchThrow } = signatureArgs(arguments, "DocumentApp.openByUrl");
|
|
54
60
|
if (nargs !== 1 || !is.string(url)) matchThrow();
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
57
|
-
|
|
61
|
+
const match = url.match(/\/document\/d\/([a-zA-Z0-9-_]+)/);
|
|
62
|
+
if (!match || !match[1]) {
|
|
63
|
+
throw new Error(`Invalid document URL: ${url}`);
|
|
64
|
+
}
|
|
65
|
+
return this.openById(match[1]);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
getActiveDocument() {
|
|
@@ -242,6 +242,7 @@ class ShadowDocument {
|
|
|
242
242
|
__name: sectionName,
|
|
243
243
|
__twig: sectionTree,
|
|
244
244
|
// also store the original resource item
|
|
245
|
+
__segmentId: sectionId,
|
|
245
246
|
...section,
|
|
246
247
|
};
|
|
247
248
|
this.__elementMap.set(sectionName, sectionElement);
|
|
@@ -280,6 +281,7 @@ class ShadowDocument {
|
|
|
280
281
|
__type: 'FOOTNOTE',
|
|
281
282
|
__name: footnoteName,
|
|
282
283
|
__twig: footnoteTree,
|
|
284
|
+
__segmentId: footnoteId,
|
|
283
285
|
...footnote,
|
|
284
286
|
};
|
|
285
287
|
this.__elementMap.set(footnoteName, footnoteElement);
|
|
@@ -46,12 +46,6 @@ export class FakeDriveApp {
|
|
|
46
46
|
return file ? newFakeDriveFile(file) : null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* get folder by Id
|
|
51
|
-
* folders can get files
|
|
52
|
-
* @param {string} id
|
|
53
|
-
* @returns {FakeDriveFolder|null}
|
|
54
|
-
*/
|
|
55
49
|
getFolderById(id) {
|
|
56
50
|
const file = Drive.Files.get(id, {}, { allow404: true })
|
|
57
51
|
return file ? newFakeDriveFolder(file) : null
|
|
@@ -143,7 +143,7 @@ class FakeFolderApp {
|
|
|
143
143
|
const result = Syncit.fxStreamUpMedia({ fields: minFields, blob, file: { mimeType, name, ...file } })
|
|
144
144
|
const { data, response } = result
|
|
145
145
|
checkResponse(data?.id, response, false)
|
|
146
|
-
|
|
146
|
+
ScriptApp.__behavior.addFile(data.id)
|
|
147
147
|
improveFileCache(data.id, data)
|
|
148
148
|
return DriveApp.__settleClass(result.data)
|
|
149
149
|
|