Store Locator Plus® was updated with a “pub/sub” model a few years ago. This feature is a managed interface for jQuery Callbacks that allows disparate Javascript functions to interact with each other. It works much like hooks and filters in WordPress, but solely within the JavaScript runtime space.
The basic premise is any JavaScript function can call a “publish” method that fires off a stack of callbacks.
slp_Filter('slp_map_ready').publish(cslmap);
What is run by those callbacks is set by having a JavaScript module call a “subscribe” method.
Location bulk actions allow for users to work on multiple locations at once. Export a list of locations, delete select locations from the location table, geocode locations ,and more.
2209.12
Export locations is our first attempt at making bulk actions work more like a single page application.
Instead of doing a form post and submitting the location list, then reading in some PHP-driven variables to set the JavaScript behavior — in 2209.12 the export process bypasses the form submit.
Bypass is handled by using a pub/sub module with a dynamic filter name “exportBulkAction”. This tells the main SLP JavaScript process to execute the callback stack instead of pushing a form submit. This allows the AJAX call to happen immediately without a full page re-render.
2208.15
The apply button processing is handled in the main plugin’s admin.js file.
It uses jQuery to process form variables and prepare the #locationForm to be submitted.
When submitted it sends the request back to WordPress for standard “full page load” processing.
The process starts with a standard form submit from the Bulk Actions “Apply” button.
WordPress sets up a JavaScript variable location_import that has info about the action “export”.
This triggers a jQuery(document).ready() call that fires another request via AJAX back to WordPress with some new parameters in place.
That AJAX call eventually sends back a CSV file that is stored in a DOM div that was prepared with the jQuery() document loader when the refreshed page (post original submit) was rendered.
All development is done on personal forked copies (origin) of the primary Store Locator Plus® repositories (upstream). Upstream is considered the “single source of truth” for the current code state.
To begin work, fork the main Store Locator Plus® repository (upstream) to a personal repo (origin).
When code is ready to be integrated with current ongoing development effort, issue a pull request for your origin:update/<something> (see branch conventions below) branch into the upstream:develop branch. When the pull request is approved and merged, the upstream:develop branch be ready for the rest of the team.
Monitor upstream:develop regularly and be sure to fetch-and-merge (aka “pull”) the upstream:develop branch into origin:develop. Any in-progress origin:update/<something> branches that have not had a pull request issues against upstream should be rebased onto the new origin:develop branch. Make sure all conflicts are resolved.
Store Locator Plus® uses SCSS files compiled by the Sass application to generate the custom CSS files for the application. To process these files you’ll need to have a builder application, such as Webpack via Node, or an IDE with a file watcher in place. Proper generation of the CSS compilations is critical to creating a proper application interface.
Eventually our process will move to a managed script to build the application stack as we move toward JavaScript interfaces, such as React, as a core of the user experience. In the meantime the IDE file watcher approach is in place.
The production branch. This branch should always point to production releases – the code released to the public or in use on production systems.
Staging
The release candidate branch. Prerelease plugins or the plugins/components that are published to dashbeta for the SaaS offering.
Test / Develop
The branch being used for testing integration of ongoing development branches.
Version Identifiers
Starting in August 2022 all product versions will follow a YYMMDD.xx standard where YYMMDD is based on the development start date for primary product releases (Store Locator Plus® for WordPress, MySLP Dashboard,etc.) For related product releases (Power, Front-End, etc.) the date should try to match the main product/service it is related to but this is not a strict rule – rather a suggest to help users easily identify related releases.
The YYMMDD should be 2 digits each and xx should always start at 01 for the first revision. For example 220815.01 is the first iteration for an app that started development on 220815.
Code Formatting / Linting
JavaScript
The preferred linting library is ESLint due to the ongoing React support.
An excess number of option value lookups was discovered while testing Store Locator Plus® 5.13.8 where the option/label_directions was searched multiple times for a single map render. To reduce load on servers this should only happen once, at the start of the initial map rendering.
This is not a blatant “it did something wrong” or “didn’t do something it should” type of bug. This is a performance and resource usage bug and may require some new architcture.
The manifestation:
Debugging path… investigation
Looking for label_directions in the code…
The initial search through the plugins reveals places the settings are created, set, stored, and managed but not the “JavaScript hook” being fired via /wp-json (the REST API for WordPress). So we need to look deeper.
Looking for options/ in the code…
Let’s try the start of the REST API path after the generic /wp-json/store-locator-plus/v2/ part…
We can ignore the assets/ directory… but what else is in here…
Ahhh… the slp_core.js call to slplus.rest_url + ‘options/’ + attribute looks sus…a
Solution (planned for SLP 5.14)
There was a problem in slp_core.js with value testing.
Turns out an empty value causes multiple REST calls. The following is an invalid test to see if a property exists, which is what we want here. Previously it was checking the value was SET and had a “non-falsy” value. In JavaScript lots of things are “kind of true” or “kind of false” (aka Falsy), for example the empty string “” is FALSE. That is not what we want here.
In slp_core.js we ant to replace the if (!<var>) with if (! var.hasOwnProperty()) …
Cause
The call to /v2/options/label_directions was firing once for every location because the default value for the directions label is empty (“”). This was evaluating to false, which forced the SLP JavaScript to query the server to get the value, which set it to “”.
For each location that was rendered the setting was checked… “is the label for directions set?” or more accurately “Is the label for directions set and NOT empty?”. Every time it came back saying “no, it is NOT set” or rather “It IS empty” … so the code would then go ask the REST API server for the label value.
The front end sends a request back to WordPress via AJAX to request a list of locations. Part of that request encodes all of the form field entries, including the category drop down selection, into a query-encoded (key/value pairs with an & delimiter) string that is send in the formData property to the backend.
The category ID is not being parsed and filtered properly.
Looking At The Data Request
Via browser developer tools and the network I/O inspector, XHR filter…
HTTP Method: POST URL: /wp-admin/admin-ajax.php Request Payload…
You can see the formdata property in the POST body along with the encoded form data including the cat=5 entry. This is where the requests tells the back end to limit results to those that have WordPress category ID #5 attached to the location.
Diving Into The Code
On the back end the AJAX is routed through the Store Locator Plus® base plugin via the standard WordPress hooks and filters. These are setup in the SLP_AJAX class via this add_ajax_hooks method:
Where the csl_ajax_search eventually finds its way to the find_locations method.
Debugging the find_locations method it looks like the formdata variable coming in via the superglobal $_REQUEST variable is not being set properly.
Digging Into The Malformed Variables
Looking at the call stack the form data is mishandled causing the & in the query string to be encoded as &, this causes wp_parse_args() to set the property name incorrect as you can see in the debugging screenshot below.
Turns out the wp_kses_post() is too aggressive with the data munging and converts & to & which then throws wp_parse_args() for a loop.
Instead we need to use esc_url_raw() to leave most URL entities intact.
The setting to display the category selector on the front end stopped working. The Power add on is active, but with SLP 5.13.X prerelease the drop down no longer appears.
This is very likely due to escaping of output that is overly aggressive on what it filters out on the front end.
Tech Overview
The base plugin renders the map , search form and results on the front end using shortcodes. The [slplus] shortcode renders the entire UX based on a combination of HTML and secondary shortcodes. These secondary shortcodes are parsed by the SLP base plugin to do things like “put the search form here” or “put the results here”. Within each of those custom shortcodes is another set of shortcodes, for example for the search form “put the address input box here” or “put the radius selector here”.
One of those additional shortcodes is provided by the Power add on to “put the category selector drop down here”.
That is not being processed.
Code Path
SLP_Power_UI.php
This attaches the method for rendering additional SLP search form features. This method is getting called.
public function add_hooks_and_filters()
...
add_filter( 'shortcode_slp_searchelement' , array( $this , 'process_slp_search_element_shortcode' ) );
This method is not getting called when the [slplus] shortcode is rendered…
public function process_slp_search_element_shortcode( $attributes ) {
The shortcode_slp_searchelement filter is only fired from this piece of code:
SLP_UI.php
Provided by the base plugin, this handles most of the front end initialization and execution.
This line of code is being called to apply filters…
public function create_SearchElement( $attributes, $content = null ) {
$attributes = apply_filters( 'shortcode_slp_searchelement', $attributes );
Checking Call Order
⚠️ Evaluating the call execution order, the above apply_filters is being called BEFORE the power add on has run the SLP_POWER_UI add_hooks_and_filters() , noted above, to hook onto that filter call.
Power Add Hooks & Filters
Here is the call stack when Power add_hooks_and_filters is run…
The call to SLP_BaseClass_Addon->createobject_UserInterface() is fired during the execution of the ‘wp_enqueue_scripts’ action hook as defined in base_class.addon.php in the slp_init() method…
Here is the call stack , CALLED FIRST, for creating the search element:
slp_render_shortcode() is called to start this process per the WordPress add_shortcode() method in SLP_UI :: initialize().
That means do_shortcode() is being called very early —
This is due to a side effect of all the fuckery WordPress Core has been doing to deal with the block mode editor. In order to support new block themes (like Twenty Twenty Two) WP_Block->render() calls render_block_core_post_content() super early. Before any script enqueue.
WP Core: render_block_core_post_content() calls the “the_content” filter way earlier than non-block themes:
Attach creatobject_UserInterface Calls To New Hook
With the block theme, firing that method via the ‘wp_enqueue_scripts’ method is too late for WP Block Themes.
There are multiple methods that could be used to hook this, but which one will be BEFORE the WP Block rendering calls the ‘the_content’ filter AND still work for older themes?
Option 1 : Hook to ‘the_content’ filter with an earlier precedence on the weight
The main concern here is that a ‘the_content’ script does not get called everywhere a ‘wp_enqueue_scripts’ hook gets called. The question is whether AJAX and REST calls run through here.
Results
Did not work, broke a lot of features and setup in the plugin.
Option 2 : Attach to an earlier hook…
Candidates…
block_parser_class — in function parse_blocks() probably not called on non-block themes.
template_include — before template_canvas.php which appears to be before template_canvas where the first line is get_the_block_template(); the assumption is template_canvas is only called for block based themes.
Concerns
‘template_include’ hook may get called too often, but it is a classic filter that has been around for a while and should work for both block and legacy templates.