Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue/2159 validate settings #2233

Merged
merged 18 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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