feat: Elementor subscription widget#1817
feat: Elementor subscription widget#1817sapayth wants to merge 19 commits intoweDevsOfficial:developfrom
Conversation
WalkthroughThis PR introduces comprehensive Elementor integration for WPUF by adding native widgets for forms and subscription plans, Elementor-specific styling and JavaScript, integration bootstrap with lifecycle hooks, and adjustments to form field rendering and asset management to support Elementor editor preview mode. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (11)
assets/css/elementor-subscription-plans.css (2)
65-68: Redundant margin shorthand.
margin: 0 0 0 0is equivalent tomargin: 0..wpuf-sub-plan-name { - margin: 0 0 0 0; + margin: 0; line-height: 1.3; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/css/elementor-subscription-plans.css` around lines 65 - 68, The CSS rule .wpuf-sub-plan-name uses a redundant shorthand `margin: 0 0 0 0`; change it to the canonical `margin: 0` (or remove the margin declaration if zero is the default) to simplify the stylesheet and keep semantics identical; update the rule containing .wpuf-sub-plan-name (where line-height: 1.3 is set) to use `margin: 0` instead of the four-value shorthand.
35-53: Three identical@media (min-width: 768px)blocks can be consolidated.These three separate media query blocks share the same breakpoint and could be merged into one for slightly cleaner CSS.
♻️ Suggested consolidation
-/* Plans Per Row: 2 columns */ -@media (min-width: 768px) { - .wpuf-subscription-plans-wrapper.wpuf-sub-plans-2 .wpuf-subscription-plans-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -/* Plans Per Row: 3 columns */ -@media (min-width: 768px) { - .wpuf-subscription-plans-wrapper.wpuf-sub-plans-3 .wpuf-subscription-plans-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -/* Plans Per Row: 4 columns */ -@media (min-width: 768px) { - .wpuf-subscription-plans-wrapper.wpuf-sub-plans-4 .wpuf-subscription-plans-grid { - grid-template-columns: repeat(4, 1fr); - } -} +/* Plans Per Row: 2-4 columns (tablet and up) */ +@media (min-width: 768px) { + .wpuf-subscription-plans-wrapper.wpuf-sub-plans-2 .wpuf-subscription-plans-grid { + grid-template-columns: repeat(2, 1fr); + } + .wpuf-subscription-plans-wrapper.wpuf-sub-plans-3 .wpuf-subscription-plans-grid { + grid-template-columns: repeat(3, 1fr); + } + .wpuf-subscription-plans-wrapper.wpuf-sub-plans-4 .wpuf-subscription-plans-grid { + grid-template-columns: repeat(4, 1fr); + } +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/css/elementor-subscription-plans.css` around lines 35 - 53, Consolidate the three identical `@media` (min-width: 768px) blocks into a single media query and move the three grid-template-columns rules inside it: target .wpuf-subscription-plans-wrapper.wpuf-sub-plans-2 .wpuf-subscription-plans-grid, .wpuf-subscription-plans-wrapper.wpuf-sub-plans-3 .wpuf-subscription-plans-grid, and .wpuf-subscription-plans-wrapper.wpuf-sub-plans-4 .wpuf-subscription-plans-grid each with their respective grid-template-columns: repeat(2, 1fr), repeat(3, 1fr), and repeat(4, 1fr); remove the duplicate `@media` wrappers so only one media query remains.assets/less/elementor-frontend-forms.less (3)
109-137: Replace vendor-prefixed placeholder selectors with the standard::placeholder.Stylelint flags all vendor-prefixed placeholder selectors here. The standard
::placeholderpseudo-element has broad support (all modern browsers). The:-moz-placeholder(lines 121-125) targets Firefox 4–18 and:-ms-input-placeholder/::-ms-input-placeholdertarget legacy IE/Edge — both long EOL. Unless your browser support matrix explicitly requires these, simplify to:♻️ Suggested replacement
- // Hide placeholder option - &.wpuf-elementor-hide-placeholder input::-webkit-input-placeholder, - &.wpuf-elementor-hide-placeholder textarea::-webkit-input-placeholder { - opacity: 0; - visibility: hidden; - } - - &.wpuf-elementor-hide-placeholder input::-moz-placeholder, - &.wpuf-elementor-hide-placeholder textarea::-moz-placeholder { - opacity: 0; - visibility: hidden; - } - - &.wpuf-elementor-hide-placeholder input:-moz-placeholder, - &.wpuf-elementor-hide-placeholder textarea:-moz-placeholder { - opacity: 0; - visibility: hidden; - } - - &.wpuf-elementor-hide-placeholder input:-ms-input-placeholder, - &.wpuf-elementor-hide-placeholder textarea:-ms-input-placeholder { - opacity: 0; - visibility: hidden; - } - - &.wpuf-elementor-hide-placeholder input::-ms-input-placeholder, - &.wpuf-elementor-hide-placeholder textarea::-ms-input-placeholder { - opacity: 0; - visibility: hidden; - } + // Hide placeholder option + &.wpuf-elementor-hide-placeholder input::placeholder, + &.wpuf-elementor-hide-placeholder textarea::placeholder { + opacity: 0; + visibility: hidden; + }If legacy browser support is required, consider using Autoprefixer in the build pipeline instead of hand-writing vendor prefixes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/less/elementor-frontend-forms.less` around lines 109 - 137, Remove the vendor-prefixed placeholder rules and replace them with a single standard ::placeholder rule for the .wpuf-elementor-hide-placeholder selector (targeting input and textarea). Locate the blocks that reference input::-webkit-input-placeholder, input::-moz-placeholder, input:-moz-placeholder, input:-ms-input-placeholder, input::-ms-input-placeholder (and the matching textarea selectors) and consolidate them into one rule using &.wpuf-elementor-hide-placeholder input::placeholder and &.wpuf-elementor-hide-placeholder textarea::placeholder, keeping the same declarations (opacity: 0; visibility: hidden;). Ensure no vendor-prefixed placeholder selectors remain in the file.
251-254: Global.lityrule leaks outside the widget wrapper scope.This rule applies globally to the page. While this is likely intentional (popups need to overlay Elementor modals), it could affect other
.lityinstances outside the widget context. Consider adding a comment explaining why it's intentionally global, or scoping it more narrowly if possible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/less/elementor-frontend-forms.less` around lines 251 - 254, The global .lity rule is leaking outside the widget scope; either scope it to the widget/popup container (e.g., change the selector from .lity to a more specific selector like .elementor-widget .lity or .elementor-popup .lity) so it only elevates popups inside Elementor, or if the global behavior is intentional keep the .lity selector but add a clear comment above the rule explaining why it must be global to ensure Lity popups overlay Elementor modals; update the selector and CSS rule (or add the explanatory comment) and adjust any z-index values accordingly in the existing .lity block.
50-55: Mixed indentation on line 53.Line 53 uses spaces while the rest of the file uses tabs. Minor inconsistency.
.wpuf-fields .wpuf-radio-block, .wpuf-fields .wpuf-checkbox-block { display: flex; - align-items: center; + align-items: center; margin-bottom: 6px; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/less/elementor-frontend-forms.less` around lines 50 - 55, The block for selectors ".wpuf-fields .wpuf-radio-block, .wpuf-fields .wpuf-checkbox-block" has mixed indentation—line with "align-items: center;" uses spaces while the file uses tabs; fix by replacing the leading spaces on that line with a single tab to match the rest of the file's indentation style so the rule block uses consistent tabs for all properties.includes/Assets.php (1)
424-427: Consider adding$this->suffixfor production minification.The new
elementor-subscription-plansJS handle hardcodes the non-minified filename. Several other frontend scripts (e.g.,frontend-format line 390) use$this->suffixto load.min.jsin production. If a minified build is planned, this should follow the same pattern.'elementor-subscription-plans' => [ - 'src' => WPUF_ASSET_URI . '/js/elementor-subscription-plans.js', + 'src' => WPUF_ASSET_URI . '/js/elementor-subscription-plans' . $this->suffix . '.js', 'in_footer' => true, ],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Assets.php` around lines 424 - 427, The 'elementor-subscription-plans' asset registration currently hardcodes the non-minified filename; update the src value to append $this->suffix (same pattern used by other handles like 'frontend-form') so production loads the .min.js when appropriate; locate the 'elementor-subscription-plans' array in the Assets class and change the 'src' construction to use WPUF_ASSET_URI . '/js/elementor-subscription-plans' . $this->suffix . '.js' (ensuring consistency with how $this->suffix is used elsewhere).includes/Integrations/Elementor/Widget.php (2)
475-508: Extract the repeated focus selector string into a variable.The long
:focusselector string is duplicated three times (Lines 483, 492, 500) for background, border, and box-shadow controls. This mirrors the pattern used for the normal-state$input_selectoron Line 388.♻️ Proposed refactor
$this->start_controls_tab( 'tab_input_focus', [ 'label' => __( 'Focus', 'wp-user-frontend' ) ] ); +$input_focus_selector = '{{WRAPPER}} .wpuf-form input[type="text"]:focus, {{WRAPPER}} .wpuf-form input[type="email"]:focus, {{WRAPPER}} .wpuf-form input[type="url"]:focus, {{WRAPPER}} .wpuf-form input[type="password"]:focus, {{WRAPPER}} .wpuf-form input[type="number"]:focus, {{WRAPPER}} .wpuf-form textarea:focus, {{WRAPPER}} .wpuf-form select:focus'; + $this->add_control( 'input_focus_bg_color', [ 'label' => __( 'Background Color', 'wp-user-frontend' ), 'type' => Controls_Manager::COLOR, - 'selectors' => [ - '{{WRAPPER}} .wpuf-form input[type="text"]:focus, ...' => 'background-color: {{VALUE}};', - ], + 'selectors' => [ $input_focus_selector => 'background-color: {{VALUE}};' ], ] );Apply similarly for
input_focus_borderandinput_focus_box_shadow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/Widget.php` around lines 475 - 508, Introduce a reusable variable (e.g. $input_focus_selector) containing the long focus selector string and replace the repeated selector occurrences in the input focus controls: use $input_focus_selector for the 'selectors' entry in add_control('input_focus_bg_color', ...) and for the 'selector' option in Group_Control_Border::get_type() (name 'input_focus_border') and Group_Control_Box_Shadow::get_type() (name 'input_focus_box_shadow'); ensure the variable mirrors the existing $input_selector pattern and that each control references the variable instead of duplicating the string.
164-188: Unboundedget_postsquery withposts_per_page => -1.On sites with a large number of WPUF forms, this loads all form posts into memory to populate the Elementor dropdown. This runs every time the widget settings panel is opened.
Consider adding a reasonable cap (e.g., 100) or using a search-based control for better scalability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/Widget.php` around lines 164 - 188, The get_wpuf_forms method currently calls get_posts with 'posts_per_page' => -1 which can load all forms into memory; change this to a safe default cap (e.g., 'posts_per_page' => 100) to avoid unbounded queries and update the call so it can be overridden via a new filter or argument (e.g., allow a 'wpuf_elementor_forms_limit' filter) so sites can increase it if needed; keep populating $options the same and continue returning apply_filters('wpuf_elementor_form_options', $options, $this).includes/Integrations/Elementor/Subscription_Plans_Widget.php (3)
1014-1015: Unescaped output ofget_render_attribute_string.Same pattern as in
Widget.php—$this->get_render_attribute_string('wrapper')is echoed directly. This is standard Elementor practice (the method handles its own escaping), but aphpcs:ignorecomment should be added for consistency.♻️ Add PHPCS suppression for clarity
-echo '<div ' . $this->get_render_attribute_string( 'wrapper' ) . '>'; +echo '<div ' . $this->get_render_attribute_string( 'wrapper' ) . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Elementor handles escaping🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines 1014 - 1015, The call to $this->get_render_attribute_string('wrapper') in Subscription_Plans_Widget is intentionally echoed without additional escaping; add a PHPCS suppression comment immediately before the echo of that unescaped output to document the rationale and silence the linter (use the appropriate rule, e.g. phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped) so the line echo '<div ' . $this->get_render_attribute_string( 'wrapper' ) . '>'; is clearly marked as safe.
1199-1395: Massive code duplication in feature-limit rendering — extract a helper.Lines 1214–1346 repeat the exact same pattern ~15 times: check if a post-type limit key exists and is non-zero, then branch on
-1for "Unlimited X" vssprintf('%d X allowed', ...). This is a textbook DRY violation that makes the method ~200 lines longer than necessary and makes adding new post types error-prone.♻️ Proposed refactor — loop over a config array
$limit_labels = [ 'post' => [ __( 'Unlimited posts', 'wp-user-frontend' ), __( '%d posts allowed', 'wp-user-frontend' ) ], 'page' => [ __( 'Unlimited pages', 'wp-user-frontend' ), __( '%d pages allowed', 'wp-user-frontend' ) ], 'user_request' => [ __( 'Unlimited user requests', 'wp-user-frontend' ), __( '%d user requests allowed', 'wp-user-frontend' ) ], 'product' => [ __( 'Unlimited products', 'wp-user-frontend' ), __( '%d products allowed', 'wp-user-frontend' ) ], 'wp_block' => [ __( 'Unlimited reusable blocks', 'wp-user-frontend' ), __( '%d reusable blocks allowed', 'wp-user-frontend' ) ], 'wp_template' => [ __( 'Unlimited templates', 'wp-user-frontend' ), __( '%d templates allowed', 'wp-user-frontend' ) ], 'wp_template_part' => [ __( 'Unlimited template parts', 'wp-user-frontend' ), __( '%d template parts allowed', 'wp-user-frontend' ) ], 'wp_navigation' => [ __( 'Unlimited navigation menus', 'wp-user-frontend' ), __( '%d navigation menus allowed', 'wp-user-frontend' ) ], 'wp_global_styles' => [ __( 'Unlimited global styles', 'wp-user-frontend' ), __( '%d global styles allowed', 'wp-user-frontend' ) ], 'wp_font_family' => [ __( 'Unlimited font families', 'wp-user-frontend' ), __( '%d font families allowed', 'wp-user-frontend' ) ], 'wp_font_face' => [ __( 'Unlimited font faces', 'wp-user-frontend' ), __( '%d font faces allowed', 'wp-user-frontend' ) ], 'product_variation' => [ __( 'Unlimited product variations', 'wp-user-frontend' ), __( '%d product variations allowed', 'wp-user-frontend' ) ], 'shop_order' => [ __( 'Unlimited shop orders', 'wp-user-frontend' ), __( '%d shop orders allowed', 'wp-user-frontend' ) ], 'shop_order_refund' => [ __( 'Unlimited shop refunds', 'wp-user-frontend' ), __( '%d shop refunds allowed', 'wp-user-frontend' ) ], 'shop_coupon' => [ __( 'Unlimited shop coupons', 'wp-user-frontend' ), __( '%d shop coupons allowed', 'wp-user-frontend' ) ], ]; foreach ( $limit_labels as $key => [ $unlimited_label, $counted_label ] ) { if ( isset( $all_limits[ $key ] ) && '0' !== $all_limits[ $key ] ) { if ( '-1' === $all_limits[ $key ] ) { $features_list[] = $unlimited_label; } else { $features_list[] = sprintf( $counted_label, intval( $all_limits[ $key ] ) ); } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines 1199 - 1395, The render_features_list method duplicates the same limit-checking pattern for many post-type keys; refactor by extracting the repeated logic into a helper (e.g., build_limit_feature or a loop using a $limit_labels config) that takes $all_limits, $features_list (by reference) and a mapping of key => [unlimited_label, counted_label]; inside render_features_list replace the repeated if-blocks with a single foreach over $limit_labels that checks isset($all_limits[$key]) && '0' !== $all_limits[$key], appends the unlimited_label when '-1' === $all_limits[$key], otherwise sprintf the counted_label with intval($all_limits[$key]) — keep other special cases (featured items, expiry, recurring, trial, mail) unchanged.
920-960:meta_keyusage inget_subscription_planstriggers slow unindexed queries.Lines 935–936, 940–941, and 946 use
meta_keywithorderby => meta_value_num. On sites with largewp_postmetatables this results in slow queries sincemeta_keyalone doesn't leverage indexes efficiently. This is a known WordPress performance concern.Consider adding a custom database index on
(meta_key, meta_value)or documenting that the subscription post count should remain low.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines 920 - 960, get_subscription_plans uses $args['meta_key'] + orderby => meta_value_num which causes full unindexed scans on large wp_postmeta; fix by adding a dedicated DB index on (meta_key, meta_value) for wp_postmeta during plugin activation (implement an activation routine like create_postmeta_index or add_postmeta_index using dbDelta/ALTER TABLE and call it from register_activation_hook), so queries generated by get_subscription_plans/get_posts can use the index; alternatively (or as a fallback) avoid ordering in SQL by removing meta_key/order args in get_subscription_plans and instead fetch unsorted posts then sort them in PHP using wpuf()->subscription->get_subscription_meta before returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@assets/js/elementor-subscription-plans.js`:
- Around line 14-41: The click handler uses e.target and assumes DOM nodes
exist, causing missed toggles and null errors; update the handler to use
e.target.closest('.wpuf-sub-features-toggle') to find the toggle button, then
guard all DOM lookups (featuresList, seeMoreBtn, seeLessBtn, hiddenItems) with
null checks before accessing or mutating them, and ensure you set/get the
data-expanded attribute on the found button variable (instead of raw e.target)
so nested clicks work reliably.
In `@includes/Frontend.php`:
- Around line 71-75: The current check uses did_action('elementor/loaded') and
is_built_with_elementor($post->ID) to skip enqueueing
'wpuf-frontend-subscriptions', which breaks the [wpuf_sub_pack] shortcode when
placed in Elementor Text/Shortcode widgets; instead, stop skipping that
stylesheet solely because the page is built with Elementor—always enqueue
'wpuf-frontend-subscriptions' (and still enqueue
'wpuf-elementor-subscription-plans' when the Elementor widget is present) by
removing the conditional around wp_enqueue_style('wpuf-frontend-subscriptions')
in the code that sets $is_elementor_page so the shortcode markup retains its
Tailwind-based styles.
In `@includes/Integrations/Elementor/Elementor.php`:
- Around line 31-38: The wp_dequeue_style calls inside enqueue_styles have PHPCS
spacing issues around their arguments; update each call (wp_dequeue_style for
'wpuf-frontend-forms', 'wpuf-layout1' .. 'wpuf-layout5') to follow WordPress
coding standards by adding a single space after the opening parenthesis and
before the argument (e.g., change wp_dequeue_style('handle') to
wp_dequeue_style( 'handle' ) for all five calls) and ensure there are no extra
spaces between the function name and the opening parenthesis.
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php`:
- Around line 1140-1152: The pack ID comparison and billing amount check are
type-mismatched: change the loose equality in $is_completed to a strict
type-safe comparison (e.g., cast both $current_pack_id and $pack->ID to the same
type before comparing) and remove the impossible strict float-to-string check by
comparing $billing_amount as a number (e.g., $billing_amount === 0.0 or
(float)$billing_amount === 0.0) or by formatting to a string before comparing;
update the references in the $is_completed expression and the billing-amount
branch that reads $billing_amount === '0.00' to use consistent types for
reliable results.
- Around line 1060-1068: The local variable $settings in render_plan_card() is
unused; remove the assignment "$settings = $this->get_settings_for_display();"
to eliminate the unused variable warning (PHPMD) and avoid unnecessary calls to
get_settings_for_display(); keep the rest of render_plan_card() intact and
ensure no other code in that method relies on $settings.
- Around line 1110-1115: The code is calling _n() with dynamic values
($trial_duration_type) which aren’t translatable; replace this by mapping known
period keys (e.g., 'day', 'week', 'month', 'year') to literal translatable
singular/plural strings and then call _n() with those mapped strings and the
$trial_duration count (for example map 'day' =>
['singular'=>'day','plural'=>'days'] and use _n( 'day', 'days', $trial_duration,
'wp-user-frontend' ) ), then use that returned pluralized label when building
$trial_text; update the same pattern where $trial_duration_type is used (the
other occurrences in Subscription_Plans_Widget methods around the
trial/recurring text) so all pluralization uses static translatable strings
instead of dynamic values.
- Around line 1137-1189: In render_button remove the unused $user_id declaration
and stop appending duplicate href attributes: compute $button_url (and add the
disabled class) conditionally when $is_completed is true (e.g. set $button_url
to 'javascript:void(0)' and include 'wpuf-sub-button-disabled' in the class
list) before calling add_render_attribute for 'button-' . $pack->ID so the href
is set once; then remove the later add_render_attribute calls that attempted to
override href/class for the completed pack.
In `@includes/Integrations/Elementor/Widget.php`:
- Around line 1065-1071: In the render() method of the Elementor widget,
sanitize the $form_id from get_settings_for_display() before it’s used in the
shortcode string: ensure $form_id is cast to an integer (e.g., via (int) or
intval) immediately after it’s retrieved/checked so the value interpolated into
the shortcode is a safe integer; update references in render() where $form_id is
used (the shortcode interpolation) to use the sanitized integer variable.
- Around line 1122-1253: The script currently overrides window.onerror globally,
only targets the first widget (document.querySelector), and uses unbounded
retries in initializeTinyMCE; change it to: (1) scope to all widgets by using
document.querySelectorAll('.wpuf-elementor-widget-wrapper') and iterate each
wrapper to find its textareas, (2) remove the global window.onerror override —
instead attach a local error handler to each wrapper or use try/catch around
third‑party calls to suppress only expected errors, and (3) replace the
recursive unbounded polling in initializeTinyMCE with a bounded retry/backoff
(e.g., a retry counter or maxAttempts parameter) and fail gracefully if
wp.editor.initialize never becomes available; keep references to
initializeTinyMCE, wrapper/textareas selection, window.onerror removal, and the
setTimeout restore logic so you can update those exact spots.
- Around line 1108-1117: The wrapper attribute string is being passed through
wp_kses_post() which is for sanitizing HTML content and may strip valid
Elementor attributes; remove wp_kses_post() and output the result of
$this->get_render_attribute_string('wrapper') directly since
get_render_attribute_string() already escapes attribute values. Update the echo
that currently uses wp_kses_post( $this->get_render_attribute_string( 'wrapper'
) ) to echo only $this->get_render_attribute_string( 'wrapper' ) to preserve
attributes and avoid double-filtering.
---
Duplicate comments:
In `@assets/css/elementor-frontend-forms.css`:
- Around line 85-109: The compiled CSS contains vendor-prefixed placeholder
selectors (e.g., .wpuf-elementor-widget-wrapper.wpuf-elementor-hide-placeholder
input::-webkit-input-placeholder, ::-moz-placeholder, :-ms-input-placeholder,
::-ms-input-placeholder) that should be fixed in the LESS source
(elementor-frontend-forms.less); update the LESS to use the standard
::placeholder selector (and remove the vendor-prefixed variants) for the
.wpuf-elementor-widget-wrapper.wpuf-elementor-hide-placeholder input/textarea
rules, then rebuild so the generated assets no longer include the deprecated
vendor prefixes.
---
Nitpick comments:
In `@assets/css/elementor-subscription-plans.css`:
- Around line 65-68: The CSS rule .wpuf-sub-plan-name uses a redundant shorthand
`margin: 0 0 0 0`; change it to the canonical `margin: 0` (or remove the margin
declaration if zero is the default) to simplify the stylesheet and keep
semantics identical; update the rule containing .wpuf-sub-plan-name (where
line-height: 1.3 is set) to use `margin: 0` instead of the four-value shorthand.
- Around line 35-53: Consolidate the three identical `@media` (min-width: 768px)
blocks into a single media query and move the three grid-template-columns rules
inside it: target .wpuf-subscription-plans-wrapper.wpuf-sub-plans-2
.wpuf-subscription-plans-grid, .wpuf-subscription-plans-wrapper.wpuf-sub-plans-3
.wpuf-subscription-plans-grid, and
.wpuf-subscription-plans-wrapper.wpuf-sub-plans-4 .wpuf-subscription-plans-grid
each with their respective grid-template-columns: repeat(2, 1fr), repeat(3,
1fr), and repeat(4, 1fr); remove the duplicate `@media` wrappers so only one media
query remains.
In `@assets/less/elementor-frontend-forms.less`:
- Around line 109-137: Remove the vendor-prefixed placeholder rules and replace
them with a single standard ::placeholder rule for the
.wpuf-elementor-hide-placeholder selector (targeting input and textarea). Locate
the blocks that reference input::-webkit-input-placeholder,
input::-moz-placeholder, input:-moz-placeholder, input:-ms-input-placeholder,
input::-ms-input-placeholder (and the matching textarea selectors) and
consolidate them into one rule using &.wpuf-elementor-hide-placeholder
input::placeholder and &.wpuf-elementor-hide-placeholder textarea::placeholder,
keeping the same declarations (opacity: 0; visibility: hidden;). Ensure no
vendor-prefixed placeholder selectors remain in the file.
- Around line 251-254: The global .lity rule is leaking outside the widget
scope; either scope it to the widget/popup container (e.g., change the selector
from .lity to a more specific selector like .elementor-widget .lity or
.elementor-popup .lity) so it only elevates popups inside Elementor, or if the
global behavior is intentional keep the .lity selector but add a clear comment
above the rule explaining why it must be global to ensure Lity popups overlay
Elementor modals; update the selector and CSS rule (or add the explanatory
comment) and adjust any z-index values accordingly in the existing .lity block.
- Around line 50-55: The block for selectors ".wpuf-fields .wpuf-radio-block,
.wpuf-fields .wpuf-checkbox-block" has mixed indentation—line with "align-items:
center;" uses spaces while the file uses tabs; fix by replacing the leading
spaces on that line with a single tab to match the rest of the file's
indentation style so the rule block uses consistent tabs for all properties.
In `@includes/Assets.php`:
- Around line 424-427: The 'elementor-subscription-plans' asset registration
currently hardcodes the non-minified filename; update the src value to append
$this->suffix (same pattern used by other handles like 'frontend-form') so
production loads the .min.js when appropriate; locate the
'elementor-subscription-plans' array in the Assets class and change the 'src'
construction to use WPUF_ASSET_URI . '/js/elementor-subscription-plans' .
$this->suffix . '.js' (ensuring consistency with how $this->suffix is used
elsewhere).
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php`:
- Around line 1014-1015: The call to
$this->get_render_attribute_string('wrapper') in Subscription_Plans_Widget is
intentionally echoed without additional escaping; add a PHPCS suppression
comment immediately before the echo of that unescaped output to document the
rationale and silence the linter (use the appropriate rule, e.g. phpcs:ignore
WordPress.Security.EscapeOutput.OutputNotEscaped) so the line echo '<div ' .
$this->get_render_attribute_string( 'wrapper' ) . '>'; is clearly marked as
safe.
- Around line 1199-1395: The render_features_list method duplicates the same
limit-checking pattern for many post-type keys; refactor by extracting the
repeated logic into a helper (e.g., build_limit_feature or a loop using a
$limit_labels config) that takes $all_limits, $features_list (by reference) and
a mapping of key => [unlimited_label, counted_label]; inside
render_features_list replace the repeated if-blocks with a single foreach over
$limit_labels that checks isset($all_limits[$key]) && '0' !== $all_limits[$key],
appends the unlimited_label when '-1' === $all_limits[$key], otherwise sprintf
the counted_label with intval($all_limits[$key]) — keep other special cases
(featured items, expiry, recurring, trial, mail) unchanged.
- Around line 920-960: get_subscription_plans uses $args['meta_key'] + orderby
=> meta_value_num which causes full unindexed scans on large wp_postmeta; fix by
adding a dedicated DB index on (meta_key, meta_value) for wp_postmeta during
plugin activation (implement an activation routine like create_postmeta_index or
add_postmeta_index using dbDelta/ALTER TABLE and call it from
register_activation_hook), so queries generated by
get_subscription_plans/get_posts can use the index; alternatively (or as a
fallback) avoid ordering in SQL by removing meta_key/order args in
get_subscription_plans and instead fetch unsorted posts then sort them in PHP
using wpuf()->subscription->get_subscription_meta before returning.
In `@includes/Integrations/Elementor/Widget.php`:
- Around line 475-508: Introduce a reusable variable (e.g.
$input_focus_selector) containing the long focus selector string and replace the
repeated selector occurrences in the input focus controls: use
$input_focus_selector for the 'selectors' entry in
add_control('input_focus_bg_color', ...) and for the 'selector' option in
Group_Control_Border::get_type() (name 'input_focus_border') and
Group_Control_Box_Shadow::get_type() (name 'input_focus_box_shadow'); ensure the
variable mirrors the existing $input_selector pattern and that each control
references the variable instead of duplicating the string.
- Around line 164-188: The get_wpuf_forms method currently calls get_posts with
'posts_per_page' => -1 which can load all forms into memory; change this to a
safe default cap (e.g., 'posts_per_page' => 100) to avoid unbounded queries and
update the call so it can be overridden via a new filter or argument (e.g.,
allow a 'wpuf_elementor_forms_limit' filter) so sites can increase it if needed;
keep populating $options the same and continue returning
apply_filters('wpuf_elementor_form_options', $options, $this).
| document.addEventListener('click', function(e) { | ||
| if (e.target.classList.contains('wpuf-sub-features-toggle')) { | ||
| e.preventDefault(); | ||
| var button = e.target; | ||
| var packId = button.getAttribute('data-pack-id'); | ||
| var isExpanded = button.getAttribute('data-expanded') === 'true'; | ||
| var featuresList = document.getElementById('wpuf-sub-features-list-' + packId); | ||
| var seeMoreBtn = featuresList.parentElement.querySelector('.wpuf-sub-features-see-more'); | ||
| var seeLessBtn = featuresList.parentElement.querySelector('.wpuf-sub-features-see-less'); | ||
| var hiddenItems = featuresList.querySelectorAll('.wpuf-sub-feature-hidden'); | ||
|
|
||
| if (isExpanded) { | ||
| // Collapse | ||
| hiddenItems.forEach(function(item) { | ||
| item.style.display = 'none'; | ||
| }); | ||
| seeMoreBtn.style.display = ''; | ||
| seeLessBtn.style.display = 'none'; | ||
| button.setAttribute('data-expanded', 'false'); | ||
| } else { | ||
| // Expand | ||
| hiddenItems.forEach(function(item) { | ||
| item.style.display = 'flex'; | ||
| }); | ||
| seeMoreBtn.style.display = 'none'; | ||
| seeLessBtn.style.display = ''; | ||
| seeLessBtn.setAttribute('data-expanded', 'true'); | ||
| } |
There was a problem hiding this comment.
Harden delegated click handling to avoid missed clicks and null errors.
Clicks on nested elements won’t trigger because e.target may not carry the toggle class, and missing DOM nodes will throw. Using closest() plus null guards makes this robust.
🔧 Proposed fix
-document.addEventListener('click', function(e) {
- if (e.target.classList.contains('wpuf-sub-features-toggle')) {
- e.preventDefault();
- var button = e.target;
+document.addEventListener('click', function(e) {
+ var button = e.target.closest('.wpuf-sub-features-toggle');
+ if (button) {
+ e.preventDefault();
var packId = button.getAttribute('data-pack-id');
var isExpanded = button.getAttribute('data-expanded') === 'true';
var featuresList = document.getElementById('wpuf-sub-features-list-' + packId);
+ if (!featuresList) {
+ return;
+ }
var seeMoreBtn = featuresList.parentElement.querySelector('.wpuf-sub-features-see-more');
var seeLessBtn = featuresList.parentElement.querySelector('.wpuf-sub-features-see-less');
+ if (!seeMoreBtn || !seeLessBtn) {
+ return;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@assets/js/elementor-subscription-plans.js` around lines 14 - 41, The click
handler uses e.target and assumes DOM nodes exist, causing missed toggles and
null errors; update the handler to use
e.target.closest('.wpuf-sub-features-toggle') to find the toggle button, then
guard all DOM lookups (featuresList, seeMoreBtn, seeLessBtn, hiddenItems) with
null checks before accessing or mutating them, and ensure you set/get the
data-expanded attribute on the found button variable (instead of raw e.target)
so nested clicks work reliably.
| // Skip loading frontend-subscriptions CSS on Elementor pages (Elementor widget has its own CSS) | ||
| $is_elementor_page = did_action( 'elementor/loaded' ) && isset( $post->ID ) && \Elementor\Plugin::$instance->db->is_built_with_elementor( $post->ID ); | ||
| if ( ! $is_elementor_page ) { | ||
| wp_enqueue_style( 'wpuf-frontend-subscriptions' ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, let's understand the repository structure
echo "=== Repository structure ==="
git ls-files | grep -E "(Elementor|elementor|Subscription)" | head -20
echo ""
echo "=== Searching for Subscription widget file ==="
fd -a -i "subscription" -i "widget" -0 2>/dev/null | head -20
echo ""
echo "=== Searching for Elementor-related files ==="
fd -a -i "elementor" -0 2>/dev/null | head -20Repository: weDevsOfficial/wp-user-frontend
Length of output: 1289
🏁 Script executed:
#!/bin/bash
# Run the suggested verification script
echo "=== Inspecting for Elementor subscription widget dependencies ==="
fd -a "Subscription_Plans_Widget.php" -0 2>/dev/null | xargs -0 rg -n "get_style_depends|get_script_depends|wp_enqueue_style|wp_enqueue_script|subscription-plans|frontend-subscriptions" 2>/dev/null || echo "File not found, searching more broadly..."
echo ""
echo "=== Broader search for Elementor widgets ==="
fd -a "\.php$" -0 2>/dev/null | xargs -0 rg -l "elementor.*widget|Elementor.*Widget" 2>/dev/null | head -10Repository: weDevsOfficial/wp-user-frontend
Length of output: 988
🏁 Script executed:
#!/bin/bash
# Check the context in Frontend.php around lines 71-75
echo "=== Frontend.php context ==="
sed -n '50,90p' includes/Frontend.php
echo ""
echo "=== Search for wpuf-frontend-subscriptions registration ==="
rg -n "wpuf-frontend-subscriptions" --type php -A 3 -B 3
echo ""
echo "=== Search for subscription shortcode ==="
rg -n "\[.*subscription.*\]|add_shortcode.*subscription" --type php -A 3 -B 3 | head -50Repository: weDevsOfficial/wp-user-frontend
Length of output: 7447
🏁 Script executed:
#!/bin/bash
# Check what CSS files exist and are enqueued for subscriptions
echo "=== Subscription-related CSS files ==="
fd -a "\.css$" | rg -i "subscription"
echo ""
echo "=== Check subscription shortcode definition ==="
rg -n "add_shortcode" includes/ --type php | rg -i "subscription"
echo ""
echo "=== Check for shortcode handlers ==="
fd -a "\.php$" | xargs rg -l "add_shortcode" | xargs rg -A 10 "add_shortcode.*subscription" 2>/dev/nullRepository: weDevsOfficial/wp-user-frontend
Length of output: 2669
🏁 Script executed:
#!/bin/bash
# Find the subscription_packs method
echo "=== Searching for subscription_packs method ==="
rg -n "function subscription_packs" --type php -A 30
echo ""
echo "=== Check if shortcode can render subscription plan list ==="
rg -n "wpuf_sub_pack" includes/ --type php -A 5 -B 2 | head -40Repository: weDevsOfficial/wp-user-frontend
Length of output: 8129
🏁 Script executed:
#!/bin/bash
# Compare the CSS files to see what classes they style
echo "=== Check elementor-subscription-plans CSS ==="
head -50 assets/css/elementor-subscription-plans.css
echo ""
echo "=== Check frontend-subscriptions CSS ==="
head -50 assets/css/frontend-subscriptions.cssRepository: weDevsOfficial/wp-user-frontend
Length of output: 3449
Shortcode will lose styling on Elementor-built pages if used in Elementor text/shortcode widget.
The code skips wpuf-frontend-subscriptions CSS whenever Elementor is active on the page, assuming only the Elementor widget will render. However, the [wpuf_sub_pack] shortcode can also be placed in Elementor's Shortcode/Text widget, where it depends on wpuf-frontend-subscriptions CSS (which uses Tailwind utilities). The Elementor widget CSS (wpuf-elementor-subscription-plans) has completely different styling and won't apply to shortcode markup.
Either always load the frontend-subscriptions CSS alongside the Elementor widget CSS, or conditionally skip it only if the Elementor subscription widget is actually present on the page (not just if the page is Elementor-built).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Frontend.php` around lines 71 - 75, The current check uses
did_action('elementor/loaded') and is_built_with_elementor($post->ID) to skip
enqueueing 'wpuf-frontend-subscriptions', which breaks the [wpuf_sub_pack]
shortcode when placed in Elementor Text/Shortcode widgets; instead, stop
skipping that stylesheet solely because the page is built with Elementor—always
enqueue 'wpuf-frontend-subscriptions' (and still enqueue
'wpuf-elementor-subscription-plans' when the Elementor widget is present) by
removing the conditional around wp_enqueue_style('wpuf-frontend-subscriptions')
in the code that sets $is_elementor_page so the shortcode markup retains its
Tailwind-based styles.
| public function enqueue_styles() { | ||
| // Dequeue all WPUF hardcoded styles so Elementor styles can work properly | ||
| wp_dequeue_style('wpuf-frontend-forms'); | ||
| wp_dequeue_style('wpuf-layout1'); | ||
| wp_dequeue_style('wpuf-layout2'); | ||
| wp_dequeue_style('wpuf-layout3'); | ||
| wp_dequeue_style('wpuf-layout4'); | ||
| wp_dequeue_style('wpuf-layout5'); |
There was a problem hiding this comment.
Fix PHPCS spacing in the dequeue calls.
Current formatting will fail WordPress coding standards checks.
🧹 Proposed fix
- wp_dequeue_style('wpuf-frontend-forms');
- wp_dequeue_style('wpuf-layout1');
- wp_dequeue_style('wpuf-layout2');
- wp_dequeue_style('wpuf-layout3');
- wp_dequeue_style('wpuf-layout4');
- wp_dequeue_style('wpuf-layout5');
+ wp_dequeue_style( 'wpuf-frontend-forms' );
+ wp_dequeue_style( 'wpuf-layout1' );
+ wp_dequeue_style( 'wpuf-layout2' );
+ wp_dequeue_style( 'wpuf-layout3' );
+ wp_dequeue_style( 'wpuf-layout4' );
+ wp_dequeue_style( 'wpuf-layout5' );🧰 Tools
🪛 GitHub Check: Run PHPCS inspection
[failure] 36-36:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 36-36:
Expected 1 spaces after opening parenthesis; 0 found
[failure] 35-35:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 35-35:
Expected 1 spaces after opening parenthesis; 0 found
[failure] 34-34:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 34-34:
Expected 1 spaces after opening parenthesis; 0 found
[failure] 33-33:
Expected 1 spaces before closing parenthesis; 0 found
[failure] 33-33:
Expected 1 spaces after opening parenthesis; 0 found
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Elementor.php` around lines 31 - 38, The
wp_dequeue_style calls inside enqueue_styles have PHPCS spacing issues around
their arguments; update each call (wp_dequeue_style for 'wpuf-frontend-forms',
'wpuf-layout1' .. 'wpuf-layout5') to follow WordPress coding standards by adding
a single space after the opening parenthesis and before the argument (e.g.,
change wp_dequeue_style('handle') to wp_dequeue_style( 'handle' ) for all five
calls) and ensure there are no extra spaces between the function name and the
opening parenthesis.
| protected function render_plan_card( $pack, $current_pack_id, $current_pack_status ) { | ||
| $settings = $this->get_settings_for_display(); | ||
| $billing_amount = isset( $pack->meta_value['billing_amount'] ) ? floatval( $pack->meta_value['billing_amount'] ) : 0; | ||
| $recurring_pay = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['recurring_pay'] ?? '' ); | ||
| $trial_status = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['trial_status'] ?? '' ); | ||
| $billing_cycle_number = isset( $pack->meta_value['billing_cycle_number'] ) ? $pack->meta_value['billing_cycle_number'] : 1; | ||
| $cycle_period = isset( $pack->meta_value['cycle_period'] ) ? $pack->meta_value['cycle_period'] : 'month'; | ||
| $trial_duration = isset( $pack->meta_value['trial_duration'] ) ? $pack->meta_value['trial_duration'] : 0; | ||
| $trial_duration_type = isset( $pack->meta_value['trial_duration_type'] ) ? $pack->meta_value['trial_duration_type'] : 'day'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Remove unused $settings variable.
$settings is assigned on Line 1061 via $this->get_settings_for_display() but is never referenced in render_plan_card(). This was also flagged by PHPMD.
♻️ Proposed fix
protected function render_plan_card( $pack, $current_pack_id, $current_pack_status ) {
- $settings = $this->get_settings_for_display();
$billing_amount = isset( $pack->meta_value['billing_amount'] ) ? floatval( $pack->meta_value['billing_amount'] ) : 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| protected function render_plan_card( $pack, $current_pack_id, $current_pack_status ) { | |
| $settings = $this->get_settings_for_display(); | |
| $billing_amount = isset( $pack->meta_value['billing_amount'] ) ? floatval( $pack->meta_value['billing_amount'] ) : 0; | |
| $recurring_pay = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['recurring_pay'] ?? '' ); | |
| $trial_status = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['trial_status'] ?? '' ); | |
| $billing_cycle_number = isset( $pack->meta_value['billing_cycle_number'] ) ? $pack->meta_value['billing_cycle_number'] : 1; | |
| $cycle_period = isset( $pack->meta_value['cycle_period'] ) ? $pack->meta_value['cycle_period'] : 'month'; | |
| $trial_duration = isset( $pack->meta_value['trial_duration'] ) ? $pack->meta_value['trial_duration'] : 0; | |
| $trial_duration_type = isset( $pack->meta_value['trial_duration_type'] ) ? $pack->meta_value['trial_duration_type'] : 'day'; | |
| protected function render_plan_card( $pack, $current_pack_id, $current_pack_status ) { | |
| $billing_amount = isset( $pack->meta_value['billing_amount'] ) ? floatval( $pack->meta_value['billing_amount'] ) : 0; | |
| $recurring_pay = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['recurring_pay'] ?? '' ); | |
| $trial_status = wpuf_is_checkbox_or_toggle_on( $pack->meta_value['trial_status'] ?? '' ); | |
| $billing_cycle_number = isset( $pack->meta_value['billing_cycle_number'] ) ? $pack->meta_value['billing_cycle_number'] : 1; | |
| $cycle_period = isset( $pack->meta_value['cycle_period'] ) ? $pack->meta_value['cycle_period'] : 'month'; | |
| $trial_duration = isset( $pack->meta_value['trial_duration'] ) ? $pack->meta_value['trial_duration'] : 0; | |
| $trial_duration_type = isset( $pack->meta_value['trial_duration_type'] ) ? $pack->meta_value['trial_duration_type'] : 'day'; |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1061-1061: Avoid unused local variables such as '$settings'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines
1060 - 1068, The local variable $settings in render_plan_card() is unused;
remove the assignment "$settings = $this->get_settings_for_display();" to
eliminate the unused variable warning (PHPMD) and avoid unnecessary calls to
get_settings_for_display(); keep the rest of render_plan_card() intact and
ensure no other code in that method relies on $settings.
| // Trial Description | ||
| if ( $billing_amount > 0 && $recurring_pay && $trial_status && $trial_duration > 0 ) { | ||
| $duration = _n( $trial_duration_type, $trial_duration_type . 's', $trial_duration, 'wp-user-frontend' ); | ||
| $trial_text = sprintf( __( 'Trial available for first %1$s %2$s', 'wp-user-frontend' ), $trial_duration, $duration ); | ||
| echo '<div class="wpuf-sub-trial-description">' . esc_html( $trial_text ) . '</div>'; | ||
| } |
There was a problem hiding this comment.
Incorrect _n() usage — passes raw values instead of translatable strings.
_n( $trial_duration_type, $trial_duration_type . 's', ... ) passes dynamic values like 'day' / 'days' as the singular/plural form arguments. These strings are not registered in the translation catalog, so _n() cannot look them up — it just returns the raw English string. The same pattern appears on Lines 1363 and 1380 with manual 's' suffix appending.
🛡️ Proposed fix — use a mapping for known period types
+/**
+ * Get the human-readable duration label.
+ */
+private function get_duration_label( $type, $count ) {
+ $labels = [
+ 'day' => [ __( '%d day', 'wp-user-frontend' ), __( '%d days', 'wp-user-frontend' ) ],
+ 'week' => [ __( '%d week', 'wp-user-frontend' ), __( '%d weeks', 'wp-user-frontend' ) ],
+ 'month' => [ __( '%d month', 'wp-user-frontend' ), __( '%d months', 'wp-user-frontend' ) ],
+ 'year' => [ __( '%d year', 'wp-user-frontend' ), __( '%d years', 'wp-user-frontend' ) ],
+ ];
+
+ if ( isset( $labels[ $type ] ) ) {
+ return sprintf( _n( $labels[ $type ][0], $labels[ $type ][1], $count, 'wp-user-frontend' ), $count );
+ }
+
+ return $count . ' ' . $type;
+}Then replace the _n() call:
-$duration = _n( $trial_duration_type, $trial_duration_type . 's', $trial_duration, 'wp-user-frontend' );
-$trial_text = sprintf( __( 'Trial available for first %1$s %2$s', 'wp-user-frontend' ), $trial_duration, $duration );
+$trial_text = sprintf(
+ __( 'Trial available for first %s', 'wp-user-frontend' ),
+ $this->get_duration_label( $trial_duration_type, $trial_duration )
+);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines
1110 - 1115, The code is calling _n() with dynamic values ($trial_duration_type)
which aren’t translatable; replace this by mapping known period keys (e.g.,
'day', 'week', 'month', 'year') to literal translatable singular/plural strings
and then call _n() with those mapped strings and the $trial_duration count (for
example map 'day' => ['singular'=>'day','plural'=>'days'] and use _n( 'day',
'days', $trial_duration, 'wp-user-frontend' ) ), then use that returned
pluralized label when building $trial_text; update the same pattern where
$trial_duration_type is used (the other occurrences in Subscription_Plans_Widget
methods around the trial/recurring text) so all pluralization uses static
translatable strings instead of dynamic values.
| protected function render_button( $pack, $current_pack_id, $current_pack_status, $billing_amount ) { | ||
| $user_id = get_current_user_id(); | ||
|
|
||
| $is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status ); | ||
|
|
||
| // Get button labels from WPUF settings | ||
| if ( ! is_user_logged_in() ) { | ||
| $button_text = wpuf_get_option( 'logged_out_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Sign Up', 'wp-user-frontend' ); | ||
| } elseif ( $billing_amount === 0.0 || $billing_amount === '0.00' ) { | ||
| $button_text = wpuf_get_option( 'free_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Free', 'wp-user-frontend' ); | ||
| } else { | ||
| $button_text = wpuf_get_option( 'logged_in_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Buy Now', 'wp-user-frontend' ); | ||
| } | ||
|
|
||
| // Build button URL | ||
| if ( ! is_user_logged_in() ) { | ||
| $query_args = [ | ||
| 'action' => 'register', | ||
| 'type' => 'wpuf_sub', | ||
| 'pack_id' => $pack->ID, | ||
| ]; | ||
| $button_url = wp_registration_url(); | ||
| } else { | ||
| $query_args = [ | ||
| 'action' => 'wpuf_pay', | ||
| 'type' => 'pack', | ||
| 'pack_id' => $pack->ID, | ||
| ]; | ||
| $button_url = get_permalink( wpuf_get_option( 'payment_page', 'wpuf_payment' ) ); | ||
| } | ||
|
|
||
| $button_url = add_query_arg( $query_args, $button_url ); | ||
|
|
||
| $this->add_render_attribute( | ||
| 'button-' . $pack->ID, | ||
| [ | ||
| 'class' => [ 'wpuf-sub-button', 'wpuf-sub-button-' . $pack->ID ], | ||
| 'href' => esc_url( $button_url ), | ||
| ] | ||
| ); | ||
|
|
||
| if ( $is_completed ) { | ||
| $this->add_render_attribute( 'button-' . $pack->ID, 'href', 'javascript:void(0)' ); | ||
| $this->add_render_attribute( 'button-' . $pack->ID, 'class', 'wpuf-sub-button-disabled' ); | ||
| } | ||
|
|
||
| echo '<div class="wpuf-sub-button-wrapper">'; | ||
| echo '<a ' . $this->get_render_attribute_string( 'button-' . $pack->ID ) . '>' . esc_html( $button_text ) . '</a>'; | ||
| echo '</div>'; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the actual file to verify the code and line numbers
cat -n includes/Integrations/Elementor/Subscription_Plans_Widget.php | sed -n '1137,1189p'Repository: weDevsOfficial/wp-user-frontend
Length of output: 2411
🏁 Script executed:
# Let's also check if $user_id is used anywhere in the render_button method
sed -n '1137,1189p' includes/Integrations/Elementor/Subscription_Plans_Widget.php | grep -c '\$user_id'Repository: weDevsOfficial/wp-user-frontend
Length of output: 76
🏁 Script executed:
# Check the broader context around render_button
cat -n includes/Integrations/Elementor/Subscription_Plans_Widget.php | sed -n '1137,1200p'Repository: weDevsOfficial/wp-user-frontend
Length of output: 2722
🏁 Script executed:
# Check if Elementor source is available in the repository
find . -name "*.php" -path "*/elementor*" -type f | head -20Repository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
# Search for add_render_attribute usage patterns in the codebase
rg "add_render_attribute" -A 2 --max-count=10 -t phpRepository: weDevsOfficial/wp-user-frontend
Length of output: 1990
🏁 Script executed:
# Let's check the class hierarchy to understand which class's add_render_attribute is being used
grep -n "class Subscription_Plans_Widget" includes/Integrations/Elementor/Subscription_Plans_Widget.phpRepository: weDevsOfficial/wp-user-frontend
Length of output: 131
🏁 Script executed:
# Check the parent class and imports
head -50 includes/Integrations/Elementor/Subscription_Plans_Widget.php | grep -E "(extends|use|namespace)"Repository: weDevsOfficial/wp-user-frontend
Length of output: 406
🏁 Script executed:
# Let's verify the actual behavior by checking how Elementor is likely being used
# Search for any vendor files or check if we can find pattern of how add_render_attribute handles array vs string params
rg "add_render_attribute.*\[" -A 5 includes/Integrations/Elementor/ | head -30Repository: weDevsOfficial/wp-user-frontend
Length of output: 703
🏁 Script executed:
# Let's also check if there are any comments or patterns in Widget.php that show how add_render_attribute is expected to be used
cat -n includes/Integrations/Elementor/Widget.php | sed -n '1,100p' | grep -A 10 "add_render_attribute"Repository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
# Check if composer.json or lock file reveals Elementor version
find . -name "composer.*" -type f | xargs cat 2>/dev/null | grep -i elementor || echo "No elementor dependency found"Repository: weDevsOfficial/wp-user-frontend
Length of output: 104
🏁 Script executed:
# Look for any inline Elementor documentation or comments in the codebase
rg "add_render_attribute" -B 3 includes/Integrations/Elementor/Subscription_Plans_Widget.php | head -40Repository: weDevsOfficial/wp-user-frontend
Length of output: 858
🏁 Script executed:
# Check if there are any test files that might show expected behavior
find . -name "*test*" -type f | xargs grep -l "add_render_attribute" 2>/dev/null || echo "No tests found"Repository: weDevsOfficial/wp-user-frontend
Length of output: 89
🏁 Script executed:
# Let's check if there are similar patterns in the Widget.php file where it also uses add_render_attribute multiple times
cat -n includes/Integrations/Elementor/Widget.php | grep -B 5 -A 5 "add_render_attribute.*class"Repository: weDevsOfficial/wp-user-frontend
Length of output: 760
🏁 Script executed:
# Check if javascript:void(0) is actually a valid href or if this is a no-op button pattern
rg "javascript:void" includes/Repository: weDevsOfficial/wp-user-frontend
Length of output: 223
🏁 Script executed:
# Check the render output to see if there might be other processing that normalizes attributes
cat -n includes/Integrations/Elementor/Subscription_Plans_Widget.php | sed -n '1186,1188p'Repository: weDevsOfficial/wp-user-frontend
Length of output: 278
🏁 Script executed:
# Let's check how get_render_attribute_string might work by looking for any custom implementations
rg "get_render_attribute_string" -B 5 -A 10 includes/Integrations/Elementor/Repository: weDevsOfficial/wp-user-frontend
Length of output: 6243
🏁 Script executed:
# Let's also check if there are any WordPress/Elementor version constraints in composer.json or requirements
find . -name "*.php" -type f | head -5 | xargs grep -h "Elementor\|elementor" | grep -i "version\|require" | head -10Repository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
# Let's verify the method signature in parent class by checking if there's any Elementor documentation or vendor files
find . -name "vendor" -o -name "node_modules" | head -5Repository: weDevsOfficial/wp-user-frontend
Length of output: 90
🏁 Script executed:
# Let's see if we can find any example of how the get_render_attribute_string output looks
# by examining the render method output more carefully
cat -n includes/Integrations/Elementor/Subscription_Plans_Widget.php | sed -n '1014,1027p'Repository: weDevsOfficial/wp-user-frontend
Length of output: 530
🏁 Script executed:
# Check what's in the vendor directory and if Elementor is there
ls -la ./assets/vendor/ 2>/dev/null | head -20Repository: weDevsOfficial/wp-user-frontend
Length of output: 980
🏁 Script executed:
# Look for Elementor Widget_Base class definition
find ./assets/vendor -name "*.php" -path "*elementor*" -type f 2>/dev/null | grep -i widget | head -10Repository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
# Alternative: Check composer.json or package files for Elementor version
cat composer.json 2>/dev/null | grep -i elementor || echo "No elementor in root composer"Repository: weDevsOfficial/wp-user-frontend
Length of output: 104
🏁 Script executed:
# Look for add_render_attribute method in vendor
find ./assets/vendor -name "*.php" -type f 2>/dev/null | xargs grep -l "function add_render_attribute" 2>/dev/null | head -3Repository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
# Let's verify one more time the exact behavior by checking Elementor's documented behavior
# Check if there are any inline comments or documentation in the code about this
rg "overwrite|add_render_attribute" -B 2 -A 2 includes/Integrations/Elementor/Widget.php | head -30Repository: weDevsOfficial/wp-user-frontend
Length of output: 802
Remove unused variable and fix href attribute override.
Line 1138 declares $user_id but never uses it—remove it. More critically, lines 1173–1179 set the button's href attribute, then line 1182 attempts to override it for completed packs. Elementor's add_render_attribute() with the default $overwrite=false appends attributes rather than replacing them, resulting in invalid HTML with duplicate href values: href="url" href="javascript:void(0)". The browser uses the first value, rendering the second call ineffective.
Fix by setting $button_url conditionally before the initial add_render_attribute call:
Proposed fix
protected function render_button( $pack, $current_pack_id, $current_pack_status, $billing_amount ) {
- $user_id = get_current_user_id();
-
$is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status );
// ... button text and URL logic ...
+ // Override href for completed packs before setting render attributes
+ if ( $is_completed ) {
+ $button_url = 'javascript:void(0)';
+ }
+
$this->add_render_attribute(
'button-' . $pack->ID,
[
'class' => [ 'wpuf-sub-button', 'wpuf-sub-button-' . $pack->ID ],
'href' => esc_url( $button_url ),
]
);
if ( $is_completed ) {
- $this->add_render_attribute( 'button-' . $pack->ID, 'href', 'javascript:void(0)' );
$this->add_render_attribute( 'button-' . $pack->ID, 'class', 'wpuf-sub-button-disabled' );
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| protected function render_button( $pack, $current_pack_id, $current_pack_status, $billing_amount ) { | |
| $user_id = get_current_user_id(); | |
| $is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status ); | |
| // Get button labels from WPUF settings | |
| if ( ! is_user_logged_in() ) { | |
| $button_text = wpuf_get_option( 'logged_out_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Sign Up', 'wp-user-frontend' ); | |
| } elseif ( $billing_amount === 0.0 || $billing_amount === '0.00' ) { | |
| $button_text = wpuf_get_option( 'free_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Free', 'wp-user-frontend' ); | |
| } else { | |
| $button_text = wpuf_get_option( 'logged_in_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Buy Now', 'wp-user-frontend' ); | |
| } | |
| // Build button URL | |
| if ( ! is_user_logged_in() ) { | |
| $query_args = [ | |
| 'action' => 'register', | |
| 'type' => 'wpuf_sub', | |
| 'pack_id' => $pack->ID, | |
| ]; | |
| $button_url = wp_registration_url(); | |
| } else { | |
| $query_args = [ | |
| 'action' => 'wpuf_pay', | |
| 'type' => 'pack', | |
| 'pack_id' => $pack->ID, | |
| ]; | |
| $button_url = get_permalink( wpuf_get_option( 'payment_page', 'wpuf_payment' ) ); | |
| } | |
| $button_url = add_query_arg( $query_args, $button_url ); | |
| $this->add_render_attribute( | |
| 'button-' . $pack->ID, | |
| [ | |
| 'class' => [ 'wpuf-sub-button', 'wpuf-sub-button-' . $pack->ID ], | |
| 'href' => esc_url( $button_url ), | |
| ] | |
| ); | |
| if ( $is_completed ) { | |
| $this->add_render_attribute( 'button-' . $pack->ID, 'href', 'javascript:void(0)' ); | |
| $this->add_render_attribute( 'button-' . $pack->ID, 'class', 'wpuf-sub-button-disabled' ); | |
| } | |
| echo '<div class="wpuf-sub-button-wrapper">'; | |
| echo '<a ' . $this->get_render_attribute_string( 'button-' . $pack->ID ) . '>' . esc_html( $button_text ) . '</a>'; | |
| echo '</div>'; | |
| } | |
| protected function render_button( $pack, $current_pack_id, $current_pack_status, $billing_amount ) { | |
| $is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status ); | |
| // Get button labels from WPUF settings | |
| if ( ! is_user_logged_in() ) { | |
| $button_text = wpuf_get_option( 'logged_out_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Sign Up', 'wp-user-frontend' ); | |
| } elseif ( $billing_amount === 0.0 || $billing_amount === '0.00' ) { | |
| $button_text = wpuf_get_option( 'free_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Free', 'wp-user-frontend' ); | |
| } else { | |
| $button_text = wpuf_get_option( 'logged_in_label', 'wpuf_subscription_settings', '' ); | |
| $button_text = $button_text ? $button_text : __( 'Buy Now', 'wp-user-frontend' ); | |
| } | |
| // Build button URL | |
| if ( ! is_user_logged_in() ) { | |
| $query_args = [ | |
| 'action' => 'register', | |
| 'type' => 'wpuf_sub', | |
| 'pack_id' => $pack->ID, | |
| ]; | |
| $button_url = wp_registration_url(); | |
| } else { | |
| $query_args = [ | |
| 'action' => 'wpuf_pay', | |
| 'type' => 'pack', | |
| 'pack_id' => $pack->ID, | |
| ]; | |
| $button_url = get_permalink( wpuf_get_option( 'payment_page', 'wpuf_payment' ) ); | |
| } | |
| $button_url = add_query_arg( $query_args, $button_url ); | |
| // Override href for completed packs before setting render attributes | |
| if ( $is_completed ) { | |
| $button_url = 'javascript:void(0)'; | |
| } | |
| $this->add_render_attribute( | |
| 'button-' . $pack->ID, | |
| [ | |
| 'class' => [ 'wpuf-sub-button', 'wpuf-sub-button-' . $pack->ID ], | |
| 'href' => esc_url( $button_url ), | |
| ] | |
| ); | |
| if ( $is_completed ) { | |
| $this->add_render_attribute( 'button-' . $pack->ID, 'class', 'wpuf-sub-button-disabled' ); | |
| } | |
| echo '<div class="wpuf-sub-button-wrapper">'; | |
| echo '<a ' . $this->get_render_attribute_string( 'button-' . $pack->ID ) . '>' . esc_html( $button_text ) . '</a>'; | |
| echo '</div>'; | |
| } |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1138-1138: Avoid unused local variables such as '$user_id'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines
1137 - 1189, In render_button remove the unused $user_id declaration and stop
appending duplicate href attributes: compute $button_url (and add the disabled
class) conditionally when $is_completed is true (e.g. set $button_url to
'javascript:void(0)' and include 'wpuf-sub-button-disabled' in the class list)
before calling add_render_attribute for 'button-' . $pack->ID so the href is set
once; then remove the later add_render_attribute calls that attempted to
override href/class for the completed pack.
| $is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status ); | ||
|
|
||
| // Get button labels from WPUF settings | ||
| if ( ! is_user_logged_in() ) { | ||
| $button_text = wpuf_get_option( 'logged_out_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Sign Up', 'wp-user-frontend' ); | ||
| } elseif ( $billing_amount === 0.0 || $billing_amount === '0.00' ) { | ||
| $button_text = wpuf_get_option( 'free_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Free', 'wp-user-frontend' ); | ||
| } else { | ||
| $button_text = wpuf_get_option( 'logged_in_label', 'wpuf_subscription_settings', '' ); | ||
| $button_text = $button_text ? $button_text : __( 'Buy Now', 'wp-user-frontend' ); | ||
| } |
There was a problem hiding this comment.
Bug: Float-to-string strict comparison on Line 1146 is always false.
$billing_amount is cast to floatval() on Line 1062, so $billing_amount === '0.00' (strict comparison of float to string) will never be true. Only the $billing_amount === 0.0 branch can match. Additionally, Line 1140 uses loose == to compare pack IDs, which can produce surprising matches between different types.
🐛 Proposed fix
-$is_completed = ( $current_pack_id == $pack->ID && 'completed' === $current_pack_status );
+$is_completed = ( (int) $current_pack_id === $pack->ID && 'completed' === $current_pack_status );
// ...
-} elseif ( $billing_amount === 0.0 || $billing_amount === '0.00' ) {
+} elseif ( $billing_amount === 0.0 ) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Subscription_Plans_Widget.php` around lines
1140 - 1152, The pack ID comparison and billing amount check are
type-mismatched: change the loose equality in $is_completed to a strict
type-safe comparison (e.g., cast both $current_pack_id and $pack->ID to the same
type before comparing) and remove the impossible strict float-to-string check by
comparing $billing_amount as a number (e.g., $billing_amount === 0.0 or
(float)$billing_amount === 0.0) or by formatting to a string before comparing;
update the references in the $is_completed expression and the billing-amount
branch that reads $billing_amount === '0.00' to use consistent types for
reliable results.
| protected function render() { | ||
| $settings = $this->get_settings_for_display(); | ||
| $form_id = isset( $settings['form_id'] ) ? $settings['form_id'] : null; | ||
|
|
||
| if ( empty( $form_id ) ) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Sanitize $form_id as integer before shortcode interpolation.
$form_id is taken directly from widget settings and interpolated into a shortcode string on Line 1108 without being cast to int. Although Elementor's SELECT control constrains the UI value, server-side sanitization is still necessary as a defense-in-depth measure against tampered requests.
🛡️ Proposed fix
$settings = $this->get_settings_for_display();
-$form_id = isset( $settings['form_id'] ) ? $settings['form_id'] : null;
+$form_id = isset( $settings['form_id'] ) ? absint( $settings['form_id'] ) : 0;
-if ( empty( $form_id ) ) {
+if ( ! $form_id ) {
return;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Widget.php` around lines 1065 - 1071, In the
render() method of the Elementor widget, sanitize the $form_id from
get_settings_for_display() before it’s used in the shortcode string: ensure
$form_id is cast to an integer (e.g., via (int) or intval) immediately after
it’s retrieved/checked so the value interpolated into the shortcode is a safe
integer; update references in render() where $form_id is used (the shortcode
interpolation) to use the sanitized integer variable.
| $shortcode_str = '[wpuf_form id="' . $form_id . '"]'; | ||
| $output = do_shortcode( $shortcode_str ); | ||
|
|
||
| $is_elementor = class_exists( '\Elementor\Plugin' ) && ( | ||
| ( isset( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode() ) | ||
| || ( isset( \Elementor\Plugin::$instance->preview ) && \Elementor\Plugin::$instance->preview->is_preview_mode() ) | ||
| ); | ||
|
|
||
| echo '<div ' . wp_kses_post( $this->get_render_attribute_string( 'wrapper' ) ) . '>'; | ||
| echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- shortcode output |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n includes/Integrations/Elementor/Widget.php | sed -n '1100,1125p'Repository: weDevsOfficial/wp-user-frontend
Length of output: 1586
🌐 Web query:
Elementor get_render_attribute_string escapes output HTML attributes
💡 Result:
get_render_attribute_string() is designed to output safe HTML attributes, and (via WordPress escaping) it will escape quotes/angle brackets/etc. in attribute values. Elementor’s own docs recommend using add_render_attribute() / get_render_attribute_string() for this and note you should escape user data for attributes. [1]
A common “it’s escaping my HTML entities” confusion is actually WordPress’s esc_attr() behavior: it does not double-encode existing entities (e.g. & stays &), and when the browser parses the HTML, the attribute value becomes a literal &. WordPress documents this (“Will never double encode entities”), and it’s a known/intentional behavior discussed in core tickets. [2] [3]
What to do
- Don’t pre-escape before calling
add_render_attribute()(avoidesc_attr()twice). Pass the raw value and let Elementor/WordPress escape for the attribute context. [1] [2] - If you really need the attribute to contain the literal text
&(not&), you must double-encode it yourself before adding it (so the HTML contains&amp;), e.g. using WordPress’s lower-level escaping with double-encoding (this need is exactly why core discusses double-encoding in tickets). [3]
Sources: Elementor dev docs on rendering attributes [1]; WordPress esc_attr() behavior [2]; WordPress core discussion of esc_attr() not double-encoding entities and why that matters [3].
🌐 Web query:
WordPress wp_kses_post function purpose HTML attribute sanitization
💡 Result:
wp_kses_post( $data ) is WordPress’s “allow the same HTML you can normally put in post content” sanitizer. It’s primarily used to prevent XSS while still permitting common formatting markup (links, paragraphs, images, etc.). It expects unslashed input and returns the filtered HTML. Internally it’s just a wrapper for wp_kses( $data, 'post' ). [1]
How it sanitizes HTML attributes
When wp_kses_post() runs, WordPress:
- Keeps only allowed tags for the
postcontext (the “post content” allowlist). - For allowed tags, it keeps only allowed attributes for each tag; any other attributes are stripped.
- The
postallowlist also includes “global” attributes that WordPress considers standard (historically including things likeclass,id,style,title). [4]
Customizing which attributes are allowed
To change what attributes/tags are allowed for a context like post, use the wp_kses_allowed_html filter (note: tags/attribute names must be lowercase in the allowlist). [2]
Example: allow the sizes attribute on <img> in the post context (so it won’t be removed by KSES): [3][2]
add_filter( 'wp_kses_allowed_html', function( $allowed, $context ) {
if ( $context === 'post' && isset( $allowed['img'] ) ) {
$allowed['img']['sizes'] = true;
}
return $allowed;
}, 10, 2 );Sources: [1] WordPress Developer Reference: wp_kses_post() (developer.wordpress.org), [2] WordPress Developer Reference: wp_kses_allowed_html hook (developer.wordpress.org), [3] Example of adding sizes via wp_kses_allowed_html (florianbrinkmann.com), [4] WordPress core discussion on standard/global attributes in post KSES allowlist (core.trac.wordpress.org)
Citations:
- 1: https://developer.wordpress.org/reference/functions/wp_kses_post/?utm_source=openai
- 2: https://developer.wordpress.org/reference/hooks/wp_kses_allowed_html/?utm_source=openai
- 3: https://florianbrinkmann.com/en/filtered-block-markup-6018/?utm_source=openai
- 4: https://core.trac.wordpress.org/ticket/20210?utm_source=openai
Remove wp_kses_post() — it's incorrect for attribute strings.
On line 1116, wp_kses_post() is applied to the output of get_render_attribute_string('wrapper'), which returns escaped HTML attributes. wp_kses_post() is designed to sanitize HTML content (allowing tags like <a>, <img>, etc.) and filters only allowed tags and attributes in the post context. It is not appropriate for attribute strings and could strip Elementor-specific attributes that aren't in WordPress's post allowlist. Since get_render_attribute_string() already escapes attribute values internally, you can safely echo its output directly.
🛡️ Proposed fix
-echo '<div ' . wp_kses_post( $this->get_render_attribute_string( 'wrapper' ) ) . '>';
+// Elementor's get_render_attribute_string() already escapes attribute values.
+echo '<div ' . $this->get_render_attribute_string( 'wrapper' ) . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $shortcode_str = '[wpuf_form id="' . $form_id . '"]'; | |
| $output = do_shortcode( $shortcode_str ); | |
| $is_elementor = class_exists( '\Elementor\Plugin' ) && ( | |
| ( isset( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode() ) | |
| || ( isset( \Elementor\Plugin::$instance->preview ) && \Elementor\Plugin::$instance->preview->is_preview_mode() ) | |
| ); | |
| echo '<div ' . wp_kses_post( $this->get_render_attribute_string( 'wrapper' ) ) . '>'; | |
| echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- shortcode output | |
| $shortcode_str = '[wpuf_form id="' . $form_id . '"]'; | |
| $output = do_shortcode( $shortcode_str ); | |
| $is_elementor = class_exists( '\Elementor\Plugin' ) && ( | |
| ( isset( \Elementor\Plugin::$instance->editor ) && \Elementor\Plugin::$instance->editor->is_edit_mode() ) | |
| || ( isset( \Elementor\Plugin::$instance->preview ) && \Elementor\Plugin::$instance->preview->is_preview_mode() ) | |
| ); | |
| // Elementor's get_render_attribute_string() already escapes attribute values. | |
| echo '<div ' . $this->get_render_attribute_string( 'wrapper' ) . '>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped | |
| echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- shortcode output |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Widget.php` around lines 1108 - 1117, The
wrapper attribute string is being passed through wp_kses_post() which is for
sanitizing HTML content and may strip valid Elementor attributes; remove
wp_kses_post() and output the result of
$this->get_render_attribute_string('wrapper') directly since
get_render_attribute_string() already escapes attribute values. Update the echo
that currently uses wp_kses_post( $this->get_render_attribute_string( 'wrapper'
) ) to echo only $this->get_render_attribute_string( 'wrapper' ) to preserve
attributes and avoid double-filtering.
| <script> | ||
| (function() { | ||
| // Suppress non-critical errors for fields that require external scripts | ||
| // (Google Maps, barrating) that may not be loaded in Elementor preview | ||
| var originalErrorHandler = window.onerror; | ||
| window.onerror = function(msg, url, line, col, error) { | ||
| // Suppress Google Maps and barrating errors in Elementor preview | ||
| if (msg && ( | ||
| msg.indexOf('google is not defined') !== -1 || | ||
| msg.indexOf('barrating is not a function') !== -1 || | ||
| msg.indexOf('$(...).barrating is not a function') !== -1 | ||
| )) { | ||
| return true; // Suppress error | ||
| } | ||
| // Call original error handler for other errors | ||
| if (originalErrorHandler) { | ||
| return originalErrorHandler.apply(this, arguments); | ||
| } | ||
| return false; | ||
| }; | ||
|
|
||
| var wrapper = document.querySelector('.wpuf-elementor-widget-wrapper'); | ||
| if (wrapper) { | ||
| var textareas = wrapper.querySelectorAll('textarea.wp-editor-area'); | ||
|
|
||
| // Function to initialize TinyMCE for a textarea | ||
| function initializeTinyMCE(textarea) { | ||
| var editorId = textarea.id; | ||
|
|
||
| // Wait for wp.editor to be available | ||
| if (typeof wp === 'undefined' || typeof wp.editor === 'undefined') { | ||
| setTimeout(function() { | ||
| initializeTinyMCE(textarea); | ||
| }, 100); | ||
| return false; | ||
| } | ||
|
|
||
| if (typeof wp.editor.initialize !== 'function') { | ||
| return false; | ||
| } | ||
|
|
||
| // Check if already initialized | ||
| if (typeof tinymce !== 'undefined' && tinymce.get(editorId)) { | ||
| return true; | ||
| } | ||
|
|
||
| // Get editor settings - try to match WordPress default settings | ||
| // Check if there's a wp-editor-wrap parent to determine settings | ||
| var wrap = textarea.closest('.wp-editor-wrap'); | ||
| var isTeeny = wrap && wrap.classList.contains('html-active') ? false : (wrap && wrap.classList.contains('tmce-active') ? false : true); | ||
|
|
||
| var editorSettings = { | ||
| tinymce: { | ||
| wpautop: true, | ||
| toolbar1: 'bold,italic,bullist,numlist,link', | ||
| toolbar2: '', | ||
| media_buttons: false, | ||
| resize: true | ||
| }, | ||
| quicktags: false, | ||
| textarea_name: textarea.name || editorId | ||
| }; | ||
|
|
||
| // If teeny mode, use simpler toolbar | ||
| if (isTeeny || textarea.closest('.wpuf-fields').querySelector('.mce-tinymce')) { | ||
| editorSettings.tinymce.toolbar1 = 'bold,italic,bullist,numlist'; | ||
| editorSettings.teeny = true; | ||
| } | ||
|
|
||
| try { | ||
| wp.editor.initialize(editorId, editorSettings); | ||
| return true; | ||
| } catch(e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| // Check existing instances and initialize if needed | ||
| textareas.forEach(function(textarea) { | ||
| var editorId = textarea.id; | ||
| if (typeof tinymce === 'undefined' || !tinymce.get(editorId)) { | ||
| initializeTinyMCE(textarea); | ||
| } | ||
| }); | ||
|
|
||
| // Check after a delay to see if TinyMCE initializes or retry if needed | ||
| setTimeout(function() { | ||
| textareas.forEach(function(textarea) { | ||
| var editorId = textarea.id; | ||
| if (typeof tinymce === 'undefined' || !tinymce.get(editorId)) { | ||
| initializeTinyMCE(textarea); | ||
| } | ||
| }); | ||
| }, 500); | ||
|
|
||
| // Also check when Elementor triggers content refresh | ||
| if (typeof elementorFrontend !== 'undefined' && elementorFrontend.hooks) { | ||
| elementorFrontend.hooks.addAction('frontend/element_ready/wpuf-form.default', function($scope) { | ||
| setTimeout(function() { | ||
| var textareas = $scope.find('textarea.wp-editor-area'); | ||
| textareas.each(function(idx, textarea) { | ||
| var editorId = textarea.id; | ||
| if (typeof tinymce === 'undefined' || !tinymce.get(editorId)) { | ||
| if (typeof wp !== 'undefined' && typeof wp.editor !== 'undefined' && typeof wp.editor.initialize === 'function') { | ||
| try { | ||
| wp.editor.initialize(editorId, { | ||
| tinymce: { | ||
| wpautop: true, | ||
| toolbar1: 'bold,italic,bullist,numlist,link', | ||
| toolbar2: '', | ||
| media_buttons: false | ||
| }, | ||
| quicktags: false | ||
| }); | ||
| } catch(e) { | ||
| // Silent fail | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }, 100); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Restore original error handler after initialization | ||
| setTimeout(function() { | ||
| if (originalErrorHandler) { | ||
| window.onerror = originalErrorHandler; | ||
| } | ||
| }, 2000); | ||
| })(); |
There was a problem hiding this comment.
Global window.onerror override is fragile and risks masking real errors.
Several concerns with this inline script:
-
Multiple widget bug (Line 1143):
document.querySelector('.wpuf-elementor-widget-wrapper')only selects the first matching element. If a page has multiple WPUF form widgets, only the first one will have TinyMCE initialized. Usedocument.querySelectorAllor scope to the current widget's container. -
Global error suppression (Lines 1126–1141): Overriding
window.onerrorto swallow Google Maps and barrating errors can mask legitimate issues in other scripts. The 2-second restore timeout (Line 1248) is arbitrary — TinyMCE init may take longer, or other code may fire errors in the interim. -
Unbounded retry (Lines 1148–1157):
initializeTinyMCEcalls itself viasetTimeoutwith no retry limit whenwp.editorisn't available, creating a potential infinite polling loop.
🔧 Suggested improvements
-(function() {
- // Suppress non-critical errors...
- var originalErrorHandler = window.onerror;
- window.onerror = function(msg, url, line, col, error) {
- ...
- };
-
- var wrapper = document.querySelector('.wpuf-elementor-widget-wrapper');
+(function() {
+ var MAX_RETRIES = 50; // ~5 seconds at 100ms
+
+ // Scope to the current widget instance instead of a global selector
+ var wrapper = document.querySelector('[data-id="<?php echo esc_attr( $this->get_id() ); ?>"] .wpuf-elementor-widget-wrapper');
if (wrapper) {
var textareas = wrapper.querySelectorAll('textarea.wp-editor-area');
- function initializeTinyMCE(textarea) {
+ function initializeTinyMCE(textarea, retries) {
+ retries = retries || 0;
var editorId = textarea.id;
if (typeof wp === 'undefined' || typeof wp.editor === 'undefined') {
- setTimeout(function() {
- initializeTinyMCE(textarea);
- }, 100);
+ if (retries < MAX_RETRIES) {
+ setTimeout(function() {
+ initializeTinyMCE(textarea, retries + 1);
+ }, 100);
+ }
return false;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@includes/Integrations/Elementor/Widget.php` around lines 1122 - 1253, The
script currently overrides window.onerror globally, only targets the first
widget (document.querySelector), and uses unbounded retries in
initializeTinyMCE; change it to: (1) scope to all widgets by using
document.querySelectorAll('.wpuf-elementor-widget-wrapper') and iterate each
wrapper to find its textareas, (2) remove the global window.onerror override —
instead attach a local error handler to each wrapper or use try/catch around
third‑party calls to suppress only expected errors, and (3) replace the
recursive unbounded polling in initializeTinyMCE with a bounded retry/backoff
(e.g., a retry counter or maxAttempts parameter) and fail gracefully if
wp.editor.initialize never becomes available; keep references to
initializeTinyMCE, wrapper/textareas selection, window.onerror removal, and the
setTimeout restore logic so you can update those exact spots.
|
My feedback on the subscription plan widget, also needs to have UF - Widget Name (UF - Subscription Plans). |
closes #1409
Summary
This PR introduce a new Elementor widgets: Subscription Plans. Users can now drag-and-drop subscription plans directly into Elementor pages with full visual customization through Elementor's style controls — no shortcodes needed.
What's New
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Chores