@gov-cy/govcy-express-services 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (32) hide show
  1. package/README.md +1051 -83
  2. package/package.json +10 -3
  3. package/src/auth/cyLoginAuth.mjs +2 -1
  4. package/src/index.mjs +20 -1
  5. package/src/middleware/cyLoginAuth.mjs +11 -1
  6. package/src/middleware/govcyCsrf.mjs +15 -1
  7. package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
  8. package/src/middleware/govcyFileUpload.mjs +36 -0
  9. package/src/middleware/govcyFileViewHandler.mjs +161 -0
  10. package/src/middleware/govcyFormsPostHandler.mjs +1 -1
  11. package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
  12. package/src/middleware/govcyPDFRender.mjs +3 -1
  13. package/src/middleware/govcyPageHandler.mjs +3 -3
  14. package/src/middleware/govcyPageRender.mjs +10 -0
  15. package/src/middleware/govcyReviewPageHandler.mjs +4 -1
  16. package/src/middleware/govcyReviewPostHandler.mjs +1 -1
  17. package/src/middleware/govcySuccessPageHandler.mjs +2 -3
  18. package/src/public/js/govcyFiles.js +299 -0
  19. package/src/public/js/govcyForms.js +19 -8
  20. package/src/resources/govcyResources.mjs +85 -4
  21. package/src/utils/govcyApiDetection.mjs +17 -0
  22. package/src/utils/govcyApiRequest.mjs +30 -5
  23. package/src/utils/govcyApiResponse.mjs +31 -0
  24. package/src/utils/govcyConstants.mjs +5 -1
  25. package/src/utils/govcyDataLayer.mjs +211 -11
  26. package/src/utils/govcyExpressions.mjs +1 -1
  27. package/src/utils/govcyFormHandling.mjs +81 -5
  28. package/src/utils/govcyHandleFiles.mjs +307 -0
  29. package/src/utils/govcyLoadSubmissionDataAPIs.mjs +10 -3
  30. package/src/utils/govcySubmitData.mjs +186 -106
  31. package/src/utils/govcyTempSave.mjs +2 -1
  32. package/src/utils/govcyValidator.mjs +7 -0
@@ -0,0 +1,161 @@
1
+ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
2
+ import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
3
+ import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
4
+ import { ALLOWED_FILE_MIME_TYPES } from "../utils/govcyConstants.mjs";
5
+ import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
6
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
7
+ import { logger } from '../utils/govcyLogger.mjs';
8
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
9
+ import { isMagicByteValid, pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
10
+
11
+ export function govcyFileViewHandler() {
12
+ return async (req, res, next) => {
13
+ try {
14
+ const { siteId, pageUrl, elementName } = req.params;
15
+
16
+ // Create a deep copy of the service to avoid modifying the original
17
+ let serviceCopy = req.serviceData;
18
+
19
+ // Get the download file configuration
20
+ const downloadCfg = serviceCopy?.site?.fileDownloadAPIEndpoint;
21
+ // Check if download file configuration is available
22
+ if (!downloadCfg?.url || !downloadCfg?.clientKey || !downloadCfg?.serviceId) {
23
+ return handleMiddlewareError(`File download APU configuration not found`, 404, next);
24
+ }
25
+
26
+ // Environment vars
27
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
28
+ let url = getEnvVariable(downloadCfg.url || "", false);
29
+ const clientKey = getEnvVariable(downloadCfg.clientKey || "", false);
30
+ const serviceId = getEnvVariable(downloadCfg.serviceId || "", false);
31
+ const dsfGtwKey = getEnvVariable(downloadCfg?.dsfgtwApiKey || "", "");
32
+ const method = (downloadCfg?.method || "GET").toLowerCase();
33
+
34
+ // Check if the upload API is configured correctly
35
+ if (!url || !clientKey) {
36
+ return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
37
+ }
38
+
39
+
40
+ // ⤵️ Find the current page based on the URL
41
+ const page = getPageConfigData(serviceCopy, pageUrl);
42
+
43
+ // deep copy the page template to avoid modifying the original
44
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
45
+
46
+ // ----- Conditional logic comes here
47
+ // ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
48
+ // const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
49
+ // if (conditionResult.result === false) {
50
+ // logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
51
+ // return handleMiddlewareError(`Page condition evaluated to true on POST — skipping form save and redirecting`, 404, next);
52
+ // }
53
+
54
+ // Validate the field: Only allow delete if the page contains a fileInput with the given name
55
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
56
+ if (!fileInputElement) {
57
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
58
+ }
59
+
60
+ //check the reference number
61
+ const referenceNo = dataLayer.getSiteLoadDataReferenceNumber(req.session, siteId);
62
+ if (!referenceNo) {
63
+ return handleMiddlewareError(`Missing submission reference number`, 404, next);
64
+ }
65
+
66
+ //get element data
67
+ const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
68
+
69
+ // If the element data is not found, return an error response
70
+ if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
71
+ return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
72
+ }
73
+
74
+ // Construct the URL with tag being the elementName
75
+ url += `/${encodeURIComponent(referenceNo)}/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
76
+
77
+ // Get the user
78
+ const user = dataLayer.getUser(req.session);
79
+ // Perform the upload request
80
+ const response = await govcyApiRequest(
81
+ method,
82
+ url,
83
+ {},
84
+ true,
85
+ user,
86
+ {
87
+ accept: "text/plain",
88
+ "client-key": clientKey,
89
+ "service-id": serviceId,
90
+ ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
91
+ },
92
+ 3,
93
+ allowSelfSignedCerts
94
+ );
95
+
96
+ // If not succeeded, handle error
97
+ if (!response?.Succeeded) {
98
+ return handleMiddlewareError(`fileDownloadAPIEndpoint returned succeeded false`, 500, next);
99
+ }
100
+
101
+ // Check if the response contains the expected data
102
+ if (!response?.Data?.contentType || !response?.Data?.fileName || !response?.Data?.base64) {
103
+ return handleMiddlewareError(`fileDownloadAPIEndpoint - Missing contentType, fileName or base64 in response data`, 500, next);
104
+ }
105
+
106
+ // Get filename
107
+ const filename = response?.Data?.fileName || "filename";
108
+ const fallbackFilename = asciiFallback(filename);
109
+ const utf8Filename = encodeRFC5987(filename);
110
+
111
+ // Decode base64 to binary
112
+ const fileBuffer = Buffer.from(response.Data.base64, 'base64');
113
+
114
+ // file type checks
115
+ // 1. Check declared mimetype
116
+ if (!ALLOWED_FILE_MIME_TYPES.includes(response?.Data?.contentType)) {
117
+ return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (MIME not allowed)`, 500, next);
118
+ }
119
+
120
+ // 2. Check actual file content (magic bytes) matches claimed MIME type
121
+ if (!isMagicByteValid(fileBuffer, response?.Data?.contentType)) {
122
+ return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (magic byte mismatch)`, 500, next);
123
+ }
124
+
125
+ // Check if Buffer is empty
126
+ if (!fileBuffer || fileBuffer.length === 0) {
127
+ return handleMiddlewareError(`fileDownloadAPIEndpoint - File is empty or invalid`, 500, next);
128
+ }
129
+ // Send the file to the browser
130
+ res.type(response?.Data?.contentType);
131
+ res.setHeader(
132
+ 'Content-Disposition',
133
+ `inline; filename="${fallbackFilename}"; filename*=UTF-8''${utf8Filename}`
134
+ );
135
+ res.send(fileBuffer);
136
+
137
+ } catch (error) {
138
+ logger.error("Error in govcyViewFileHandler middleware:", error.message);
139
+ return next(error); // Pass the error to the next middleware
140
+ }
141
+ };
142
+ }
143
+
144
+ //------------------------------------------------------------------------------
145
+ // Helper functions
146
+ // rfc5987 encoding for filename*
147
+ function encodeRFC5987(str) {
148
+ return encodeURIComponent(str)
149
+ .replace(/['()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
150
+ .replace(/%(7C|60|5E)/g, (m, p) => '%' + p); // | ` ^
151
+ }
152
+
153
+ // ASCII fallback for old browsers
154
+ function asciiFallback(str) {
155
+ return str
156
+ .replace(/[^\x20-\x7E]/g, '') // strip non-ASCII
157
+ .replace(/[/\\?%*:|"<>]/g, '-') // reserved chars
158
+ .replace(/[\r\n]/g, ' ') // drop newlines
159
+ .replace(/"/g, "'") // no quotes
160
+ || 'download';
161
+ }
@@ -43,7 +43,7 @@ export function govcyFormsPostHandler() {
43
43
  }
44
44
 
45
45
  // const formData = req.body; // Submitted data
46
- const formData = getFormData(formElement.params.elements, req.body); // Submitted data
46
+ const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
47
47
 
48
48
  // ☑️ Start validation from top-level form elements
49
49
  const validationErrors = validateFormElements(formElement.params.elements, formData);
@@ -3,6 +3,8 @@ import * as govcyResources from "../resources/govcyResources.mjs";
3
3
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
4
4
  import { logger } from "../utils/govcyLogger.mjs";
5
5
  import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
6
+ import { errorResponse } from '../utils/govcyApiResponse.mjs';
7
+ import { isApiRequest } from '../utils/govcyApiDetection.mjs';
6
8
 
7
9
  /**
8
10
  * Middleware function to handle HTTP errors and render appropriate error pages.
@@ -49,9 +51,8 @@ export function govcyHttpErrorHandler(err, req, res, next) {
49
51
 
50
52
  res.status(statusCode);
51
53
 
52
- // Return JSON if the request expects it
53
- if (req.headers.accept && req.headers.accept.includes("application/json")) {
54
- return res.json({ error: message });
54
+ if (isApiRequest(req)) {
55
+ return res.status(statusCode).json(errorResponse(statusCode, message));
55
56
  }
56
57
 
57
58
  // Render an error page for non-JSON requests
@@ -6,6 +6,7 @@ import { logger } from "../utils/govcyLogger.mjs";
6
6
  * Middleware function to render PDFs using the GovCy Frontend Renderer.
7
7
  * This function takes the processed page data and template, and generates the final PDF response.
8
8
  */
9
+ /* c8 ignore start */
9
10
  export function govcyPDFRender() {
10
11
  return async (req, res) => {
11
12
  try {
@@ -29,4 +30,5 @@ export function govcyPDFRender() {
29
30
  res.status(500).send('Unable to generate PDF at this time.');
30
31
  }
31
32
  };
32
- }
33
+ }
34
+ /* c8 ignore end */
@@ -53,8 +53,7 @@ export function govcyPageHandler() {
53
53
  element.params.method = "POST";
54
54
  // ➕ Add CSRF token
55
55
  element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
56
- element.params.elements.push(govcyResources.staticResources.elements["govcyFormsJs"]);
57
-
56
+
58
57
  // 🔍 Find the first button with `prototypeNavigate`
59
58
  const button = element.params.elements.find(subElement =>
60
59
  // subElement.element === "button" && subElement.params.prototypeNavigate
@@ -88,7 +87,7 @@ export function govcyPageHandler() {
88
87
  }
89
88
  //--------- End of Handle Validation Errors ---------
90
89
 
91
- populateFormData(element.params.elements, theData,validationErrors);
90
+ populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang, null, req.query.route);
92
91
  // if there are validation errors, add an error summary
93
92
  if (validationErrors?.errorSummary?.length > 0) {
94
93
  element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
@@ -117,3 +116,4 @@ export function govcyPageHandler() {
117
116
  }
118
117
  };
119
118
  }
119
+
@@ -1,4 +1,5 @@
1
1
  import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
2
+ import * as govcyResources from "../resources/govcyResources.mjs";
2
3
 
3
4
  /**
4
5
  * Middleware function to render pages using the GovCy Frontend Renderer.
@@ -6,8 +7,17 @@ import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
6
7
  */
7
8
  export function renderGovcyPage() {
8
9
  return (req, res) => {
10
+ const afterBody = {
11
+ name: "afterBody",
12
+ elements: [
13
+ govcyResources.staticResources.elements["govcyLoadingOverlay"],
14
+ govcyResources.staticResources.elements["govcyFormsJs"]
15
+ ]
16
+ };
17
+ // Initialize the renderer
9
18
  const renderer = new govcyFrontendRenderer();
10
19
  const { processedPage } = req;
20
+ processedPage.pageTemplate.sections.push(afterBody);
11
21
  const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
12
22
  res.send(html);
13
23
  };
@@ -79,7 +79,10 @@ export function govcyReviewPageHandler() {
79
79
  //--------- End Handle Validation Errors ---------
80
80
 
81
81
  // Add elements to the main section, the H1, summary list, the submit button and the JS
82
- mainElements.push(pageH1, summaryList, submitButton, govcyResources.staticResources.elements["govcyFormsJs"]);
82
+ mainElements.push(pageH1,
83
+ summaryList,
84
+ submitButton
85
+ );
83
86
  // Append generated summary list to the page template
84
87
  pageTemplate.sections.push({ name: "main", elements: mainElements });
85
88
 
@@ -118,7 +118,7 @@ export function govcyReviewPostHandler() {
118
118
 
119
119
  //-- Send email to user
120
120
  // Generate the email body
121
- let emailBody = generateSubmitEmail(service, submissionData.print_friendly_data, referenceNo, req);
121
+ let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
122
122
  logger.debug("Email generated:", emailBody);
123
123
  // Send the email
124
124
  sendEmail(service.site.title[service.site.lang],emailBody,[dataLayer.getUser(req.session).email], "eMail").catch(err => {
@@ -78,7 +78,7 @@ export function govcySuccessPageHandler(isPDF = false) {
78
78
  }
79
79
  }
80
80
 
81
- let summaryList = submissionData.renderer_data;
81
+ let summaryList = submissionData.rendererData;
82
82
 
83
83
  let mainElements = [];
84
84
  // Add elements to the main section
@@ -87,8 +87,7 @@ export function govcySuccessPageHandler(isPDF = false) {
87
87
  weHaveSendYouAnEmail,
88
88
  pdfLink,
89
89
  theDataFromYourRequest,
90
- summaryList,
91
- govcyResources.staticResources.elements["govcyFormsJs"]
90
+ summaryList
92
91
  );
93
92
  // Append generated summary list to the page template
94
93
  pageTemplate.sections.push({ name: "main", elements: mainElements });
@@ -0,0 +1,299 @@
1
+ // 🔍 Select all file inputs that have the .govcy-file-upload class
2
+ var fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
3
+
4
+ // select overlay and app root elements
5
+ var _govcyOverlay = document.getElementById("govcy--loadingOverlay");
6
+ var _govcyAppRoot = document.getElementById("govcy--body");
7
+
8
+ // Accessibility: Keep track of previously focused element and disabled elements
9
+ var _govcyPrevFocus = null;
10
+ var _govcyDisabledEls = [];
11
+
12
+ // 🔁 Loop over each file input and attach a change event listener
13
+ fileInputs.forEach(function(input) {
14
+ input.addEventListener('change', _uploadFileEventHandler);
15
+ });
16
+
17
+ /**
18
+ * Disables all focusable elements within a given root element
19
+ * @param {*} root The root element whose focusable children will be disabled
20
+ */
21
+ function disableFocusables(root) {
22
+ var sel = 'a[href],area[href],button,input,select,textarea,iframe,summary,[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
23
+ var nodes = root.querySelectorAll(sel);
24
+ _govcyDisabledEls = [];
25
+ for (var i = 0; i < nodes.length; i++) {
26
+ var el = nodes[i];
27
+ if (_govcyOverlay.contains(el)) continue; // don’t disable overlay itself
28
+ var prev = el.getAttribute('tabindex');
29
+ el.setAttribute('data-prev-tabindex', prev === null ? '' : prev);
30
+ el.setAttribute('tabindex', '-1');
31
+ _govcyDisabledEls.push(el);
32
+ }
33
+ root.setAttribute('aria-hidden', 'true'); // hide from AT on fallback
34
+ root.setAttribute('aria-busy', 'true');
35
+ }
36
+
37
+ /**
38
+ * Restores all focusable elements within a given root element
39
+ * @param {*} root The root element whose focusable children will be restored
40
+ */
41
+ function restoreFocusables(root) {
42
+ for (var i = 0; i < _govcyDisabledEls.length; i++) {
43
+ var el = _govcyDisabledEls[i];
44
+ var prev = el.getAttribute('data-prev-tabindex');
45
+ if (prev === '') el.removeAttribute('tabindex'); else el.setAttribute('tabindex', prev);
46
+ el.removeAttribute('data-prev-tabindex');
47
+ }
48
+ _govcyDisabledEls = [];
49
+ root.removeAttribute('aria-hidden');
50
+ root.removeAttribute('aria-busy');
51
+ }
52
+
53
+ /**
54
+ * Traps tab key navigation within the overlay
55
+ * @param {*} e The event
56
+ * @returns
57
+ */
58
+ function trapTab(e) {
59
+ if (e.key !== 'Tab') return;
60
+ var focusables = _govcyOverlay.querySelectorAll('a[href],button,input,select,textarea,[tabindex]:not([tabindex="-1"])');
61
+ if (focusables.length === 0) { e.preventDefault(); _govcyOverlay.focus(); return; }
62
+ var first = focusables[0], last = focusables[focusables.length - 1];
63
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
64
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
65
+ }
66
+
67
+ /**
68
+ * Shows the loading spinner overlay and traps focus within it
69
+ */
70
+ function showLoadingSpinner() {
71
+ _govcyPrevFocus = document.activeElement;
72
+ _govcyOverlay.setAttribute('aria-hidden', 'false');
73
+ _govcyOverlay.setAttribute('tabindex', '-1');
74
+ _govcyOverlay.style.display = 'flex';
75
+ document.documentElement.style.overflow = 'hidden';
76
+
77
+ if ('inert' in HTMLElement.prototype) { // progressive enhancement
78
+ _govcyAppRoot.inert = true;
79
+ } else {
80
+ disableFocusables(_govcyAppRoot);
81
+ document.addEventListener('keydown', trapTab, true);
82
+ }
83
+ _govcyOverlay.focus();
84
+ }
85
+
86
+ /**
87
+ * Hides the loading spinner overlay and restores focus to the previously focused element
88
+ */
89
+ function hideLoadingSpinner() {
90
+ _govcyOverlay.style.display = 'none';
91
+ _govcyOverlay.setAttribute('aria-hidden', 'true');
92
+ document.documentElement.style.overflow = '';
93
+
94
+ if ('inert' in HTMLElement.prototype) {
95
+ _govcyAppRoot.inert = false;
96
+ } else {
97
+ restoreFocusables(_govcyAppRoot);
98
+ document.removeEventListener('keydown', trapTab, true);
99
+ }
100
+ if (_govcyPrevFocus && _govcyPrevFocus.focus) _govcyPrevFocus.focus();
101
+ }
102
+
103
+
104
+ /**
105
+ * Handles the upload of a file event
106
+ *
107
+ * @param {object} event The event
108
+ */
109
+ function _uploadFileEventHandler(event) {
110
+ var input = event.target;
111
+ var messages = {
112
+ "uploadSuccesful": {
113
+ "el": "Το αρχείο ανεβαστηκε",
114
+ "en": "File uploaded successfully",
115
+ "tr": "File uploaded successfully"
116
+ },
117
+ "uploadFailed": {
118
+ "el": "Αποτυχια ανεβασης",
119
+ "en": "File upload failed",
120
+ "tr": "File upload failed"
121
+ },
122
+ "uploadFailed406": {
123
+ "el": "Το επιλεγμένο αρχείο είναι κενό",
124
+ "en": "The selected file is empty",
125
+ "tr": "The selected file is empty"
126
+ },
127
+ "uploadFailed407": {
128
+ "el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
129
+ "en": "The selected file must be a JPG, JPEG, PNG or PDF",
130
+ "tr": "The selected file must be a JPG, JPEG, PNG or PDF"
131
+ },
132
+ "uploadFailed408": {
133
+ "el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
134
+ "en": "The selected file must be a JPG, JPEG, PNG or PDF",
135
+ "tr": "The selected file must be a JPG, JPEG, PNG or PDF"
136
+ },
137
+ "uploadFailed409": {
138
+ "el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 4MB",
139
+ "en": "The selected file must be smaller than 4MB",
140
+ "tr": "The selected file must be smaller than 4MB"
141
+ }
142
+ };
143
+
144
+ // 🔐 Get the CSRF token from a hidden input field (generated by your backend)
145
+ var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
146
+ var csrfToken = csrfEl ? csrfEl.value : '';
147
+
148
+ // 🔧 Define siteId and pageUrl (you can dynamically extract these later)
149
+ var siteId = window._govcySiteId || "";
150
+ var pageUrl = window._govcyPageUrl || "";
151
+ var lang = window._govcyLang || "el";
152
+
153
+ // 📦 Grab the selected file
154
+ var file = event.target.files[0];
155
+ var elementName = input.name; // Form field's `name` attribute
156
+ var elementId = input.id; // Form field's `id` attribute
157
+
158
+ if (!file) return; // Exit if no file was selected
159
+
160
+ // Show loading spinner
161
+ showLoadingSpinner();
162
+
163
+ // 🧵 Prepare form-data payload for the API
164
+ var formData = new FormData();
165
+ formData.append('file', file); // Attach the actual file
166
+ formData.append('elementName', elementName); // Attach the field name for backend lookup
167
+
168
+ // 🚀 CHANGED: using fetch instead of axios.post
169
+ fetch(`/apis/${siteId}/${pageUrl}/upload`, {
170
+ method: "POST",
171
+ headers: {
172
+ "X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
173
+ },
174
+ body: formData
175
+ })
176
+ .then(function(response) {
177
+ // 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
178
+ if (!response.ok) {
179
+ return response.json().then(function(errData) {
180
+ throw { response: { data: errData } };
181
+ });
182
+ }
183
+ return response.json();
184
+ })
185
+ .then(function(data) {
186
+ // ✅ Success response
187
+ var sha256 = data.Data.sha256;
188
+ var fileId = data.Data.fileId;
189
+
190
+ // 📝 Store returned metadata in hidden fields if needed
191
+ // document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
192
+ // document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
193
+
194
+ // Hide loading spinner
195
+ hideLoadingSpinner();
196
+
197
+ // Render the file view
198
+ _renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
199
+
200
+ // Accessibility: Update ARIA live region with success message
201
+ var statusRegion = document.getElementById('_govcy-upload-status');
202
+ if (statusRegion) {
203
+ setTimeout(function() {
204
+ statusRegion.textContent = messages.uploadSuccesful[lang];
205
+ }, 200);
206
+ setTimeout(function() {
207
+ statusRegion.textContent = '';
208
+ }, 5000);
209
+ }
210
+ })
211
+ .catch(function(err) {
212
+ // ⚠️ Show an error message if upload fails
213
+ var errorMessage = messages.uploadFailed;
214
+ var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
215
+
216
+ if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
217
+ errorMessage = messages["uploadFailed" + errorCode];
218
+ }
219
+
220
+ // Hide loading spinner
221
+ hideLoadingSpinner();
222
+ // Render the file input with error
223
+ _renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
224
+
225
+ // Re-bind the file input's change handler
226
+ var newInput = document.getElementById(elementId);
227
+ if (newInput) {
228
+ newInput.addEventListener('change', _uploadFileEventHandler);
229
+ }
230
+
231
+ // Accessibility: Focus on the form field
232
+ document.getElementById(elementId)?.focus();
233
+ });
234
+ }
235
+
236
+
237
+ /**
238
+ * Renders a file element in the DOM
239
+ *
240
+ * @param {string} elementState The element state. Can be "fileInput" or "fileView"
241
+ * @param {string} elementId The element id
242
+ * @param {string} elementName The element name
243
+ * @param {string} fileId The file id
244
+ * @param {string} sha256 The sha256
245
+ * @param {object} errorMessage The error message in all supported languages
246
+ */
247
+ function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
248
+
249
+ // Grab the query string part (?foo=bar&route=something)
250
+ var queryString = window.location.search;
251
+ // Parse it
252
+ var params = new URLSearchParams(queryString);
253
+ // Get the "route" value (null if not present)
254
+ var route = params.get("route");
255
+
256
+ // Create an instance of GovcyFrontendRendererBrowser
257
+ var renderer = new GovcyFrontendRendererBrowser();
258
+ var lang = window._govcyLang || "el";
259
+ // Define the input data
260
+ var inputData =
261
+ {
262
+ "site": {
263
+ "lang": lang
264
+ }
265
+ };
266
+ var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
267
+ var fileElement = fileInputMap[elementName];
268
+ fileElement.element = elementState;
269
+ if (errorMessage != null) fileElement.params.error = errorMessage;
270
+ if (fileId != null) fileElement.params.fileId = fileId;
271
+ if (sha256 != null) fileElement.params.sha256 = sha256;
272
+ if (elementState == "fileView") {
273
+ fileElement.params.visuallyHiddenText = fileElement.params.label;
274
+ // TODO: Also need to set the `view` and `download` URLs
275
+ fileElement.params.viewHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/view-file/" + elementName;
276
+ fileElement.params.viewTarget = "_blank";
277
+ fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/delete-file/" + elementName
278
+ + (route !== null ? "?route=" + encodeURIComponent(route) : "");
279
+ }
280
+ // Construct the JSONTemplate
281
+ var JSONTemplate = {
282
+ "elements": [fileElement]
283
+ };
284
+
285
+ //render HTML into string
286
+ var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
287
+ var outerElement = document.getElementById(`${elementId}-outer-control`)
288
+ || document.getElementById(`${elementId}-input-control`)
289
+ || document.getElementById(`${elementId}-view-control`);
290
+
291
+ if (outerElement) {
292
+ //remove all classes from outerElement
293
+ outerElement.className = "";
294
+ //set the id of the outerElement to `${elementId}-outer-control`
295
+ outerElement.id = `${elementId}-outer-control`;
296
+ //update DOM and initialize the JS components
297
+ renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
298
+ }
299
+ }
@@ -1,20 +1,31 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
2
  // --- Show conditionals for checked radios ---
3
- document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked').forEach(radio => {
4
- const targetId = radio.getAttribute('data-aria-controls');
5
- const targetElement = document.getElementById(targetId);
3
+ // CHANGED: NodeList.prototype.forEach is not in IE11 → use Array.prototype.forEach.call
4
+ // CHANGED: Arrow function → function
5
+ Array.prototype.forEach.call(
6
+ document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked'),
7
+ function (radio) { // CHANGED: arrow → function
8
+ // CHANGED: const → var (ES5)
9
+ var targetId = radio.getAttribute('data-aria-controls');
10
+ var targetElement = document.getElementById(targetId);
6
11
 
7
- if (targetElement) {
8
- targetElement.classList.remove('govcy-radio__conditional--hidden');
12
+ if (targetElement) {
13
+ targetElement.classList.remove('govcy-radio__conditional--hidden');
14
+ }
9
15
  }
10
- });
16
+ );
17
+
11
18
  // --- Disable submit button after form submission ---
12
- document.querySelectorAll('form').forEach(form => {
19
+ // CHANGED: NodeList.forEach Array.prototype.forEach.call
20
+ Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) { // CHANGED
13
21
  form.addEventListener('submit', function (e) {
14
- const submitButton = form.querySelector('[type="submit"]');
22
+ // CHANGED: const var (ES5)
23
+ var submitButton = form.querySelector('[type="submit"]');
15
24
  if (submitButton) {
16
25
  submitButton.disabled = true;
17
26
  submitButton.setAttribute('aria-disabled', 'true');
27
+ // (Optional) announce busy state for AT:
28
+ // submitButton.setAttribute('aria-busy', 'true'); // CHANGED: optional a11y improvement
18
29
  }
19
30
  });
20
31
  });