Feature/user directory elementor widget#1825
Conversation
Implements a limited User Directory in the free version, including setup wizard UI, REST API, shortcode handler, and directory limit logic. Adds new assets, React components, admin menu, post type registration, and views for directory/profile layouts. Pro-only features are previewed with disabled states and upgrade prompts, following the Pro Preview pattern.
Added ! prefix to purple color classes in layout helpers and Tailwind config to ensure higher specificity. Updated safelist in Tailwind config to include both prefixed and non-prefixed classes, and adjusted button class usage for consistency.
Improves member count calculation in the REST API and displays live member counts in the directory list. Refines input styles for number fields, updates the toast notification component for better UX, and adds a Pro badge to the 'New Directory' button when the directory limit is reached.
Introduces a new 'Files' tab to the user profile with grouped file display and filtering, excluding private message attachments. Updates Gruntfile.js to add user directory build/watch tasks and ensures profile size is passed to the template. Also adds the corresponding template file for the new tab.
Introduced a wide range of new utility classes to form-builder.css for layout, spacing, sizing, display, border, background, and other style properties. These additions improve flexibility and consistency for admin UI components and support more granular styling options.
Moved user directory assets and configs to modules/user-directory, updated Gruntfile.js to reflect new paths, and adjusted build/watch tasks. Renamed Tailwind and Webpack configs, added PostCSS config, and updated package files to support the new module structure.
Updated sort option values from 'ID' to 'id' and adjusted related logic for consistency. Removed free/pro checks from handlers to simplify state updates. Improved UI/UX for avatar and sort options, added Pro-locked features with badges and tooltips, and enhanced descriptions for better clarity.
Changed primary color scheme from emerald to purple across helpers and Tailwind config. Updated profile layout-2 to add a cover photo section, adjust class names, and improve header overlap. Cleaned up the About tab by removing Pro upgrade prompts and related UI elements.
Changed Tailwind primary and hover colors from purple to emerald green in tailwind.config.js. Updated related CSS class in App.js to use emerald color for upgrade link, ensuring visual consistency with the new color scheme.
Updated avatar rendering to use the same logic as the directory listing, prioritizing custom profile photos, then Gravatar, and finally user initials as a fallback. Improved initials calculation and font sizing for better visual consistency. Simplified markup to show either the avatar image or initials, not both, and ensured consistent sizing and styling.
Add early return if there are no items to display in the pagination shortcode. Also update the current page styling to use layout-specific color classes for better theme consistency.
Refactored multiple files in the user-directory module, including Admin_Menu.php, Directory.php, Helpers.php, Post_Type.php, PrettyUrls.php, Shortcode.php, and profile layout. Updated Toast.js in the frontend and regenerated several minified CSS files to reflect style changes.
Updated both Directory API and Shortcode to skip applying the 'role__in' filter if 'all' is present in the roles array. This prevents unnecessary filtering when all roles should be included.
Updated default and saved settings to include all profile tabs for better Pro compatibility. Improved REST API to merge settings with defaults and existing values, and to sanitize and save all profile tab-related fields. Adjusted frontend JS and wizard defaults to reflect all tabs, and fixed minor issues with sort option selection and profile slug decoding.
Introduce a reusable SingleSelect dropdown and refactor User Directory UI/UX. Key changes: - Add src/js/user-directory/components/common/SingleSelect.js: new reusable single-select dropdown with grouped options and ProBadge support. - Refactor StepAdvanced to use SingleSelect for sort and gallery size, simplify Pro feature handling, change free avatar default from 192→128 and adjust avatar size isFree flags, and convert several hover-only Pro badges into clickable upgrade links. - Update StepLayout to show Pro badge on hover with an upgrade link, simplify border/opacity logic and add local hover state. - Update App.js ProBadge into a clickable upgrade link and reposition/remove some hover-only Pro UI (New Directory button now shows badge inline when limit reached; limit warning block removed/streamlined). - package.json: ensure user-directory build/dev scripts run npm ci before npm run build/dev. - Remove get_current_page() from modules/user-directory/Shortcode.php (cleanup). These changes centralize dropdown logic, improve upgrade CTA consistency, and tidy UI behavior for Pro-locked features.
Replace the native <select> for the profile_base field with a SingleSelect component, passing profileBases as options and adapting the onChange to call the existing handleChange ({ target: { name, value } }). Remove the now-unused inputStyle constant and its comment block. This simplifies the markup and delegates select rendering/styling to the SingleSelect component.
Safelist new button and focus utilities in the user-directory Tailwind config and update DirectoryWizard and StepTabs components. DirectoryWizard: replace verbose button classnames with wpuf-btn-white, add focus ring utilities to Cancel/Prev/Next/Next (submit) buttons for better accessibility, adjust the footer container style to ensure correct left/right positioning and enforce background color. StepTabs: only render the ProBadge on hover to reduce visual clutter. These changes centralize button styles and improve keyboard focus visibility and layout consistency.
…ro-functionality' into feature/user_directory_elementor_widget # Conflicts: # assets/css/admin/subscriptions.min.css # assets/css/ai-form-builder.min.css # assets/css/forms-list.min.css # assets/css/frontend-subscriptions.min.css
WalkthroughThis PR adds comprehensive Elementor integration for the User Directory widget, including new CSS styling, widget component implementation, rewrite rules management for pretty URLs, and asset enqueuing logic to support the widget in Elementor editor and frontend contexts. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 5
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟡 Minor comments (19)
src/js/user-directory/components/common/Toast.js-72-79 (1)
72-79:⚠️ Potential issue | 🟡 MinorDuplicate dismiss logic risks double
onCloseinvocation.The close button duplicates the auto-dismiss logic. If a user clicks close while the auto-dismiss timer is in flight,
onClosemay fire twice. Extract the dismiss logic into a shared handler and guard against duplicate calls.🔧 Proposed fix reusing the shared handler
<button - onClick={() => { - setIsLeaving(true); - setTimeout(() => { - setIsVisible(false); - onClose && onClose(); - }, 300); - }} + onClick={handleClose} className="wpuf-ml-4 wpuf-flex-shrink-0 wpuf-inline-flex wpuf-bg-transparent wpuf-border-0 wpuf-p-0 wpuf-text-white hover:wpuf-text-gray-200 focus:wpuf-outline-none wpuf-cursor-pointer" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/Toast.js` around lines 72 - 79, Extract the dismiss sequence into a single handler (e.g., dismissToast) and replace the duplicated inline logic in the close button and the auto-dismiss timer with calls to that handler; inside dismissToast call setIsLeaving(true), start the 300ms timeout to setIsVisible(false) and call onClose(), and guard against double invocation with a local ref/flag (e.g., dismissedRef or isDismissing) so subsequent calls are no-ops; update places that previously called setIsLeaving/setIsVisible/onClose directly to call dismissToast instead and ensure the flag is checked/set atomically to prevent duplicate onClose calls.src/js/user-directory/components/common/LayoutCard.js-73-81 (1)
73-81:⚠️ Potential issue | 🟡 MinorAvoid
#as fallback forupgradeUrl.When
upgradeUrlis not provided, the link defaults to#, which scrolls to the page top—confusing for users. Consider hiding the link entirely or using a more meaningful fallback.🛠️ Proposed fix to conditionally render the upgrade link
- <a - href={upgradeUrl || '#'} - target="_blank" - rel="noopener noreferrer" - className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors" - onClick={(e) => e.stopPropagation()} - > - {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')} - </a> + {upgradeUrl && ( + <a + href={upgradeUrl} + target="_blank" + rel="noopener noreferrer" + className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors" + onClick={(e) => e.stopPropagation()} + > + {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')} + </a> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/LayoutCard.js` around lines 73 - 81, The anchor in LayoutCard currently falls back to href="#" when upgradeUrl is falsy which causes unwanted scrolling; change the JSX to conditionally render the upgrade link only when upgradeUrl is truthy (or explicitly hide it) instead of using "#" as a fallback, keeping the same attributes (target, rel, className, onClick) and i18n label; locate the anchor in the LayoutCard component and replace the unconditional <a href={upgradeUrl || '#'} ...> with a conditional render that only outputs the anchor when upgradeUrl is provided.src/js/user-directory/utils/avatarSizeHelper.js-14-14 (1)
14-14:⚠️ Potential issue | 🟡 MinorMisleading comment: layout-6 has the largest avatar size, not medium.
The comment says "Grid layout - medium avatars" but
265is the largest size in the map. This could confuse future maintainers.- 'layout-6': '265' // Grid layout - medium avatars + 'layout-6': '265' // Grid layout - large avatars🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/utils/avatarSizeHelper.js` at line 14, The comment for the 'layout-6' entry in avatarSizeHelper.js is misleading—'layout-6': '265' is the largest avatar size, not "medium"; update the comment next to the 'layout-6' mapping (in the avatar size map or function that returns sizes) to accurately reflect that this is the largest grid avatar size (e.g., "Grid layout - largest avatars") so future maintainers aren’t confused.src/js/user-directory/components/common/Tooltip.js-3-5 (1)
3-5:⚠️ Potential issue | 🟡 MinorUnstable tooltip ID on each render and deprecated
substrusage.
tooltipIdis regenerated on every render, which can cause accessibility issues with screen readers when the tooltip becomes visible (the ID changes between renders).String.prototype.substr()is deprecated; usesubstring()orslice()instead.🔧 Proposed fix using useMemo for stable ID
-import { useState } from '@wordpress/element'; +import { useState, useMemo } from '@wordpress/element'; const Tooltip = ( { content, children, className = '' } ) => { const [visible, setVisible] = useState(false); - const tooltipId = `wpuf-tooltip-${Math.random().toString(36).substr(2, 9)}`; + const tooltipId = useMemo( + () => `wpuf-tooltip-${Math.random().toString(36).slice(2, 11)}`, + [] + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/Tooltip.js` around lines 3 - 5, Tooltip currently generates tooltipId on every render and uses deprecated String.prototype.substr; change tooltipId to a stable value created once per component lifetime (e.g., useRef or useMemo) and replace substr(2, 9) with slice(2, 11) or substring(2, 11) to avoid deprecated API; update the Tooltip component to compute tooltipId via useRef/useMemo so it doesn't change across renders and use slice/substring for the character extraction.src/js/user-directory/components/common/DeleteConfirmModal.js-47-58 (1)
47-58:⚠️ Potential issue | 🟡 MinorLocalize action button labels.
At Line 50 and Line 57,
Cancel/Deleteare hardcoded and skip translation.Proposed fix
<button onClick={onCancel} className="wpuf-w-[101px] wpuf-h-[50px] wpuf-rounded-md wpuf-border wpuf-border-gray-300 wpuf-bg-white wpuf-text-gray-700 wpuf-font-medium hover:wpuf-bg-gray-50 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[23px] wpuf-text-[16px] wpuf-leading-[24px]"> - Cancel + {__( 'Cancel', 'wp-user-frontend' )} </button> @@ <button onClick={onConfirm} className="wpuf-w-[151px] wpuf-h-[50px] wpuf-rounded-md wpuf-bg-[`#EF4444`] wpuf-text-white wpuf-font-medium wpuf-shadow-sm hover:wpuf-bg-red-600 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[25px] wpuf-text-[16px] wpuf-leading-[24px]" style={{ boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)' }}> - Delete + {__( 'Delete', 'wp-user-frontend' )} </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/DeleteConfirmModal.js` around lines 47 - 58, Replace the hardcoded button labels "Cancel" and "Delete" in the DeleteConfirmModal component with localized strings; update the two buttons (the one with onClick={onCancel} and the one with onClick={onConfirm}) to use your app's i18n function (e.g., t('cancel') and t('delete')) or accept translated label props, and ensure you import or obtain the translator (e.g., useTranslation or passed-in t) at the top of DeleteConfirmModal so both labels are rendered via the translation function instead of literal text.modules/user-directory/views/directory/template-parts/pagination-shortcode.php-41-43 (1)
41-43:⚠️ Potential issue | 🟡 MinorClamp and default
current_pagebefore pagination math.At Line 41, direct casting from
$pagination['current_page']can produce out-of-range values and invalid prev/next URLs. Normalize it to[1..$total]and default safely when missing.Proposed fix
-$current = (int) $pagination['current_page']; -$total = (int) $pagination['total_pages']; +$total = max( 1, (int) $pagination['total_pages'] ); +$current = isset( $pagination['current_page'] ) ? (int) $pagination['current_page'] : 1; +$current = max( 1, min( $current, $total ) );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/directory/template-parts/pagination-shortcode.php` around lines 41 - 43, Normalize and clamp the current page before using it in pagination math: ensure $total is set from $pagination['total_pages'] then set $current from $pagination['current_page'] with a safe default (1) and clamp it to the range 1..$total to avoid out-of-range prev/next URLs; update the logic around the $current and $total variables in pagination-shortcode.php so all downstream calculations use the clamped $current value.src/js/user-directory/components/common/Header.js-17-17 (1)
17-17:⚠️ Potential issue | 🟡 MinorBuild
upgradeUrlwith encoded query params.At Line 17, manual concatenation with raw
utmcan produce malformed URLs ifutmcontains reserved characters.Proposed fix
- const upgradeUrl = (wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/') + '?utm_source=' + utm + '&utm_medium=wpuf-header'; + const upgradeBase = wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/'; + const upgrade = new URL( upgradeBase ); + upgrade.searchParams.set( 'utm_source', utm ); + upgrade.searchParams.set( 'utm_medium', 'wpuf-header' ); + const upgradeUrl = upgrade.toString();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/Header.js` at line 17, The upgradeUrl is constructed by string concatenation and can break when utm contains reserved characters; update the build for upgradeUrl (the const upgradeUrl that uses wpuf.upgradeUrl and utm) to assemble query parameters using URLSearchParams or encodeURIComponent for utm and other params so values are percent-encoded, e.g. derive base = wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/' then append a properly encoded query string (utm_source and utm_medium) instead of concatenating raw utm.src/js/user-directory/components/common/SingleSelect.js-40-43 (1)
40-43:⚠️ Potential issue | 🟡 MinorHandle falsy-but-valid selected values correctly.
Line 40 treats values like
0as unselected, so the placeholder can show even when a real option is selected.Proposed fix
- if (!value) return placeholder || __('Select...', 'wp-user-frontend'); + if (value === undefined || value === null || value === '') { + return placeholder || __('Select...', 'wp-user-frontend'); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/common/SingleSelect.js` around lines 40 - 43, The current early-return treats any falsy value (e.g., 0) as unselected; update the conditional in the label-rendering logic so it only returns the placeholder when value is null or undefined (not when value === 0 or other valid falsy values). In other words, change the check around the value variable used before calling options.find (the block referencing options.find, placeholder and __('Select...', 'wp-user-frontend')) to explicitly test value === null || value === undefined, then proceed to find the matching option and return option.label or placeholder as before.modules/user-directory/views/directory/template-parts/social-icons.php-33-33 (1)
33-33:⚠️ Potential issue | 🟡 MinorHarden external links opened in new tabs.
Lines [33], [42], [51], and [60] should include
noreferreralongsidenoopenerfor better tabnabbing/privacy hardening.Proposed fix
-<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon"> +<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon"> ... -<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon"> +<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon"> ... -<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon"> +<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon"> ... -<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon"> +<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">Also applies to: 42-42, 51-51, 60-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/directory/template-parts/social-icons.php` at line 33, Update the external social link anchors (the <a> elements that use esc_url($facebook_url), esc_url($twitter_url), esc_url($linkedin_url), esc_url($instagram_url) and have class "wpuf-social-icon") to include "noreferrer" in the rel attribute alongside "noopener" (i.e., change rel="noopener" to rel="noopener noreferrer") for all four anchor tags so external links opened in new tabs are hardened for tabnabbing/privacy.includes/Integrations/Elementor/User_Directory_Widget.php-146-147 (1)
146-147:⚠️ Potential issue | 🟡 MinorAdd
relattributes to links opened withtarget="_blank".Lines [146-147] and [1339-1340] should include
rel="noopener noreferrer"for tabnabbing/privacy hardening.Proposed fix
-__( 'No user directory found. <a href="%s" target="_blank">Create one</a> first.', 'wp-user-frontend' ), +__( 'No user directory found. <a href="%s" target="_blank" rel="noopener noreferrer">Create one</a> first.', 'wp-user-frontend' ), ... -__( '<a href="%s" target="_blank">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ), +__( '<a href="%s" target="_blank" rel="noopener noreferrer">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ),Also applies to: 1339-1340
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@includes/Integrations/Elementor/User_Directory_Widget.php` around lines 146 - 147, Update the two hardcoded anchor tags that open in a new tab to include rel="noopener noreferrer": find the translation strings passed to __() in includes/Integrations/Elementor/User_Directory_Widget.php (the strings containing '<a href="%s" target="_blank">Create one</a>' and the other occurrence later in the file) and add rel="noopener noreferrer" inside the <a> tag so they become '<a href="%s" target="_blank" rel="noopener noreferrer">...'. Ensure both occurrences are updated so the anchor markup returned by the __() calls includes the rel attribute.modules/user-directory/views/profile/template-parts/user-avatar.php-18-24 (1)
18-24:⚠️ Potential issue | 🟡 MinorHarden
$userand$sizeinputs before rendering.Lines [18-24] should verify
WP_Usertype and clamp size to a positive integer to prevent notices and invalid dimensions.Proposed fix
-if ( ! $user ) { +if ( empty( $user ) || ! ( $user instanceof WP_User ) ) { return; } // Get avatar size from parameter or use default -$size = isset( $size ) ? intval( $size ) : 128; +$size = isset( $size ) ? max( 1, (int) $size ) : 128; $wrapper_class = isset( $wrapper_class ) ? $wrapper_class : '';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/profile/template-parts/user-avatar.php` around lines 18 - 24, Ensure the template hardens $user and $size before rendering: verify $user is a WP_User (use instanceof WP_User or resolve it with get_user_by when $user may be an ID) and return early if it cannot be resolved; coerce $size to an int and clamp it to a positive value (e.g., size = max(1, intval($size))) to avoid zero/negative dimensions; also ensure $wrapper_class has a safe default (e.g., empty string) and consider sanitizing it before output; update the checks in user-avatar.php around the $user/$size/$wrapper_class handling to implement these validations.modules/user-directory/views/directory/template-parts/sort-field.php-18-19 (1)
18-19:⚠️ Potential issue | 🟡 MinorInitialize
$all_databefore nested key access.Lines [18-19] assume
$all_dataexists and is an array. Add a local fallback to avoid notices in edge include paths.Proposed fix
+ $all_data = ( isset( $all_data ) && is_array( $all_data ) ) ? $all_data : []; $orderby = ! empty( $all_data['orderby'] ) ? $all_data['orderby'] : 'id'; $order = ! empty( $all_data['order'] ) ? $all_data['order'] : 'desc';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/directory/template-parts/sort-field.php` around lines 18 - 19, Ensure $all_data is defined as an array before accessing its keys: add a local fallback that checks if $all_data exists and is an array (otherwise set it to an empty array) immediately before the lines that compute $orderby and $order so the expressions using $all_data['orderby'] and $all_data['order'] cannot trigger notices; keep the rest of the logic that sets $orderby and $order unchanged.assets/js/wpuf-user-directory-frontend.js-47-64 (1)
47-64:⚠️ Potential issue | 🟡 Minor
removeUrlParam()drops hash fragments and can rewrite URLs incorrectly.Lines [47-64] should use
URLparsing instead of string split logic so#fragmentand edge-case query strings are preserved.Proposed fix
function removeUrlParam(url, param) { - var urlParts = url.split('?'); - - if (urlParts.length < 2) { - return url; - } - - var params = new URLSearchParams(urlParts[1]); - params.delete(param); - - var newParams = params.toString(); - - if (newParams) { - return urlParts[0] + '?' + newParams; - } - - return urlParts[0]; + var parsedUrl = new URL(url, window.location.origin); + parsedUrl.searchParams.delete(param); + return parsedUrl.toString(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/js/wpuf-user-directory-frontend.js` around lines 47 - 64, The removeUrlParam function currently splits on '?' which drops hash fragments and mis-handles edge-case query strings; replace the manual split with the URL API: parse the input with new URL(url, window.location.href) to support absolute and relative URLs, use urlObj.searchParams.delete(param) to remove the parameter, and return urlObj.href (or urlObj.toString()) so the original pathname, preserved query ordering, and hash fragment remain intact; if URL construction throws, fall back to returning the original url unchanged.src/js/user-directory/components/DirectoryWizard.js-131-134 (1)
131-134:⚠️ Potential issue | 🟡 MinorPotential issue with JSON parsing on error responses.
If the server returns a non-JSON error response (e.g., HTML error page),
response.json()will throw, and this exception won't be caught gracefully since it's inside the try block but before the main error handling.🛠️ Suggested improvement
if (!response.ok) { - const data = await response.json(); - throw new Error(data.message || __('Something went wrong', 'wp-user-frontend')); + let errorMessage = __('Something went wrong', 'wp-user-frontend'); + try { + const data = await response.json(); + errorMessage = data.message || errorMessage; + } catch (parseError) { + // Response wasn't JSON, use default message + } + throw new Error(errorMessage); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/DirectoryWizard.js` around lines 131 - 134, The error handling path currently assumes response.json() will succeed; update the non-ok branch in DirectoryWizard.js so you attempt to parse JSON inside its own try/catch and fall back to await response.text() (or a generic message) when JSON parsing fails, then throw a new Error that uses the parsed message/text or a fallback __('Something went wrong', 'wp-user-frontend'); locate the existing if (!response.ok) block and replace the single response.json() call with this safe-parse-and-fallback logic to avoid unhandled exceptions when the server returns non-JSON error bodies.src/js/user-directory/components/steps/StepBasics.js-67-81 (1)
67-81:⚠️ Potential issue | 🟡 MinorMissing cleanup for debounce timeout on unmount.
The
searchTimeoutReftimeout is not cleared when the component unmounts, which could cause a memory leak or state update on an unmounted component.🛠️ Suggested fix
// Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) { setShowUserDropdown(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); + // Clear any pending search timeout + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } }; }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/js/user-directory/components/steps/StepBasics.js` around lines 67 - 81, The debounced timeout stored in searchTimeoutRef used by handleSearchChange is never cleared on unmount; add a useEffect in the StepBasics component that on mount returns a cleanup function which checks searchTimeoutRef.current and calls clearTimeout(searchTimeoutRef.current) (and optionally sets it to null) to avoid leaks or setState-after-unmount when searchUsers runs; keep existing handleSearchChange, searchUsers and setSearchTerm logic unchanged.assets/js/ud-search-shortcode.js-52-63 (1)
52-63:⚠️ Potential issue | 🟡 MinorMissing error handling for non-JSON responses and no request timeout.
The fetch call doesn't handle cases where the server returns non-JSON content (e.g., HTML error page) and has no timeout, which could leave the UI in a loading state indefinitely.
🛠️ Suggested improvement
- fetch(apiUrl + '?' + params.toString(), { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); + + fetch(apiUrl + '?' + params.toString(), { credentials: 'same-origin', + signal: controller.signal, }) - .then(res => res.json()) + .then(res => { + clearTimeout(timeoutId); + if (!res.ok) { + throw new Error('Network response was not ok'); + } + const contentType = res.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error('Response was not JSON'); + } + return res.json(); + }) .then(data => { if (data && data.success) { onSuccess(data); } else { onError(data); } }) - .catch(onError); + .catch(err => { + clearTimeout(timeoutId); + onError(err); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@assets/js/ud-search-shortcode.js` around lines 52 - 63, The fetch call using apiUrl + '?' + params.toString() should handle non-JSON responses and enforce a timeout: wrap the fetch in an AbortController with a setTimeout to abort after a chosen timeout, pass controller.signal to fetch, and clear the timer on completion; after receiving the Response check response.ok and attempt to parse JSON inside a try/catch (if parsing fails or response.ok is false, create a descriptive error object and call onError), otherwise call onSuccess with the parsed data; ensure the existing .catch still calls onError for network/abort errors.modules/user-directory/views/profile/layout-2.php-95-95 (1)
95-95:⚠️ Potential issue | 🟡 MinorHarden new-tab links with
rel="noopener noreferrer".Both anchors open new tabs and should include
relto prevent opener access.Proposed fix
- <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600"> + <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600"> @@ - <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline"> + <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline">Also applies to: 122-122
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/profile/layout-2.php` at line 95, The anchor that renders contact links (the <a> with href="<?php echo esc_url( $contact_item['value'] ); ?>" and target="_blank") must be hardened by adding rel="noopener noreferrer"; update that anchor (and the other similar anchor at the other occurrence) to include rel="noopener noreferrer" whenever target="_blank" is present so the rendered link prevents opener access.modules/user-directory/views/profile/template-parts/file-2.php-219-219 (1)
219-219:⚠️ Potential issue | 🟡 MinorAdd
relfor new-tab file links.This link opens in a new tab and should include
rel="noopener noreferrer"for tabnabbing protection.Proposed fix
- <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" class="!wpuf-block"> + <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-block">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/views/profile/template-parts/file-2.php` at line 219, The anchor that opens $file_url in a new tab (the <a ... target="_blank" class="!wpuf-block"> element) must include rel="noopener noreferrer" to prevent tabnabbing; update that anchor in file-2.php to add rel="noopener noreferrer" alongside the existing attributes.modules/user-directory/Helpers.php-618-623 (1)
618-623:⚠️ Potential issue | 🟡 MinorNormalize
file/filestab key mapping.Label map uses
fileswhile other defaults/configs usefile, causing inconsistent tab labels and lookups.Proposed fix
$labels = [ 'about' => __( 'About', 'wp-user-frontend' ), 'posts' => __( 'Posts', 'wp-user-frontend' ), 'comments' => __( 'Comments', 'wp-user-frontend' ), + 'file' => __( 'Files', 'wp-user-frontend' ), 'files' => __( 'Files', 'wp-user-frontend' ), 'activity' => __( 'Activity', 'wp-user-frontend' ), ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@modules/user-directory/Helpers.php` around lines 618 - 623, The labels map in Helpers.php uses the key 'files' which is inconsistent with the rest of the codebase that expects 'file'; update the $labels array to use 'file' instead of 'files' (in the associative array defined around $labels) so tab label lookups and defaults align, and scan related usages for $labels, get_tab_label or similar helpers to ensure they reference 'file' consistently.
ℹ️ Review info
Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 322b89b7-09c7-438d-903b-ded508f62716
⛔ Files ignored due to path filters (22)
assets/css/wpuf-user-directory-free.css.mapis excluded by!**/*.mapassets/images/user-directory/confetti.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-1.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-2.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-3.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-4.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-5.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-6.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-1.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-2.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-3.pngis excluded by!**/*.pngassets/images/user-directory/round-grids.pngis excluded by!**/*.pngassets/images/user-directory/sidecards.pngis excluded by!**/*.pngassets/images/user-directory/square-grids.pngis excluded by!**/*.pngassets/images/user-directory/table.pngis excluded by!**/*.pngassets/images/user-directory/thumb-male-1.svgis excluded by!**/*.svgassets/images/user-directory/thumb-male-2.svgis excluded by!**/*.svgassets/images/user-directory/thumb-male-3.svgis excluded by!**/*.svgassets/images/user-directory/wide-sidecards.pngis excluded by!**/*.pngassets/js/wpuf-user-directory-free.js.mapis excluded by!**/*.mapmodules/user-directory/package-lock.jsonis excluded by!**/package-lock.jsonpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (68)
Gruntfile.jsassets/css/admin/subscriptions.min.cssassets/css/admin/wpuf-module.cssassets/css/ai-form-builder.min.cssassets/css/elementor-user-directory.cssassets/css/forms-list.min.cssassets/css/frontend-subscriptions.min.cssassets/css/wpuf-user-directory-free.cssassets/css/wpuf-user-directory-frontend.cssassets/js/admin/wpuf-module.jsassets/js/ud-search-shortcode.jsassets/js/wpuf-user-directory-free.asset.phpassets/js/wpuf-user-directory-free.jsassets/js/wpuf-user-directory-free.js.LICENSE.txtassets/js/wpuf-user-directory-frontend.jsincludes/Assets.phpincludes/Free/Free_Loader.phpincludes/Integrations.phpincludes/Integrations/Elementor/Elementor.phpincludes/Integrations/Elementor/User_Directory_Widget.phpincludes/functions/modules.phpmodules/user-directory/Admin_Menu.phpmodules/user-directory/Api/Directory.phpmodules/user-directory/DirectoryStyles.phpmodules/user-directory/Helpers.phpmodules/user-directory/Post_Type.phpmodules/user-directory/PrettyUrls.phpmodules/user-directory/Shortcode.phpmodules/user-directory/User_Directory.phpmodules/user-directory/package.jsonmodules/user-directory/postcss.config.jsmodules/user-directory/tailwind.config.jsmodules/user-directory/views/admin-page.phpmodules/user-directory/views/directory/layout-3.phpmodules/user-directory/views/directory/template-parts/pagination-shortcode.phpmodules/user-directory/views/directory/template-parts/row-3.phpmodules/user-directory/views/directory/template-parts/search-field.phpmodules/user-directory/views/directory/template-parts/social-icons.phpmodules/user-directory/views/directory/template-parts/sort-field.phpmodules/user-directory/views/profile/layout-2.phpmodules/user-directory/views/profile/template-parts/about-2.phpmodules/user-directory/views/profile/template-parts/comments-2.phpmodules/user-directory/views/profile/template-parts/file-2.phpmodules/user-directory/views/profile/template-parts/posts-2.phpmodules/user-directory/views/profile/template-parts/user-avatar.phpmodules/user-directory/webpack.config.jspackage.jsonpostcss.user-directory.config.jssrc/js/user-directory/App.jssrc/js/user-directory/components/DirectoryList.jssrc/js/user-directory/components/DirectoryWizard.jssrc/js/user-directory/components/common/DeleteConfirmModal.jssrc/js/user-directory/components/common/Header.jssrc/js/user-directory/components/common/LayoutCard.jssrc/js/user-directory/components/common/MultiSelect.jssrc/js/user-directory/components/common/SingleSelect.jssrc/js/user-directory/components/common/Toast.jssrc/js/user-directory/components/common/Tooltip.jssrc/js/user-directory/components/steps/StepAdvanced.jssrc/js/user-directory/components/steps/StepBasics.jssrc/js/user-directory/components/steps/StepLayout.jssrc/js/user-directory/components/steps/StepProfile.jssrc/js/user-directory/components/steps/StepTabs.jssrc/js/user-directory/index.jssrc/js/user-directory/styles/main.csssrc/js/user-directory/utils/avatarSizeHelper.jstailwind.config.jswpuf-functions.php
| const SEARCH_DEBOUNCE = 300; | ||
| let debounceTimeout = null; |
There was a problem hiding this comment.
Critical: Shared debounce state causes race conditions with multiple directories.
The debounceTimeout variable is declared at the module scope and shared across all directory instances on a page. If a user types in one directory's search and then quickly types in another, the debounce will be cancelled for the first directory.
Move the debounce timeout into initUserDirectorySearch to scope it per instance:
🐛 Proposed fix
(function(window, document) {
'use strict';
const SEARCH_DEBOUNCE = 300;
- let debounceTimeout = null;
function fetchUsers({
// ... unchanged
}) {
// ... unchanged
}
function initUserDirectorySearch(container, blockId, pageId) {
let currentRequestId = 0;
+ let debounceTimeout = null;
// ... rest of function
// Search input handler
input.addEventListener('input', function(e) {
const value = e.target.value.trim();
if (debounceTimeout) clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
performSearch(value, 1);
}, SEARCH_DEBOUNCE);
});
// ... rest of function
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@assets/js/ud-search-shortcode.js` around lines 13 - 14, The module-scoped
debounceTimeout (declared alongside SEARCH_DEBOUNCE) is shared across all
directory instances causing cross-instance cancellation; move the
debounceTimeout variable into the initUserDirectorySearch function so each call
gets its own timeout state, update uses inside initUserDirectorySearch (and any
inner handlers like the input event listener) to reference the instance-scoped
debounceTimeout, and remove/replace the module-level debounceTimeout declaration
to avoid the race condition.
| $file_url = wp_get_attachment_url( $file->ID ); | ||
| $file_type = get_post_mime_type( $file->ID ); | ||
| $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) ); | ||
| $is_image = strpos( $file_type, 'image/' ) === 0; | ||
| $is_pdf = $file_type === 'application/pdf'; |
There was a problem hiding this comment.
Guard against missing attachment URLs before calling pathinfo().
If wp_get_attachment_url() returns false, pathinfo() will throw and break profile rendering.
Proposed fix
- $file_url = wp_get_attachment_url( $file->ID );
- $file_type = get_post_mime_type( $file->ID );
- $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
+ $file_url = wp_get_attachment_url( $file->ID );
+ if ( ! $file_url ) {
+ continue;
+ }
+ $file_type = (string) get_post_mime_type( $file->ID );
+ $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
@@
- <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>
+ <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>Also applies to: 245-246
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/user-directory/views/profile/template-parts/file-2.php` around lines
201 - 205, Guard against wp_get_attachment_url() returning false before calling
pathinfo by checking the $file_url truthiness; in the block that sets $file_url,
$file_type, $file_extension, $is_image, $is_pdf (and the similar block at the
other occurrence) only call pathinfo() when $file_url is non-false, otherwise
set $file_extension to an empty string (or null) and adjust $is_image/$is_pdf
using $file_type alone or default false; update the assignments around the
wp_get_attachment_url($file->ID) call and the $file_extension =
strtoupper(pathinfo(...)) usage to perform this guard so profile rendering won’t
break when the attachment URL is missing.
…ectory_elementor_widget # Conflicts: # assets/css/admin/subscriptions.min.css # assets/css/admin/wpuf-module.css # assets/css/ai-form-builder.min.css # assets/css/forms-list.min.css # assets/css/frontend-subscriptions.min.css # assets/js/ud-search-shortcode.js # assets/js/wpuf-user-directory-free.asset.php # assets/js/wpuf-user-directory-free.js # assets/js/wpuf-user-directory-free.js.map # assets/js/wpuf-user-directory-frontend.js # includes/Free/Free_Loader.php # includes/functions/modules.php # modules/user-directory/Api/Directory.php # modules/user-directory/DirectoryStyles.php # modules/user-directory/Helpers.php # modules/user-directory/Post_Type.php # modules/user-directory/PrettyUrls.php # modules/user-directory/Shortcode.php # modules/user-directory/User_Directory.php # modules/user-directory/tailwind.config.js # modules/user-directory/views/directory/layout-3.php # modules/user-directory/views/directory/template-parts/pagination-shortcode.php # modules/user-directory/views/directory/template-parts/row-3.php # modules/user-directory/views/directory/template-parts/search-field.php # modules/user-directory/views/directory/template-parts/social-icons.php # modules/user-directory/views/profile/layout-2.php # modules/user-directory/views/profile/template-parts/comments-2.php # modules/user-directory/views/profile/template-parts/file-2.php # modules/user-directory/views/profile/template-parts/posts-2.php # modules/user-directory/webpack.config.js # postcss.user-directory.config.js # src/js/user-directory/App.js # src/js/user-directory/components/DirectoryList.js # src/js/user-directory/components/DirectoryWizard.js # src/js/user-directory/components/common/DeleteConfirmModal.js # src/js/user-directory/components/common/MultiSelect.js # src/js/user-directory/components/common/SingleSelect.js # src/js/user-directory/components/common/Tooltip.js # src/js/user-directory/components/steps/StepAdvanced.js # src/js/user-directory/components/steps/StepTabs.js # src/js/user-directory/utils/avatarSizeHelper.js
|
Issues found so far: @sapayth vai
|
…ectory_elementor_widget
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
includes/Integrations/Elementor/Elementor.php (1)
78-97: Gate User Directory assets behind widget presence detection.
enqueue_styles()andenqueue_scripts()fire onelementor/frontend/after_enqueue_*hooks for every Elementor page, unconditionally loadingwpuf-user-directory-frontend,wpuf-elementor-user-directory, and Pro's UD styles/scripts on unrelated pages. This duplicates work already handled by the User_Directory_Widget'sget_style_depends()andget_script_depends()methods.Additionally,
wp_localize_script('wpuf-ud-search-shortcode', 'wpufUserDirectorySearch', ...)adds a global JS object on every Elementor page regardless of whether the widget is used.Refactor to check if the current page's
_elementor_datacontains"widgetType":"wpuf-user-directory"before enqueuing these assets, using the same pattern already established elsewhere in the class (e.g.,maybe_flush_rules_on_elementor_save(),register_elementor_page_rewrite_rules()).Also applies to: lines 122-152
🤖 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 78 - 97, The Elementor enqueues are firing on every Elementor page; update the enqueue_styles() and enqueue_scripts() logic to first detect whether the current post's _elementor_data contains the widgetType "wpuf-user-directory" (use the same pattern as maybe_flush_rules_on_elementor_save() / register_elementor_page_rewrite_rules() for reading/parsing _elementor_data), and only then call wp_register_style/wp_enqueue_style/wp_enqueue_script and wp_localize_script('wpuf-ud-search-shortcode', ...). Apply the same gated check to the corresponding assets around lines 122-152 so Pro UD styles/scripts and the global wpufUserDirectorySearch object are only added when the Elementor page actually contains the wpuf-user-directory widget.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@includes/Integrations/Elementor/Elementor.php`:
- Around line 388-399: The get_posts multiline call assigned to $pages violates
PHPCS multiline-call rules: move the opening parenthesis of get_posts to end the
line (i.e., put get_posts( on its own line) and ensure the closing parenthesis
is on its own line after the array; reformat the array argument so each
key/value pair stays on its own line. Apply the identical formatting change to
the fallback get_posts call used later (the other get_posts([...]) block) so
both calls follow the same multiline-call pattern.
- Around line 38-42: The register_elementor_page_rewrite_rules() function
currently runs heavy DB work on every init; change it to read cached slugs from
a transient (e.g. 'wpuf_ud_elementor_page_slugs') and only run the expensive
queries on cache miss, using WP_Query with 'fields' => 'ids' and a sensible
pagination cap instead of posts_per_page => -1; move the one-time backfill scan
so it only runs under is_admin() (or during plugin activation/CLI) rather than
on every init; update maybe_flush_rules_on_elementor_save() to delete the
transient to invalidate cache and ensure flush_rewrite_rules() is only called
from admin context and only when slugs actually changed.
- Around line 353-372: In maybe_flush_rules_on_elementor_save (hooked to
elementor/editor/after_save) stop re-reading _elementor_data from DB and instead
parse the provided $data payload to detect '"widgetType":"wpuf-user-directory"';
read the existing flag via get_post_meta($post_id,
'_wpuf_has_ud_elementor_widget', true) and only call update_post_meta or
delete_post_meta when the flag actually needs to change, and call
flush_rewrite_rules() only when the flag transitions (0→1 or 1→0) so rewrites
are flushed on real state changes.
- Around line 425-432: The rewrite rule currently uses only $page->post_name and
doesn't handle hierarchical pages or regex-special characters; replace that with
the page's full URI via get_page_uri($page->ID), escape it with preg_quote(...,
'/') when building the regex, and use that escaped full path for both the
pattern and the pagename query var (so pagename gets the full hierarchical path
returned by get_page_uri); update the loop that builds rules (the code around
add_rewrite_rule in Elementor.php) to use get_page_uri($page->ID) and preg_quote
on the path before constructing the '^.../([^/]+)/?$' pattern and the
'index.php?pagename=...&wpuf_user_profile=$matches[1]' replacement.
---
Nitpick comments:
In `@includes/Integrations/Elementor/Elementor.php`:
- Around line 78-97: The Elementor enqueues are firing on every Elementor page;
update the enqueue_styles() and enqueue_scripts() logic to first detect whether
the current post's _elementor_data contains the widgetType "wpuf-user-directory"
(use the same pattern as maybe_flush_rules_on_elementor_save() /
register_elementor_page_rewrite_rules() for reading/parsing _elementor_data),
and only then call wp_register_style/wp_enqueue_style/wp_enqueue_script and
wp_localize_script('wpuf-ud-search-shortcode', ...). Apply the same gated check
to the corresponding assets around lines 122-152 so Pro UD styles/scripts and
the global wpufUserDirectorySearch object are only added when the Elementor page
actually contains the wpuf-user-directory widget.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e4a7e269-1e68-4ab0-a9fa-c0ad81b40f38
📒 Files selected for processing (2)
includes/Assets.phpincludes/Integrations/Elementor/Elementor.php
✅ Files skipped from review due to trivial changes (1)
- includes/Assets.php
| // of which module is active, ensuring the rules are always registered. | ||
| add_action( 'init', [ $this, 'register_elementor_page_rewrite_rules' ], 5 ); | ||
|
|
||
| // Flush rewrite rules when Elementor saves a page with our widget | ||
| add_action( 'elementor/editor/after_save', [ $this, 'maybe_flush_rules_on_elementor_save' ], 10, 2 ); |
There was a problem hiding this comment.
Critical performance issue: heavy DB work on every init.
register_elementor_page_rewrite_rules() is hooked to init (line 39), so it runs on every request — frontend, admin, AJAX, REST, cron.
- The fast path (lines 388–399) runs a
get_postswithmeta_queryandposts_per_page => -1per request. - The fallback (lines 404–423) is far worse: when no flagged pages exist (the common case for installs that don't use the widget), it scans all published pages and issues an N+1
get_post_metaper page on every init — forever, since nothing will ever back-fill the flag in that scenario. flush_rewrite_rules()at line 421 can fire mid-request from any context.
Recommended approach:
- Cache the slugs in a transient/option keyed by something cheap to invalidate (the
maybe_flush_rules_on_elementor_save()hook is the natural invalidator). Read from cache oninit; only run the query on cache miss. - Only run the one-time back-fill scan in admin context (e.g. behind
is_admin()) or trigger it explicitly (activation hook / admin notice / WP-CLI), so it never runs on a frontend page load. - Drop
posts_per_page => -1in favor of a sensible cap (fields => 'ids'also avoids hydrating full post objects).
♻️ Sketch of a transient-backed approach
public function register_elementor_page_rewrite_rules() {
- // Fast path: query only pages that have been flagged via the meta key.
- // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
- $pages = get_posts( [
- 'post_type' => 'page',
- 'post_status' => 'publish',
- 'posts_per_page' => -1,
- 'meta_query' => [
- [
- 'key' => '_wpuf_has_ud_elementor_widget',
- 'value' => '1',
- 'compare' => '=',
- ],
- ],
- ] );
-
- // Fallback: ... (scan all pages on every init)
+ $slugs = get_transient( 'wpuf_ud_elementor_page_slugs' );
+ if ( false === $slugs ) {
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ $page_ids = get_posts( [
+ 'post_type' => 'page',
+ 'post_status' => 'publish',
+ 'posts_per_page' => 200,
+ 'fields' => 'ids',
+ 'meta_query' => [
+ [
+ 'key' => '_wpuf_has_ud_elementor_widget',
+ 'value' => '1',
+ ],
+ ],
+ ] );
+ $slugs = array_filter( array_map( static function ( $id ) {
+ $p = get_post( $id );
+ return $p ? $p->post_name : null;
+ }, $page_ids ) );
+ set_transient( 'wpuf_ud_elementor_page_slugs', $slugs, DAY_IN_SECONDS );
+ }
+
+ foreach ( $slugs as $slug ) {
+ add_rewrite_rule(
+ '^' . preg_quote( $slug, '/' ) . '/([^/]+)/?$',
+ 'index.php?pagename=' . $slug . '&wpuf_user_profile=$matches[1]',
+ 'top'
+ );
+ }
}And invalidate via delete_transient( 'wpuf_ud_elementor_page_slugs' ) inside maybe_flush_rules_on_elementor_save().
Also applies to: 385-433
🤖 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 38 - 42, The
register_elementor_page_rewrite_rules() function currently runs heavy DB work on
every init; change it to read cached slugs from a transient (e.g.
'wpuf_ud_elementor_page_slugs') and only run the expensive queries on cache
miss, using WP_Query with 'fields' => 'ids' and a sensible pagination cap
instead of posts_per_page => -1; move the one-time backfill scan so it only runs
under is_admin() (or during plugin activation/CLI) rather than on every init;
update maybe_flush_rules_on_elementor_save() to delete the transient to
invalidate cache and ensure flush_rewrite_rules() is only called from admin
context and only when slugs actually changed.
| public function maybe_flush_rules_on_elementor_save( $post_id, $data ) { | ||
| $post = get_post( $post_id ); | ||
|
|
||
| if ( ! $post || 'page' !== $post->post_type ) { | ||
| return; | ||
| } | ||
|
|
||
| $elementor_data = get_post_meta( $post_id, '_elementor_data', true ); | ||
|
|
||
| $has_widget = ! empty( $elementor_data ) && is_string( $elementor_data ) && strpos( $elementor_data, '"widgetType":"wpuf-user-directory"' ) !== false; | ||
|
|
||
| // Keep a queryable meta flag so register_elementor_page_rewrite_rules() can find | ||
| // these pages efficiently without scanning all _elementor_data values on every init. | ||
| if ( $has_widget ) { | ||
| update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' ); | ||
| flush_rewrite_rules(); | ||
| } else { | ||
| delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Use the $data parameter and only flush on actual state change.
Two issues here:
- The
$dataargument fromelementor/editor/after_savealready contains the editor payload — there's no need to re-fetch_elementor_datafrom the DB. Using it also resolves theUnusedFormalParameterwarning from PHPMD. flush_rewrite_rules()runs on every save where the widget is present, even when the meta flag was already'1'(no state change). Symmetrically, removing the widget (delete_post_meta) does not flush, so stale rules can remain registered until the next save that does have the widget. Flush should be conditional on the flag actually transitioning.
♻️ Proposed refactor
-public function maybe_flush_rules_on_elementor_save( $post_id, $data ) {
- $post = get_post( $post_id );
-
- if ( ! $post || 'page' !== $post->post_type ) {
- return;
- }
-
- $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
-
- $has_widget = ! empty( $elementor_data ) && is_string( $elementor_data ) && strpos( $elementor_data, '"widgetType":"wpuf-user-directory"' ) !== false;
-
- // Keep a queryable meta flag so register_elementor_page_rewrite_rules() can find
- // these pages efficiently without scanning all _elementor_data values on every init.
- if ( $has_widget ) {
- update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' );
- flush_rewrite_rules();
- } else {
- delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' );
- }
-}
+public function maybe_flush_rules_on_elementor_save( $post_id, $data ) {
+ $post = get_post( $post_id );
+ if ( ! $post || 'page' !== $post->post_type ) {
+ return;
+ }
+
+ $has_widget = $this->elementor_data_has_ud_widget( $data );
+ $had_widget = (bool) get_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', true );
+
+ if ( $has_widget === $had_widget ) {
+ return; // nothing changed, no need to flush
+ }
+
+ if ( $has_widget ) {
+ update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' );
+ } else {
+ delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' );
+ }
+
+ // Invalidate any cache used by register_elementor_page_rewrite_rules(), then flush.
+ flush_rewrite_rules();
+}
+
+private function elementor_data_has_ud_widget( $data ) {
+ if ( is_array( $data ) ) {
+ $json = wp_json_encode( $data );
+ } elseif ( is_string( $data ) ) {
+ $json = $data;
+ } else {
+ return false;
+ }
+ return is_string( $json ) && strpos( $json, '"widgetType":"wpuf-user-directory"' ) !== false;
+}🧰 Tools
🪛 GitHub Check: Run PHPCS inspection
[warning] 353-353:
The method parameter $data is never used
🪛 PHPMD (2.15.0)
[warning] 353-353: Avoid unused parameters such as '$data'. (undefined)
(UnusedFormalParameter)
🤖 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 353 - 372, In
maybe_flush_rules_on_elementor_save (hooked to elementor/editor/after_save) stop
re-reading _elementor_data from DB and instead parse the provided $data payload
to detect '"widgetType":"wpuf-user-directory"'; read the existing flag via
get_post_meta($post_id, '_wpuf_has_ud_elementor_widget', true) and only call
update_post_meta or delete_post_meta when the flag actually needs to change, and
call flush_rewrite_rules() only when the flag transitions (0→1 or 1→0) so
rewrites are flushed on real state changes.
| $pages = get_posts( [ | ||
| 'post_type' => 'page', | ||
| 'post_status' => 'publish', | ||
| 'posts_per_page' => -1, | ||
| 'meta_query' => [ | ||
| [ | ||
| 'key' => '_wpuf_has_ud_elementor_widget', | ||
| 'value' => '1', | ||
| 'compare' => '=', | ||
| ], | ||
| ], | ||
| ] ); |
There was a problem hiding this comment.
Fix PHPCS multiline-call formatting violations.
The opening ( must end the line and the closing ) must be on its own line, per PHPCS report on lines 388, 399, 405, 409.
🎨 Proposed fix
- $pages = get_posts( [
- 'post_type' => 'page',
- ...
- ] );
+ $pages = get_posts(
+ [
+ 'post_type' => 'page',
+ ...
+ ]
+ );Apply the same pattern to the get_posts( [ ... ] ) call inside the fallback at lines 405–409.
Also applies to: 405-409
🧰 Tools
🪛 GitHub Check: Run PHPCS inspection
[failure] 399-399:
Closing parenthesis of a multi-line function call must be on a line by itself
[warning] 392-392:
Detected usage of meta_query, possible slow query.
[failure] 388-388:
Opening parenthesis of a multi-line function call must be the last content on the line
🤖 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 388 - 399, The
get_posts multiline call assigned to $pages violates PHPCS multiline-call rules:
move the opening parenthesis of get_posts to end the line (i.e., put get_posts(
on its own line) and ensure the closing parenthesis is on its own line after the
array; reformat the array argument so each key/value pair stays on its own line.
Apply the identical formatting change to the fallback get_posts call used later
(the other get_posts([...]) block) so both calls follow the same multiline-call
pattern.
| foreach ( $pages as $page ) { | ||
| $page_slug = $page->post_name; | ||
| add_rewrite_rule( | ||
| '^' . $page_slug . '/([^/]+)/?$', | ||
| 'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]', | ||
| 'top' | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP -C5 '\b(add_rewrite_rule|wpuf_user_profile)\b' --type=phpRepository: weDevsOfficial/wp-user-frontend
Length of output: 6883
🏁 Script executed:
cat -n modules/user-directory/PrettyUrls.php | head -120Repository: weDevsOfficial/wp-user-frontend
Length of output: 4387
🏁 Script executed:
rg -n 'get_page_uri' --type=phpRepository: weDevsOfficial/wp-user-frontend
Length of output: 57
🏁 Script executed:
rg -n 'wpuf_ud_rewrite' --type=php -B3 -A3Repository: weDevsOfficial/wp-user-frontend
Length of output: 1672
🏁 Script executed:
web_search
Does WordPress pagename query parameter work with nested/hierarchical page pathsRepository: weDevsOfficial/wp-user-frontend
Length of output: 169
🌐 Web query:
WordPress pagename query parameter hierarchical nested pages
💡 Result:
In WordPress, the 'pagename' query parameter handles hierarchical nested pages by using the full path of the page hierarchy as its value. For a nested page structure like Parent > Child > Grandchild, the URL /parent/child/grandchild/ sets the 'pagename' query var to 'parent/child/grandchild'. WordPress then uses get_page_by_path('parent/child/grandchild') to retrieve the correct page object based on this full path. Key mechanisms: - Rewrite rules: WP_Rewrite::page_rewrite_rules adds the tag '%pagename%' with regex '(.?.+?)' and generates rules matching page permalinks to 'pagename=$matches[0]'. The regex (.?.+?) captures the full path including slashes. - Query var access: Use get_query_var('pagename') to retrieve the full hierarchical path string. - Page lookup: WP_Query uses this path with get_page_by_path, which queries the database for pages matching the path segments via post_name and post_parent relationships. - Hierarchical support: Pages are hierarchical post_type='page', so child pages have post_parent set to parent ID, enabling path-based resolution. Example usage: $pagename = get_query_var('pagename'); // e.g., 'parent/child' $page = get_page_by_path($pagename); This works for permalinks; without pretty permalinks, use ?pagename=parent/child/grandchild directly. Flush rewrite rules after permalink changes.
Citations:
- 1: https://developer.wordpress.org/reference/functions/get_page_by_path/
- 2: https://github.com/WordPress/WordPress/blob/master/wp-includes/class-wp-rewrite.php
- 3: https://github.com/WordPress/wordpress-develop/blob/6.8/src/wp-includes/rewrite.php
- 4: https://developer.wordpress.org/reference/classes/wp_rewrite/rewrite_rules
- 5: https://wppaste.com/reference/classes/wp_rewrite/page_rewrite_rules
- 6: https://stackoverflow.com/questions/4837006/how-can-i-get-the-current-page-name-in-wordpress
- 7: https://codex.wordpress.org/Function_Reference/get_query_var
- 8: https://codex.wordpress.org/WordPress_Query_Vars
- 9: https://wordpress.stackexchange.com/questions/257899/add-rewrite-rule-not-working-for-page-var/257977
Rewrite rule does not support hierarchical pages.
The pattern uses only post_name (leaf slug) and cannot match nested pages. For a page at /parent/child/, the URL /parent/child/username/ won't match the rule expecting /child/username/. While WordPress's pagename parameter supports full hierarchical paths, this code doesn't provide them.
Additionally, unlike the PrettyUrls implementation (which has the same limitation), this code lacks preg_quote() protection for regex special characters in the slug. Both should use get_page_uri() to build the full hierarchical path:
Suggested fix
foreach ( $pages as $page ) {
- $page_slug = $page->post_name;
- add_rewrite_rule(
- '^' . $page_slug . '/([^/]+)/?$',
- 'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]',
- 'top'
- );
+ $page_uri = get_page_uri( $page ); // returns 'parent/child' for nested pages
+ if ( empty( $page_uri ) ) {
+ continue;
+ }
+ add_rewrite_rule(
+ '^' . preg_quote( $page_uri, '/' ) . '/([^/]+)/?$',
+ 'index.php?pagename=' . $page_uri . '&wpuf_user_profile=$matches[1]',
+ 'top'
+ );
}📝 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.
| foreach ( $pages as $page ) { | |
| $page_slug = $page->post_name; | |
| add_rewrite_rule( | |
| '^' . $page_slug . '/([^/]+)/?$', | |
| 'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]', | |
| 'top' | |
| ); | |
| } | |
| foreach ( $pages as $page ) { | |
| $page_uri = get_page_uri( $page ); // returns 'parent/child' for nested pages | |
| if ( empty( $page_uri ) ) { | |
| continue; | |
| } | |
| add_rewrite_rule( | |
| '^' . preg_quote( $page_uri, '/' ) . '/([^/]+)/?$', | |
| 'index.php?pagename=' . $page_uri . '&wpuf_user_profile=$matches[1]', | |
| 'top' | |
| ); | |
| } |
🤖 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 425 - 432, The
rewrite rule currently uses only $page->post_name and doesn't handle
hierarchical pages or regex-special characters; replace that with the page's
full URI via get_page_uri($page->ID), escape it with preg_quote(..., '/') when
building the regex, and use that escaped full path for both the pattern and the
pagename query var (so pagename gets the full hierarchical path returned by
get_page_uri); update the loop that builds rules (the code around
add_rewrite_rule in Elementor.php) to use get_page_uri($page->ID) and preg_quote
on the path before constructing the '^.../([^/]+)/?$' pattern and the
'index.php?pagename=...&wpuf_user_profile=$matches[1]' replacement.

depends on #1778
closes #1410, closes #1445, closes #1446
Summary
This PR adds an Elementor widget so the directory can be embedded anywhere using the Elementor page builder — no shortcodes required.
What's New
Technical Notes
WeDevs\Wpuf\Integrations\Elementor\User_Directory_WidgetSummary by CodeRabbit
Release Notes