Plugin Directory

Changeset 3226477


Ignore:
Timestamp:
01/21/2025 07:52:19 PM (15 months ago)
Author:
parasolbrowsing
Message:

Updating to version 1.1

Location:
parasol
Files:
5 added
3 edited

Legend:

Unmodified
Added
Removed
  • parasol/trunk/config.php

    r3223111 r3226477  
    11<?php
    2 if (!defined('ABSPATH')) {
    3   exit;
     2/**
     3 * Configuration file for Parasol Plugin
     4 *
     5 * This file contains configuration settings for the Parasol plugin.
     6 * It defines constants, API keys, URLs, and other settings used by the plugin.
     7 *
     8 * @package Parasol
     9 */
     10
     11if ( ! defined( 'ABSPATH' ) ) {
     12    exit;
    413}
    514
    6 define('PARASOL_API', 'https://api.parasolbrowsing.com');
    7 define('PARASOL_API_KEY', 'cf20c948293a16db47d5e5e0371ec883');
    8 define('PARASOL_CLOUDFRONT_KEY_ID', 'K2X7KRV1T2NPUT');
    9 define('PARASOL_ENV', 'prod');
    10 define('PARASOL_PRIVATE_KEY', '-----BEGIN RSA PRIVATE KEY-----
     15define( 'PARASOL_API', 'https://api.parasolbrowsing.com' );
     16define( 'PARASOL_API_KEY', 'cf20c948293a16db47d5e5e0371ec883' );
     17define( 'PARASOL_CLOUDFRONT_KEY_ID', 'K2X7KRV1T2NPUT' );
     18define( 'PARASOL_ENV', 'prod' );
     19define(
     20    'PARASOL_PRIVATE_KEY',
     21    '-----BEGIN RSA PRIVATE KEY-----
    1122MIIEpQIBAAKCAQEA7DLfPUF7UtKIEB2+ZVjFf/7ATac97mmoPpI27ZKYw5Lp5fVH
    1223nSo1X3AiA4dynhXLXmXG/k1KkIMQ58nfYBk6Gs4u/SiGOAcHfj+mOKnJvQ+Hip2S
     
    3445MYCOmKZhBUwQlTjUgB6tmK9c72irtWjWCtPmp327hePW3fToO0QyC/TtolJzuZmI
    3546mBCqyIJqdf/NJeQSqxUoNU7A2cb6sok0aoWwSs5Pg5WXWEBPbzJv5Gg=
    36 -----END RSA PRIVATE KEY-----');
    37 define('PARASOL_VERSION', '1.0');
    38 define('PARASOL_WIDGET', 'https://widget.parasolbrowsing.com/' . PARASOL_ENV);
     47-----END RSA PRIVATE KEY-----'
     48);
     49define( 'PARASOL_VERSION', '1.1' );
     50define( 'PARASOL_WIDGET', 'https://widget.parasolbrowsing.com/' . PARASOL_ENV );
  • parasol/trunk/parasol.php

    r3223111 r3226477  
    11<?php
    2 /*
    3 Plugin Name: Parasol
    4 Plugin URI: https://parasolbrowsing.com
    5 Description: Monetize your website without ads or data tracking
    6 Version: 1.0
    7 License: GPLv2 or later
    8 */
    9 
    10 if (!defined('ABSPATH')) {
    11   exit;
    12 }
    13 
    14 if (isset($_SERVER['REQUEST_URI']) && basename(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI']))) === 'favicon.ico') {
    15   return;
    16 }
    17 
    18 if (is_admin() && !wp_doing_ajax() && !wp_doing_cron()) {
    19   register_deactivation_hook(__FILE__, 'parasol_deactivation');
    20   return;
    21 }
    22 
    23 if (is_admin() || defined('DOING_AJAX') || defined('REST_REQUEST') || defined('DOING_CRON')) {
    24   return;
    25 }
    26 
    27 if (session_status() === PHP_SESSION_NONE) {
    28   session_start();
    29 }
    30 
    31 require_once plugin_dir_path(__FILE__) . 'config.php';
    32 
    33 $parasol_signed_widget_url = parasol_generate_signed_url(PARASOL_WIDGET, PARASOL_VERSION);
    34 $parasol_signed_api_url = parasol_generate_signed_url(PARASOL_API);
    35 
    36 add_action('init', 'parasol_initialize');
    37 
     2/**
     3 * Plugin Name: Parasol
     4 * Plugin URI: https://parasolbrowsing.com
     5 * Description: Monetize your website without ads or data tracking
     6 * Version: 1.1
     7 * License: GPLv2 or later
     8 *
     9 * @package Parasol
     10 */
     11
     12if ( ! defined( 'ABSPATH' ) ) {
     13    exit;
     14}
     15
     16if ( isset( $_SERVER['REQUEST_URI'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) === 'favicon.ico' ) {
     17    return;
     18}
     19
     20if ( is_admin() && ! wp_doing_ajax() && ! wp_doing_cron() ) {
     21    register_deactivation_hook( __FILE__, 'parasol_deactivation' );
     22    return;
     23}
     24
     25if ( is_admin() || defined( 'DOING_AJAX' ) || defined( 'REST_REQUEST' ) || defined( 'DOING_CRON' ) ) {
     26    return;
     27}
     28
     29if ( session_status() === PHP_SESSION_NONE ) {
     30    session_start();
     31}
     32
     33require_once plugin_dir_path( __FILE__ ) . 'config.php';
     34
     35$parasol_signed_api_url    = parasol_generate_signed_url( PARASOL_API );
     36$parasol_signed_widget_url = parasol_generate_signed_url( PARASOL_WIDGET, PARASOL_VERSION );
     37
     38add_action( 'init', 'parasol_initialize' );
     39
     40/**
     41 * Initializes the Parasol integration.
     42 *
     43 * This function handles the initialization of the Parasol integration by:
     44 * - Verifying OAuth nonces and processing OAuth session tokens passed via URL parameters.
     45 * - Redirecting to a clean URL if OAuth processing occurs.
     46 * - Disabling Parasol if certain conditions are met (e.g., `parasol_disabled` transient or `parasol_opt_out` cookie is set).
     47 * - Fetching and storing whitelist domains from the Parasol API if they are not available in the transient.
     48 * - Generating and setting necessary cookies, including API token and widget URL data.
     49 * - Enqueuing Parasol assets (JavaScript).
     50 *
     51 * @global string $parasol_signed_api_url   The signed API URL for the Parasol API.
     52 * @global string $parasol_signed_widget_url The signed widget URL for the Parasol widget.
     53 *
     54 * @return void
     55 */
    3856function parasol_initialize() {
    39   global $parasol_signed_api_url;
    40 
    41   if (isset($_GET['parasol_opt_out'])) {
    42     $clean_url = remove_query_arg('parasol_opt_out');
    43     set_transient('parasol_opt_out_' . session_id(), true, 1800);
    44     wp_redirect($clean_url);
    45     exit;
    46   }
    47 
    48   if (isset($_GET['parasol_oauth_nonce'])) {
    49     $parasol_oauth_nonce = sanitize_text_field(wp_unslash($_GET['parasol_oauth_nonce']));
    50     $clean_url = remove_query_arg('parasol_oauth_nonce');
    51     if (wp_verify_nonce($parasol_oauth_nonce, 'parasol_oauth_nonce')) {
    52       if (isset($_GET['parasol_oauth_session_token'])) {
    53         set_transient('parasol_session_token_' . session_id(), sanitize_text_field(wp_unslash($_GET['parasol_oauth_session_token'])), 30);
    54         $clean_url = remove_query_arg('parasol_oauth_session_token', $clean_url);
    55       } 
    56     }
    57     wp_redirect($clean_url);
    58     exit;
    59   }
    60 
    61   if (get_transient('parasol_disabled') || get_transient('parasol_opt_out_' . session_id())) {
    62     return;
    63   }
    64 
    65   if (!get_transient('parasol_whitelist_domains')) {
    66     $http_response = wp_remote_post(PARASOL_API . '/GetSite' . $parasol_signed_api_url, [
    67       'body'    => wp_json_encode([
    68         'apiToken' => parasol_generate_token([]),
    69         'currentVersion' => PARASOL_VERSION,
    70       ]),
    71       'headers' => [
    72         'Content-Type' => 'application/json',
    73       ],
    74       'timeout' => 10,
    75     ]);
    76 
    77     if (is_wp_error($http_response) || wp_remote_retrieve_response_code($http_response) !== 200) {
    78       if (wp_remote_retrieve_response_code($http_response) == 401) {
    79         set_transient('parasol_disabled', true);
    80       }
    81       return;
    82     }
    83 
    84     $parasol_site_data = json_decode(wp_remote_retrieve_body($http_response), true);
    85     if (!$parasol_site_data) {
    86         return;
    87     }
    88 
    89     set_transient('parasol_whitelist_domains', $parasol_site_data['whitelistDomains'] ?? []);
    90   }
    91 
    92   parasol_disable_domains();
    93   add_action('wp_enqueue_scripts', 'parasol_enqueue_assets');
    94 }
    95 
     57    global $parasol_signed_api_url, $parasol_signed_widget_url;
     58
     59    if ( isset( $_GET['parasol_oauth_nonce'] ) ) {
     60        $parasol_oauth_nonce = sanitize_text_field( wp_unslash( $_GET['parasol_oauth_nonce'] ) );
     61        $clean_url           = remove_query_arg( 'parasol_oauth_nonce' );
     62        if ( wp_verify_nonce( $parasol_oauth_nonce, 'parasol_oauth_nonce' ) ) {
     63            if ( isset( $_GET['parasol_oauth_session_token'] ) ) {
     64                setcookie( 'parasol_session_token', sanitize_text_field( wp_unslash( $_GET['parasol_oauth_session_token'] ) ), strtotime( '+1 minute' ), '/', '', true, false );
     65                $clean_url = remove_query_arg( 'parasol_oauth_session_token', $clean_url );
     66            }
     67        }
     68        wp_safe_redirect( $clean_url );
     69        exit();
     70    }
     71
     72    if ( get_transient( 'parasol_disabled' ) || isset( $_COOKIE['parasol_opt_out'] ) ) {
     73        return;
     74    }
     75
     76    if ( ! get_transient( 'parasol_whitelist_domains' ) ) {
     77        $http_response = wp_remote_post(
     78            PARASOL_API . '/GetSite' . $parasol_signed_api_url,
     79            array(
     80                'body'    => wp_json_encode(
     81                    array(
     82                        'apiToken'       => parasol_generate_token(
     83                            array()
     84                        ),
     85                        'currentVersion' => PARASOL_VERSION,
     86                    )
     87                ),
     88                'headers' => array(
     89                    'Content-Type' => 'application/json',
     90                ),
     91                'timeout' => 10,
     92            )
     93        );
     94
     95        if ( is_wp_error( $http_response ) || wp_remote_retrieve_response_code( $http_response ) !== 200 ) {
     96            if ( wp_remote_retrieve_response_code( $http_response ) === 401 ) {
     97                set_transient( 'parasol_disabled', true );
     98            }
     99            return;
     100        }
     101
     102        $parasol_site_data = json_decode( wp_remote_retrieve_body( $http_response ), true );
     103        if ( ! $parasol_site_data ) {
     104                return;
     105        }
     106
     107        set_transient( 'parasol_whitelist_domains', $parasol_site_data['whitelistDomains'] ?? array() );
     108    }
     109
     110    $api_token_payload = array(
     111        'idempotencyKey' => uniqid( session_id(), true ),
     112        'parasolKey'     => hash( 'sha256', session_id() . '6a39c2ce25815beb37872e8f7f2eb7ae' ),
     113    );
     114
     115    $parasol_data = array(
     116        'apiDomain'            => PARASOL_API,
     117        'apiToken'             => parasol_generate_token( $api_token_payload ),
     118        'apiUrlQueryString'    => $parasol_signed_api_url,
     119        'devMode'              => PARASOL_ENV === 'dev',
     120        'oauthNonce'           => wp_create_nonce( 'parasol_oauth_nonce' ),
     121        'widgetDomain'         => PARASOL_WIDGET,
     122        'widgetUrlQueryString' => $parasol_signed_widget_url,
     123    );
     124
     125    setcookie( 'parasol_data', str_replace( array( '+', '/', '=' ), array( '-', '_', '' ), base64_encode( wp_json_encode( $parasol_data ) ) ), strtotime( '+10 minute' ), '/', '', true, false );
     126    parasol_disable_domains();
     127    add_action( 'wp_enqueue_scripts', 'parasol_enqueue_assets' );
     128}
     129
     130/**
     131 * Enqueues the Parasol JavaScript file.
     132 *
     133 * This function enqueues the Parasol JavaScript file (`parasol.js`) with a signed widget URL appended as a query parameter.
     134 * The script is loaded in the footer (`true` for the last parameter) and is registered with the version defined in
     135 * the `PARASOL_VERSION` constant.
     136 *
     137 * @global string $parasol_signed_widget_url A signed URL used as a query parameter for the script.
     138 *
     139 * @return void
     140 */
    96141function parasol_enqueue_assets() {
    97   global $parasol_signed_api_url;
    98 
    99   $parasol_signed_widget_url = parasol_generate_signed_url(PARASOL_WIDGET, PARASOL_VERSION);
    100 
    101   $api_token_payload = [
    102     'idempotencyKey' => uniqid(session_id(), true),
    103     'parasolKey' => hash('sha256', session_id() . '6a39c2ce25815beb37872e8f7f2eb7ae')
    104   ];
    105 
    106   $is_new_session = false;
    107   if (!get_transient('parasol_session_' . session_id())) {
    108     set_transient('parasol_session_' . session_id(), true, 1800);
    109     $is_new_session = true;
    110   }
    111 
    112   wp_enqueue_style('parasol-styles', PARASOL_WIDGET . '/parasol.css' . $parasol_signed_widget_url, [], PARASOL_VERSION);
    113   wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', [], true, true);
    114   wp_enqueue_script('parasol-js', PARASOL_WIDGET . '/parasol.js' . $parasol_signed_widget_url, [], PARASOL_VERSION, true);
    115   wp_localize_script('parasol-js', 'parasol', [
    116     'apiDomain' => PARASOL_API,
    117     'apiToken' => parasol_generate_token($api_token_payload),
    118     'apiUrlQueryString' => $parasol_signed_api_url,
    119     'devMode' => PARASOL_ENV === 'dev',
    120     'newSession' => $is_new_session,
    121     'oauthNonce' => wp_create_nonce('parasol_oauth_nonce'),
    122     'sessionToken' => get_transient('parasol_session_token_' . session_id()) ?? '',
    123     'widgetDomain' => PARASOL_WIDGET,
    124     'widgetUrlQueryString' => $parasol_signed_widget_url,
    125   ]);
    126 
    127   delete_transient('parasol_session_token_' . session_id());
    128 }
    129 
    130 function parasol_generate_token($payload) {
    131   $payload = array_merge($payload, [
    132     'timestamp' => time(),
    133     'url' => parasol_get_current_url(),
    134   ]);
    135 
    136   $payloadJson = wp_json_encode($payload);
    137   $signature = hash_hmac('sha256', $payloadJson, PARASOL_API_KEY);
    138   $token = [
    139     'payload' => $payloadJson,
    140     'signature' => $signature
    141   ];
    142 
    143   return base64_encode(wp_json_encode($token));
    144 }
    145 
     142    global $parasol_signed_widget_url;
     143    wp_enqueue_script( 'parasol-js', PARASOL_WIDGET . '/parasol.js' . $parasol_signed_widget_url, array(), PARASOL_VERSION, true );
     144}
     145
     146/**
     147 * Generates a signed token for a given payload.
     148 *
     149 * This function takes a payload (an array), adds a timestamp and the current URL, then creates a
     150 * JSON-encoded representation of the payload. It generates a signature using the HMAC SHA-256
     151 * algorithm with the payload and a secret API key, and returns a base64-encoded token that includes
     152 * the payload and its signature.
     153 *
     154 * @param array $payload The initial data to include in the token.
     155 *
     156 * @return string A base64-encoded token consisting of the payload and its signature.
     157 */
     158function parasol_generate_token( $payload ) {
     159    $payload = array_merge(
     160        $payload,
     161        array(
     162            'timestamp' => time(),
     163            'url'       => parasol_get_current_url(),
     164        )
     165    );
     166
     167    $payload_json = wp_json_encode( $payload );
     168    $signature    = hash_hmac( 'sha256', $payload_json, PARASOL_API_KEY );
     169    $token        = array(
     170        'payload'   => $payload_json,
     171        'signature' => $signature,
     172    );
     173
     174    return base64_encode( wp_json_encode( $token ) );
     175}
     176
     177/**
     178 * Retrieves the current URL based on the server's protocol, host, and request URI.
     179 *
     180 * This function constructs the full URL by checking if HTTPS is enabled and appending
     181 * the sanitized `HTTP_HOST` and `REQUEST_URI` from the `$_SERVER` superglobal.
     182 *
     183 * @return string The full URL, including the protocol (http or https).
     184 */
    146185function parasol_get_current_url() {
    147   $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://';
    148   return $protocol . sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'] ?? '')) . sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? ''));
    149 }
    150 
    151 function parasol_generate_signed_url($url, $version = '') {
    152   $policy = wp_json_encode([
    153     "Statement" => [
    154       [
    155         "Resource" => $url . '/*' . ($version ? '?ver=' . $version : ''),
    156         "Condition" => ["DateLessThan" => ["AWS:EpochTime" => time() + 600]],
    157       ],
    158     ],
    159   ]);
    160 
    161   $encoded_policy = parasol_url_safe_base64_encode($policy);
    162   openssl_sign($policy, $signature, PARASOL_PRIVATE_KEY, OPENSSL_ALGO_SHA1);
    163   $encoded_signature = parasol_url_safe_base64_encode($signature);
    164 
    165   $query_delimiter = $version ? 'ver=' . $version . '&' : '';
    166   return "?{$query_delimiter}Policy={$encoded_policy}&Signature={$encoded_signature}&Key-Pair-Id=" . PARASOL_CLOUDFRONT_KEY_ID;
    167 }
    168 
    169 function parasol_url_safe_base64_encode($value) {
    170   return str_replace(['+', '=', '/'], ['-', '_', '~'], base64_encode($value));
    171 }
    172 
     186    $protocol = ( ! empty( $_SERVER['HTTPS'] ) && 'on' === $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
     187    return $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ?? '' ) ) . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) );
     188}
     189
     190/**
     191 * Generates a signed URL for accessing a resource with a specified expiration time.
     192 *
     193 * This function creates a signed URL for a given resource by encoding a policy
     194 * that includes a condition for expiration (600 seconds from the current time).
     195 * The URL is signed using a private key, and the resulting signature is included
     196 * in the URL as a query parameter.
     197 *
     198 * @param string $url     The URL of the resource to be signed.
     199 * @param string $version The version of the resource (optional).
     200 *
     201 * @return string The signed URL with the policy and signature as query parameters.
     202 */
     203function parasol_generate_signed_url( $url, $version = '' ) {
     204    $policy = wp_json_encode(
     205        array(
     206            'Statement' => array(
     207                array(
     208                    'Resource'  => $url . '/*' . ( $version ? '?ver=' . $version : '' ),
     209                    'Condition' => array( 'DateLessThan' => array( 'AWS:EpochTime' => time() + 600 ) ),
     210                ),
     211            ),
     212        )
     213    );
     214
     215    $encoded_policy = parasol_url_safe_base64_encode( $policy );
     216    openssl_sign( $policy, $signature, PARASOL_PRIVATE_KEY, OPENSSL_ALGO_SHA1 );
     217    $encoded_signature = parasol_url_safe_base64_encode( $signature );
     218
     219    $query_delimiter = $version ? 'ver=' . $version . '&' : '';
     220    return "?{$query_delimiter}Policy={$encoded_policy}&Signature={$encoded_signature}&Key-Pair-Id=" . PARASOL_CLOUDFRONT_KEY_ID;
     221}
     222
     223/**
     224 * Encodes a value using base64 encoding and replaces URL unsafe characters.
     225 *
     226 * This function performs base64 encoding on the input value and then replaces
     227 * the characters that are not URL-safe (`+`, `/`, `=`) with their URL-safe
     228 * equivalents (`-`, `_`, `~`) to make the encoded string safe for use in URLs.
     229 *
     230 * @param string $value The value to be base64 encoded.
     231 *
     232 * @return string The URL-safe base64 encoded string.
     233 */
     234function parasol_url_safe_base64_encode( $value ) {
     235    return str_replace( array( '+', '=', '/' ), array( '-', '_', '~' ), base64_encode( $value ) );
     236}
     237
     238/**
     239 * Cleans up transients on plugin deactivation.
     240 *
     241 * This function deletes specific transients related to the Parasol plugin (`parasol_disabled`
     242 * and `parasol_whitelist_domains`) to clean up any stored data when the plugin is deactivated.
     243 *
     244 * @return void
     245 */
    173246function parasol_deactivation() {
    174   delete_transient('parasol_disabled');
    175   delete_transient('parasol_whitelist_domains');
    176 }
    177 
     247    delete_transient( 'parasol_disabled' );
     248    delete_transient( 'parasol_whitelist_domains' );
     249}
     250
     251/**
     252 * Disables certain domains and enqueues custom actions.
     253 *
     254 * This function hooks into WordPress actions to disable certain domains by adding a custom
     255 * content security policy and dequeueing third-party scripts. It hooks `parasol_add_custom_content_security_policy`
     256 * to `send_headers` and `parasol_dequeue_third_party_scripts` to `wp_enqueue_scripts` with a priority of 100.
     257 *
     258 * @return void
     259 */
    178260function parasol_disable_domains() {
    179   add_action('send_headers', 'parasol_add_custom_content_security_policy');
    180   add_action('wp_enqueue_scripts', 'parasol_dequeue_third_party_scripts', 100);
    181 }
    182 
     261    add_action( 'send_headers', 'parasol_add_custom_content_security_policy' );
     262    add_action( 'wp_enqueue_scripts', 'parasol_dequeue_third_party_scripts', 100 );
     263}
     264
     265/**
     266 * Adds a custom Content Security Policy (CSP) header.
     267 *
     268 * This function generates a custom CSP header based on the domains stored in the `parasol_whitelist_domains`
     269 * transient. It defines directives for script, style, worker, and frame sources, and then adds the CSP header
     270 * to the response using the `header()` function. It supports wildcard domains and includes `unsafe-inline` for
     271 * script and style sources.
     272 *
     273 * @return void
     274 */
    183275function parasol_add_custom_content_security_policy() {
    184   $wildcard_domains = array_map(function($domain) {
    185     return '*.' . $domain;
    186   }, (get_transient('parasol_whitelist_domains') ?? []));
    187 
    188   $csp_directives = [
    189     'script-src' => array_merge(["'self'", "'unsafe-inline'"], $wildcard_domains),
    190     'style-src' => array_merge(["'self'", "'unsafe-inline'"], $wildcard_domains),
    191     'worker-src' => array_merge(["'self'", 'blob:'], $wildcard_domains),
    192     'frame-src' => array_merge(["'self'"], $wildcard_domains),
    193   ];
    194 
    195   $csp_header = parasol_build_csp_header($csp_directives);
    196   if ($csp_header) {
    197     header("Content-Security-Policy: $csp_header");
    198   }
    199 }
    200 
    201 function parasol_build_csp_header(array $directives): string {
    202   $header_parts = [];
    203 
    204   foreach ($directives as $directive => $sources) {
    205     $header_parts[] = "{$directive} " . implode(' ', $sources) . ';';
    206   }
    207 
    208   return implode(' ', $header_parts);
    209 }
    210 
     276    $wildcard_domains = array_map(
     277        function ( $domain ) {
     278            return '*.' . $domain;
     279        },
     280        ( get_transient( 'parasol_whitelist_domains' ) ?? array() )
     281    );
     282
     283    $csp_directives = array(
     284        'script-src' => array_merge( array( "'self'", "'unsafe-inline'" ), $wildcard_domains ),
     285        'style-src'  => array_merge( array( "'self'", "'unsafe-inline'" ), $wildcard_domains ),
     286        'worker-src' => array_merge( array( "'self'", 'blob:' ), $wildcard_domains ),
     287        'frame-src'  => array_merge( array( "'self'" ), $wildcard_domains ),
     288    );
     289
     290    $csp_header = parasol_build_csp_header( $csp_directives );
     291    if ( $csp_header ) {
     292        header( "Content-Security-Policy: $csp_header" );
     293    }
     294}
     295
     296/**
     297 * Builds a Content Security Policy (CSP) header from the provided directives.
     298 *
     299 * This function takes an array of CSP directives, where each directive is associated with a list of sources.
     300 * It formats the directives and their corresponding sources into a valid CSP header string, which can be used
     301 * in an HTTP response to define security policies for various resources like scripts, styles, and frames.
     302 *
     303 * @param array $directives An associative array of CSP directives and their corresponding sources.
     304 *
     305 * @return string A string representing the formatted CSP header.
     306 */
     307function parasol_build_csp_header( array $directives ): string {
     308    $header_parts = array();
     309
     310    foreach ( $directives as $directive => $sources ) {
     311        $header_parts[] = "{$directive} " . implode( ' ', $sources ) . ';';
     312    }
     313
     314    return implode( ' ', $header_parts );
     315}
     316
     317/**
     318 * Dequeues third-party scripts not hosted on allowed domains.
     319 *
     320 * This function checks the list of currently queued scripts and dequeues any third-party scripts
     321 * whose source domain is not in the list of allowed domains. The allowed domains are retrieved
     322 * from the `parasol_whitelist_domains` transient, along with the site’s own domain.
     323 *
     324 * The function uses `parasol_is_allowed_script_domain()` to verify whether a script's domain is allowed.
     325 *
     326 * @return void
     327 */
    211328function parasol_dequeue_third_party_scripts() {
    212   global $wp_scripts;
    213 
    214   $allowed_domains = array_merge((get_transient('parasol_whitelist_domains') ?? []), [wp_parse_url(get_site_url(), PHP_URL_HOST)]);
    215 
    216   foreach ($wp_scripts->queue as $script_handle) {
    217     $script_obj = $wp_scripts->registered[$script_handle] ?? null;
    218 
    219     if ($script_obj && $script_obj->src && !parasol_is_allowed_script_domain($script_obj->src, $allowed_domains)) {
    220       wp_dequeue_script($script_handle);
    221     }
    222   }
    223 }
    224 
    225 function parasol_is_allowed_script_domain(string $script_src, array $allowed_domains): bool {
    226   $script_domain = wp_parse_url($script_src, PHP_URL_HOST);
    227   return $script_domain ? in_array(get_root_domain($script_domain), $allowed_domains, true) : true;
    228 }
    229 
    230 function get_root_domain(string $domain): string {
    231   $parts = explode('.', $domain);
    232  
    233   if (count($parts) > 2) {
    234     return implode('.', array_slice($parts, -2));
    235   }
    236 
    237   return $domain;
    238 }
     329    global $wp_scripts;
     330
     331    $allowed_domains = array_merge( ( get_transient( 'parasol_whitelist_domains' ) ?? array() ), array( wp_parse_url( get_site_url(), PHP_URL_HOST ) ) );
     332
     333    foreach ( $wp_scripts->queue as $script_handle ) {
     334        $script_obj = $wp_scripts->registered[ $script_handle ] ?? null;
     335
     336        if ( $script_obj && $script_obj->src && ! parasol_is_allowed_script_domain( $script_obj->src, $allowed_domains ) ) {
     337            wp_dequeue_script( $script_handle );
     338        }
     339    }
     340}
     341
     342/**
     343 * Checks if a script's domain is allowed.
     344 *
     345 * This function extracts the domain from the given script URL and compares it to the list of allowed
     346 * domains. It checks if the root domain of the script’s source is in the allowed domains list. If the
     347 * script’s domain is not found or no domain is present in the URL, it returns `true` (indicating the script is allowed).
     348 *
     349 * @param string $script_src   The URL of the script to check.
     350 * @param array  $allowed_domains The list of allowed domains.
     351 *
     352 * @return bool `true` if the script’s domain is allowed, otherwise `false`.
     353 */
     354function parasol_is_allowed_script_domain( string $script_src, array $allowed_domains ): bool {
     355    $script_domain = wp_parse_url( $script_src, PHP_URL_HOST );
     356    return $script_domain ? in_array( get_root_domain( $script_domain ), $allowed_domains, true ) : true;
     357}
     358
     359/**
     360 * Extracts the root domain from a given domain.
     361 *
     362 * This function takes a domain and returns the root domain by extracting the last two segments
     363 * (e.g., for `sub.example.com`, it will return `example.com`). If the domain has two or fewer segments,
     364 * the original domain is returned.
     365 *
     366 * @param string $domain The domain to extract the root from.
     367 *
     368 * @return string The root domain (last two segments) or the original domain if it has two or fewer segments.
     369 */
     370function get_root_domain( string $domain ): string {
     371    $parts = explode( '.', $domain );
     372
     373    if ( count( $parts ) > 2 ) {
     374        return implode( '.', array_slice( $parts, -2 ) );
     375    }
     376
     377    return $domain;
     378}
  • parasol/trunk/readme.txt

    r3224076 r3226477  
    55Tested up to: 6.7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.0
     7Stable tag: 1.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    8787== Changelog ==
    8888
    89 = 1.0.0 =
     89= 1.1 =
     90* Updates to Parasol asset loading.
     91
     92= 1.0 =
    9093* Initial release of Parasol.
    9194* Added options for site visitors to access content.
     
    9497
    9598== Upgrade Notice ==
     99
     100= 1.1 =
     101Updates to Parasol asset loading.
    96102
    97103= 1.0 =
Note: See TracChangeset for help on using the changeset viewer.