`, then you need to escape that in your form definition:
diff --git a/plugins/form/assets/form-styles.css b/plugins/form/assets/form-styles.css
index fc0083e..1099c9c 100644
--- a/plugins/form/assets/form-styles.css
+++ b/plugins/form/assets/form-styles.css
@@ -1,80 +1 @@
-.form-group.has-errors { background: rgba(255, 0, 0, 0.05); border: 1px solid rgba(255, 0, 0, 0.2); border-radius: 3px; margin: 0 -5px; padding: 0 5px; }
-
-.form-errors { color: #b52b27; }
-
-.form-honeybear { visibility: hidden; position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); }
-
-.form-errors p { margin: 0; }
-
-.form-input-file input { display: none; }
-
-.form-input-file .dz-default.dz-message { position: absolute; text-align: center; left: 0; right: 0; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); margin: 0; }
-
-.form-input-file.dropzone { position: relative; min-height: 70px; border-radius: 3px; margin-bottom: .85rem; border: 2px dashed #ccc; color: #aaa; padding: 0.5rem; }
-
-.form-input-file.dropzone .dz-preview { margin: 0.5rem; }
-
-.form-input-file.dropzone .dz-preview:hover { z-index: 2; }
-
-.form-input-file.dropzone .dz-preview .dz-error-message { min-width: 140px; width: auto; }
-
-.form-input-file.dropzone .dz-preview .dz-image, .form-input-file.dropzone .dz-preview.dz-file-preview .dz-image { border-radius: 3px; z-index: 1; }
-
-.form-tabs .tabs-nav { display: flex; padding-top: 1px; margin-bottom: -1px; }
-
-.form-tabs .tabs-nav a { flex: 1; transition: color 0.5s ease, background 0.5s ease; cursor: pointer; text-align: center; padding: 10px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #eee; border-radius: 5px 5px 0 0; }
-
-.form-tabs .tabs-nav a.active { border: 1px solid #eee; border-bottom: 1px solid transparent; margin: 0 -1px; }
-
-.form-tabs .tabs-nav a.active span { color: #000; }
-
-.form-tabs .tabs-nav span { display: inline-block; line-height: 1.1; }
-
-.form-tabs.subtle .tabs-nav { margin-right: 0 !important; }
-
-.form-tabs .tabs-content .tab__content { display: none; padding-top: 2rem; }
-
-.form-tabs .tabs-content .tab__content.active { display: block; }
-
-.checkboxes { display: inline-block; }
-
-.checkboxes label { display: inline; cursor: pointer; position: relative; padding: 0 0 0 20px; margin-right: 15px; }
-
-.checkboxes label:before { content: ""; display: inline-block; width: 20px; height: 20px; left: 0; margin-top: 0; margin-right: 10px; position: absolute; border-radius: 3px; border: 1px solid #e6e6e6; }
-
-.checkboxes input[type=checkbox] { display: none; }
-
-.checkboxes input[type=checkbox]:checked + label:before { content: "\2713"; font-size: 20px; line-height: 1; text-align: center; }
-
-.checkboxes.toggleable label { margin-right: 0; }
-
-.form-field-toggleable .checkboxes.toggleable { margin-right: 5px; vertical-align: middle; }
-
-.form-field-toggleable .checkboxes + label { display: inline-block; }
-
-.switch-toggle { display: inline-flex; overflow: hidden; border-radius: 3px; line-height: 35px; border: 1px solid #eee; }
-
-.switch-toggle input[type=radio] { position: absolute; visibility: hidden; display: none; }
-
-.switch-toggle label { display: inline-block; cursor: pointer; padding: 0 15px; margin: 0; white-space: nowrap; color: inherit; transition: background-color 0.5s ease; }
-
-.switch-toggle input.highlight:checked + label { background: #333; color: #fff; }
-
-.switch-toggle input:checked + label { color: #fff; background: #999; }
-
-/* Signature Pad */
-.signature-pad { position: relative; display: flex; flex-direction: column; font-size: 10px; width: 100%; height: 100%; max-width: 700px; max-height: 460px; border: 1px solid #f0f0f0; background-color: #fff; padding: 16px; }
-
-.signature-pad--body { position: relative; flex: 1; border: 1px solid #f6f6f6; min-height: 100px; }
-
-.signature-pad--body canvas { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 4px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset; }
-
-.signature-pad--footer { color: #C3C3C3; text-align: center; font-size: 1.2em; }
-
-.signature-pad--actions { display: flex; justify-content: space-between; margin-top: 8px; }
-
-[data-grav-field="array"] .form-row { display: flex; align-items: center; margin-bottom: 0.5rem; }
-
-[data-grav-field="array"] .form-row > input, [data-grav-field="array"] .form-row > textarea { margin: 0 0.5rem; display: inline-block; }
-
-/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybS1zdHlsZXMuY3NzIiwic291cmNlcyI6WyJmb3JtLXN0eWxlcy5zY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIiRmb3JtLWJvcmRlci1jb2xvcjogI2VlZTtcbiRmb3JtLWFjdGl2ZS1jb2xvcjogIzAwMDtcblxuLmZvcm0tZ3JvdXAuaGFzLWVycm9ycyB7XG4gICAgYmFja2dyb3VuZDogcmdiYSgyNTUsMCwwLDAuMDUpO1xuICAgIGJvcmRlcjogMXB4IHNvbGlkIHJnYmEoMjU1LDAsMCwwLjIpO1xuICAgIGJvcmRlci1yYWRpdXM6IDNweDtcbiAgICBtYXJnaW46IDAgLTVweDtcbiAgICBwYWRkaW5nOiAwIDVweDtcbn1cblxuLmZvcm0tZXJyb3JzIHtcbiAgICBjb2xvcjogI2I1MmIyNztcbn1cblxuLmZvcm0taG9uZXliZWFyIHtcbiAgICB2aXNpYmlsaXR5OiBoaWRkZW47XG4gICAgcG9zaXRpb246IGFic29sdXRlICFpbXBvcnRhbnQ7XG4gICAgaGVpZ2h0OiAxcHg7XG4gICAgd2lkdGg6IDFweDtcbiAgICBvdmVyZmxvdzogaGlkZGVuO1xuICAgIGNsaXA6IHJlY3QoMXB4LCAxcHgsIDFweCwgMXB4KTtcbn1cblxuLmZvcm0tZXJyb3JzIHAge1xuICAgIG1hcmdpbjogMDtcbn1cblxuLmZvcm0taW5wdXQtZmlsZSB7XG5cbiAgICBpbnB1dCB7XG4gICAgICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgfVxuXG4gICAgLmR6LWRlZmF1bHQuZHotbWVzc2FnZSB7XG4gICAgICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgICAgICBsZWZ0OiAwO1xuICAgICAgICByaWdodDogMDtcbiAgICAgICAgdG9wOiA1MCU7XG4gICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgtNTAlKTtcbiAgICAgICAgbWFyZ2luOiAwO1xuICAgIH1cblxuICAgICYuZHJvcHpvbmUge1xuICAgICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgICAgIG1pbi1oZWlnaHQ6IDcwcHg7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDNweDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogLjg1cmVtO1xuICAgICAgICBib3JkZXI6IDJweCBkYXNoZWQgI2NjYztcbiAgICAgICAgY29sb3I6ICNhYWE7XG4gICAgICAgIHBhZGRpbmc6IDAuNXJlbTtcblxuICAgICAgICAuZHotcHJldmlldyB7XG4gICAgICAgICAgICBtYXJnaW46IDAuNXJlbTtcblxuICAgICAgICAgICAgJjpob3ZlciB7XG4gICAgICAgICAgICAgICAgei1pbmRleDogMjtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLmR6LWVycm9yLW1lc3NhZ2Uge1xuICAgICAgICAgICAgICAgIG1pbi13aWR0aDogMTQwcHg7XG4gICAgICAgICAgICAgICAgd2lkdGg6IGF1dG87XG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIC5kei1pbWFnZSxcbiAgICAgICAgICAgICYuZHotZmlsZS1wcmV2aWV3IC5kei1pbWFnZSB7XG4gICAgICAgICAgICAgICAgYm9yZGVyLXJhZGl1czogM3B4O1xuICAgICAgICAgICAgICAgIHotaW5kZXg6IDE7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59XG5cblxuXG4vLyBOZXcgSlMgcG93ZXJlZCB0YWJzXG4uZm9ybS10YWJzIHtcblxuICAgIC50YWJzLW5hdiB7XG4gICAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICAgIHBhZGRpbmctdG9wOiAxcHg7XG5cbiAgICAgICAgbWFyZ2luLWJvdHRvbTogLTFweDtcblxuICAgICAgICBhIHtcbiAgICAgICAgICAgIGZsZXg6IDE7XG4gICAgICAgICAgICB0cmFuc2l0aW9uOiBjb2xvciAwLjVzIGVhc2UsIGJhY2tncm91bmQgMC41cyBlYXNlO1xuICAgICAgICAgICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICAgICAgICAgdGV4dC1hbGlnbjpjZW50ZXI7XG4gICAgICAgICAgICBwYWRkaW5nOiAxMHB4O1xuICAgICAgICAgICAgZGlzcGxheTogZmxleDtcbiAgICAgICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XG4gICAgICAgICAgICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgICAgICAgICAgIGJvcmRlci1ib3R0b206IDFweCBzb2xpZCAkZm9ybS1ib3JkZXItY29sb3I7XG4gICAgICAgICAgICBib3JkZXItcmFkaXVzOiA1cHggNXB4IDAgMDtcblxuICAgICAgICAgICAgJi5hY3RpdmUge1xuICAgICAgICAgICAgICAgIGJvcmRlcjogMXB4IHNvbGlkICRmb3JtLWJvcmRlci1jb2xvcjtcbiAgICAgICAgICAgICAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgdHJhbnNwYXJlbnQ7XG4gICAgICAgICAgICAgICAgbWFyZ2luOiAwIC0xcHg7XG5cbiAgICAgICAgICAgICAgICBzcGFuIHtcbiAgICAgICAgICAgICAgICAgICAgY29sb3I6ICRmb3JtLWFjdGl2ZS1jb2xvcjtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cblxuICAgICAgICBzcGFuIHtcbiAgICAgICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgICAgICAgIGxpbmUtaGVpZ2h0OiAxLjE7XG4gICAgICAgIH1cblxuICAgIH1cblxuICAgICYuc3VidGxlIC50YWJzLW5hdiB7XG4gICAgICAgIG1hcmdpbi1yaWdodDogMCAhaW1wb3J0YW50O1xuICAgIH1cblxuICAgIC50YWJzLWNvbnRlbnQge1xuXG4gICAgICAgIC50YWJfX2NvbnRlbnQge1xuICAgICAgICAgICAgZGlzcGxheTogbm9uZTtcbiAgICAgICAgICAgIHBhZGRpbmctdG9wOiAycmVtO1xuXG4gICAgICAgICAgICAmLmFjdGl2ZSB7XG4gICAgICAgICAgICAgICAgZGlzcGxheTogYmxvY2s7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59XG5cbi8vIENoZWNrYm94ZXNcbi5jaGVja2JveGVzIHtcbiAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG5cbiAgICBsYWJlbCB7XG4gICAgICAgIGRpc3BsYXk6IGlubGluZTtcbiAgICAgICAgY3Vyc29yOiBwb2ludGVyO1xuICAgICAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgICAgIHBhZGRpbmc6IDAgMCAwIDIwcHg7XG4gICAgICAgIG1hcmdpbi1yaWdodDogMTVweDtcblxuICAgIH1cbiAgICBsYWJlbDpiZWZvcmUge1xuICAgICAgICBjb250ZW50OlwiXCI7XG4gICAgICAgIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgICAgICAgd2lkdGg6IDIwcHg7XG4gICAgICAgIGhlaWdodDogMjBweDtcbiAgICAgICAgbGVmdDogMDtcbiAgICAgICAgbWFyZ2luLXRvcDogMDtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAxMHB4O1xuICAgICAgICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDNweDtcblxuICAgICAgICBib3JkZXI6IDFweCBzb2xpZCAjZTZlNmU2O1xuXG4gICAgfVxuICAgIGlucHV0W3R5cGU9Y2hlY2tib3hdIHtcbiAgICAgICAgZGlzcGxheTogbm9uZTtcbiAgICB9XG4gICAgaW5wdXRbdHlwZT1jaGVja2JveF06Y2hlY2tlZCArIGxhYmVsOmJlZm9yZSB7XG4gICAgICAgIGNvbnRlbnQ6XCJcXDI3MTNcIjtcbiAgICAgICAgZm9udC1zaXplOiAyMHB4O1xuICAgICAgICBsaW5lLWhlaWdodDogMTtcbiAgICAgICAgdGV4dC1hbGlnbjogY2VudGVyO1xuICAgIH1cblxuICAgICYudG9nZ2xlYWJsZSBsYWJlbHtcbiAgICAgICAgbWFyZ2luLXJpZ2h0OiAwO1xuICAgIH1cbn1cblxuLy8gVG9nZ2xlYWJsZVxuLmZvcm0tZmllbGQtdG9nZ2xlYWJsZSB7XG4gICAgLmNoZWNrYm94ZXMudG9nZ2xlYWJsZSB7XG4gICAgICAgIG1hcmdpbi1yaWdodDogNXB4O1xuICAgICAgICB2ZXJ0aWNhbC1hbGlnbjogbWlkZGxlO1xuICAgIH1cbiAgICAuY2hlY2tib3hlcyArIGxhYmVsIHtcbiAgICAgICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xuICAgIH1cbn1cblxuLy8gVG9nZ2xlc1xuLnN3aXRjaC10b2dnbGUge1xuICAgIGRpc3BsYXk6IGlubGluZS1mbGV4O1xuICAgIG92ZXJmbG93OiBoaWRkZW47XG4gICAgYm9yZGVyLXJhZGl1czogM3B4O1xuICAgIGxpbmUtaGVpZ2h0OiAzNXB4O1xuICAgIGJvcmRlcjogMXB4IHNvbGlkICRmb3JtLWJvcmRlci1jb2xvcjtcblxuICAgIGlucHV0W3R5cGU9cmFkaW9dIHtcbiAgICAgICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgICAgICB2aXNpYmlsaXR5OiBoaWRkZW47XG4gICAgICAgIGRpc3BsYXk6IG5vbmU7XG4gICAgfVxuXG4gICAgbGFiZWwge1xuICAgICAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XG4gICAgICAgIGN1cnNvcjogcG9pbnRlcjtcbiAgICAgICAgcGFkZGluZzogMCAxNXB4O1xuICAgICAgICBtYXJnaW46IDA7XG4gICAgICAgIHdoaXRlLXNwYWNlOiBub3dyYXA7XG4gICAgICAgIGNvbG9yOiBpbmhlcml0O1xuICAgICAgICB0cmFuc2l0aW9uOiBiYWNrZ3JvdW5kLWNvbG9yIDAuNXMgZWFzZTtcbiAgICB9XG5cbiAgICBpbnB1dC5oaWdobGlnaHQ6Y2hlY2tlZCArIGxhYmVsIHtcbiAgICAgICAgYmFja2dyb3VuZDogIzMzMztcbiAgICAgICAgY29sb3I6ICNmZmY7XG4gICAgfVxuXG4gICAgaW5wdXQ6Y2hlY2tlZCArIGxhYmVsIHtcbiAgICAgICAgY29sb3I6ICNmZmY7XG4gICAgICAgIGJhY2tncm91bmQ6ICM5OTk7XG4gICAgfVxuXG5cbn1cblxuLyogU2lnbmF0dXJlIFBhZCAqL1xuLnNpZ25hdHVyZS1wYWQge1xuICAgIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgICBkaXNwbGF5OiAtd2Via2l0LWJveDtcbiAgICBkaXNwbGF5OiAtbXMtZmxleGJveDtcbiAgICBkaXNwbGF5OiBmbGV4O1xuICAgIC13ZWJraXQtYm94LW9yaWVudDogdmVydGljYWw7XG4gICAgLXdlYmtpdC1ib3gtZGlyZWN0aW9uOiBub3JtYWw7XG4gICAgLW1zLWZsZXgtZGlyZWN0aW9uOiBjb2x1bW47XG4gICAgZmxleC1kaXJlY3Rpb246IGNvbHVtbjtcbiAgICBmb250LXNpemU6IDEwcHg7XG4gICAgd2lkdGg6IDEwMCU7XG4gICAgaGVpZ2h0OiAxMDAlO1xuICAgIG1heC13aWR0aDogNzAwcHg7XG4gICAgbWF4LWhlaWdodDogNDYwcHg7XG4gICAgYm9yZGVyOiAxcHggc29saWQgI2YwZjBmMDtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZmZmO1xuICAgIHBhZGRpbmc6IDE2cHg7XG59XG5cbi5zaWduYXR1cmUtcGFkLS1ib2R5IHtcbiAgICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gICAgLXdlYmtpdC1ib3gtZmxleDogMTtcbiAgICAtbXMtZmxleDogMTtcbiAgICBmbGV4OiAxO1xuICAgIGJvcmRlcjogMXB4IHNvbGlkICNmNmY2ZjY7XG4gICAgbWluLWhlaWdodDogMTAwcHg7XG59XG5cbi5zaWduYXR1cmUtcGFkLS1ib2R5IGNhbnZhcyB7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIGxlZnQ6IDA7XG4gICAgdG9wOiAwO1xuICAgIHdpZHRoOiAxMDAlO1xuICAgIGhlaWdodDogMTAwJTtcbiAgICBib3JkZXItcmFkaXVzOiA0cHg7XG4gICAgYm94LXNoYWRvdzogMCAwIDVweCByZ2JhKDAsIDAsIDAsIDAuMDIpIGluc2V0O1xufVxuXG4uc2lnbmF0dXJlLXBhZC0tZm9vdGVyIHtcbiAgICBjb2xvcjogI0MzQzNDMztcbiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgZm9udC1zaXplOiAxLjJlbTtcbn1cblxuLnNpZ25hdHVyZS1wYWQtLWFjdGlvbnMge1xuICAgIGRpc3BsYXk6IC13ZWJraXQtYm94O1xuICAgIGRpc3BsYXk6IC1tcy1mbGV4Ym94O1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgLXdlYmtpdC1ib3gtcGFjazoganVzdGlmeTtcbiAgICAtbXMtZmxleC1wYWNrOiBqdXN0aWZ5O1xuICAgIGp1c3RpZnktY29udGVudDogc3BhY2UtYmV0d2VlbjtcbiAgICBtYXJnaW4tdG9wOiA4cHg7XG59XG5cbltkYXRhLWdyYXYtZmllbGQ9XCJhcnJheVwiXSAuZm9ybS1yb3cge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgICBtYXJnaW4tYm90dG9tOiAwLjVyZW07XG59XG5cbltkYXRhLWdyYXYtZmllbGQ9XCJhcnJheVwiXSAuZm9ybS1yb3cgPiBpbnB1dCxcbltkYXRhLWdyYXYtZmllbGQ9XCJhcnJheVwiXSAuZm9ybS1yb3cgPiB0ZXh0YXJlYVxue1xuICAgIG1hcmdpbjogMCAwLjVyZW07XG4gICAgZGlzcGxheTogaW5saW5lLWJsb2NrO1xufVxuXG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBR0EsQUFBQSxXQUFXLEFBQUEsV0FBVyxDQUFDLEVBQ25CLFVBQVUsRUFBRSxxQkFBa0IsRUFDOUIsTUFBTSxFQUFFLEdBQUcsQ0FBQyxLQUFLLENBQUMsb0JBQWlCLEVBQ25DLGFBQWEsRUFBRSxHQUFHLEVBQ2xCLE1BQU0sRUFBRSxNQUFNLEVBQ2QsT0FBTyxFQUFFLEtBQUssR0FDakI7O0FBRUQsQUFBQSxZQUFZLENBQUMsRUFDVCxLQUFLLEVBQUUsT0FBTyxHQUNqQjs7QUFFRCxBQUFBLGVBQWUsQ0FBQyxFQUNaLFVBQVUsRUFBRSxNQUFNLEVBQ2xCLFFBQVEsRUFBRSxtQkFBbUIsRUFDN0IsTUFBTSxFQUFFLEdBQUcsRUFDWCxLQUFLLEVBQUUsR0FBRyxFQUNWLFFBQVEsRUFBRSxNQUFNLEVBQ2hCLElBQUksRUFBRSx3QkFBd0IsR0FDakM7O0FBRUQsQUFBQSxZQUFZLENBQUMsQ0FBQyxDQUFDLEVBQ1gsTUFBTSxFQUFFLENB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 */
+.form-group.has-errors{background:rgba(255,0,0,.05);border:1px solid rgba(255,0,0,.2);border-radius:3px;margin:0 -5px;padding:0 5px}.form-errors{color:#b52b27}.form-honeybear{visibility:hidden;position:absolute !important;height:1px;width:1px;overflow:hidden;clip:rect(1px, 1px, 1px, 1px)}.form-errors p{margin:0}.form-input-file input{display:none}.form-input-file .dz-default.dz-message{position:absolute;text-align:center;left:0;right:0;top:50%;transform:translateY(-50%);margin:0}.form-input-file.dropzone{position:relative;min-height:70px;border-radius:3px;margin-bottom:.85rem;border:2px dashed #ccc;color:#aaa;padding:.5rem}.form-input-file.dropzone .dz-preview{margin:.5rem}.form-input-file.dropzone .dz-preview:hover{z-index:2}.form-input-file.dropzone .dz-preview .dz-error-message{min-width:140px;width:auto}.form-input-file.dropzone .dz-preview .dz-image,.form-input-file.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:3px;z-index:1}.form-tabs .tabs-nav{display:flex;padding-top:1px;margin-bottom:-1px}.form-tabs .tabs-nav a{flex:1;transition:color .5s ease,background .5s ease;cursor:pointer;text-align:center;padding:10px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #ccc;border-radius:5px 5px 0 0}.form-tabs .tabs-nav a.active{border:1px solid #ccc;border-bottom:1px solid rgba(0,0,0,0);margin:0 -1px}.form-tabs .tabs-nav a.active span{color:#000}.form-tabs .tabs-nav span{display:inline-block;line-height:1.1}.form-tabs.subtle .tabs-nav{margin-right:0 !important}.form-tabs .tabs-content .tab__content{display:none;padding-top:2rem}.form-tabs .tabs-content .tab__content.active{display:block}.checkboxes{display:inline-block}.checkboxes label{display:inline;cursor:pointer;position:relative;padding:0 0 0 20px;margin-right:15px}.checkboxes label:before{content:"";display:inline-block;width:20px;height:20px;left:0;margin-top:0;margin-right:10px;position:absolute;border-radius:3px;border:1px solid #e6e6e6}.checkboxes input[type=checkbox]{display:none}.checkboxes input[type=checkbox]:checked+label:before{content:"✓";font-size:20px;line-height:1;text-align:center}.checkboxes.toggleable label{margin-right:0}.form-field-toggleable .checkboxes.toggleable{margin-right:5px;vertical-align:middle}.form-field-toggleable .checkboxes+label{display:inline-block}.switch-toggle{display:inline-flex;overflow:hidden;border-radius:3px;line-height:35px;border:1px solid #ccc}.switch-toggle input[type=radio]{position:absolute;visibility:hidden;display:none}.switch-toggle label{display:inline-block;cursor:pointer;padding:0 15px;margin:0;white-space:nowrap;color:inherit;transition:background-color .5s ease}.switch-toggle input.highlight:checked+label{background:#333;color:#fff}.switch-toggle input:checked+label{color:#fff;background:#999}.signature-pad{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:10px;width:100%;height:100%;max-width:700px;max-height:460px;border:1px solid #f0f0f0;background-color:#fff;padding:16px}.signature-pad--body{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;border:1px solid #f6f6f6;min-height:100px}.signature-pad--body canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.signature-pad--footer{color:#c3c3c3;text-align:center;font-size:1.2em}.signature-pad--actions{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-top:8px}[data-grav-field=array] .form-row{display:flex;align-items:center;margin-bottom:.5rem}[data-grav-field=array] .form-row>input,[data-grav-field=array] .form-row>textarea{margin:0 .5rem;display:inline-block}.form-data.basic-captcha .form-input-wrapper{border:1px solid #ccc;border-radius:5px;display:flex;overflow:hidden}.form-data.basic-captcha .form-input-prepend{display:flex;color:#333;background-color:#ccc;flex-shrink:0}.form-data.basic-captcha .form-input-prepend img{margin:0}.form-data.basic-captcha .form-input-prepend button>svg{margin:0 8px;width:18px;height:18px}.form-data.basic-captcha input.form-input{border:0}/*# sourceMappingURL=form-styles.css.map */
diff --git a/plugins/form/blueprints.yaml b/plugins/form/blueprints.yaml
index 4ba82a4..84f95c2 100644
--- a/plugins/form/blueprints.yaml
+++ b/plugins/form/blueprints.yaml
@@ -1,8 +1,8 @@
name: Form
slug: form
type: plugin
-version: 5.1.6
-description: Enables the forms handling
+version: 7.1.2
+description: Enables forms handling and processing
icon: check-square
author:
name: Team Grav
@@ -196,3 +196,150 @@ form:
label: PLUGIN_FORM.RECAPTCHA_SECRET_KEY
help: PLUGIN_FORM.RECAPTCHA_SECRET_KEY_HELP
default: ''
+
+ turnstile_captcha:
+ type: section
+ title: PLUGIN_FORM.TURNSTILE_CAPTCHA
+
+ fields:
+ turnstile.theme:
+ type: select
+ label: PLUGIN_FORM.RECAPTCHA_THEME
+ default: light
+ options:
+ light: PLUGIN_FORM.RECAPTCHA_THEME_LIGHT
+ dark: PLUGIN_FORM.RECAPTCHA_THEME_DARK
+ turnstile.site_key:
+ type: text
+ label: PLUGIN_FORM.RECAPTCHA_SITE_KEY
+ help: PLUGIN_FORM.RECAPTCHA_SITE_KEY_HELP
+ default: ''
+ turnstile.secret_key:
+ type: text
+ label: PLUGIN_FORM.RECAPTCHA_SECRET_KEY
+ help: PLUGIN_FORM.RECAPTCHA_SECRET_KEY_HELP
+ default: ''
+
+ basic_captcha:
+ type: section
+ title: PLUGIN_FORM.BASIC_CAPTCHA
+
+ fields:
+ basic_captcha.type:
+ type: elements
+ label: PLUGIN_FORM.BASIC_CAPTCHA_TYPE
+ default: 'characters'
+ size: medium
+ options:
+ characters: Random Characters
+ math: Math Puzzle
+ fields:
+ characters:
+ type: element
+ fields:
+ basic_captcha.chars.length:
+ type: range
+ label: PLUGIN_FORM.BASIC_CAPTCHA_LENGTH
+ default: 6
+ validate:
+ min: 4
+ max: 12
+ append: characters
+ basic_captcha.chars.font:
+ type: select
+ label: PLUGIN_FORM.BASIC_CAPTCHA_FONT
+ default: zxx-noise.ttf
+ options:
+ 'zxx-noise.ttf': zxx-Noise
+ 'zxx-xed.ttf': zxx-Xed
+ 'zxx-camo.ttf': zxx-Camo
+ 'zxx-sans.ttf': zxx-Sans
+ basic_captcha.chars.size:
+ type: range
+ label: PLUGIN_FORM.BASIC_CAPTCHA_SIZE
+ default: 24
+ append: px
+ validate:
+ min: 12
+ max: 32
+ step: 2
+ basic_captcha.chars.bg:
+ type: colorpicker
+ size: small
+ label: PLUGIN_FORM.BASIC_CAPTCHA_BG_COLOR
+ default: '#ffffff'
+ basic_captcha.chars.text:
+ type: colorpicker
+ size: small
+ label: PLUGIN_FORM.BASIC_CAPTCHA_TEXT_COLOR
+ default: '#000000'
+ basic_captcha.chars.start_x:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_START_X
+ default: 5
+ append: px
+ size: small
+ validate:
+ min: 0
+ type: number
+ basic_captcha.chars.start_y:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_START_Y
+ default: 30
+ append: px
+ size: small
+ validate:
+ min: 0
+ type: number
+ basic_captcha.chars.box_width:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_BOX_WIDTH
+ default: 135
+ append: px
+ size: small
+ validate:
+ min: 0
+ type: number
+ basic_captcha.chars.box_height:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_BOX_HEIGHT
+ default: 40
+ append: px
+ size: small
+ validate:
+ min: 0
+ type: number
+ math:
+ type: element
+ fields:
+ basic_captcha.math.min:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_MIN
+ default: 1
+ size: small
+ validate:
+ min: 0
+ type: number
+ basic_captcha.math.max:
+ type: number
+ label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_MAX
+ default: 10
+ size: small
+ validate:
+ min: 1
+ type: number
+ basic_captcha.math.operators:
+ type: selectize
+ selectize:
+ options:
+ - value: '+'
+ text: '+ Addition'
+ - value: '-'
+ text: '- Subtraction'
+ - value: '*'
+ text: 'x Multiplication'
+ - value: '/'
+ text: '/ Division'
+ label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_OPERATORS
+ validate:
+ type: commalist
diff --git a/plugins/form/classes/BasicCaptcha.php b/plugins/form/classes/BasicCaptcha.php
index 4f5a169..61bc10b 100644
--- a/plugins/form/classes/BasicCaptcha.php
+++ b/plugins/form/classes/BasicCaptcha.php
@@ -32,20 +32,20 @@ class BasicCaptcha
// calculator
if ($operator === '-') {
if ($first_num < $second_num) {
- $result = "$second_num-$first_num";
- $captcha_code = $second_num-$first_num;
+ $result = "$second_num - $first_num";
+ $captcha_code = $second_num - $first_num;
} else {
$result = "$first_num-$second_num";
$captcha_code = $first_num - $second_num;
}
} elseif ($operator === '*') {
- $result = "{$first_num}x{$second_num}";
- $captcha_code = $first_num - $second_num;
+ $result = "{$first_num} x {$second_num}";
+ $captcha_code = $first_num * $second_num;
} elseif ($operator === '/') {
- $result = "$first_num/ second_num";
+ $result = "$first_num / second_num";
$captcha_code = $first_num / $second_num;
} elseif ($operator === '+') {
- $result = "$first_num+$second_num";
+ $result = "$first_num + $second_num";
$captcha_code = $first_num + $second_num;
}
} else {
diff --git a/plugins/form/classes/Form.php b/plugins/form/classes/Form.php
index 87a3c22..a02c559 100644
--- a/plugins/form/classes/Form.php
+++ b/plugins/form/classes/Form.php
@@ -119,7 +119,7 @@ class Form implements FormInterface, ArrayAccess
$this->items = $form;
} else {
// Otherwise get all forms in the page.
- $forms = $page->forms();
+ $forms = $page->getForms();
if ($name) {
// If form with given name was found, use that.
$this->items = $forms[$name] ?? [];
diff --git a/plugins/form/classes/Forms.php b/plugins/form/classes/Forms.php
index f71bd84..8294f5c 100644
--- a/plugins/form/classes/Forms.php
+++ b/plugins/form/classes/Forms.php
@@ -113,7 +113,7 @@ class Forms
*/
protected function getPageParameters(PageInterface $page, ?string $name): array
{
- $forms = $page->forms();
+ $forms = $page->getForms();
if ($name) {
// If form with given name was found, use that.
diff --git a/plugins/form/classes/TwigExtension.php b/plugins/form/classes/TwigExtension.php
index 7074255..41d417b 100644
--- a/plugins/form/classes/TwigExtension.php
+++ b/plugins/form/classes/TwigExtension.php
@@ -90,21 +90,28 @@ class TwigExtension extends AbstractExtension
return null;
}
+ // If field has already been prepared, we do not need to do anything.
+ if (!empty($field['prepared'])) {
+ return $field;
+ }
+
// Check if we have just a list of fields (no name given).
- if (is_int($name)) {
+ $fieldName = (string)($field['name'] ?? $name);
+ if (!is_string($name) || $name === '') {
// Look at the field.name and if not set, fall back to the key.
- $name = (string)($field['name'] ?? $name);
+ $name = $fieldName;
}
// Make sure that the field has a name.
- $name = $name ?? $field['name'] ?? null;
- if (!is_string($name) || $name === '') {
+ if ($name === '') {
return null;
}
// Prefix name with the parent name if needed.
if (str_starts_with($name, '.')) {
- $name = $parent ? $parent . $name : (string)substr($name, 1);
+ $plainName = (string)substr($name, 1);
+ $field['plain_name'] = $plainName;
+ $name = $parent ? $parent . $name : $plainName;
} elseif (isset($options['key'])) {
$name = str_replace('*', $options['key'], $name);
}
@@ -125,6 +132,7 @@ class TwigExtension extends AbstractExtension
// Always set field name.
$field['name'] = $name;
+ $field['prepared'] = true;
return $field;
}
diff --git a/plugins/form/composer.lock b/plugins/form/composer.lock
index 23aa529..5ddb549 100644
--- a/plugins/form/composer.lock
+++ b/plugins/form/composer.lock
@@ -73,5 +73,5 @@
"platform-overrides": {
"php": "7.3.6"
},
- "plugin-api-version": "2.0.0"
+ "plugin-api-version": "2.2.0"
}
diff --git a/plugins/form/form.php b/plugins/form/form.php
index 5f8625b..eb19d62 100644
--- a/plugins/form/form.php
+++ b/plugins/form/form.php
@@ -4,6 +4,7 @@ namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use DateTime;
+use Doctrine\Common\Cache\Cache;
use Exception;
use Grav\Common\Data\ValidationException;
use Grav\Common\Debugger;
@@ -18,10 +19,13 @@ use Grav\Common\Utils;
use Grav\Common\Uri;
use Grav\Common\Yaml;
use Grav\Framework\Form\Interfaces\FormInterface;
+use Grav\Framework\Psr7\Response;
use Grav\Framework\Route\Route;
+use Grav\Plugin\Form\BasicCaptcha;
use Grav\Plugin\Form\Form;
use Grav\Plugin\Form\Forms;
use Grav\Plugin\Form\TwigExtension;
+use Grav\Common\HTTP\Client;
use ReCaptcha\ReCaptcha;
use ReCaptcha\RequestMethod\CurlPost;
use RecursiveArrayIterator;
@@ -31,6 +35,7 @@ use RocketTheme\Toolbox\File\YamlFile;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\Event\Event;
use RuntimeException;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Twig\Environment;
use Twig\Extension\CoreExtension;
use Twig\Extension\EscaperExtension;
@@ -54,11 +59,9 @@ class FormPlugin extends Plugin
/** @var Form */
protected $form;
- /** @var array */
+ /** @var array[]|FormInterface[] */
protected $forms = [];
- /** @var array */
- protected $flat_forms = [];
- /** @var array */
+ /** @var FormInterface[] */
protected $active_forms = [];
/** @var array */
protected $json_response = [];
@@ -70,7 +73,7 @@ class FormPlugin extends Plugin
*/
public static function checkRequirements(): bool
{
- return version_compare(GRAV_VERSION, '1.6', '>');
+ return version_compare(GRAV_VERSION, '1.7', '>');
}
/**
@@ -83,18 +86,13 @@ class FormPlugin extends Plugin
}
return [
- 'onPluginsInitialized' => [
- ['autoload', 100000],
- ['onPluginsInitialized', 0]
- ],
+ 'onPluginsInitialized' => ['onPluginsInitialized', 0],
'onTwigExtensions' => ['onTwigExtensions', 0],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0]
];
}
/**
- * [onPluginsInitialized:100000] Composer autoload.
- *
* @return ClassLoader
*/
public function autoload()
@@ -114,10 +112,9 @@ class FormPlugin extends Plugin
$this->grav['forms'] = function () {
$forms = new Forms();
-
- $grav = Grav::instance();
$event = new Event(['forms' => $forms]);
- $grav->fireEvent('onFormRegisterTypes', $event);
+
+ $this->grav->fireEvent('onFormRegisterTypes', $event);
return $forms;
};
@@ -130,12 +127,19 @@ class FormPlugin extends Plugin
return;
}
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+
// Mini Keep-Alive Logic
- $task = $this->grav['uri']->param('task');
- if ($task && $task === 'keep-alive') {
- exit;
+ $task = $uri->param('task');
+ if ($task === 'keep-alive') {
+ $response = new Response(200);
+
+ $this->grav->close($response);
}
+ $this->processBasicCaptchaImage($uri);
+
$this->enable([
'onPageProcessed' => ['onPageProcessed', 0],
'onPagesInitialized' => ['onPagesInitialized', 0],
@@ -161,16 +165,16 @@ class FormPlugin extends Plugin
/**
* Process forms after page header processing, but before caching
*
- * @param Event $e
+ * @param Event $event
* @return void
*/
- public function onPageProcessed(Event $e): void
+ public function onPageProcessed(Event $event): void
{
/** @var PageInterface $page */
- $page = $e['page'];
+ $page = $event['page'];
- $pageForms = $page->forms();
- if (!$pageForms) {
+ $forms = $page->getForms();
+ if (!$forms) {
return;
}
@@ -184,23 +188,18 @@ class FormPlugin extends Plugin
}
$parent = $current && $current !== $page ? $current : null;
- $page_route = $page->home() ? '/' : $page->route();
-
// If the form was in the modular page, we need to add the form into the parent page as well.
if ($parent) {
- $parent->addForms($pageForms);
- $parent_route = $parent->home() ? '/' : $parent->route();
+ $parent->addForms($forms);
}
- /** @var Forms $forms */
- $forms = $this->grav['forms'];
-
// Store the page forms in the forms instance
- foreach ($pageForms as $name => $form) {
- if (isset($parent, $parent_route)) {
- $this->addForm($parent_route, $forms->createPageForm($parent, $name, $form));
+ foreach ($forms as $name => $form) {
+ if ($parent) {
+ $this->addFormDefinition($parent, $name, $form);
}
- $this->addForm($page_route, $forms->createPageForm($page, $name, $form));
+
+ $this->addFormDefinition($page, $name, $form);
}
}
@@ -257,7 +256,7 @@ class FormPlugin extends Plugin
if ($form instanceof Form) {
// Post the form
$isJson = $uri->extension() === 'json';
- $task = $uri->post('task') ?? $uri->param('task');
+ $task = (string)($uri->post('task') ?? $uri->param('task'));
if ($isJson) {
if ($task === 'store-state') {
@@ -286,10 +285,13 @@ class FormPlugin extends Plugin
if ($this->json_response && $page->template() !== 'form') {
$status = $this->json_response['status'] ?? null;
- header('Content-Type: application/json');
- http_response_code($status === 'error' ? 400 : 200);
- echo json_encode($this->json_response);
- exit;
+ $response = new Response(
+ $status !== 'error' ? 200 : 400,
+ ['Content-Type' => 'application/json'],
+ json_encode($this->json_response, JSON_THROW_ON_ERROR)
+ );
+
+ $this->grav->close($response);
}
}
@@ -313,11 +315,25 @@ class FormPlugin extends Plugin
// There is no active form to be posted.
// Check all the forms for the current page; we are looking for forms with remember state turned on with random unique id.
+ /** @var Forms $forms */
+ $forms = $this->grav['forms'];
+
/** @var Route $route */
$route = $this->grav['route'];
$pageForms = $this->forms[$route->getRoute()] ?? [];
- foreach ($pageForms as $formName => $form) {
+ /**
+ * @var string $name
+ * @var array|FormInterface $form
+ */
+ foreach ($pageForms as $name => $form) {
+ if (is_array($form)) {
+ $form = $this->createForm($page, $name, $form);
+ }
+ if (!$form instanceof FormInterface) {
+ continue;
+ }
+
if ($form->get('remember_redirect')) {
// Found one; we need to check if unique id is set.
$formParam = $form->get('uniqueid_param', 'fid');
@@ -328,8 +344,6 @@ class FormPlugin extends Plugin
$form->setUniqueId($uniqueId);
$form->initialize();
- /** @var Forms $forms */
- $forms = $this->grav['forms'];
$forms->setActiveForm($form);
break;
@@ -337,7 +351,7 @@ class FormPlugin extends Plugin
// Append unique id to the URL and redirect.
$route = $route->withGravParam($formParam, $form->getUniqueId());
- $page->redirect((string)$route->toString());
+ $page->redirect($route->toString());
// TODO: Do we want to add support for multiple forms with remembered state?
break;
@@ -398,7 +412,7 @@ class FormPlugin extends Plugin
/**
* Make form accessible from twig.
*
- * @param Event $event
+ * @param Event|null $event
* @return void
*/
public function onTwigVariables(Event $event = null): void
@@ -428,6 +442,7 @@ class FormPlugin extends Plugin
* @param Event $event
* @return void
* @throws Exception
+ * @throws TransportExceptionInterface
*/
public function onFormProcessed(Event $event): void
{
@@ -499,6 +514,55 @@ class FormPlugin extends Plugin
return;
}
+ break;
+ case 'basic-captcha':
+ $captcha = new BasicCaptcha();
+ $captcha_value = trim($form->value('basic-captcha'));
+ if (!$captcha->validateCaptcha($captcha_value)) {
+ $message = $params['message'] ?? $this->grav['language']->translate('PLUGIN_FORM.ERROR_BASIC_CAPTCHA');
+
+ $this->grav->fireEvent('onFormValidationError', new Event([
+ 'form' => $form,
+ 'message' => $message
+ ]));
+
+ $event->stopPropagation();
+ return;
+ }
+ break;
+ case 'turnstile':
+ /** @var Uri $uri */
+ $uri = $this->grav['uri'];
+
+ $turnstile_config = $this->config->get('plugins.form.turnstile');
+ $secret = $turnstile_config['secret_key'] ?? null;
+ $token = $form->getValue('cf-turnstile-response') ?? null;
+ $ip = Uri::ip();
+
+ $client = Client::getClient();
+ $response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [
+ 'body' => [
+ 'secret' => $secret,
+ 'response' => $token,
+ 'remoteip' => $ip
+ ]
+ ]);
+
+ $content = $response->toArray();
+
+ if (!$content['success']) {
+ $message = $params['message'] ?? $this->grav['language']->translate('PLUGIN_FORM.ERROR_BASIC_CAPTCHA');
+
+ $this->grav->fireEvent('onFormValidationError', new Event([
+ 'form' => $form,
+ 'message' => $message
+ ]));
+
+ $this->grav['log']->addWarning('Form Turnstile invalid: [' . $uri->route() . '] ' . json_encode($content));
+ $event->stopPropagation();
+ return;
+ }
+
break;
case 'timestamp':
$label = $params['label'] ?? 'Timestamp';
@@ -544,8 +608,7 @@ class FormPlugin extends Plugin
$this->grav['messages']->add($form->message, 'success');
}
- $event['redirect'] = $url;
- $event->stopPropagation();
+ $this->grav->redirect($url);
break;
case 'reset':
if (Utils::isPositive($params)) {
@@ -736,6 +799,7 @@ class FormPlugin extends Plugin
*/
public function onFormValidationError(Event $event): void
{
+ /** @var FormInterface $form */
$form = $event['form'];
if (isset($event['message'])) {
$form->status = 'error';
@@ -743,6 +807,7 @@ class FormPlugin extends Plugin
$form->messages = $event['messages'];
}
+ /** @var Uri $uri */
$uri = $this->grav['uri'];
$route = $uri->route();
@@ -752,8 +817,7 @@ class FormPlugin extends Plugin
/** @var Pages $pages */
$pages = $this->grav['pages'];
- $page = $pages->dispatch($route, true);
-
+ $page = $pages->find($route, true);
if ($page) {
unset($this->grav['page']);
$this->grav['page'] = $page;
@@ -762,14 +826,32 @@ class FormPlugin extends Plugin
$event->stopPropagation();
}
+ /**
+ * Add a form definition to the forms plugin
+ *
+ * @param PageInterface $page
+ * @return void
+ */
+ public function addFormDefinition(PageInterface $page, string $name, array $form): void
+ {
+ $route = ($page->home() ? '/' : $page->route()) ?? '/';
+
+ if (!isset($this->forms[$route][$name])) {
+ $form['_page_routable'] = !$page->isModule();
+
+ $this->forms[$route][$name] = $form;
+ $this->recache_forms = true;
+ }
+ }
+
/**
* Add a form to the forms plugin
*
- * @param string|null $page_route
+ * @param string|null $route
* @param FormInterface|null $form
* @return void
*/
- public function addForm(?string $page_route, ?FormInterface $form)
+ public function addForm(?string $route, ?FormInterface $form): void
{
if (null === $form) {
return;
@@ -777,10 +859,10 @@ class FormPlugin extends Plugin
$name = $form->getName();
- if (!isset($this->forms[$page_route][$name])) {
- $this->forms[$page_route][$name] = $form;
+ if (!isset($this->forms[$route][$name])) {
+ $form['_page_routable'] = true;
- $this->flattenForms();
+ $this->forms[$route][$name] = $form;
$this->recache_forms = true;
}
}
@@ -788,45 +870,128 @@ class FormPlugin extends Plugin
/**
* function to get a specific form
*
- * @param null|array|string $data optional form `name`
+ * @param string|array|null $data Optional form name or ['name' => $name, 'route' => $route]
* @return FormInterface|null
*/
- public function getForm($data = null)
+ public function getForm($data = null): ?FormInterface
{
+ /** @var Pages $pages */
+ $pages = $this->grav['pages'];
+
+ // Handle parameters.
if (is_array($data)) {
- $form_name = $data['name'] ?? null;
- $page_route = $data['route'] ?? null;
+ $name = (string)($data['name'] ?? '');
+ $route = (string)($data['route'] ?? '');
} elseif (is_string($data)) {
- $form_name = $data;
- $page_route = null;
+ $name = $data;
+ $route = '';
} else {
- $form_name = null;
- $page_route = null;
+ $name = '';
+ $route = '';
}
- // if no form name, use the first form found in the page
- if (!$form_name) {
- // If page route not provided, use the current page
- if (!$page_route) {
- // Get page route with a fallback using current URI if page not initialized yet
- $page_route = $this->grav['page']->route() ?: $this->getCurrentPageRoute();
- }
-
- if (!empty($this->forms[$page_route])) {
- $forms = $this->forms[$page_route];
- $first_form = reset($forms) ?: null;
- return $first_form;
- }
-
- // Try to get page by defined route first or get current if not found
- $page = $this->grav['pages']->find($page_route) ?: $this->grav['page'];
-
- // Try looking up in the defined page
- return $this->grav['forms']->createPageForm($page);
+ // Return always the same form instance.
+ $form = $this->active_forms[$route][$name] ?? null;
+ if ($form) {
+ return $form;
}
- // return the form you are looking for if available
- return $this->getFormByName($form_name);
+ $unnamed = $name === '';
+ $routed = $route !== '';
+
+ // Get the page.
+ if ($routed) {
+ // Use fixed route for the form.
+ $route_provided = true;
+
+ $page = $pages->find($route, true);
+ } else {
+ // Search form from the current page first.
+ $route_provided = false;
+
+ /** @var PageInterface|null $page */
+ $page = $this->grav['page'] ?? null;
+ if ($page) {
+ $route = $page->route();
+ } else {
+ // Get page route with a fallback using current URI if page is not yet initialized.
+ $route = $this->getCurrentPageRoute();
+ $page = $pages->find($route);
+ }
+ }
+
+ // Attempt to find the form from the page.
+ if ('' !== $route) {
+ $forms = $this->forms[$route] ?? [];
+
+ if (!$unnamed) {
+ // Get form by the name.
+ $form = $forms[$name] ?? null;
+ } else {
+ // Get the first form.
+ $form = reset($forms) ?: null;
+ $name = key($forms);
+ }
+ }
+
+ // Search the form from the other pages.
+ if (null === $form) {
+ // First check if we requested a specific form which didn't exist.
+ if ($route_provided || $unnamed) {
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage(sprintf('Form %s not found in page %s', $name ?? 'unnamed', $route), 'warning');
+
+ return null;
+ }
+
+ // Attempt to find any form with given name.
+ $forms = $this->findFormByName($name);
+ $first = reset($forms);
+ if (!$first) {
+ return null;
+ }
+
+ // Check for naming conflicts.
+ if (count($forms) > 1) {
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage(sprintf('Fetching form by its name, but there are multiple pages with the same form name %s', $name), 'warning');
+ }
+
+ [$route, $name, $form] = $first;
+
+ $page = $pages->find($route);
+ }
+
+ // Form can be saved as an array or an object. If it's an array, we need to create object from it.
+ if (is_array($form)) {
+ // Form was cached as an array, try to create the object.
+ if (null === $page) {
+ /** @var Debugger $debugger */
+ $debugger = $this->grav['debugger'];
+ $debugger->addMessage(sprintf('Form %s cannot be created as page %s does not exist', $name, $route), 'warning');
+
+ return null;
+ }
+
+ $form = $this->createForm($page, $name, $form);
+ }
+
+ // Register form to the active forms to get the same instance back next time.
+ $this->active_forms[$route][$name] = $form;
+ if ($unnamed) {
+ $this->active_forms[$route][''] = $form;
+ }
+
+ // Also make aliases if route was not provided to the method.
+ if (!$routed) {
+ $this->active_forms[''][$name] = $form;
+ if ($unnamed) {
+ $this->active_forms[''][''] = $form;
+ }
+ }
+
+ return $form;
}
/**
@@ -834,7 +999,7 @@ class FormPlugin extends Plugin
*
* @return array
*/
- public function getFormFieldTypes()
+ public function getFormFieldTypes(): array
{
return [
'avatar' => [
@@ -906,7 +1071,7 @@ class FormPlugin extends Plugin
*
* - fillWithCurrentDateTime
*
- * @param Form $form
+ * @param FormInterface $form
* @return void
*/
protected function process($form)
@@ -921,43 +1086,33 @@ class FormPlugin extends Plugin
/**
* Get current page's route
*
- * @return mixed
+ * @return string
*/
protected function getCurrentPageRoute()
{
$path = $this->grav['uri']->route();
- $path = $path ?: '/';
- return $path;
+
+ return $path ?: '/';
}
/**
- * Retrieve a form based on the form name
+ * Return all forms matching the given name.
*
- * @param string $form_name
- * @param string $unique_id
- * @return mixed
+ * @param string $name
+ * @return array
*/
- protected function getFormByName($form_name, $unique_id = '')
+ protected function findFormByName(string $name): array
{
- $form = $this->active_forms[$form_name] ?? null;
- if (!$form) {
- $form = $this->flat_forms[$form_name] ?? null;
-
- if (!$form) {
- return null;
+ $list = [];
+ foreach ($this->forms as $route => $forms) {
+ foreach ($forms as $key => $form) {
+ if ($name === $key && !empty($form['_page_routable'])) {
+ $list[] = [$route, $key, $form];
+ }
}
-
- if ('' === $unique_id) {
- // Reset form to change the cached unique id and to fire onFormInitialized event.
- $form->setUniqueId('');
- $form->reset();
- }
-
- // Register form to the active forms to get the same instance back next time.
- $this->active_forms[$form_name] = $form;
}
- return $form;
+ return $list;
}
/**
@@ -965,12 +1120,11 @@ class FormPlugin extends Plugin
*
* @return bool
*/
- protected function shouldProcessForm()
+ protected function shouldProcessForm(): bool
{
+ /** @var Uri $uri */
$uri = $this->grav['uri'];
- $nonce = $uri->post('form-nonce');
- $status = $nonce ? true : false; // php72 quirk?
- $refresh_prevention = null;
+ $status = (bool)$uri->post('form-nonce');
if ($status && $form = $this->form()) {
// Make sure form is something we recognize.
@@ -1009,29 +1163,14 @@ class FormPlugin extends Plugin
return $status;
}
- /**
- * Flatten the forms array into something that can be more easily searched
- *
- * @return void
- */
- protected function flattenForms()
- {
- $this->flat_forms = Utils::arrayFlatten($this->forms);
- }
-
/**
* Get the current form, should already be processed but can get it directly from the page if necessary
*
* @param PageInterface|null $page
- * @return Form|null
+ * @return FormInterface|null
*/
protected function form(PageInterface $page = null)
{
- // Regenerate list of flat_forms if not already populated
- if (empty($this->flat_forms)) {
- $this->flattenForms();
- }
-
/** @var Forms $forms */
$forms = $this->grav['forms'];
@@ -1050,11 +1189,16 @@ class FormPlugin extends Plugin
$form_name = $page ? $page->slug() : null;
}
- $form = $this->getFormByName($form_name, $unique_id);
+ $form = $form_name ? $this->getForm($form_name) : null;
+ if ($form && '' === $unique_id) {
+ // Reset form to change the cached unique id and to fire onFormInitialized event.
+ $form->setUniqueId('');
+ $form->reset();
+ }
// last attempt using current page's form
if (!$form && $page) {
- $form = $forms->createPageForm($page);
+ $form = $this->createForm($page);
}
if ($form) {
@@ -1073,20 +1217,16 @@ class FormPlugin extends Plugin
/**
* @param PageInterface $page
- * @param string|int|null $name
- * @param array $form
- * @return Form|null
- * @deprecated
+ * @param string|null $name
+ * @param array|null $form
+ * @return FormInterface|null
*/
- protected function createForm(PageInterface $page, $name = null, $form = null)
+ protected function createForm(PageInterface $page, string $name = null, array $form = null): ?FormInterface
{
+ /** @var Forms $forms */
+ $forms = $this->grav['forms'];
- $header = $page->header();
- if (isset($header->form) || isset($header->forms)) {
- return new Form($page, $name, $form);
- }
-
- return null;
+ return $forms->createPageForm($page, $name, $form);
}
/**
@@ -1094,18 +1234,20 @@ class FormPlugin extends Plugin
*
* @return void
*/
- protected function loadCachedForms()
+ protected function loadCachedForms(): void
{
// Get and set the cache of forms if it exists
try {
- [$forms] = $this->grav['cache']->fetch($this->getFormCacheId());
- } catch (Exception $e) {
- // Couldn't fetch cached forms.
- $forms = null;
+ /** @var Cache $cache */
+ $cache = $this->grav['cache'];
+ [$forms] = $cache->fetch($this->getFormCacheId());
+ } catch (Exception $e) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Unserializing cached forms failed: %s', $e->getMessage()), 'error');
+
+ $forms = null;
}
if (!is_array($forms)) {
@@ -1113,9 +1255,8 @@ class FormPlugin extends Plugin
}
// Only update the forms if it's not empty
- if (!empty($forms)) {
+ if ($forms) {
$this->forms = array_merge($this->forms, $forms);
- $this->flattenForms();
}
}
@@ -1124,13 +1265,19 @@ class FormPlugin extends Plugin
*
* @return void
*/
- protected function saveCachedForms()
+ protected function saveCachedForms(): void
{
// Save the current state of the forms to cache
- if ($this->recache_forms) {
- $this->recache_forms = false;
- $this->grav['cache']->save($this->getFormCacheId(), [$this->forms]);
+ if (!$this->recache_forms) {
+ return;
}
+
+ $this->recache_forms = false;
+
+ /** @var Cache $cache */
+ $cache = $this->grav['cache'];
+
+ $cache->save($this->getFormCacheId(), [$this->forms]);
}
/**
@@ -1138,9 +1285,12 @@ class FormPlugin extends Plugin
*
* @return string
*/
- protected function getFormCacheId()
+ protected function getFormCacheId(): string
{
- return $this->grav['pages']->getPagesCacheId() . '-form-plugin';
+ /** @var Pages $pages */
+ $pages = $this->grav['pages'];
+
+ return $pages->getPagesCacheId() . '-form-plugin';
}
/**
@@ -1152,16 +1302,25 @@ class FormPlugin extends Plugin
*/
protected function udate($format = 'u', $raw = false)
{
-
- $utimestamp = microtime(true);
-
if ($raw) {
return date($format);
}
+ $utimestamp = microtime(true);
$timestamp = floor($utimestamp);
$milliseconds = round(($utimestamp - $timestamp) * 1000000);
return date(preg_replace('`(?path() === '/forms-basic-captcha-image.jpg') {
+ $captcha = new BasicCaptcha();
+ $code = $captcha->getCaptchaCode();
+ $image = $captcha->createCaptchaImage($code);
+ $captcha->renderCaptchaImage($image);
+ exit;
+ }
+ }
}
diff --git a/plugins/form/form.yaml b/plugins/form/form.yaml
index 01bf085..29793ca 100644
--- a/plugins/form/form.yaml
+++ b/plugins/form/form.yaml
@@ -17,4 +17,25 @@ recaptcha:
version: 2-checkbox
theme: light
site_key:
- secret_key:
\ No newline at end of file
+ secret_key:
+turnstile:
+ theme: light # options: [light | dark]
+ site_key:
+ secret_key:
+
+basic_captcha:
+ type: characters # options: [characters | math]
+ chars:
+ length: 6 # number of chars to output
+ font: zxx-noise.ttf # options: [zxx-noise.ttf | zxx-camo.ttf | zxx-xed.ttf | zxx-sans.ttf]
+ bg: '#cccccc' # 6-char hex color
+ text: '#333333' # 6-char hex color
+ size: 24 # font size in px
+ start_x: 5 # start position in x direction in px
+ start_y: 30 # start position in y direction in px
+ box_width: 135 # box width in px
+ box_height: 40 # box height in px
+ math:
+ min: 1 # smallest digit
+ max: 12 # largest digit
+ operators: ['+','-','*'] # operators that can be used in math
diff --git a/plugins/form/languages.yaml b/plugins/form/languages.yaml
index 4b2f06d..c6a288f 100644
--- a/plugins/form/languages.yaml
+++ b/plugins/form/languages.yaml
@@ -10,6 +10,7 @@ en:
DESTINATION_HELP: "The location where the files should be uploaded to"
ACCEPT: "Allowed MIME Types"
ACCEPT_HELP: "A list of MIME Types that are allowed for upload"
+ ERROR_BASIC_CAPTCHA: "Captcha failed for this form, please try again"
ERROR_VALIDATING_CAPTCHA: "reCAPTCHA bot protection has identified this form submission is problematic"
DATA_SUMMARY: "Here is the summary of what you wrote to us:"
NO_FORM_DATA: "No form data available"
@@ -72,6 +73,22 @@ en:
DESTINATION_NOT_SPECIFIED: "Destination not specified"
INVALID_MIME_TYPE: "The MIME type %s for the file %s is not accepted."
INVALID_FILE_EXTENSION: "The File Extension for the file %s is not accepted."
+ BASIC_CAPTCHA: "Basic Captcha"
+ BASIC_CAPTCHA_TYPE: "Captcha challenge type"
+ BASIC_CAPTCHA_LENGTH: "Number of characters"
+ BASIC_CAPTCHA_FONT: "TTF Font"
+ BASIC_CAPTCHA_SIZE: "Font size"
+ BASIC_CAPTCHA_BG_COLOR: "Background color"
+ BASIC_CAPTCHA_TEXT_COLOR: "Text color"
+ BASIC_CAPTCHA_START_X: "Text start x-position"
+ BASIC_CAPTCHA_START_Y: "Text start y-position"
+ BASIC_CAPTCHA_BOX_WIDTH: "Image width"
+ BASIC_CAPTCHA_BOX_HEIGHT: "Image height"
+ BASIC_CAPTCHA_MATH_MIN: "Minimum number"
+ BASIC_CAPTCHA_MATH_MAX: "Maximum number"
+ BASIC_CAPTCHA_MATH_OPERATORS: "Mathematical operators (randomized)"
+ TURNSTILE_CAPTCHA: "Cloudflare Turnstile Captcha"
+
eu:
PLUGIN_FORM:
NOT_VALIDATED: "Formularioa ez da baliozkotu. Beharrezkoa den eremu bat edo gehiago falta dira."
diff --git a/plugins/form/scss/form-styles.scss b/plugins/form/scss/form-styles.scss
index bd26070..8682bad 100644
--- a/plugins/form/scss/form-styles.scss
+++ b/plugins/form/scss/form-styles.scss
@@ -1,4 +1,4 @@
-$form-border-color: #eee;
+$form-border-color: #ccc;
$form-active-color: #000;
.form-group.has-errors {
@@ -287,3 +287,29 @@ $form-active-color: #000;
display: inline-block;
}
+.form-data.basic-captcha {
+ .form-input-wrapper {
+ border: 1px solid $form-border-color;
+ border-radius: 5px;
+ display: flex;
+ overflow: hidden;
+ }
+ .form-input-prepend {
+ display: flex;
+ color: #333;
+ background-color: #ccc;
+ flex-shrink: 0;
+ img {
+ margin: 0;
+ }
+ button > svg {
+ margin: 0 8px;
+ width: 18px;
+ height: 18px;
+ }
+ }
+ input.form-input {
+ border: 0;
+ }
+}
+
diff --git a/plugins/form/templates/forms/default/field.html.twig b/plugins/form/templates/forms/default/field.html.twig
index 9edfe94..1510441 100644
--- a/plugins/form/templates/forms/default/field.html.twig
+++ b/plugins/form/templates/forms/default/field.html.twig
@@ -10,7 +10,7 @@
{% set default = field.default %}
{% set toggleable = field.toggleable ?? false %}
{% if toggleable %}
- {% set originalValue = originalValue is defined ? originalValue : value %}
+ {% set originalValue = originalValue ?? value %}
{% set toggleableChecked = originalValue is not null %}
{% elseif field.overridable %}
{% set toggleable = true %}
diff --git a/plugins/form/templates/forms/default/fields.html.twig b/plugins/form/templates/forms/default/fields.html.twig
index 6ce3e1a..dd015dc 100644
--- a/plugins/form/templates/forms/default/fields.html.twig
+++ b/plugins/form/templates/forms/default/fields.html.twig
@@ -1,10 +1,10 @@
{% set fields = prepare_form_fields(fields, name) %}
+{% set originalValue = null %}
{% if fields|length %}
{% block outer_markup_field_open %}{% endblock %}
{% for field_name, field in fields %}
{% set value = form ? form.value(field.name) : data.value(field.name) %}
{% set field_templates = include_form_field(field.type, field_layout, fallback_field ?? 'text') %}
-
{% block inner_markup_field_open %}{% endblock %}
{% block field %}
{% include field_templates %}
diff --git a/plugins/form/templates/forms/default/form.html.twig b/plugins/form/templates/forms/default/form.html.twig
index cf0fb17..57ed7b6 100644
--- a/plugins/form/templates/forms/default/form.html.twig
+++ b/plugins/form/templates/forms/default/form.html.twig
@@ -145,6 +145,7 @@
{% if form.isEnabled() ?? true %}
{% for button in form.buttons %}
+ {% if not button.access or authorize(button.access) %}
{% if button.outerclasses is defined %}{% endif %}
{% if button.url %}
@@ -190,6 +191,7 @@
{% endembed %}
{% if button.outerclasses is defined %}
{% endif %}
+ {% endif %}
{% endfor %}
{% endif %}
diff --git a/plugins/form/templates/forms/fields/basic-captcha/basic-captcha.html.twig b/plugins/form/templates/forms/fields/basic-captcha/basic-captcha.html.twig
index e7dd890..68d9c7a 100644
--- a/plugins/form/templates/forms/fields/basic-captcha/basic-captcha.html.twig
+++ b/plugins/form/templates/forms/fields/basic-captcha/basic-captcha.html.twig
@@ -5,7 +5,7 @@
{% block prepend %}