Skip to content

feat: Elementor subscription widget#1817

Open
sapayth wants to merge 19 commits intoweDevsOfficial:developfrom
sapayth:feat/elementor_subscription_widget
Open

feat: Elementor subscription widget#1817
sapayth wants to merge 19 commits intoweDevsOfficial:developfrom
sapayth:feat/elementor_subscription_widget

Conversation

@sapayth
Copy link
Member

@sapayth sapayth commented Feb 23, 2026

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

  • Elementor Subscription Plans Widget — Display subscription plans in a customizable grid layout (1–4 columns) with full style controls for plan name, price, billing cycle, trial description, features list, and subscribe button. Supports sorting by custom order, plan ID, or price. Features list auto-generates from plan settings (post limits, product limits, trial info, recurring status, etc.) with an expandable "See more" toggle for plans with 6+ features.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added full Elementor page builder integration with dedicated widgets for forms and subscription plans
    • Introduced responsive grid layouts and interactive toggles for subscription plan features in Elementor
    • Implemented Elementor-specific styling for forms with customizable inputs, labels, and submit buttons
  • Bug Fixes

    • Fixed potential errors with Google Maps initialization in page builder preview environments
  • Chores

    • Updated translation files

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Walkthrough

This 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

Cohort / File(s) Summary
Elementor Integration Bootstrap
includes/Integrations.php, includes/Integrations/Elementor/Elementor.php
Adds Elementor integration class that registers hooks for widgets, categories, and asset enqueuing; manages frontend/editor asset loads and includes form asset initialization with TinyMCE and Google Maps conditional support.
Elementor Widgets
includes/Integrations/Elementor/Widget.php, includes/Integrations/Elementor/Subscription_Plans_Widget.php
Implements two Elementor widgets: a form widget with dynamic form selector and extensive styling controls, and a subscription plans widget with grid layout, plan cards, pricing, features, and expandable feature lists.
Elementor Assets
Gruntfile.js, includes/Assets.php, assets/css/elementor-frontend-forms.css, assets/css/elementor-subscription-plans.css, assets/less/elementor-frontend-forms.less, assets/js/elementor-subscription-plans.js
Introduces LESS-to-CSS build mapping, asset registration for forms and subscription plan styles/scripts, form input/field styling with focus states and custom controls, subscription plan grid layouts and card styling, and feature toggle JavaScript for expandable items.
Form Field Rendering
includes/Fields/Field_Contract.php, includes/Fields/Form_Field_Post_Content.php, includes/Fields/Form_Field_Post_Excerpt.php
Adds use_admin_markup() helper method to determine whether to render admin or frontend markup, enabling Elementor preview to use frontend styles while preserving admin behavior in other contexts; minor whitespace formatting in post field editors.
Plugin Core & Asset Management
wpuf.php, includes/Frontend.php, wpuf-functions.php, includes/Admin/template-parts/modal-v4.2.php
Conditionally initializes Frontend for Elementor editor/AJAX contexts, makes subscription CSS loading conditional on non-Elementor pages, adds Google Maps API availability check before map initialization, and removes debug logging in modal template.
Translation Updates
languages/wp-user-frontend.pot
Updates POT file metadata and adds translation references for new Elementor integration points across widget controls and UI strings.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • Elementor integration feature (general): The changes implement concrete Elementor widget registration, asset management, and shortcode-based form rendering that fulfill native Elementor support requirements.

Possibly related PRs

Suggested labels

needs: dev review

Poem

🐰 Hops with joy
Where Elementor's builders play,
Forms and plans now have their say,
Widgets dance in grids so fine,
Frontend styles align divine,
WPUF blooms bright today! 🌸🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements Elementor widget features but the linked issue #1409 describes a plugin update notification problem unrelated to Elementor widgets or subscription functionality. Verify the linked issue is correct. If #1409 is the intended issue, either update it or link the PR to the correct issue describing the Elementor subscription widget feature request.
Out of Scope Changes check ⚠️ Warning The PR includes changes beyond Elementor subscription widget: comprehensive form widget implementation, integration setup, field compatibility changes, and various frontend asset management modifications that are out of scope from the stated objective. Either update PR objectives to include full Elementor integration scope or split this into multiple focused PRs, each addressing specific features (forms widget, subscriptions widget, integration setup separately).
Title check ❓ Inconclusive The title refers to Elementor subscription widget, but the PR implements comprehensive Elementor integration beyond just the subscription widget, including form widgets, asset management, and field compatibility. Consider a more comprehensive title like 'feat: Add Elementor integration with form and subscription widgets' to better represent the full scope of changes in this PR.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 95.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (11)
assets/css/elementor-subscription-plans.css (2)

65-68: Redundant margin shorthand.

margin: 0 0 0 0 is equivalent to margin: 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 ::placeholder pseudo-element has broad support (all modern browsers). The :-moz-placeholder (lines 121-125) targets Firefox 4–18 and :-ms-input-placeholder / ::-ms-input-placeholder target 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 .lity rule 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 .lity instances 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->suffix for production minification.

The new elementor-subscription-plans JS handle hardcodes the non-minified filename. Several other frontend scripts (e.g., frontend-form at line 390) use $this->suffix to load .min.js in 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 :focus selector 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_selector on 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_border and input_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: Unbounded get_posts query with posts_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 of get_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 a phpcs:ignore comment 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 -1 for "Unlimited X" vs sprintf('%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_key usage in get_subscription_plans triggers slow unindexed queries.

Lines 935–936, 940–941, and 946 use meta_key with orderby => meta_value_num. On sites with large wp_postmeta tables this results in slow queries since meta_key alone 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).

Comment on lines +14 to +41
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');
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +71 to +75
// 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' );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -20

Repository: 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 -10

Repository: 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 -50

Repository: 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/null

Repository: 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 -40

Repository: 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.css

Repository: 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.

Comment on lines +31 to +38
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');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +1060 to +1068
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';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +1110 to +1115
// 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>';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1137 to +1189
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>';
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -20

Repository: 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 php

Repository: 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.php

Repository: 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 -30

Repository: 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 -40

Repository: 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 -10

Repository: 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 -5

Repository: 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 -20

Repository: 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 -10

Repository: 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 -3

Repository: 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 -30

Repository: 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.

Suggested change
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.

Comment on lines +1140 to +1152
$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' );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1065 to +1071
protected function render() {
$settings = $this->get_settings_for_display();
$form_id = isset( $settings['form_id'] ) ? $settings['form_id'] : null;

if ( empty( $form_id ) ) {
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1108 to +1117
$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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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. &amp; stays &amp;), 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() (avoid esc_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 &amp; (not &), you must double-encode it yourself before adding it (so the HTML contains &amp;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 post context (the “post content” allowlist).
  • For allowed tags, it keeps only allowed attributes for each tag; any other attributes are stripped.
  • The post allowlist also includes “global” attributes that WordPress considers standard (historically including things like class, 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:


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.

Suggested change
$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.

Comment on lines +1122 to +1253
<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);
})();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Global window.onerror override is fragile and risks masking real errors.

Several concerns with this inline script:

  1. 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. Use document.querySelectorAll or scope to the current widget's container.

  2. Global error suppression (Lines 1126–1141): Overriding window.onerror to 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.

  3. Unbounded retry (Lines 1148–1157): initializeTinyMCE calls itself via setTimeout with no retry limit when wp.editor isn'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.

@anik-fahmid
Copy link
Collaborator

https://youtu.be/JQ_5zAci2vY

My feedback on the subscription plan widget, also needs to have UF - Widget Name (UF - Subscription Plans).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants