@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.
Files changed (33) hide show
  1. package/gasmess/tanaike/for_simple_test.js +3 -8
  2. package/gasmess/tanaike/sample5.js +63 -0
  3. package/ghissues/image-size-inconsistency-issue.sh +46 -0
  4. package/ghissues/review-sandbox-listing-issue.sh +45 -0
  5. package/package.json +3 -3
  6. package/src/services/advdocs/fakeadvdocs.js +6 -1
  7. package/src/services/advdocs/fakeadvdocuments.js +7 -16
  8. package/src/services/advdrive/fakeadvdrivefiles.js +13 -3
  9. package/src/services/advdrive/fakeadvdrivepermissions.js +6 -3
  10. package/src/services/advsheets/fakeadvsheets.js +0 -7
  11. package/src/services/advsheets/fakeadvsheetsdevelopermetadata.js +4 -2
  12. package/src/services/advsheets/fakeadvsheetsspreadsheets.js +18 -5
  13. package/src/services/advsheets/fakeadvsheetsvalues.js +4 -2
  14. package/src/services/advslides/fakeadvslides.js +10 -12
  15. package/src/services/documentapp/app.js +25 -3
  16. package/src/services/documentapp/elementhelpers.js +6 -0
  17. package/src/services/documentapp/fakedocumentapp.js +11 -3
  18. package/src/services/documentapp/shadowdocument.js +2 -0
  19. package/src/services/driveapp/fakedriveapp.js +0 -6
  20. package/src/services/driveapp/fakedrivefile.js +2 -3
  21. package/src/services/driveapp/fakefolderapp.js +1 -1
  22. package/src/services/scriptapp/behavior.js +290 -11
  23. package/src/services/slidesapp/app.js +25 -4
  24. package/src/services/slidesapp/fakepresentation.js +3 -1
  25. package/src/services/slidesapp/fakeslidesapp.js +5 -1
  26. package/src/services/spreadsheetapp/app.js +26 -9
  27. package/src/services/spreadsheetapp/fakeprotection.js +447 -0
  28. package/src/services/spreadsheetapp/fakesheet.js +62 -6
  29. package/src/services/spreadsheetapp/fakesheetrange.js +25 -0
  30. package/src/services/spreadsheetapp/fakespreadsheet.js +41 -2
  31. package/src/services/spreadsheetapp/fakespreadsheetapp.js +9 -1
  32. package/src/support/proxies.js +14 -3
  33. 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
- const spreadsheet = SpreadsheetApp.openById("###");
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.16",
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: this.__allowed(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
- const params = {documentId: this.__allowed(documentId), ...(options || {}) };
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
- const {data} = Syncit.fxDriveGet ({ id: this.drive.__allowed(id), prop: apiProp, method: 'get', params, allow404, allowCache: true })
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
- return updateOrCreate ({method: 'update', file, blob, fileId: this.drive.__allowed(fileId) , fields, params})
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: this.drive.__allowed(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: this.drive.__allowed(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: this.drive.__allowed(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
- const params = { fileId: this.drive.__allowed(fileId), ...(optionalArgs || {}) };
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: this.sheets.__allowed(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: this.sheets.__allowed(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
- 'Sheets']
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: this.sheets.__allowed(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
- const params = { spreadsheetId: this.sheets.__allowed(id), ...options };
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 (data.spreadsheetId);
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
- const params = { spreadsheetId: this.sheets.__allowed(spreadsheetId), range, ...options };
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: this.sheets.__allowed(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
- const { data } = this._call('get', { presentationId: this.slides.__allowed(presentationId) }, Syncit.fxSlidesGet);
18
- return data;
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: this.slides.__allowed(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 = "DocumentApp";
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
- console.log('...activating proxy for', name)
20
- _app = newFakeDocumentApp();
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 id = url.match(/\/d\/(.+?)\//)[1];
56
- if (!id) throw new Error("Invalid document URL");
57
- return this.openById(id);
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
@@ -147,9 +147,8 @@ class FakeDriveFile extends FakeDriveMeta {
147
147
  name
148
148
  }, this.getId())
149
149
 
150
-
151
- return newFakeDriveFile(data)
152
-
150
+ ScriptApp.__behavior.addFile(data.id);
151
+ return newFakeDriveFile(data);
153
152
  }
154
153
 
155
154
  }
@@ -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
- Drive.__addAllowed(data.id)
146
+ ScriptApp.__behavior.addFile(data.id)
147
147
  improveFileCache(data.id, data)
148
148
  return DriveApp.__settleClass(result.data)
149
149