@mcpher/gas-fakes 2.0.7 → 2.0.8

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/package.json CHANGED
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "name": "@mcpher/gas-fakes",
35
35
  "author": "bruce mcpherson",
36
- "version": "2.0.7",
36
+ "version": "2.0.8",
37
37
  "license": "MIT",
38
38
  "main": "main.js",
39
39
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
@@ -627,7 +627,7 @@ export class FakeForm {
627
627
  * @returns {string} The form URL.
628
628
  */
629
629
  getPublishedUrl() {
630
- return `https://docs.google.com/forms/d/e/${this.getId()}/viewform`;
630
+ return `https://docs.google.com/forms/d/${this.getId()}/viewform`;
631
631
  }
632
632
 
633
633
  /**
@@ -638,6 +638,62 @@ export class FakeForm {
638
638
  return this.__resource.responderUri;
639
639
  }
640
640
 
641
+ /**
642
+ * Internal method to get scraped metadata from the published form with caching.
643
+ * @returns {object} The scraped metadata or null.
644
+ */
645
+ __getScrapedMetadata() {
646
+ if (this.__scrapedMetadata) return this.__scrapedMetadata;
647
+
648
+ const formFile = this.__file;
649
+
650
+ // 1. Capture original state
651
+ const originalAccess = formFile.getSharingAccess();
652
+ const originalPermission = formFile.getSharingPermission();
653
+
654
+ try {
655
+ // 2. Open access temporarily
656
+ formFile.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
657
+
658
+ // 3. Fetch the HTML
659
+ const publishUrl = this.getPublishedUrl();
660
+ const formHtml = UrlFetchApp.fetch(publishUrl).getContentText();
661
+
662
+ const metadata = { entryMap: {} };
663
+ const fbzxMatch = formHtml.match(/name="fbzx" value="([^"]+)"/);
664
+ if (fbzxMatch) metadata.fbzx = fbzxMatch[1];
665
+
666
+ const historyMatch = formHtml.match(/name="pageHistory" value="([^"]+)"/);
667
+ if (historyMatch) metadata.pageHistory = historyMatch[1];
668
+
669
+ // Scrape entry IDs. They are usually in a script block like FB_PUBLIC_LOAD_DATA_
670
+ // or just search for entry.ID in the HTML.
671
+ // A common pattern is [123456789, "Question Title", ...]
672
+ const entryMatches = formHtml.matchAll(/entry\.(\d+)/g);
673
+ for (const match of entryMatches) {
674
+ // This is a bit crude but might help identify all available entry IDs
675
+ metadata.entryMap[match[1]] = true;
676
+ }
677
+
678
+ this.__scrapedMetadata = metadata;
679
+ return metadata;
680
+
681
+ } catch (e) {
682
+ console.warn(`[gas-fakes] Failed to fetch form metadata: ${e.message}`);
683
+ return null;
684
+ } finally {
685
+ // 4. Reset to original state
686
+ formFile.setSharing(originalAccess, originalPermission);
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Internal method to clear the scraped metadata cache.
692
+ */
693
+ __clearScrapedMetadata() {
694
+ this.__scrapedMetadata = null;
695
+ }
696
+
641
697
  /**
642
698
  * Gets the URL to respond to the form
643
699
  * https://github.com/brucemcpherson/gas-fakes/issues/111
@@ -115,21 +115,54 @@ export class FakeFormResponse {
115
115
  const responderUri = this.__form.__getResponderUri();
116
116
  const url = responderUri.replace('/viewform', '/formResponse');
117
117
  const payload = {};
118
- console.log('url', url)
119
- console.log('edit url', this.__form.getEditUrl())
120
- console.log('responder uri', this.__form.__getResponderUri())
121
- console.log('published url', this.__form.getPublishedUrl())
122
- console.log('form id', this.__form.getId())
123
-
124
118
 
125
119
  this.getItemResponses().forEach(itemResponse => {
126
120
  const item = itemResponse.getItem();
127
- const id = item.getId();
128
- payload[`entry.${id}`] = itemResponse.getResponse();
121
+ const response = itemResponse.getResponse();
122
+ const itemType = item.getType().toString();
123
+
124
+ if ((itemType === 'CHECKBOX_GRID' || itemType === 'GRID') && Array.isArray(response)) {
125
+ const gridRows = item.__resource.questionGroupItem?.questions || [];
126
+
127
+ response.forEach((rowResponse, rowIndex) => {
128
+ if (rowResponse && (Array.isArray(rowResponse) ? rowResponse.length > 0 : true)) {
129
+ const rowQuestionIdHex = gridRows[rowIndex]?.questionId;
130
+ if (rowQuestionIdHex) {
131
+ const rowQuestionIdDecimal = Utils.fromHex(rowQuestionIdHex);
132
+ payload[`entry.${rowQuestionIdDecimal}`] = rowResponse;
133
+ }
134
+ }
135
+ });
136
+ } else {
137
+ const questionIdHex = item.__resource.questionItem?.question?.questionId;
138
+ if (questionIdHex) {
139
+ const questionIdDecimal = Utils.fromHex(questionIdHex);
140
+ payload[`entry.${questionIdDecimal}`] = response;
141
+ }
142
+ }
129
143
  });
130
144
 
131
145
  if (Object.keys(payload).length > 0) {
132
-
146
+ // Dynamic page history based on actual form structure
147
+ const pageCount = this.__form.getItems(FormApp.ItemType.PAGE_BREAK).length + 1;
148
+ payload.pageHistory = Array.from({length: pageCount}, (_, i) => i).join(',');
149
+ payload.fvv = '1';
150
+
151
+ // Use cached metadata if available
152
+ const metadata = this.__form.__getScrapedMetadata();
153
+ if (metadata?.fbzx) payload.fbzx = metadata.fbzx;
154
+
155
+ // Build the payload string manually to handle multiple values for the same key (checkboxes)
156
+ const payloadParts = [];
157
+ Object.keys(payload).forEach(key => {
158
+ const value = payload[key];
159
+ if (Array.isArray(value)) {
160
+ value.forEach(v => payloadParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`));
161
+ } else {
162
+ payloadParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
163
+ }
164
+ });
165
+ const payloadString = payloadParts.join('&');
133
166
 
134
167
  // we need to do many things here to allowe access to the form as there are no formapp methods to add reponses.
135
168
  // first save the current file permissions
@@ -144,29 +177,24 @@ export class FakeFormResponse {
144
177
 
145
178
  try {
146
179
 
147
- // --- YOUR SUBMISSION LOGIC HERE ---
148
- console.log("Form is temporarily public. Submitting...");
180
+ // --- SUBMISSION LOGIC ---
149
181
  const response = UrlFetchApp.fetch(url, {
150
182
  method: 'post',
151
- payload,
183
+ payload: payloadString,
184
+ contentType: 'application/x-www-form-urlencoded',
152
185
  muteHttpExceptions: true
153
186
  });
154
187
 
155
- if (response.getContentText().includes("recorded")) {
156
- console.log("Success! Data pushed to Google Sheets.");
157
- }
158
-
159
188
  if (response?.getResponseCode() !== 200) {
160
189
  throw new Error(`Failed to submit form response: ${response.getResponseCode()}`);
161
190
  }
162
191
 
163
- } catch (e) {
164
- console.error("Submission failed: " + e.toString());
192
+ // Successful submission, clear the scraped metadata cache to get a fresh fbzx next time
193
+ this.__form.__clearScrapedMetadata();
194
+
165
195
  } finally {
166
196
  // 3. Reset to exactly how it was before
167
- // This closes the 401/403 hole immediately
168
197
  formFile.setSharing(originalAccess, originalPermission);
169
- console.log("Permissions reset to original state.");
170
198
  }
171
199
  }
172
200