Plugin Directory

Changeset 3491200


Ignore:
Timestamp:
03/25/2026 07:54:36 PM (7 days ago)
Author:
klimentp
Message:

1.1.2 - Security update for API keys

Location:
draftseo-ai
Files:
26 added
4 edited

Legend:

Unmodified
Added
Removed
  • draftseo-ai/trunk/README.md

    r3478994 r3491200  
    173173## Changelog
    174174
    175 ### 1.1.1
    176 Plugin images update
     175### 1.1.2
     176
     177Security update: fixes API token authentication on WordPress sites where the Application Passwords feature or a security plugin was blocking server-to-server requests from DraftSEO.AI before they could be validated.
    177178
    178179### 1.1.0
  • draftseo-ai/trunk/draftseo-ai.php

    r3479027 r3491200  
    44 * Plugin URI: https://draftseo.ai/wp-plugin
    55 * Description: Publish AI-generated blogs from DraftSEO.AI platform directly to WordPress. Transfers images from Nebius CDN to WordPress media library while maintaining SEO optimization.
    6  * Version: 1.1.1
     6 * Version: 1.1.2
    77 * Author: DraftSEO.AI
    88 * Author URI: https://draftseo.ai
     
    3838
    3939// Define plugin constants
    40 define('DRAFTSEO_VERSION', '1.0.4');
     40define('DRAFTSEO_VERSION', '1.1.2');
    4141define('DRAFTSEO_PLUGIN_DIR', plugin_dir_path(__FILE__));
    4242define('DRAFTSEO_PLUGIN_URL', plugin_dir_url(__FILE__));
  • draftseo-ai/trunk/includes/class-rest-api.php

    r3478117 r3491200  
    2222   
    2323    /**
     24     * Prevent WordPress's own authentication layer from rejecting DraftSEO
     25     * Bearer token requests before our permission_callback can run.
     26     *
     27     * WordPress 5.6+ intercepts "Authorization: Bearer <token>" headers at the
     28     * REST authentication layer (via Application Passwords). When the token does
     29     * not match any WordPress Application Password, WordPress returns
     30     * "restx_logged_out" (401) before our verify_api_key permission_callback is
     31     * ever called — making every server-to-server call from DraftSEO.AI fail on
     32     * sites where no WordPress user is logged in, regardless of whether the API
     33     * key itself is valid.
     34     *
     35     * The fix: hook into rest_authentication_errors and, when the incoming
     36     * request targets a /draftseo/v1/ endpoint and carries a "Bearer ds_" token,
     37     * return null (no opinion) so WordPress hands control to our
     38     * permission_callback. The actual key validation still happens there.
     39     *
     40     * This pattern is used by WooCommerce, Jetpack, and other plugins that
     41     * implement their own REST auth alongside WordPress core.
     42     *
     43     * @param WP_Error|null|bool $result Current authentication error, null, or true.
     44     * @return WP_Error|null|bool
     45     */
     46    public static function bypass_wp_auth_for_draftseo_endpoints($result) {
     47        // If WordPress has already positively authenticated the user, leave it alone.
     48        if (true === $result || is_user_logged_in()) {
     49            return $result;
     50        }
     51
     52        // Only intervene for requests to our own namespace.
     53        $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
     54        if (strpos($request_uri, '/draftseo/v1/') === false) {
     55            return $result;
     56        }
     57
     58        // Extract the Authorization header (Apache may move it to REDIRECT_*).
     59        $auth_header = '';
     60        if (!empty($_SERVER['HTTP_AUTHORIZATION'])) {
     61            $auth_header = $_SERVER['HTTP_AUTHORIZATION'];
     62        } elseif (!empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
     63            $auth_header = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
     64        }
     65
     66        // Only bypass for DraftSEO Bearer tokens (prefixed ds_).
     67        // Any other token (Application Passwords, cookies, etc.) goes through
     68        // WordPress's normal auth flow unchanged.
     69        if (strpos($auth_header, 'Bearer ds_') === 0) {
     70            return null; // No opinion — let permission_callback decide.
     71        }
     72
     73        return $result;
     74    }
     75
     76    /**
    2477     * Register REST API routes
    2578     */
    2679    public static function register_routes() {
     80        // Prevent WordPress's Application Passwords layer from rejecting our
     81        // Bearer tokens before verify_api_key has a chance to run.
     82        add_filter('rest_authentication_errors', array(__CLASS__, 'bypass_wp_auth_for_draftseo_endpoints'), 10, 1);
     83
    2784        // Get WordPress users
    2885        register_rest_route(self::NAMESPACE, '/users', array(
     
    98155            'methods' => 'POST',
    99156            'callback' => array(__CLASS__, 'remote_disconnect'),
     157            'permission_callback' => array(__CLASS__, 'verify_api_key')
     158        ));
     159
     160        // Site info endpoint — for Autopilot: language, site name, locale
     161        register_rest_route(self::NAMESPACE, '/site-info', array(
     162            'methods' => 'GET',
     163            'callback' => array(__CLASS__, 'get_site_info'),
     164            'permission_callback' => array(__CLASS__, 'verify_api_key')
     165        ));
     166
     167        // Posts endpoint — for Autopilot: authenticated paginated content index
     168        register_rest_route(self::NAMESPACE, '/posts', array(
     169            'methods' => 'GET',
     170            'callback' => array(__CLASS__, 'get_posts'),
    100171            'permission_callback' => array(__CLASS__, 'verify_api_key')
    101172        ));
     
    800871</style>' . "\n";
    801872    }
    802 }
     873
     874    /**
     875     * Get site metadata for Autopilot: language, name, locale.
     876     *
     877     * @param WP_REST_Request $request
     878     * @return WP_REST_Response
     879     */
     880    public static function get_site_info($request) {
     881        $locale = get_locale(); // e.g. en_US, fr_FR, de_DE
     882        return rest_ensure_response(array(
     883            'success'           => true,
     884            'site_url'          => get_site_url(),
     885            'site_name'         => get_bloginfo('name'),
     886            'site_description'  => get_bloginfo('description'),
     887            'locale'            => $locale,
     888            'language_code'     => substr($locale, 0, 2), // e.g. en, fr, de
     889            'charset'           => get_bloginfo('charset'),
     890            'woocommerce'       => class_exists('WooCommerce'),
     891        ));
     892    }
     893
     894    /**
     895     * Get paginated published posts/pages for Autopilot content indexing.
     896     * Returns slug, URL, title, and SEO metadata (Yoast / RankMath / AIOSEO).
     897     *
     898     * Query params:
     899     *   per_page  int  Items per page (1–250, default 100)
     900     *   page      int  Page number (default 1)
     901     *
     902     * @param WP_REST_Request $request
     903     * @return WP_REST_Response
     904     */
     905    public static function get_posts($request) {
     906        $per_page  = max(1, min(250, absint($request->get_param('per_page') ?: 100)));
     907        $page      = max(1, absint($request->get_param('page') ?: 1));
     908
     909        $post_types = array('post', 'page');
     910        if (class_exists('WooCommerce')) {
     911            $post_types[] = 'product';
     912        }
     913
     914        $query = new WP_Query(array(
     915            'post_type'      => $post_types,
     916            'post_status'    => 'publish',
     917            'posts_per_page' => $per_page,
     918            'paged'          => $page,
     919            'fields'         => 'ids',
     920            'no_found_rows'  => false,
     921        ));
     922
     923        $total       = (int) $query->found_posts;
     924        $total_pages = (int) ceil($total / max(1, $per_page));
     925
     926        $items = array();
     927        foreach ($query->posts as $post_id) {
     928            $post      = get_post($post_id);
     929            $permalink = get_permalink($post_id);
     930            if (!$post || !$permalink) continue;
     931
     932            // SEO plugin support: Yoast SEO → RankMath → AIO SEO → fallback empty
     933            $seo_title = get_post_meta($post_id, '_yoast_wpseo_title', true)
     934                      ?: get_post_meta($post_id, 'rank_math_title', true)
     935                      ?: get_post_meta($post_id, '_aioseop_title', true)
     936                      ?: '';
     937
     938            $seo_description = get_post_meta($post_id, '_yoast_wpseo_metadesc', true)
     939                            ?: get_post_meta($post_id, 'rank_math_description', true)
     940                            ?: get_post_meta($post_id, '_aioseop_description', true)
     941                            ?: '';
     942
     943            $items[] = array(
     944                'id'              => $post_id,
     945                'slug'            => $post->post_name,
     946                'title'           => get_the_title($post_id),
     947                'url'             => $permalink,
     948                'post_type'       => $post->post_type,
     949                'seo_title'       => $seo_title,
     950                'seo_description' => $seo_description,
     951            );
     952        }
     953
     954        return rest_ensure_response(array(
     955            'success'     => true,
     956            'total'       => $total,
     957            'total_pages' => $total_pages,
     958            'page'        => $page,
     959            'per_page'    => $per_page,
     960            'items'       => $items,
     961        ));
     962    }
     963}
  • draftseo-ai/trunk/readme.txt

    r3478994 r3491200  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.1.1
     7Stable tag: 1.1.2
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    107107== Changelog ==
    108108
    109 = 1.1.1 =
    110 
    111 Plugin images update
     109= 1.1.2 =
     110
     111Security update: fixes API token authentication on WordPress sites where the Application Passwords feature or a security plugin was blocking server-to-server requests from DraftSEO.AI before they could be validated.
    112112
    113113= 1.1.0 =
     
    235235== Upgrade Notice ==
    236236
    237 = 1.1.1 =
    238 readme update
     237= 1.1.2 =
     238Security update. Fixes API authentication failures on certain WordPress configurations that prevented DraftSEO.AI from syncing disconnect and deactivation events with your site.
    239239
    240240= 1.1.0 =
Note: See TracChangeset for help on using the changeset viewer.