diff --git a/.gitignore b/.gitignore index 555837a8..ddb30b06 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ assets/components/formit/tmp _packages/*.* !_packages/.gitignore !_packages/*.zip + +_build/gpm_resolvers/ +_build/gpm_scripts/ diff --git a/_build/gpm.yml b/_build/gpm.yml index 8c78e823..f6702071 100644 --- a/_build/gpm.yml +++ b/_build/gpm.yml @@ -43,6 +43,9 @@ systemSettings: - key: max_chars_textfield area: formit value: 125 + - key: frontend_js + value: 'js/web/formit.js' + area: formit chunks: - name: fiDefaultEmailTpl diff --git a/assets/components/formit/action.php b/assets/components/formit/action.php new file mode 100644 index 00000000..820f3fb9 --- /dev/null +++ b/assets/components/formit/action.php @@ -0,0 +1,108 @@ +lexicon->load('formit:default'); +header('Content-Type: application/json; charset=UTF-8'); + +/* Validate ajaxToken */ +$ajaxToken = $_POST['ajaxToken'] ?? ''; +if (!preg_match('/^[a-f0-9]{32}$/', $ajaxToken)) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'message' => $modx->lexicon('formit.err_config_ns') + ]); + exit; +} + +/* Retrieve stored config: session first, then cache fallback */ +$config = $_SESSION['formit'][$ajaxToken] ?? null; +if (empty($config)) { + $config = $modx->cacheManager->get('formit/props_' . $ajaxToken); +} + +if (empty($config) || !is_array($config)) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'message' => $modx->lexicon('formit.err_config_expired') + ]); + exit; +} + +/* Set up resource context from stored pageId */ +$pageId = (int) ($config['pageId'] ?? 0); +if ($pageId && $resource = $modx->getObject(modResource::class, $pageId)) { + $modx->switchContext($resource->get('context_key')); + $modx->resource = $resource; +} + +/* Parser is not auto-initialized in API mode, but required by hooks that process chunks */ +$modx->getParser(); + +/* Run FormIt — same flow as snippet.formit.php */ +$fi = new FormIt($modx, $config); +$fi->returnOutput = true; + +$fi->initialize($modx->context->get('key')); +$fi->loadRequest(); + +$fields = $fi->request->prepare(); +$output = $fi->request->handle($fields); + +/* Build JSON response */ +$response = ['success' => true]; + +if ($fi->hasErrors()) { + $response['success'] = false; + $response['errors'] = $fi->getErrors(); +} + +if ($fi->postHooks) { + $url = $fi->postHooks->getRedirectUrl(); + if (!empty($url)) { + $response['redirect_url'] = $url; + } +} + +/* Collect all placeholders for AJAX response */ +$prefix = $fi->config['placeholderPrefix']; +$prefixLen = strlen($prefix); + +$placeholders = []; +foreach ($modx->placeholders as $key => $value) { + if (strpos($key, $prefix) === 0) { + /* Remove prefix for response */ + $placeholders[substr($key, $prefixLen)] = $value; + } +} +$response['placeholders'] = $placeholders; + +if (ob_get_level() > 0) { + ob_clean(); +} +echo json_encode($response, JSON_UNESCAPED_UNICODE); +exit; diff --git a/assets/components/formit/js/web/formit.js b/assets/components/formit/js/web/formit.js new file mode 100644 index 00000000..8baf7a8e --- /dev/null +++ b/assets/components/formit/js/web/formit.js @@ -0,0 +1,282 @@ +/** + * FormIt + * + * Client-side functionality for FormIt forms. + * + * @package formit + */ +(function (window, document) { + 'use strict'; + + /** + * @param {HTMLFormElement} form + * @param {Object} [options] + * @constructor + */ + function FormIt(form, options) { + if (!(form instanceof HTMLFormElement)) { + console.error('[FormIt] First argument must be a form element.'); + return; + } + + this.form = form; + this.options = {}; + + for (var key in FormIt.defaults) { + if (FormIt.defaults.hasOwnProperty(key)) { + this.options[key] = FormIt.defaults[key]; + } + } + + if (options) { + for (var key in options) { + if (options.hasOwnProperty(key)) { + this.options[key] = options[key]; + } + } + } + + this._lastSubmitter = null; + this.form.addEventListener('click', this._onClickSubmit.bind(this)); + this.form.addEventListener('submit', this._onSubmit.bind(this)); + } + + /** + * Track which submit button was clicked. + * @param {MouseEvent} e + * @private + */ + FormIt.prototype._onClickSubmit = function (e) { + var btn = e.target.closest('[type="submit"]'); + this._lastSubmitter = btn && btn.name ? btn : null; + }; + + /** + * Global defaults. actionUrl is set by PHP via regClientScript. + */ + FormIt.defaults = { + actionUrl: '', + clearOnSuccess: true, + onBeforeSubmit: null, + onSuccess: null, + onError: null, + onComplete: null, + onRedirect: null + }; + + /** + * @param {SubmitEvent} e + * @private + */ + FormIt.prototype._onSubmit = function (e) { + if (!this.options.actionUrl) { + console.warn('[FormIt] actionUrl is not configured. Falling back to standard form submission.'); + return; + } + + e.preventDefault(); + + // beforesubmit event (cancelable) + var beforeEvent = this._dispatch('formit:beforesubmit', { form: this.form }, true); + if (beforeEvent.defaultPrevented) return; + + // callback + if (typeof this.options.onBeforeSubmit === 'function') { + if (this.options.onBeforeSubmit(this.form) === false) return; + } + + this._clearMessages(); + this._setLoading(true); + + var submitter = e.submitter || this._lastSubmitter || this.form.querySelector('[type="submit"]'); + var formData = new FormData(this.form); + + // Include the submit button so server-side submitVar check works + if (submitter && submitter.name) { + formData.append(submitter.name, submitter.value || ''); + } + + // Add ajaxToken from data-attribute + var token = this.form.getAttribute('data-formit-ajax-token'); + if (token) { + formData.append('ajaxToken', token); + } + + var self = this; + + fetch(this.options.actionUrl, { + method: 'POST', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + body: formData + }) + .then(function (response) { + if (!response.ok) { + return response.text().then(function (text) { + throw new Error('[FormIt] Server returned HTTP ' + response.status + ': ' + text.substring(0, 200)); + }); + } + + var contentType = response.headers.get('Content-Type') || ''; + if (contentType.indexOf('application/json') === -1) { + return response.text().then(function (text) { + throw new Error('[FormIt] Expected JSON but received ' + contentType + ': ' + text.substring(0, 200)); + }); + } + + return response.json(); + }) + .then(function (data) { + self._handleResponse(data); + }) + .catch(function (error) { + console.error('[FormIt] Request failed:', error); + + self._showMessage('[data-formit-error-message]', error.message || 'Request failed'); + + self._dispatch('formit:error', { data: null, error: error }); + if (typeof self.options.onError === 'function') { + self.options.onError(null, error); + } + }) + .finally(function () { + self._setLoading(false); + self._dispatch('formit:complete', {}); + if (typeof self.options.onComplete === 'function') { + self.options.onComplete(); + } + }); + }; + + /** + * @param {Object} data - Response from processForm() + * @private + */ + FormIt.prototype._handleResponse = function (data) { + var placeholders = data.placeholders || {}; + var hasFieldErrors = false; + + // Fill field errors + for (var key in placeholders) { + if (placeholders.hasOwnProperty(key) && key.indexOf('error.') === 0) { + hasFieldErrors = true; + var fieldName = key.substring(6); // Remove prefix "error." + var el = this.form.querySelector('[data-formit-error="' + fieldName + '"]'); + if (el) el.innerHTML = placeholders[key]; + } + } + + // Fill messages with alert fallback + this._showMessage('[data-formit-success-message]', placeholders.successMessage || ''); + this._showMessage('[data-formit-validation-error-message]', placeholders.validation_error_message || ''); + this._showMessage('[data-formit-error-message]', placeholders.error_message || ''); + + // Determine if this is an error response + var isError = !data.success || hasFieldErrors || placeholders.validation_error || placeholders.error_message; + + if (isError) { + this._dispatch('formit:error', { data: data }); + if (typeof this.options.onError === 'function') { + this.options.onError(data); + } + } else { + this._dispatch('formit:success', { data: data }); + if (typeof this.options.onSuccess === 'function') { + this.options.onSuccess(data); + } + + if (this.options.clearOnSuccess) { + this.form.reset(); + } + + // Handle redirect (cancelable) + if (data.redirect_url) { + var redirectEvent = this._dispatch('formit:redirect', { url: data.redirect_url }, true); + if (redirectEvent.defaultPrevented) return; + + if (typeof this.options.onRedirect === 'function') { + if (this.options.onRedirect(data.redirect_url) === false) return; + } + + window.location.href = data.redirect_url; + } + } + }; + + /** + * Show a message in a container element or fall back to alert. + * @param {string} selector + * @param {string} message + * @private + */ + FormIt.prototype._showMessage = function (selector, message) { + if (!message) return; + var el = this.form.querySelector(selector); + if (el) { + el.innerHTML = message; + } else { + alert(message); + } + }; + + /** + * Clear all message and error elements in the form. + * @private + */ + FormIt.prototype._clearMessages = function () { + this.form.querySelectorAll('[data-formit-error]').forEach(function (el) { + el.textContent = ''; + }); + this.form.querySelectorAll('[data-formit-success-message], [data-formit-validation-error-message], [data-formit-error-message]') + .forEach(function (el) { + el.textContent = ''; + }); + }; + + /** + * Toggle loading state on the form. + * @param {boolean} loading + * @private + */ + FormIt.prototype._setLoading = function (loading) { + if (loading) { + this.form.classList.add('formit-loading'); + } else { + this.form.classList.remove('formit-loading'); + } + + var buttons = this.form.querySelectorAll('[type="submit"]'); + for (var i = 0; i < buttons.length; i++) { + buttons[i].disabled = loading; + } + }; + + /** + * Dispatch a CustomEvent on the form element. + * @param {string} name + * @param {Object} detail + * @param {boolean} [cancelable] + * @returns {CustomEvent} + * @private + */ + FormIt.prototype._dispatch = function (name, detail, cancelable) { + var event = new CustomEvent(name, { + detail: detail, + bubbles: true, + cancelable: !!cancelable + }); + this.form.dispatchEvent(event); + return event; + }; + + // Auto-initialize + document.addEventListener('DOMContentLoaded', function () { + var forms = document.querySelectorAll('form[data-formit-ajax-token]'); + for (var i = 0; i < forms.length; i++) { + new FormIt(forms[i]); + } + }); + + // Expose globally + window.FormIt = FormIt; + +})(window, document); diff --git a/core/components/formit/lexicon/cs/default.inc.php b/core/components/formit/lexicon/cs/default.inc.php index 8518bbd1..89a63576 100644 --- a/core/components/formit/lexicon/cs/default.inc.php +++ b/core/components/formit/lexicon/cs/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'Nebyla zadána konfigurace formuláře.'; +$_lang['formit.err_config_expired'] = 'Konfigurace formuláře vypršela. Obnovte prosím stránku.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/de/default.inc.php b/core/components/formit/lexicon/de/default.inc.php index 01afb849..34956634 100644 --- a/core/components/formit/lexicon/de/default.inc.php +++ b/core/components/formit/lexicon/de/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'Keine Formularkonfiguration angegeben.'; +$_lang['formit.err_config_expired'] = 'Formularkonfiguration abgelaufen. Bitte laden Sie die Seite neu.'; + $_lang['formit.migrate'] = 'Verschlüsselte Daten aus versendeten Formularen migrieren'; $_lang['formit.migrate_desc'] = 'Beim Upgrade auf FormIt 3.0 wird auch die Verschlüsselungsmethode geändert, die für die Verschlüsselung der Daten aus versendeten Formularen verwendet wird. FormIt 2.x verwendete mcrypt für die Ver- und Entschlüsselung, Version 3.0 dagegen verwendet die OpenSSL-Methoden. Damit dies korrekt funktioniert, müssen die bereits verschlüsselten Formulardaten von mcrypt zu OpenSSL migriert werden.'; $_lang['formit.migrate_alert'] = 'FormIt wurde upgedatet, aber Ihre verschlüsselten Formulardaten müssen migriert werden. Klicken Sie hier, um die Migration zu starten.'; diff --git a/core/components/formit/lexicon/en/default.inc.php b/core/components/formit/lexicon/en/default.inc.php index 414bf7ac..9f6268fd 100644 --- a/core/components/formit/lexicon/en/default.inc.php +++ b/core/components/formit/lexicon/en/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find Media Source! Media Source ID is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'No form configuration specified.'; +$_lang['formit.err_config_expired'] = 'Form configuration expired. Please reload the page.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/es/default.inc.php b/core/components/formit/lexicon/es/default.inc.php index 4561c6c7..51e7d1fe 100644 --- a/core/components/formit/lexicon/es/default.inc.php +++ b/core/components/formit/lexicon/es/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'No se especificó configuración del formulario.'; +$_lang['formit.err_config_expired'] = 'La configuración del formulario ha expirado. Por favor, recargue la página.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/fr/default.inc.php b/core/components/formit/lexicon/fr/default.inc.php index adc5c9f6..21852a22 100644 --- a/core/components/formit/lexicon/fr/default.inc.php +++ b/core/components/formit/lexicon/fr/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'Aucune configuration de formulaire spécifiée.'; +$_lang['formit.err_config_expired'] = 'La configuration du formulaire a expiré. Veuillez recharger la page.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/it/default.inc.php b/core/components/formit/lexicon/it/default.inc.php index 44914fb0..cbff2d32 100644 --- a/core/components/formit/lexicon/it/default.inc.php +++ b/core/components/formit/lexicon/it/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'Nessuna configurazione del modulo specificata.'; +$_lang['formit.err_config_expired'] = 'Configurazione del modulo scaduta. Si prega di ricaricare la pagina.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/nl/default.inc.php b/core/components/formit/lexicon/nl/default.inc.php index 08ee48be..de914e29 100644 --- a/core/components/formit/lexicon/nl/default.inc.php +++ b/core/components/formit/lexicon/nl/default.inc.php @@ -102,3 +102,6 @@ $_lang['formit.all_group_text'] = 'Alle landen'; $_lang['formit.storeAttachment_mediasource_error'] = 'Kan Mediabron niet vinden! Mediabron ID is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is niet schrijfbaar! Controleer de machtigingen voor: '; + +$_lang['formit.err_config_ns'] = 'Geen formulierconfiguratie opgegeven.'; +$_lang['formit.err_config_expired'] = 'Formulierconfiguratie verlopen. Herlaad de pagina.'; diff --git a/core/components/formit/lexicon/pl/default.inc.php b/core/components/formit/lexicon/pl/default.inc.php index a9862745..82f41505 100644 --- a/core/components/formit/lexicon/pl/default.inc.php +++ b/core/components/formit/lexicon/pl/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Cant find MediaSource! Mediasource id is: '; $_lang['formit.storeAttachment_access_error'] = 'Directory is not writable! Check the permissions for: '; +$_lang['formit.err_config_ns'] = 'Nie określono konfiguracji formularza.'; +$_lang['formit.err_config_expired'] = 'Konfiguracja formularza wygasła. Proszę odświeżyć stronę.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/ru/default.inc.php b/core/components/formit/lexicon/ru/default.inc.php index 70f2e8a1..17e9677c 100644 --- a/core/components/formit/lexicon/ru/default.inc.php +++ b/core/components/formit/lexicon/ru/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Источник медиа (Media Source) не найден! ID источника: '; $_lang['formit.storeAttachment_access_error'] = 'Папка не доступна для загрузки! Проверьте права на папку: '; +$_lang['formit.err_config_ns'] = 'Конфигурация формы не указана.'; +$_lang['formit.err_config_expired'] = 'Конфигурация формы устарела. Пожалуйста, перезагрузите страницу.'; + $_lang['formit.migrate'] = 'Перенос данных с зашифрованных форм'; $_lang['formit.migrate_desc'] = 'Обновление до FormIt 3.0 также обновит метод шифрования, используемый для шифрования отправленных данных форм. FormIt 2.x использует mcrypt для шифрования и дешифрования, а в 3.0 использует методы openssl. Для правильной работы зашифрованные в настоящее время формы необходимо перенести из mcrypt в openssl.'; $_lang['formit.migrate_alert'] = 'FormIt успешно обновлен, но отправленные вами зашифрованные формы необходимо перенести. Нажмите сюда, чтобы начать перенос.'; diff --git a/core/components/formit/lexicon/sv/default.inc.php b/core/components/formit/lexicon/sv/default.inc.php index 73dfff00..27a95f44 100644 --- a/core/components/formit/lexicon/sv/default.inc.php +++ b/core/components/formit/lexicon/sv/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Kunde inte hitta Mediakällan! Mediakällans id är: '; $_lang['formit.storeAttachment_access_error'] = 'Katalogen är inte skrivbar! Kontrollera behörigheterna för: '; +$_lang['formit.err_config_ns'] = 'Ingen formulärkonfiguration angiven.'; +$_lang['formit.err_config_expired'] = 'Formulärkonfigurationen har gått ut. Vänligen ladda om sidan.'; + $_lang['formit.migrate'] = 'Migrate encrypted form submissions'; $_lang['formit.migrate_desc'] = 'Upgrading to FormIt 3.0 will also update the encryption method used for encrypting submitted form data. FormIt 2.x used mcrypt for encrypting and decrypting, but 3.0 uses the openssl methods. For this to work correctly the currently encrypted forms need to be migrated from mcrypt to openssl.'; $_lang['formit.migrate_alert'] = 'FormIt was updated, but your encrypted form submissions need to be migrated. Click here to start the migration.'; diff --git a/core/components/formit/lexicon/uk/default.inc.php b/core/components/formit/lexicon/uk/default.inc.php index 916fa18b..fa2a5d73 100644 --- a/core/components/formit/lexicon/uk/default.inc.php +++ b/core/components/formit/lexicon/uk/default.inc.php @@ -103,6 +103,9 @@ $_lang['formit.storeAttachment_mediasource_error'] = 'Джерело медіа (Media Source) не знайдено! ID джерела: '; $_lang['formit.storeAttachment_access_error'] = 'Папка недоступна для завантаження! Перевірте права на папку: '; +$_lang['formit.err_config_ns'] = 'Конфігурацію форми не вказано.'; +$_lang['formit.err_config_expired'] = 'Конфігурація форми застаріла. Будь ласка, перезавантажте сторінку.'; + $_lang['formit.migrate'] = 'Перенесення даних з зашифрованих форм'; $_lang['formit.migrate_desc'] = 'Оновлення до FormIt 3.0 також оновить метод шифрування, який використовується для шифрування відправлених даних форм. FormIt 2.x використовує mcrypt для шифрування і дешифрування, а в 3.0 використовує методи openssl. Для правильної роботи вже зашифровані форми необхідно перенести з mcrypt в openssl.'; $_lang['formit.migrate_alert'] = 'FormIt успішно оновлений, але відправлені вами зашифровані форми необхідно перенести. Натисніть сюди, щоб почати передачу.'; diff --git a/core/components/formit/src/FormIt.php b/core/components/formit/src/FormIt.php index 09508ddc..67de1be6 100644 --- a/core/components/formit/src/FormIt.php +++ b/core/components/formit/src/FormIt.php @@ -275,25 +275,27 @@ public function loadHooks($type = 'post', $config = []) } /** - * Process the form and return response array - * Does not execute redirect, but add redirect_url to response + * Process the form and return response array. + * Does not execute redirect, but adds redirect_url to response. + * + * @deprecated Use action.php AJAX endpoint instead. * * @return array */ public function processForm() { + $this->modx->log(\modX::LOG_LEVEL_WARN, '[FormIt] processForm() is deprecated.'); + $this->returnOutput = true; $this->loadRequest(); $this->request->prepare(); $this->request->handle(); - // By default form is successfull $response = [ 'success' => true, - 'message' => $this->request->config['successMessage'] + 'message' => $this->request->config['successMessage'] ?? '' ]; - // Check for errors if ($this->hasErrors()) { $response['success'] = false; $response['error_count'] = count($this->errors); @@ -301,10 +303,8 @@ public function processForm() $response['errors'] = $this->getErrors(); } - // Add the form fields to output $response['fields'] = $this->request->dictionary->fields; - // Check for redirect if ($this->postHooks && $this->hasHook('redirect')) { $response['redirect_url'] = $this->postHooks->getRedirectUrl(); } diff --git a/core/components/formit/src/FormIt/Hook/Recaptcha.php b/core/components/formit/src/FormIt/Hook/Recaptcha.php index 480131d4..545abaf4 100644 --- a/core/components/formit/src/FormIt/Hook/Recaptcha.php +++ b/core/components/formit/src/FormIt/Hook/Recaptcha.php @@ -60,13 +60,13 @@ public function process() $response = $reCaptcha->checkAnswer( $_SERVER['REMOTE_ADDR'], - $_POST['recaptcha_challenge_field'], - $_POST['recaptcha_response_field'] + $_POST['recaptcha_challenge_field'] ?? '', + $_POST['recaptcha_response_field'] ?? '' ); if (!$response->is_valid) { $this->hook->addError('recaptcha', $this->modx->lexicon('recaptcha.incorrect', array( - 'error' => $response->error != 'incorrect-captcha-sol' ? $response->error : '', + 'error' => (!empty($response->error) && $response->error != 'incorrect-captcha-sol') ? $response->error : '', ))); } else { $passed = true; diff --git a/core/components/formit/src/FormIt/Request.php b/core/components/formit/src/FormIt/Request.php index 549c45d1..60be825d 100644 --- a/core/components/formit/src/FormIt/Request.php +++ b/core/components/formit/src/FormIt/Request.php @@ -2,7 +2,6 @@ namespace Sterc\FormIt; -use Sterc\FormIt\Service\Recaptcha; use Sterc\FormIt\Service\RecaptchaService; use Sterc\FormIt\Model\FormItForm; @@ -83,7 +82,7 @@ public function prepare() /* if using recaptcha, load recaptcha html */ if ($this->formit->hasHook('recaptcha')) { $this->loadReCaptcha($this->config); - if (!empty($this->reCaptcha) && $this->reCaptcha instanceof Recaptcha) { + if (!empty($this->reCaptcha) && $this->reCaptcha instanceof RecaptchaService) { $this->reCaptcha->render($this->config); } else { $this->modx->log(\modX::LOG_LEVEL_ERROR,'[FormIt] '.$this->modx->lexicon('formit.recaptcha_err_load')); @@ -140,6 +139,31 @@ public function prepare() $newForm->validateStoreAttachment($this->config); } + /* if not a form submission, store config for AJAX handling */ + if (!$this->hasSubmission()) { + $properties = $this->config; + $properties['pageId'] = $this->modx->resource ? $this->modx->resource->get('id') : null; + + $ajaxToken = md5(serialize($properties)); + + if (session_id() !== '') { + $_SESSION['formit'][$ajaxToken] = $properties; + } + $cacheTtl = (int) $this->modx->getOption('session_gc_maxlifetime', null, 604800); + $this->modx->cacheManager->set('formit/props_' . $ajaxToken, $properties, $cacheTtl); + + $this->modx->setPlaceholder($this->config['placeholderPrefix'] . 'ajaxToken', $ajaxToken); + + /* register frontend JS if configured */ + $frontendJs = $this->modx->getOption('formit.frontend_js', null, ''); + if (!empty($frontendJs)) { + $assetsUrl = $this->formit->config['assets_url']; + $this->modx->regClientScript($assetsUrl . $frontendJs); + $this->modx->regClientScript('', true); + } + } + return $this->runPreHooks(); } @@ -221,12 +245,7 @@ public function hasSubmission() public function loadReCaptcha(array $config = array()) { if (empty($this->reCaptcha)) { - if ($this->modx->loadClass('recaptcha.FormItReCaptcha', $this->config['core_path'] . '/model/formit/', true, true)) { - $this->reCaptcha = new RecaptchaService($this->formit, $config); - } else { - $this->modx->log(\modX::LOG_LEVEL_ERROR, '[FormIt] '.$this->modx->lexicon('formit.recaptcha_err_load')); - return null; - } + $this->reCaptcha = new RecaptchaService($this->formit, $config); } return $this->reCaptcha; diff --git a/core/components/formit/src/FormIt/Service/RecaptchaService.php b/core/components/formit/src/FormIt/Service/RecaptchaService.php index 04f28df7..03d37425 100644 --- a/core/components/formit/src/FormIt/Service/RecaptchaService.php +++ b/core/components/formit/src/FormIt/Service/RecaptchaService.php @@ -135,9 +135,6 @@ public function getOptions(array $scriptProperties = array()) { * @return string */ protected function error($message = '') { - $response = new \Sterc\FormIt\Service\RecaptchaResponse(); - $response->is_valid = false; - $response->error = $message; return $message; } @@ -150,33 +147,37 @@ protected function error($message = '') { * @return \Sterc\FormIt\Service\RecaptchaResponse */ public function checkAnswer ($remoteIp, $challenge, $responseField, $extraParams = array()) { + $response = new RecaptchaResponse(); + $response->is_valid = false; + if (empty($this->config[self::OPT_PRIVATE_KEY])) { - return $this->error($this->modx->lexicon('recaptcha.no_api_key')); + $response->error = $this->modx->lexicon('recaptcha.no_api_key'); + return $response; } if (empty($remoteIp)) { - return $this->error($this->modx->lexicon('recaptcha.no_remote_ip')); + $response->error = $this->modx->lexicon('recaptcha.no_remote_ip'); + return $response; } //discard spam submissions if (empty($challenge) || empty($responseField)) { - return $this->error($this->modx->lexicon('recaptcha.empty_answer')); + $response->error = $this->modx->lexicon('recaptcha.empty_answer'); + return $response; } - $response = $this->httpPost(self::VERIFY_SERVER, "/recaptcha/api/verify", array ( + $httpResponse = $this->httpPost(self::VERIFY_SERVER, "/recaptcha/api/verify", array ( 'remoteip' => $remoteIp, 'challenge' => $challenge, 'response' => $responseField, ) + $extraParams); - $answers = explode("\n", $response[1]); - $response = new \Sterc\FormIt\Service\RecaptchaResponse(); + $answers = explode("\n", $httpResponse[1] ?? ''); if (trim($answers[0]) == 'true') { $response->is_valid = true; } else { - $response->is_valid = false; - $response->error = $answers [1]; + $response->error = $answers[1] ?? ''; } return $response; }