Skip to content

Commit

Permalink
Issue/2159 validate settings (#2233)
Browse files Browse the repository at this point in the history
Adds a possibility to validate setting fields. Supports the following
rules:
required, max, min, email, integer, matches.

Rules example:

```
'validation' => [
			[
				'rule'    => 'required',
				'message' => __( 'Field is required', 'gk-gravityview' ),
			],
			[
				'rule'    => 'max:12',
				'message' => __( 'Should be less or equal 12', 'gk-gravityview' ),
			],
		];
```
		
Currently supports only frontend validation. We can improve it with a
backend validation using the same validation config.

Added a "Validate URL with tags" rule for "'No Entries Redirect URL'"
and "Edit Entry Redirect URL" using the "required" and "matches" rules.
Here is the URL rule Regex - https://regex101.com/r/I2QSEI/1
		

💾 [Build
file](https://www.dropbox.com/scl/fi/0cwgzmrbn8abisyorq074/gravityview-2.32-619e85cb9.zip?rlkey=nfaugp0czc32sdw3i9njx8ag4&dl=1)
(619e85c).
  • Loading branch information
mrcasual authored Dec 19, 2024
2 parents b0fd18e + 585dfc2 commit 89fa414
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 99 deletions.
2 changes: 1 addition & 1 deletion assets/css/admin-views.css

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion assets/css/scss/admin-views.scss
Original file line number Diff line number Diff line change
Expand Up @@ -594,11 +594,19 @@ $gv-overlay-index: 10000;

th,
td {

span, input {
font-weight: normal !important;
}

input.gv-error {
border: 1px solid $color-red;
}

.gv-error-message {
margin-top: 4px;
color: $color-red;
}

// 2.6 field groups in the Merge Tag dropdowns inside View Settings
.gform-dropdown--merge-tags .gform-dropdown__group-text {
font-weight: 500 !important;
Expand Down
157 changes: 155 additions & 2 deletions assets/js/admin-views.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,9 @@
// Double-clicking a field/widget label opens settings
.on( 'dblclick', ".gv-fields:not(.gv-nonexistent-form-field)", vcfg.openFieldSettings )

.on( 'change', "#gravityview_settings", vcfg.zebraStripeSettings )
.on( 'change', "#gravityview_settings", vcfg.changedSettingsAction )

.on( 'click', 'div[data-js="gform-simplebar"]', vcfg.changedSettingsAction )

.on( 'click', '.gv-field-details--toggle', function( e ) {

Expand Down Expand Up @@ -280,7 +282,6 @@
$open_dialog.dialog( 'option', 'width', window_width );
});


// Make sure the user intends to leave the page before leaving.
window.addEventListener('beforeunload', ( event) => {
if ( vcfg.hasUnsavedChanges ) {
Expand Down Expand Up @@ -369,6 +370,119 @@
viewConfiguration.altKey = e.altKey;
},

/**
* Manages all actions required when the settings are updated.
*
* @since $ver$
*
* @param {Event} event
*/
changedSettingsAction: function (event) {
// Revalidate all current tab fields, as new fields may appear when settings are changed.
var $tabFields = viewGeneralSettings.metaboxObj.find( '[name^=template_settings]:visible' );
$tabFields.each( function () {
viewConfiguration.validateField( $( this ) );
} );

// Recalculate zebra stripes.
viewConfiguration.zebraStripeSettings();
},

/**
* Validates the field when its value changes.
*
* @since $ver$
*
* @param {jQuery} $field
*/
validateField: function ( $field ) {
var rules = $field.data( 'rules' );
if ( ! rules ) {
return;
}

var error = viewConfiguration.validateValue( $field.val(), rules );

$field.toggleClass( 'gv-error', !! error );

$field.parent().find( '.gv-error-message' ).remove();
if ( error ) {
$field.parent().append(
$( '<div>', { class: 'gv-error-message', text: error } )
);
}
},

/**
* Validates a value against a set of rules.
*
* @since $ver$
*
* @param {any} value - The value to validate.
* @param {Array} rules - The rules to validate against.
* @returns {String|undefined} - Error message. Empty if valid.
*/
validateValue( value, rules ) {
if ( ! rules ) {
return;
}

var validators = viewConfiguration.getValidators();
for ( var i in rules ) {
if ( ! rules.hasOwnProperty( i ) ) {
continue;
}
var ruleset = rules[ i ],
rule = ruleset.rule,
message = ruleset.message,
param = '',
isValid = true;

// Split the rule to get the rule parameter. Example: max:5, rule - max, param - 5.
if ( rule.includes( ':' ) ) {
var parts = rule.split( /:(.+)/ ); // Split only on the first ":"
rule = parts[ 0 ];
param = parts[ 1 ];
}
if ( validators[ rule ] ) {
isValid = validators[ rule ]( value, param );
}
if ( ! isValid ) {
return message;
}
}
},

/**
* Gets a list of validation callbacks.
*
* @since $ver$
*
* @returns {Object} - An object containing validation callbacks.
*/
getValidators: function () {
return {
required: function ( value ) {
return value !== null && value !== undefined && value.toString().trim() !== '';
},
max: function ( value, max ) {
return value !== null && value !== undefined && Number( value ) <= max;
},
min: function ( value, min ) {
return value !== null && value !== undefined && Number( value ) >= min;
},
email: function ( value ) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value );
},
integer: function ( value ) {
return Number.isInteger ? Number.isInteger( Number( value ) ) : Number( value ) % 1 === 0;
},
matches: function ( value, pattern ) {
return new RegExp( pattern ).test( value );
},
};
},

/**
* Update zebra striping when settings are changed
* This prevents two gray rows next to each other.
Expand Down Expand Up @@ -2767,6 +2881,10 @@
var vcfg = viewConfiguration;
var templateId = vcfg._getTemplateId();

if ( ! vcfg.validateSettingFields( e ) ) {
return false;
}

// Create the form if we're starting fresh.
// On success, this also sets the vcfg.startFreshStatus to false.
if ( vcfg.startFreshStatus ) {
Expand Down Expand Up @@ -2857,7 +2975,42 @@
}, 101 );

return false;
},

validateSettingFields: function ( e ) {

var $metabox = viewGeneralSettings.metaboxObj;

var $invalidFields = $metabox
.find( '[name^=template_settings].gv-error' )
.filter( function () {
// Get only active fields, i.e., those whose parent <tr> is not "display: none".
return $( this ).closest( 'tr.alternate' ).css( 'display' ) !== 'none';
} );

// If no invalid fields are found, return.
if ( ! $invalidFields.length ) {
return true;
}

// Prevent form submission.
e.stopImmediatePropagation();
e.preventDefault();

// Open the tab containing the invalid field.
var tabPanelId = $invalidFields.first().closest( 'div[role=tabpanel]' ).prop( 'id' );
var $tabLink = $metabox.find( '.ui-tab[aria-controls=' + tabPanelId + '] a.nav-tab' );
$tabLink.trigger( 'click' );

// Scroll to the Settings section.
window.scrollTo(
{
top: $metabox.offset().top,
behavior: 'smooth',
},
);

return false;
},

/**
Expand Down
2 changes: 1 addition & 1 deletion assets/js/admin-views.min.js

Large diffs are not rendered by default.

79 changes: 2 additions & 77 deletions future/includes/class-gv-permalinks.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ public function __construct( Plugin_Settings $settings ) {
add_filter( 'gravityview/view/settings/defaults', [ $this, 'add_view_settings' ] );

add_action( 'init', [ $this, 'maybe_update_rewrite_rules' ], 1 );
add_action( 'admin_enqueue_scripts', [ $this, 'add_view_settings_scripts' ], 1500 );

add_action( 'gravityview/shortcode/before-processing', [ $this, 'capture_view' ] );
add_action( 'gravityview/shortcode/after-processing', [ $this, 'clear_captured_view' ] );
Expand Down Expand Up @@ -508,86 +507,12 @@ public function add_view_settings( array $settings ): array {
'id' => '54c67bb5e4b07997ea3f3f58',
'url' => 'https://docs.gravitykit.com/article/57-customizing-urls',
],
'validation' => $this->entry_slug_validation(),
];

return $settings;
}

/**
* Adds inline JavaScript for the View settings.
*
* @since 2.29.0
*/
public function add_view_settings_scripts(): void {
if ( ! wp_script_is( 'gravityview_views_scripts', 'registered' ) ) {
return;
}

$js = <<<JS
( function( $ ) {
$( function() {
const getErrorMessage = ( value ) => {
if ( value.length === 0 ) {
return '';
}
if (value.length < 3) {
return '[ERROR_AT_LEAST_3]';
}
if ( ! value.match( /{entry_id}/s ) ) {
return '[ERROR_MISSING_ENTRY_ID]';
}
if ( ! value.match( /(^(?:[a-zA-Z0-9_\-]*|\{[^\}]*\})*$)/s ) ) {
return '[ERROR_NO_SPACES]';
}
return '';
}
$( '#gravityview_se_single_entry_slug' ).on( 'input', function () {
const value = $( this ).val();
const parent = $( this ).closest( 'label' );
const error = getErrorMessage( value );
const is_valid = '' === error;
parent.toggleClass( 'form-invalid form-required', ! is_valid );
$( '#publish ')
.attr( 'disabled', ! is_valid )
.toggleClass( 'disabled' , ! is_valid );
parent.find( 'span.error-message' ).remove();
if ( !is_valid ) {
parent.append( $( '<span class="error-message" style="margin-top:2px; font-size: 12px">' + error + '</span>' ) );
}
} );
} );
} )( jQuery );
JS;

$js = strtr(
$js,
[
'[ERROR_AT_LEAST_3]' => strtr(
// Translators: [count] is replaced by the amount of characters.
esc_html__( 'At least [count] characters are required.', 'gk-gravityview' ),
[ '[count]' => 3 ],
),
'[ERROR_MISSING_ENTRY_ID]' => strtr(
// Translators: [slug] will contain the slug value.
__( 'Must contain "[slug]".', 'gk-gravityview' ),
[ '[slug]' => '{entry_id}' ]
),
'[ERROR_NO_SPACES]' => esc_html__(
'Only letters, numbers, underscores and dashes are allowed.',
'gk-gravityview',
),
]
);

wp_add_inline_script( 'gravityview_views_scripts', $js );
}

/**
* Returns whether the current request is a backend validation.
Expand Down Expand Up @@ -662,7 +587,7 @@ private function entry_slug_validation(): array {
),
],
[
'rule' => 'matches:^[a-zA-Z0-9_{}\-]*$',
'rule' => 'matches:^(?:[a-zA-Z0-9_\-]|{[^}]*})*$',
'message' => esc_html__(
'Only letters, numbers, underscores and dashes are allowed.',
'gk-gravityview',
Expand Down
47 changes: 46 additions & 1 deletion future/includes/class-gv-settings-view.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ public static function defaults( $detailed = false, $group = null ) {
'type' => 'text',
'class' => 'code widefat',
'value' => '',
'placeholder' => 'https://www.example.com',
'placeholder' => 'https://www.example.com/{field:1}',
'requires' => 'no_entries_options=2',
'validation' => self::validate_url_with_tags(),
),
'no_search_results_text' => array(
'label' => __( '"No Search Results" Text', 'gk-gravityview' ),
Expand Down Expand Up @@ -515,6 +516,7 @@ public static function defaults( $detailed = false, $group = null ) {
'placeholder' => 'https://www.example.com/landing-page/',
'requires' => 'edit_redirect=2',
'merge_tags' => 'force',
'validation' => self::validate_url_with_tags(),
),
'action_label_update' => array(
'label' => __( 'Update Button Text', 'gk-gravityview' ),
Expand Down Expand Up @@ -601,6 +603,7 @@ public static function defaults( $detailed = false, $group = null ) {
'placeholder' => 'https://www.example.com/landing-page/',
'requires' => 'delete_redirect=' . \GravityView_Delete_Entry::REDIRECT_TO_URL_VALUE,
'merge_tags' => 'force',
'validation' => self::validate_url_with_tags(),
),
'is_secure' => [
'label' => __( 'Enable Enhanced Security', 'gk-gravityview' ),
Expand Down Expand Up @@ -808,4 +811,46 @@ function ( $key ) use ( $_this ) {
)
);
}

/**
* Validates URLs with merge tags.
*
* Valid format examples:
* http://foo.bar/{field:1}
* https://foo.bar/{field:1}
* https://{field:1}
* https://foo.bar/{field:1}/{another:2}
* https://foo.bar/{field:1}:8080?name=value#fragment
* http://foo.bar
* https://foo.bar/path/to/resource
* http://192.168.0.1:8080/query?name=value#fragment
* https://[2001:db8::1]:443/resource
* https://2001:0db8:85a3:0000:0000:8a2e:0370:7334/path/to?name=value#fragment
* {field:1}
* {field:1}/path/to?name=value#fragment
*
* Invalid examples:
* htp://foo.bar - Misspelled protocol http (should not match).
* foo.bar - Missing protocol (http:// or https://) or leading //.
* https://foo - Incomplete domain (e.g., .com).
* http://foo - Same as above; incomplete domain.
* foo.bar/{field:1} - Missing protocol (http:// or https://) or leading //.
* foo - No protocol, domain, or valid structure.
*
* @since $var$
*
* @return array
**/
private static function validate_url_with_tags() {
return [
[
'rule' => 'required',
'message' => __( 'Field is required', 'gk-gravityview' ),
],
[
'rule' => "matches:^s*((https?:\/\/)((\S+\.+\S+)|(\[?(\S+:)+\S+\]?)|({.*}.*))?(?::\d+)?({.*}.*)?|({.*}.*))\s*$",
'message' => __( 'Must be a valid URL. Can contain merge tags.', 'gk-gravityview' ),
],
];
}
}
Loading

0 comments on commit 89fa414

Please sign in to comment.