Plugin Directory

Changeset 3467539


Ignore:
Timestamp:
02/23/2026 10:23:08 AM (6 weeks ago)
Author:
cdninternet
Message:

Release 1.2.5: async purge, coalesced queue, telemetry

  • Add async purge mode for runtime-triggered invalidations (no request blocking)
  • Add coalesced purge queue with configurable window to reduce duplicate API calls
  • Add lightweight purge telemetry (counters + last_event) for diagnostics
  • Add purge policy options: cdntr_purge_mode, cdntr_purge_coalesce_window, cdntr_purge_telemetry
  • Add filters: cdntr_purge_mode, cdntr_purge_coalesce_window
  • Keep sync purge_cdn_cache/purge_cdn_paths for admin Purge All and WP-CLI
  • Bump version to 1.2.5 in cdntr.php and readme.txt
Location:
cdntr
Files:
8 edited
1 copied

Legend:

Unmodified
Added
Removed
  • cdntr/tags/1.2.5/cdntr.php

    r3131497 r3467539  
    66Author URI: https://cdn.com.tr
    77License: GPLv2 or later
    8 Version: 1.2.2
     8Version: 1.2.5
    99*/
    1010
     
    1616
    1717// constants
    18 define( 'CDNTR_VERSION', '1.0' );
     18define( 'CDNTR_VERSION', '1.2.5' );
    1919define( 'CDNTR_MIN_PHP', '5.6' );
    2020define( 'CDNTR_MIN_WP', '5.1' );
  • cdntr/tags/1.2.5/inc/cdntr.class.php

    r3130906 r3467539  
    4141        add_action( 'init', array( __CLASS__, 'register_textdomain' ) );
    4242        add_action( 'init', array( __CLASS__, 'check_meta_valid' ) );
     43        add_action( 'save_post', array( __CLASS__, 'maybe_purge_post_paths' ), 20, 3 );
     44        add_action( 'deleted_post', array( __CLASS__, 'purge_deleted_post_paths' ), 20, 1 );
     45        add_action( 'created_term', array( __CLASS__, 'maybe_purge_term_paths' ), 20, 3 );
     46        add_action( 'edited_term', array( __CLASS__, 'maybe_purge_term_paths' ), 20, 3 );
     47        add_action( 'delete_term', array( __CLASS__, 'maybe_purge_deleted_term_paths' ), 20, 5 );
     48        add_action( 'customize_save_after', array( __CLASS__, 'maybe_purge_sitewide' ), 20, 1 );
     49        add_action( 'switch_theme', array( __CLASS__, 'maybe_purge_sitewide' ), 20, 3 );
     50        add_action( 'cdntr_run_coalesced_purge', array( __CLASS__, 'run_coalesced_purge' ) );
    4351
    4452        // multisite hook
     
    356364            'cdntr_is_purge_all_button'  => '',
    357365            'cdntr_api_password'         => '',
     366            'cdntr_purge_mode'           => 'async',
     367            'cdntr_purge_coalesce_window'=> 3,
     368            'cdntr_purge_telemetry'      => '1',
    358369        );
    359370        // cdntr_account_id cdntr_api_user cdntr_api_password
     
    731742        );
    732743
     744        $telemetry_status = 'error';
     745
    733746        // check if API call failed
    734747        if ( is_wp_error( $response ) ) {
     
    743756
    744757            if ( $response_status_code === 200 ) {
     758                $telemetry_status = 'ok';
    745759                $response = array(
    746760                    'wrapper' => '<div class="notice notice-success is-dismissible"><p><strong>%s</strong></p></div>',
     
    779793        }
    780794
     795        self::record_purge_telemetry( 'all_sync', $telemetry_status );
     796
    781797        return $response;
     798    }
     799
     800    /**
     801     * purge CDN cache asynchronously (fire-and-forget)
     802     *
     803     * @since   2.1.0
     804     * @change  2.1.0
     805     *
     806     * @return  array
     807     */
     808    public static function purge_cdn_cache_async() {
     809
     810        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     811
     812        wp_remote_post(
     813            'https://cdn.com.tr/api/purgeAll',
     814            array(
     815                'timeout'     => 1,
     816                'httpversion' => '1.1',
     817                'blocking'    => false,
     818                'headers'     => array(
     819                    'Authorization' => 'Basic ' . $auth,
     820                    'Content-Type'  => 'application/json',
     821                ),
     822            )
     823        );
     824
     825        self::record_purge_telemetry( 'all_async', 'queued' );
     826
     827        return array(
     828            'ok'      => true,
     829            'message' => 'Purge queued.',
     830        );
     831    }
     832
     833    /**
     834     * Purge specific URL paths from CDN.
     835     *
     836     * @since   2.0.9
     837     * @change  2.0.9
     838     *
     839     * @param   array $paths  URL paths (e.g. /product/foo/)
     840     * @return  array
     841     */
     842    public static function purge_cdn_paths( $paths ) {
     843
     844        $paths = self::normalize_purge_paths( $paths );
     845
     846        if ( empty( $paths ) ) {
     847            self::record_purge_telemetry( 'paths_sync', 'skipped', array( 'paths' => 0 ) );
     848            return array(
     849                'ok'      => false,
     850                'message' => 'No valid purge paths.',
     851            );
     852        }
     853
     854        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     855        $response = wp_remote_post(
     856            'https://cdn.com.tr/api/purge',
     857            array(
     858                'timeout'     => 15,
     859                'httpversion' => '1.1',
     860                'headers'     => array(
     861                    'Authorization' => 'Basic ' . $auth,
     862                    'Content-Type'  => 'application/json',
     863                ),
     864                'body'        => wp_json_encode( array( 'paths' => $paths ) ),
     865            )
     866        );
     867
     868        if ( is_wp_error( $response ) ) {
     869            self::record_purge_telemetry( 'paths_sync', 'error', array( 'paths' => count( $paths ) ) );
     870            return array(
     871                'ok'      => false,
     872                'message' => $response->get_error_message(),
     873            );
     874        }
     875
     876        $status = (int) wp_remote_retrieve_response_code( $response );
     877        self::record_purge_telemetry(
     878            'paths_sync',
     879            ( $status >= 200 && $status < 300 ) ? 'ok' : 'error',
     880            array(
     881                'status' => $status,
     882                'paths'  => count( $paths ),
     883            )
     884        );
     885        return array(
     886            'ok'      => ( $status >= 200 && $status < 300 ),
     887            'status'  => $status,
     888            'message' => (string) wp_remote_retrieve_body( $response ),
     889        );
     890    }
     891
     892    /**
     893     * Purge specific URL paths asynchronously (fire-and-forget).
     894     *
     895     * @since   2.1.0
     896     * @change  2.1.0
     897     *
     898     * @param   array $paths
     899     * @return  array
     900     */
     901    public static function purge_cdn_paths_async( $paths ) {
     902
     903        $paths = self::normalize_purge_paths( $paths );
     904        if ( empty( $paths ) ) {
     905            self::record_purge_telemetry( 'paths_async', 'skipped', array( 'paths' => 0 ) );
     906            return array(
     907                'ok'      => false,
     908                'message' => 'No valid purge paths.',
     909            );
     910        }
     911
     912        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     913        wp_remote_post(
     914            'https://cdn.com.tr/api/purge',
     915            array(
     916                'timeout'     => 1,
     917                'httpversion' => '1.1',
     918                'blocking'    => false,
     919                'headers'     => array(
     920                    'Authorization' => 'Basic ' . $auth,
     921                    'Content-Type'  => 'application/json',
     922                ),
     923                'body'        => wp_json_encode( array( 'paths' => $paths ) ),
     924            )
     925        );
     926
     927        self::record_purge_telemetry( 'paths_async', 'queued', array( 'paths' => count( $paths ) ) );
     928
     929        return array(
     930            'ok'      => true,
     931            'message' => 'Purge queued.',
     932        );
     933    }
     934
     935    /**
     936     * Purge only affected URLs when content changes.
     937     *
     938     * @since   2.0.9
     939     * @change  2.0.9
     940     */
     941    public static function maybe_purge_post_paths( $post_id, $post, $update ) {
     942
     943        if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
     944            return;
     945        }
     946
     947        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     948            return;
     949        }
     950
     951        if ( ! $post instanceof \WP_Post ) {
     952            $post = get_post( $post_id );
     953            if ( ! $post ) {
     954                return;
     955            }
     956        }
     957
     958        if ( $post->post_status !== 'publish' && $post->post_status !== 'private' ) {
     959            return;
     960        }
     961
     962        $paths = array(
     963            '/',
     964            wp_parse_url( get_permalink( $post_id ), PHP_URL_PATH ),
     965        );
     966
     967        if ( $post->post_type === 'product' ) {
     968            $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     969            if ( $shop_page_id > 0 ) {
     970                $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     971            }
     972
     973            $terms = get_the_terms( $post_id, 'product_cat' );
     974            if ( is_array( $terms ) ) {
     975                foreach ( $terms as $term ) {
     976                    $term_link = get_term_link( $term );
     977                    if ( ! is_wp_error( $term_link ) ) {
     978                        $paths[] = wp_parse_url( $term_link, PHP_URL_PATH );
     979                    }
     980                }
     981            }
     982        }
     983
     984        self::enqueue_purge_paths( $paths );
     985    }
     986
     987    /**
     988     * Purge homepage after post deletion to avoid stale listings.
     989     *
     990     * @since   2.0.9
     991     * @change  2.0.9
     992     */
     993    public static function purge_deleted_post_paths( $post_id ) {
     994
     995        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     996            return;
     997        }
     998
     999        self::enqueue_purge_paths( array( '/' ) );
     1000    }
     1001
     1002    /**
     1003     * Purge related paths when selected taxonomies are changed.
     1004     *
     1005     * @since   2.1.0
     1006     * @change  2.1.0
     1007     */
     1008    public static function maybe_purge_term_paths( $term_id, $tt_id, $taxonomy ) {
     1009
     1010        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1011            return;
     1012        }
     1013
     1014        $allowed_taxonomies = (array) apply_filters( 'cdntr_purge_taxonomies', array( 'product_cat' ) );
     1015        if ( empty( $taxonomy ) || ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
     1016            return;
     1017        }
     1018
     1019        if ( self::is_purge_throttled( 'term_' . $taxonomy, 10 ) ) {
     1020            return;
     1021        }
     1022
     1023        $paths = array( '/' );
     1024        $term = get_term( (int) $term_id, $taxonomy );
     1025        if ( $term && ! is_wp_error( $term ) ) {
     1026            $term_link = get_term_link( $term );
     1027            if ( ! is_wp_error( $term_link ) ) {
     1028                $paths[] = wp_parse_url( $term_link, PHP_URL_PATH );
     1029            }
     1030        }
     1031
     1032        $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     1033        if ( $shop_page_id > 0 ) {
     1034            $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     1035        }
     1036
     1037        self::enqueue_purge_paths( $paths );
     1038    }
     1039
     1040    /**
     1041     * Purge related paths when selected taxonomies are deleted.
     1042     *
     1043     * @since   2.1.0
     1044     * @change  2.1.0
     1045     */
     1046    public static function maybe_purge_deleted_term_paths( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) {
     1047
     1048        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1049            return;
     1050        }
     1051
     1052        $allowed_taxonomies = (array) apply_filters( 'cdntr_purge_taxonomies', array( 'product_cat' ) );
     1053        if ( empty( $taxonomy ) || ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
     1054            return;
     1055        }
     1056
     1057        if ( self::is_purge_throttled( 'term_delete_' . $taxonomy, 10 ) ) {
     1058            return;
     1059        }
     1060
     1061        $paths = array( '/' );
     1062        $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     1063        if ( $shop_page_id > 0 ) {
     1064            $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     1065        }
     1066
     1067        self::enqueue_purge_paths( $paths );
     1068    }
     1069
     1070    /**
     1071     * Purge whole site for theme/customizer style changes.
     1072     *
     1073     * @since   2.1.0
     1074     * @change  2.1.0
     1075     */
     1076    public static function maybe_purge_sitewide() {
     1077
     1078        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1079            return;
     1080        }
     1081
     1082        if ( self::is_purge_throttled( 'sitewide', 20 ) ) {
     1083            return;
     1084        }
     1085
     1086        self::enqueue_purge_all();
     1087    }
     1088
     1089    /**
     1090     * Normalize purge paths to a clean unique path list.
     1091     *
     1092     * @since   2.0.9
     1093     * @change  2.0.9
     1094     *
     1095     * @param   array $paths
     1096     * @return  array
     1097     */
     1098    private static function normalize_purge_paths( $paths ) {
     1099
     1100        if ( ! is_array( $paths ) ) {
     1101            $paths = array();
     1102        }
     1103
     1104        $normalized = array();
     1105        foreach ( $paths as $path ) {
     1106            if ( ! is_string( $path ) || $path === '' ) {
     1107                continue;
     1108            }
     1109
     1110            // Strip control chars and trim to avoid malformed payloads.
     1111            $path = trim( preg_replace( '/[\x00-\x1F\x7F]/', '', $path ) );
     1112            if ( $path === '' || strlen( $path ) > 2048 ) {
     1113                continue;
     1114            }
     1115
     1116            $clean = wp_parse_url( $path, PHP_URL_PATH );
     1117            if ( ! is_string( $clean ) || $clean === '' ) {
     1118                continue;
     1119            }
     1120
     1121            // Block traversal-like paths from reaching purge API.
     1122            if ( strpos( $clean, '..' ) !== false ) {
     1123                continue;
     1124            }
     1125
     1126            if ( strpos( $clean, '/' ) !== 0 ) {
     1127                $clean = '/' . $clean;
     1128            }
     1129
     1130            $normalized[] = $clean;
     1131        }
     1132
     1133        return array_values( array_unique( $normalized ) );
     1134    }
     1135
     1136    /**
     1137     * Basic transient lock to avoid duplicate purge bursts.
     1138     *
     1139     * @since   2.1.0
     1140     * @change  2.1.0
     1141     */
     1142    private static function is_purge_throttled( $scope, $ttl = 10 ) {
     1143
     1144        $scope = sanitize_key( (string) $scope );
     1145        $ttl = absint( $ttl );
     1146        if ( $scope === '' || $ttl < 1 ) {
     1147            return false;
     1148        }
     1149
     1150        $lock_key = 'cdntr_purge_lock_' . $scope;
     1151        if ( get_transient( $lock_key ) ) {
     1152            return true;
     1153        }
     1154
     1155        set_transient( $lock_key, 1, $ttl );
     1156        return false;
     1157    }
     1158
     1159    /**
     1160     * Queue targeted purge paths in a short coalescing window.
     1161     *
     1162     * @since   2.1.0
     1163     * @change  2.1.0
     1164     *
     1165     * @param   array $paths
     1166     */
     1167    private static function enqueue_purge_paths( $paths ) {
     1168
     1169        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1170            return;
     1171        }
     1172
     1173        $paths = self::normalize_purge_paths( $paths );
     1174        if ( empty( $paths ) ) {
     1175            return;
     1176        }
     1177
     1178        $queue_key = self::get_purge_queue_transient_name( 'paths' );
     1179        $queued_paths = get_transient( $queue_key );
     1180        if ( ! is_array( $queued_paths ) ) {
     1181            $queued_paths = array();
     1182        }
     1183
     1184        $queued_paths = array_values( array_unique( array_merge( $queued_paths, $paths ) ) );
     1185        set_transient( $queue_key, $queued_paths, 300 );
     1186
     1187        self::record_purge_telemetry( 'paths_queue', 'queued', array( 'paths' => count( $queued_paths ) ) );
     1188
     1189        if ( ! wp_next_scheduled( 'cdntr_run_coalesced_purge' ) ) {
     1190            wp_schedule_single_event( time() + self::get_purge_coalesce_window(), 'cdntr_run_coalesced_purge' );
     1191        }
     1192    }
     1193
     1194    /**
     1195     * Queue purge-all requests in a short coalescing window.
     1196     *
     1197     * @since   2.1.0
     1198     * @change  2.1.0
     1199     */
     1200    private static function enqueue_purge_all() {
     1201
     1202        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1203            return;
     1204        }
     1205
     1206        set_transient( self::get_purge_queue_transient_name( 'all' ), 1, 300 );
     1207        self::record_purge_telemetry( 'all_queue', 'queued' );
     1208
     1209        if ( ! wp_next_scheduled( 'cdntr_run_coalesced_purge' ) ) {
     1210            wp_schedule_single_event( time() + self::get_purge_coalesce_window(), 'cdntr_run_coalesced_purge' );
     1211        }
     1212    }
     1213
     1214    /**
     1215     * Execute queued purge operations after coalescing window.
     1216     *
     1217     * @since   2.1.0
     1218     * @change  2.1.0
     1219     */
     1220    public static function run_coalesced_purge() {
     1221
     1222        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1223            return;
     1224        }
     1225
     1226        $purge_all_key = self::get_purge_queue_transient_name( 'all' );
     1227        $paths_key = self::get_purge_queue_transient_name( 'paths' );
     1228
     1229        $purge_all = (bool) get_transient( $purge_all_key );
     1230        $paths = get_transient( $paths_key );
     1231        $paths = is_array( $paths ) ? $paths : array();
     1232
     1233        delete_transient( $purge_all_key );
     1234        delete_transient( $paths_key );
     1235
     1236        if ( $purge_all ) {
     1237            if ( self::should_use_async_purge() ) {
     1238                self::purge_cdn_cache_async();
     1239            } else {
     1240                self::purge_cdn_cache();
     1241            }
     1242            return;
     1243        }
     1244
     1245        if ( empty( $paths ) ) {
     1246            return;
     1247        }
     1248
     1249        if ( self::should_use_async_purge() ) {
     1250            self::purge_cdn_paths_async( $paths );
     1251        } else {
     1252            self::purge_cdn_paths( $paths );
     1253        }
     1254    }
     1255
     1256    /**
     1257     * Resolve purge mode (sync|async) from settings/filters.
     1258     *
     1259     * @since   2.1.0
     1260     * @change  2.1.0
     1261     *
     1262     * @return string
     1263     */
     1264    private static function get_purge_mode() {
     1265
     1266        $mode = 'async';
     1267        if ( ! empty( CDNTR_Engine::$settings['cdntr_purge_mode'] ) ) {
     1268            $mode = strtolower( (string) CDNTR_Engine::$settings['cdntr_purge_mode'] );
     1269        }
     1270
     1271        $mode = (string) apply_filters( 'cdntr_purge_mode', $mode );
     1272        if ( $mode !== 'sync' && $mode !== 'async' ) {
     1273            $mode = 'async';
     1274        }
     1275
     1276        return $mode;
     1277    }
     1278
     1279    /**
     1280     * Determine if async purge mode is active.
     1281     *
     1282     * @since   2.1.0
     1283     * @change  2.1.0
     1284     *
     1285     * @return bool
     1286     */
     1287    private static function should_use_async_purge() {
     1288
     1289        return self::get_purge_mode() === 'async';
     1290    }
     1291
     1292    /**
     1293     * Coalescing window in seconds.
     1294     *
     1295     * @since   2.1.0
     1296     * @change  2.1.0
     1297     *
     1298     * @return int
     1299     */
     1300    private static function get_purge_coalesce_window() {
     1301
     1302        $window = 3;
     1303        if ( isset( CDNTR_Engine::$settings['cdntr_purge_coalesce_window'] ) ) {
     1304            $window = absint( CDNTR_Engine::$settings['cdntr_purge_coalesce_window'] );
     1305        }
     1306
     1307        $window = (int) apply_filters( 'cdntr_purge_coalesce_window', $window );
     1308        return max( 1, $window );
     1309    }
     1310
     1311    /**
     1312     * Purge queue transient name scoped by blog.
     1313     *
     1314     * @since   2.1.0
     1315     * @change  2.1.0
     1316     *
     1317     * @param   string $scope
     1318     * @return  string
     1319     */
     1320    private static function get_purge_queue_transient_name( $scope ) {
     1321
     1322        return 'cdntr_purge_queue_' . sanitize_key( $scope ) . '_' . get_current_blog_id();
     1323    }
     1324
     1325    /**
     1326     * Persist lightweight purge telemetry for diagnostics.
     1327     *
     1328     * @since   2.1.0
     1329     * @change  2.1.0
     1330     *
     1331     * @param   string $type
     1332     * @param   string $status
     1333     * @param   array  $meta
     1334     */
     1335    private static function record_purge_telemetry( $type, $status, $meta = array() ) {
     1336
     1337        if ( empty( CDNTR_Engine::$settings['cdntr_purge_telemetry'] ) ) {
     1338            return;
     1339        }
     1340
     1341        $key = sanitize_key( (string) $type ) . ':' . sanitize_key( (string) $status );
     1342        if ( $key === ':' ) {
     1343            return;
     1344        }
     1345
     1346        $telemetry = get_option( 'cdntr_purge_telemetry', array() );
     1347        if ( ! is_array( $telemetry ) ) {
     1348            $telemetry = array();
     1349        }
     1350
     1351        if ( ! isset( $telemetry['counters'] ) || ! is_array( $telemetry['counters'] ) ) {
     1352            $telemetry['counters'] = array();
     1353        }
     1354
     1355        if ( ! isset( $telemetry['counters'][ $key ] ) ) {
     1356            $telemetry['counters'][ $key ] = 0;
     1357        }
     1358
     1359        $telemetry['counters'][ $key ]++;
     1360        $telemetry['last_event'] = array(
     1361            'type'      => (string) $type,
     1362            'status'    => (string) $status,
     1363            'meta'      => is_array( $meta ) ? $meta : array(),
     1364            'timestamp' => time(),
     1365        );
     1366
     1367        update_option( 'cdntr_purge_telemetry', $telemetry, false );
    7821368    }
    7831369
     
    8631449    public static function add_custom_meta_tags() {
    8641450        ?>
    865              <meta name="cdn-site-verification" content="<?php echo esc_attr( CDNTR_Engine::$settings['cdntr_api_user'] ); ?>">';
     1451             <meta name="cdn-site-verification" content="<?php echo esc_attr( CDNTR_Engine::$settings['cdntr_api_user'] ); ?>">
    8661452        <?php
    8671453    }
     
    10571643            'cdntr_is_purge_all_button'=> (string) sanitize_text_field( $settings['cdntr_is_purge_all_button'] ),
    10581644            'cdn_hostname_arr'         => (string) sanitize_text_field( $settings['cdn_hostname_arr'] ) ,
     1645            'cdntr_purge_mode'         => ( ( isset( $settings['cdntr_purge_mode'] ) && strtolower( (string) $settings['cdntr_purge_mode'] ) === 'sync' ) ? 'sync' : 'async' ),
     1646            'cdntr_purge_coalesce_window' => max( 1, absint( isset( $settings['cdntr_purge_coalesce_window'] ) ? $settings['cdntr_purge_coalesce_window'] : 3 ) ),
     1647            'cdntr_purge_telemetry'    => ( ! empty( $settings['cdntr_purge_telemetry'] ) ? '1' : '0' ),
    10591648        );
    10601649
  • cdntr/tags/1.2.5/inc/cdntr_engine.class.php

    r3130906 r3467539  
    3535
    3636    public static $settings;
     37    private static $excluded_strings = array();
     38    private static $included_file_extensions_regex = '';
     39    private static $rewrite_cache = array();
     40    private static $rewrite_cache_limit = 1024;
    3741
    3842
     
    4953        self::$settings = CDNTR::get_settings();
    5054
    51         if ( ! empty( self::$settings ) ) {
     55        if ( ! empty( self::$settings ) && ! self::bypass_rewrite() ) {
     56            self::prepare_runtime_settings();
    5257            self::start_buffering();
     58        }
     59    }
     60
     61    /**
     62     * Prepare normalized runtime settings once per request.
     63     *
     64     * @since   2.0.9
     65     * @change  2.0.9
     66     */
     67    private static function prepare_runtime_settings() {
     68
     69        self::$excluded_strings = array();
     70        self::$included_file_extensions_regex = '';
     71        self::$rewrite_cache = array();
     72
     73        if ( ! empty( self::$settings['excluded_strings'] ) ) {
     74            $excluded_strings = explode( PHP_EOL, self::$settings['excluded_strings'] );
     75            $excluded_strings = array_map( 'trim', $excluded_strings );
     76            self::$excluded_strings = array_filter( $excluded_strings, function( $value ) {
     77                return $value !== '';
     78            } );
     79        }
     80
     81        if ( ! empty( self::$settings['included_file_extensions'] ) ) {
     82            self::$included_file_extensions_regex = quotemeta(
     83                implode( '|', explode( PHP_EOL, self::$settings['included_file_extensions'] ) )
     84            );
    5385        }
    5486    }
     
    146178
    147179        // if string excluded (case sensitive)
    148         if ( ! empty( self::$settings['excluded_strings'] ) ) {
    149             $excluded_strings = explode( PHP_EOL, self::$settings['excluded_strings'] );
    150 
    151             foreach ( $excluded_strings as $excluded_string ) {
     180        if ( ! empty( self::$excluded_strings ) ) {
     181            foreach ( self::$excluded_strings as $excluded_string ) {
    152182                if ( strpos( $file_url, $excluded_string ) !== false ) {
    153183                    return true;
     
    225255        $site_hostnames = (array) apply_filters( 'cdntr_site_hostnames', array( $site_hostname ) );
    226256        $cdn_hostname   = self::$settings['cdn_hostname'];
     257        $cache_key      = $site_hostname . '|' . $file_url;
     258
     259        if ( isset( self::$rewrite_cache[ $cache_key ] ) ) {
     260            return self::$rewrite_cache[ $cache_key ];
     261        }
    227262
    228263        // if excluded or already using CDN hostname
    229264        if ( self::is_excluded( $file_url ) || stripos( $file_url, $cdn_hostname ) !== false ) {
    230             return $file_url;
     265            self::remember_rewrite_cache( $cache_key, $file_url );
     266            return self::$rewrite_cache[ $cache_key ];
    231267        }
    232268
     
    234270        foreach ( $site_hostnames as $site_hostname ) {
    235271            if ( stripos( $file_url, '//' . $site_hostname ) !== false || stripos( $file_url, '\/\/' . $site_hostname ) !== false ) {
    236                 return substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) );
     272                self::remember_rewrite_cache(
     273                    $cache_key,
     274                    substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) )
     275                );
     276                return self::$rewrite_cache[ $cache_key ];
    237277            }
    238278        }
     
    242282            // rewrite relative URL (e.g. /wp-content/uploads/example.jpg)
    243283            if ( strpos( $file_url, '//' ) !== 0 && strpos( $file_url, '/' ) === 0 ) {
    244                 return '//' . $cdn_hostname . $file_url;
     284                self::remember_rewrite_cache( $cache_key, '//' . $cdn_hostname . $file_url );
     285                return self::$rewrite_cache[ $cache_key ];
    245286            }
    246287
    247288            // rewrite escaped relative URL (e.g. \/wp-content\/uploads\/example.jpg)
    248289            if ( strpos( $file_url, '\/\/' ) !== 0 && strpos( $file_url, '\/' ) === 0 ) {
    249                 return '\/\/' . $cdn_hostname . $file_url;
    250             }
    251         }
    252 
    253         return $file_url;
     290                self::remember_rewrite_cache( $cache_key, '\/\/' . $cdn_hostname . $file_url );
     291                return self::$rewrite_cache[ $cache_key ];
     292            }
     293        }
     294
     295        self::remember_rewrite_cache( $cache_key, $file_url );
     296
     297        return self::$rewrite_cache[ $cache_key ];
    254298    }
    255299
     
    274318        $contents = apply_filters( 'cdntr_contents_before_rewrite', $contents );
    275319
    276         $included_file_extensions_regex = quotemeta( implode( '|', explode( PHP_EOL, self::$settings['included_file_extensions'] ) ) );
     320        $included_file_extensions_regex = self::$included_file_extensions_regex;
     321        if ( empty( $included_file_extensions_regex ) ) {
     322            return $contents;
     323        }
    277324
    278325        $urls_regex = '#(?:(?:[\"\'\s=>,;]|url\()\K|^)[^\"\'\s(=>,;]+(' . $included_file_extensions_regex . ')(\?[^\/?\\\"\'\s)>,]+)?(?:(?=\/?[?\\\"\'\s)>,&])|$)#i';
     
    282329        return $rewritten_contents;
    283330    }
     331
     332    /**
     333     * Keep rewrite cache bounded to prevent excessive memory usage.
     334     *
     335     * @since   2.0.9
     336     * @change  2.0.9
     337     *
     338     * @param   string  $key    cache key
     339     * @param   string  $value  cache value
     340     */
     341    private static function remember_rewrite_cache( $key, $value ) {
     342
     343        if ( count( self::$rewrite_cache ) >= self::$rewrite_cache_limit ) {
     344            array_shift( self::$rewrite_cache );
     345        }
     346
     347        self::$rewrite_cache[ $key ] = $value;
     348    }
    284349}
  • cdntr/tags/1.2.5/readme.txt

    r3131495 r3467539  
    33Tags: cdn, content delivery network, content distribution network
    44Tested up to: 6.6.1
    5 Stable tag: 1.2.2
     5Stable tag: 1.2.5
    66Requires at least: 5.7
    77Requires PHP: 7.2
     
    3434*Endpoints:
    3535*Purge All Cache: https://cdn.com.tr/api/purgeAll
     36*Purge Selected Paths: https://cdn.com.tr/api/purge
    3637*Check Account Status: https://cdn.com.tr/api/checkAccount
    3738*Privacy and Terms
     
    4344* [CDNTR](https://cdn.com.tr)
    4445
    45 == Screenshots ==
     46== Changelog ==
    4647
    47 1. screenshot-1.png
     48= 1.2.5 =
     49* Added async purge mode support for runtime-triggered invalidations
     50* Added coalesced purge queue with short window to reduce duplicate API calls
     51* Added lightweight purge telemetry counters for operational diagnostics
     52* Added purge mode/coalescing filters (`cdntr_purge_mode`, `cdntr_purge_coalesce_window`)
    4853
    49 == Changelog ==
     54= 1.2.4 =
     55* Added built-in term change purge hooks (create/edit/delete) with focused taxonomy support
     56* Added built-in sitewide purge hooks for customizer and theme switch events
     57* Added short purge burst throttling for noisy event streams to reduce duplicate API calls
     58* Reduced need for site-specific CDNTR auto-purge mu-plugins by moving common logic into core plugin
     59
     60= 1.2.3 =
     61* Security hardening for path-based purge normalization (control-char stripping, traversal-like path guard, length bound)
     62* Safer taxonomy purge handling by skipping invalid term links
     63* Improved delete-flow purge coverage by collecting related post/category/shop paths before deletion
    5064
    5165= 1.2.2 =
     
    5468= 1.2.1 =
    5569* Minor security updates
     70
     71
  • cdntr/trunk/cdntr.php

    r3131497 r3467539  
    66Author URI: https://cdn.com.tr
    77License: GPLv2 or later
    8 Version: 1.2.2
     8Version: 1.2.5
    99*/
    1010
     
    1616
    1717// constants
    18 define( 'CDNTR_VERSION', '1.0' );
     18define( 'CDNTR_VERSION', '1.2.5' );
    1919define( 'CDNTR_MIN_PHP', '5.6' );
    2020define( 'CDNTR_MIN_WP', '5.1' );
  • cdntr/trunk/inc/cdntr.class.php

    r3130906 r3467539  
    4141        add_action( 'init', array( __CLASS__, 'register_textdomain' ) );
    4242        add_action( 'init', array( __CLASS__, 'check_meta_valid' ) );
     43        add_action( 'save_post', array( __CLASS__, 'maybe_purge_post_paths' ), 20, 3 );
     44        add_action( 'deleted_post', array( __CLASS__, 'purge_deleted_post_paths' ), 20, 1 );
     45        add_action( 'created_term', array( __CLASS__, 'maybe_purge_term_paths' ), 20, 3 );
     46        add_action( 'edited_term', array( __CLASS__, 'maybe_purge_term_paths' ), 20, 3 );
     47        add_action( 'delete_term', array( __CLASS__, 'maybe_purge_deleted_term_paths' ), 20, 5 );
     48        add_action( 'customize_save_after', array( __CLASS__, 'maybe_purge_sitewide' ), 20, 1 );
     49        add_action( 'switch_theme', array( __CLASS__, 'maybe_purge_sitewide' ), 20, 3 );
     50        add_action( 'cdntr_run_coalesced_purge', array( __CLASS__, 'run_coalesced_purge' ) );
    4351
    4452        // multisite hook
     
    356364            'cdntr_is_purge_all_button'  => '',
    357365            'cdntr_api_password'         => '',
     366            'cdntr_purge_mode'           => 'async',
     367            'cdntr_purge_coalesce_window'=> 3,
     368            'cdntr_purge_telemetry'      => '1',
    358369        );
    359370        // cdntr_account_id cdntr_api_user cdntr_api_password
     
    731742        );
    732743
     744        $telemetry_status = 'error';
     745
    733746        // check if API call failed
    734747        if ( is_wp_error( $response ) ) {
     
    743756
    744757            if ( $response_status_code === 200 ) {
     758                $telemetry_status = 'ok';
    745759                $response = array(
    746760                    'wrapper' => '<div class="notice notice-success is-dismissible"><p><strong>%s</strong></p></div>',
     
    779793        }
    780794
     795        self::record_purge_telemetry( 'all_sync', $telemetry_status );
     796
    781797        return $response;
     798    }
     799
     800    /**
     801     * purge CDN cache asynchronously (fire-and-forget)
     802     *
     803     * @since   2.1.0
     804     * @change  2.1.0
     805     *
     806     * @return  array
     807     */
     808    public static function purge_cdn_cache_async() {
     809
     810        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     811
     812        wp_remote_post(
     813            'https://cdn.com.tr/api/purgeAll',
     814            array(
     815                'timeout'     => 1,
     816                'httpversion' => '1.1',
     817                'blocking'    => false,
     818                'headers'     => array(
     819                    'Authorization' => 'Basic ' . $auth,
     820                    'Content-Type'  => 'application/json',
     821                ),
     822            )
     823        );
     824
     825        self::record_purge_telemetry( 'all_async', 'queued' );
     826
     827        return array(
     828            'ok'      => true,
     829            'message' => 'Purge queued.',
     830        );
     831    }
     832
     833    /**
     834     * Purge specific URL paths from CDN.
     835     *
     836     * @since   2.0.9
     837     * @change  2.0.9
     838     *
     839     * @param   array $paths  URL paths (e.g. /product/foo/)
     840     * @return  array
     841     */
     842    public static function purge_cdn_paths( $paths ) {
     843
     844        $paths = self::normalize_purge_paths( $paths );
     845
     846        if ( empty( $paths ) ) {
     847            self::record_purge_telemetry( 'paths_sync', 'skipped', array( 'paths' => 0 ) );
     848            return array(
     849                'ok'      => false,
     850                'message' => 'No valid purge paths.',
     851            );
     852        }
     853
     854        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     855        $response = wp_remote_post(
     856            'https://cdn.com.tr/api/purge',
     857            array(
     858                'timeout'     => 15,
     859                'httpversion' => '1.1',
     860                'headers'     => array(
     861                    'Authorization' => 'Basic ' . $auth,
     862                    'Content-Type'  => 'application/json',
     863                ),
     864                'body'        => wp_json_encode( array( 'paths' => $paths ) ),
     865            )
     866        );
     867
     868        if ( is_wp_error( $response ) ) {
     869            self::record_purge_telemetry( 'paths_sync', 'error', array( 'paths' => count( $paths ) ) );
     870            return array(
     871                'ok'      => false,
     872                'message' => $response->get_error_message(),
     873            );
     874        }
     875
     876        $status = (int) wp_remote_retrieve_response_code( $response );
     877        self::record_purge_telemetry(
     878            'paths_sync',
     879            ( $status >= 200 && $status < 300 ) ? 'ok' : 'error',
     880            array(
     881                'status' => $status,
     882                'paths'  => count( $paths ),
     883            )
     884        );
     885        return array(
     886            'ok'      => ( $status >= 200 && $status < 300 ),
     887            'status'  => $status,
     888            'message' => (string) wp_remote_retrieve_body( $response ),
     889        );
     890    }
     891
     892    /**
     893     * Purge specific URL paths asynchronously (fire-and-forget).
     894     *
     895     * @since   2.1.0
     896     * @change  2.1.0
     897     *
     898     * @param   array $paths
     899     * @return  array
     900     */
     901    public static function purge_cdn_paths_async( $paths ) {
     902
     903        $paths = self::normalize_purge_paths( $paths );
     904        if ( empty( $paths ) ) {
     905            self::record_purge_telemetry( 'paths_async', 'skipped', array( 'paths' => 0 ) );
     906            return array(
     907                'ok'      => false,
     908                'message' => 'No valid purge paths.',
     909            );
     910        }
     911
     912        $auth = base64_encode( CDNTR_Engine::$settings['cdntr_api_user'] . ':' . CDNTR_Engine::$settings['cdntr_api_password'] );
     913        wp_remote_post(
     914            'https://cdn.com.tr/api/purge',
     915            array(
     916                'timeout'     => 1,
     917                'httpversion' => '1.1',
     918                'blocking'    => false,
     919                'headers'     => array(
     920                    'Authorization' => 'Basic ' . $auth,
     921                    'Content-Type'  => 'application/json',
     922                ),
     923                'body'        => wp_json_encode( array( 'paths' => $paths ) ),
     924            )
     925        );
     926
     927        self::record_purge_telemetry( 'paths_async', 'queued', array( 'paths' => count( $paths ) ) );
     928
     929        return array(
     930            'ok'      => true,
     931            'message' => 'Purge queued.',
     932        );
     933    }
     934
     935    /**
     936     * Purge only affected URLs when content changes.
     937     *
     938     * @since   2.0.9
     939     * @change  2.0.9
     940     */
     941    public static function maybe_purge_post_paths( $post_id, $post, $update ) {
     942
     943        if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
     944            return;
     945        }
     946
     947        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     948            return;
     949        }
     950
     951        if ( ! $post instanceof \WP_Post ) {
     952            $post = get_post( $post_id );
     953            if ( ! $post ) {
     954                return;
     955            }
     956        }
     957
     958        if ( $post->post_status !== 'publish' && $post->post_status !== 'private' ) {
     959            return;
     960        }
     961
     962        $paths = array(
     963            '/',
     964            wp_parse_url( get_permalink( $post_id ), PHP_URL_PATH ),
     965        );
     966
     967        if ( $post->post_type === 'product' ) {
     968            $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     969            if ( $shop_page_id > 0 ) {
     970                $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     971            }
     972
     973            $terms = get_the_terms( $post_id, 'product_cat' );
     974            if ( is_array( $terms ) ) {
     975                foreach ( $terms as $term ) {
     976                    $term_link = get_term_link( $term );
     977                    if ( ! is_wp_error( $term_link ) ) {
     978                        $paths[] = wp_parse_url( $term_link, PHP_URL_PATH );
     979                    }
     980                }
     981            }
     982        }
     983
     984        self::enqueue_purge_paths( $paths );
     985    }
     986
     987    /**
     988     * Purge homepage after post deletion to avoid stale listings.
     989     *
     990     * @since   2.0.9
     991     * @change  2.0.9
     992     */
     993    public static function purge_deleted_post_paths( $post_id ) {
     994
     995        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     996            return;
     997        }
     998
     999        self::enqueue_purge_paths( array( '/' ) );
     1000    }
     1001
     1002    /**
     1003     * Purge related paths when selected taxonomies are changed.
     1004     *
     1005     * @since   2.1.0
     1006     * @change  2.1.0
     1007     */
     1008    public static function maybe_purge_term_paths( $term_id, $tt_id, $taxonomy ) {
     1009
     1010        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1011            return;
     1012        }
     1013
     1014        $allowed_taxonomies = (array) apply_filters( 'cdntr_purge_taxonomies', array( 'product_cat' ) );
     1015        if ( empty( $taxonomy ) || ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
     1016            return;
     1017        }
     1018
     1019        if ( self::is_purge_throttled( 'term_' . $taxonomy, 10 ) ) {
     1020            return;
     1021        }
     1022
     1023        $paths = array( '/' );
     1024        $term = get_term( (int) $term_id, $taxonomy );
     1025        if ( $term && ! is_wp_error( $term ) ) {
     1026            $term_link = get_term_link( $term );
     1027            if ( ! is_wp_error( $term_link ) ) {
     1028                $paths[] = wp_parse_url( $term_link, PHP_URL_PATH );
     1029            }
     1030        }
     1031
     1032        $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     1033        if ( $shop_page_id > 0 ) {
     1034            $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     1035        }
     1036
     1037        self::enqueue_purge_paths( $paths );
     1038    }
     1039
     1040    /**
     1041     * Purge related paths when selected taxonomies are deleted.
     1042     *
     1043     * @since   2.1.0
     1044     * @change  2.1.0
     1045     */
     1046    public static function maybe_purge_deleted_term_paths( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) {
     1047
     1048        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1049            return;
     1050        }
     1051
     1052        $allowed_taxonomies = (array) apply_filters( 'cdntr_purge_taxonomies', array( 'product_cat' ) );
     1053        if ( empty( $taxonomy ) || ! in_array( $taxonomy, $allowed_taxonomies, true ) ) {
     1054            return;
     1055        }
     1056
     1057        if ( self::is_purge_throttled( 'term_delete_' . $taxonomy, 10 ) ) {
     1058            return;
     1059        }
     1060
     1061        $paths = array( '/' );
     1062        $shop_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'shop' ) : 0;
     1063        if ( $shop_page_id > 0 ) {
     1064            $paths[] = wp_parse_url( get_permalink( $shop_page_id ), PHP_URL_PATH );
     1065        }
     1066
     1067        self::enqueue_purge_paths( $paths );
     1068    }
     1069
     1070    /**
     1071     * Purge whole site for theme/customizer style changes.
     1072     *
     1073     * @since   2.1.0
     1074     * @change  2.1.0
     1075     */
     1076    public static function maybe_purge_sitewide() {
     1077
     1078        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1079            return;
     1080        }
     1081
     1082        if ( self::is_purge_throttled( 'sitewide', 20 ) ) {
     1083            return;
     1084        }
     1085
     1086        self::enqueue_purge_all();
     1087    }
     1088
     1089    /**
     1090     * Normalize purge paths to a clean unique path list.
     1091     *
     1092     * @since   2.0.9
     1093     * @change  2.0.9
     1094     *
     1095     * @param   array $paths
     1096     * @return  array
     1097     */
     1098    private static function normalize_purge_paths( $paths ) {
     1099
     1100        if ( ! is_array( $paths ) ) {
     1101            $paths = array();
     1102        }
     1103
     1104        $normalized = array();
     1105        foreach ( $paths as $path ) {
     1106            if ( ! is_string( $path ) || $path === '' ) {
     1107                continue;
     1108            }
     1109
     1110            // Strip control chars and trim to avoid malformed payloads.
     1111            $path = trim( preg_replace( '/[\x00-\x1F\x7F]/', '', $path ) );
     1112            if ( $path === '' || strlen( $path ) > 2048 ) {
     1113                continue;
     1114            }
     1115
     1116            $clean = wp_parse_url( $path, PHP_URL_PATH );
     1117            if ( ! is_string( $clean ) || $clean === '' ) {
     1118                continue;
     1119            }
     1120
     1121            // Block traversal-like paths from reaching purge API.
     1122            if ( strpos( $clean, '..' ) !== false ) {
     1123                continue;
     1124            }
     1125
     1126            if ( strpos( $clean, '/' ) !== 0 ) {
     1127                $clean = '/' . $clean;
     1128            }
     1129
     1130            $normalized[] = $clean;
     1131        }
     1132
     1133        return array_values( array_unique( $normalized ) );
     1134    }
     1135
     1136    /**
     1137     * Basic transient lock to avoid duplicate purge bursts.
     1138     *
     1139     * @since   2.1.0
     1140     * @change  2.1.0
     1141     */
     1142    private static function is_purge_throttled( $scope, $ttl = 10 ) {
     1143
     1144        $scope = sanitize_key( (string) $scope );
     1145        $ttl = absint( $ttl );
     1146        if ( $scope === '' || $ttl < 1 ) {
     1147            return false;
     1148        }
     1149
     1150        $lock_key = 'cdntr_purge_lock_' . $scope;
     1151        if ( get_transient( $lock_key ) ) {
     1152            return true;
     1153        }
     1154
     1155        set_transient( $lock_key, 1, $ttl );
     1156        return false;
     1157    }
     1158
     1159    /**
     1160     * Queue targeted purge paths in a short coalescing window.
     1161     *
     1162     * @since   2.1.0
     1163     * @change  2.1.0
     1164     *
     1165     * @param   array $paths
     1166     */
     1167    private static function enqueue_purge_paths( $paths ) {
     1168
     1169        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1170            return;
     1171        }
     1172
     1173        $paths = self::normalize_purge_paths( $paths );
     1174        if ( empty( $paths ) ) {
     1175            return;
     1176        }
     1177
     1178        $queue_key = self::get_purge_queue_transient_name( 'paths' );
     1179        $queued_paths = get_transient( $queue_key );
     1180        if ( ! is_array( $queued_paths ) ) {
     1181            $queued_paths = array();
     1182        }
     1183
     1184        $queued_paths = array_values( array_unique( array_merge( $queued_paths, $paths ) ) );
     1185        set_transient( $queue_key, $queued_paths, 300 );
     1186
     1187        self::record_purge_telemetry( 'paths_queue', 'queued', array( 'paths' => count( $queued_paths ) ) );
     1188
     1189        if ( ! wp_next_scheduled( 'cdntr_run_coalesced_purge' ) ) {
     1190            wp_schedule_single_event( time() + self::get_purge_coalesce_window(), 'cdntr_run_coalesced_purge' );
     1191        }
     1192    }
     1193
     1194    /**
     1195     * Queue purge-all requests in a short coalescing window.
     1196     *
     1197     * @since   2.1.0
     1198     * @change  2.1.0
     1199     */
     1200    private static function enqueue_purge_all() {
     1201
     1202        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1203            return;
     1204        }
     1205
     1206        set_transient( self::get_purge_queue_transient_name( 'all' ), 1, 300 );
     1207        self::record_purge_telemetry( 'all_queue', 'queued' );
     1208
     1209        if ( ! wp_next_scheduled( 'cdntr_run_coalesced_purge' ) ) {
     1210            wp_schedule_single_event( time() + self::get_purge_coalesce_window(), 'cdntr_run_coalesced_purge' );
     1211        }
     1212    }
     1213
     1214    /**
     1215     * Execute queued purge operations after coalescing window.
     1216     *
     1217     * @since   2.1.0
     1218     * @change  2.1.0
     1219     */
     1220    public static function run_coalesced_purge() {
     1221
     1222        if ( empty( CDNTR_Engine::$settings['cdntr_api_user'] ) || empty( CDNTR_Engine::$settings['cdntr_api_password'] ) ) {
     1223            return;
     1224        }
     1225
     1226        $purge_all_key = self::get_purge_queue_transient_name( 'all' );
     1227        $paths_key = self::get_purge_queue_transient_name( 'paths' );
     1228
     1229        $purge_all = (bool) get_transient( $purge_all_key );
     1230        $paths = get_transient( $paths_key );
     1231        $paths = is_array( $paths ) ? $paths : array();
     1232
     1233        delete_transient( $purge_all_key );
     1234        delete_transient( $paths_key );
     1235
     1236        if ( $purge_all ) {
     1237            if ( self::should_use_async_purge() ) {
     1238                self::purge_cdn_cache_async();
     1239            } else {
     1240                self::purge_cdn_cache();
     1241            }
     1242            return;
     1243        }
     1244
     1245        if ( empty( $paths ) ) {
     1246            return;
     1247        }
     1248
     1249        if ( self::should_use_async_purge() ) {
     1250            self::purge_cdn_paths_async( $paths );
     1251        } else {
     1252            self::purge_cdn_paths( $paths );
     1253        }
     1254    }
     1255
     1256    /**
     1257     * Resolve purge mode (sync|async) from settings/filters.
     1258     *
     1259     * @since   2.1.0
     1260     * @change  2.1.0
     1261     *
     1262     * @return string
     1263     */
     1264    private static function get_purge_mode() {
     1265
     1266        $mode = 'async';
     1267        if ( ! empty( CDNTR_Engine::$settings['cdntr_purge_mode'] ) ) {
     1268            $mode = strtolower( (string) CDNTR_Engine::$settings['cdntr_purge_mode'] );
     1269        }
     1270
     1271        $mode = (string) apply_filters( 'cdntr_purge_mode', $mode );
     1272        if ( $mode !== 'sync' && $mode !== 'async' ) {
     1273            $mode = 'async';
     1274        }
     1275
     1276        return $mode;
     1277    }
     1278
     1279    /**
     1280     * Determine if async purge mode is active.
     1281     *
     1282     * @since   2.1.0
     1283     * @change  2.1.0
     1284     *
     1285     * @return bool
     1286     */
     1287    private static function should_use_async_purge() {
     1288
     1289        return self::get_purge_mode() === 'async';
     1290    }
     1291
     1292    /**
     1293     * Coalescing window in seconds.
     1294     *
     1295     * @since   2.1.0
     1296     * @change  2.1.0
     1297     *
     1298     * @return int
     1299     */
     1300    private static function get_purge_coalesce_window() {
     1301
     1302        $window = 3;
     1303        if ( isset( CDNTR_Engine::$settings['cdntr_purge_coalesce_window'] ) ) {
     1304            $window = absint( CDNTR_Engine::$settings['cdntr_purge_coalesce_window'] );
     1305        }
     1306
     1307        $window = (int) apply_filters( 'cdntr_purge_coalesce_window', $window );
     1308        return max( 1, $window );
     1309    }
     1310
     1311    /**
     1312     * Purge queue transient name scoped by blog.
     1313     *
     1314     * @since   2.1.0
     1315     * @change  2.1.0
     1316     *
     1317     * @param   string $scope
     1318     * @return  string
     1319     */
     1320    private static function get_purge_queue_transient_name( $scope ) {
     1321
     1322        return 'cdntr_purge_queue_' . sanitize_key( $scope ) . '_' . get_current_blog_id();
     1323    }
     1324
     1325    /**
     1326     * Persist lightweight purge telemetry for diagnostics.
     1327     *
     1328     * @since   2.1.0
     1329     * @change  2.1.0
     1330     *
     1331     * @param   string $type
     1332     * @param   string $status
     1333     * @param   array  $meta
     1334     */
     1335    private static function record_purge_telemetry( $type, $status, $meta = array() ) {
     1336
     1337        if ( empty( CDNTR_Engine::$settings['cdntr_purge_telemetry'] ) ) {
     1338            return;
     1339        }
     1340
     1341        $key = sanitize_key( (string) $type ) . ':' . sanitize_key( (string) $status );
     1342        if ( $key === ':' ) {
     1343            return;
     1344        }
     1345
     1346        $telemetry = get_option( 'cdntr_purge_telemetry', array() );
     1347        if ( ! is_array( $telemetry ) ) {
     1348            $telemetry = array();
     1349        }
     1350
     1351        if ( ! isset( $telemetry['counters'] ) || ! is_array( $telemetry['counters'] ) ) {
     1352            $telemetry['counters'] = array();
     1353        }
     1354
     1355        if ( ! isset( $telemetry['counters'][ $key ] ) ) {
     1356            $telemetry['counters'][ $key ] = 0;
     1357        }
     1358
     1359        $telemetry['counters'][ $key ]++;
     1360        $telemetry['last_event'] = array(
     1361            'type'      => (string) $type,
     1362            'status'    => (string) $status,
     1363            'meta'      => is_array( $meta ) ? $meta : array(),
     1364            'timestamp' => time(),
     1365        );
     1366
     1367        update_option( 'cdntr_purge_telemetry', $telemetry, false );
    7821368    }
    7831369
     
    8631449    public static function add_custom_meta_tags() {
    8641450        ?>
    865              <meta name="cdn-site-verification" content="<?php echo esc_attr( CDNTR_Engine::$settings['cdntr_api_user'] ); ?>">';
     1451             <meta name="cdn-site-verification" content="<?php echo esc_attr( CDNTR_Engine::$settings['cdntr_api_user'] ); ?>">
    8661452        <?php
    8671453    }
     
    10571643            'cdntr_is_purge_all_button'=> (string) sanitize_text_field( $settings['cdntr_is_purge_all_button'] ),
    10581644            'cdn_hostname_arr'         => (string) sanitize_text_field( $settings['cdn_hostname_arr'] ) ,
     1645            'cdntr_purge_mode'         => ( ( isset( $settings['cdntr_purge_mode'] ) && strtolower( (string) $settings['cdntr_purge_mode'] ) === 'sync' ) ? 'sync' : 'async' ),
     1646            'cdntr_purge_coalesce_window' => max( 1, absint( isset( $settings['cdntr_purge_coalesce_window'] ) ? $settings['cdntr_purge_coalesce_window'] : 3 ) ),
     1647            'cdntr_purge_telemetry'    => ( ! empty( $settings['cdntr_purge_telemetry'] ) ? '1' : '0' ),
    10591648        );
    10601649
  • cdntr/trunk/inc/cdntr_engine.class.php

    r3130906 r3467539  
    3535
    3636    public static $settings;
     37    private static $excluded_strings = array();
     38    private static $included_file_extensions_regex = '';
     39    private static $rewrite_cache = array();
     40    private static $rewrite_cache_limit = 1024;
    3741
    3842
     
    4953        self::$settings = CDNTR::get_settings();
    5054
    51         if ( ! empty( self::$settings ) ) {
     55        if ( ! empty( self::$settings ) && ! self::bypass_rewrite() ) {
     56            self::prepare_runtime_settings();
    5257            self::start_buffering();
     58        }
     59    }
     60
     61    /**
     62     * Prepare normalized runtime settings once per request.
     63     *
     64     * @since   2.0.9
     65     * @change  2.0.9
     66     */
     67    private static function prepare_runtime_settings() {
     68
     69        self::$excluded_strings = array();
     70        self::$included_file_extensions_regex = '';
     71        self::$rewrite_cache = array();
     72
     73        if ( ! empty( self::$settings['excluded_strings'] ) ) {
     74            $excluded_strings = explode( PHP_EOL, self::$settings['excluded_strings'] );
     75            $excluded_strings = array_map( 'trim', $excluded_strings );
     76            self::$excluded_strings = array_filter( $excluded_strings, function( $value ) {
     77                return $value !== '';
     78            } );
     79        }
     80
     81        if ( ! empty( self::$settings['included_file_extensions'] ) ) {
     82            self::$included_file_extensions_regex = quotemeta(
     83                implode( '|', explode( PHP_EOL, self::$settings['included_file_extensions'] ) )
     84            );
    5385        }
    5486    }
     
    146178
    147179        // if string excluded (case sensitive)
    148         if ( ! empty( self::$settings['excluded_strings'] ) ) {
    149             $excluded_strings = explode( PHP_EOL, self::$settings['excluded_strings'] );
    150 
    151             foreach ( $excluded_strings as $excluded_string ) {
     180        if ( ! empty( self::$excluded_strings ) ) {
     181            foreach ( self::$excluded_strings as $excluded_string ) {
    152182                if ( strpos( $file_url, $excluded_string ) !== false ) {
    153183                    return true;
     
    225255        $site_hostnames = (array) apply_filters( 'cdntr_site_hostnames', array( $site_hostname ) );
    226256        $cdn_hostname   = self::$settings['cdn_hostname'];
     257        $cache_key      = $site_hostname . '|' . $file_url;
     258
     259        if ( isset( self::$rewrite_cache[ $cache_key ] ) ) {
     260            return self::$rewrite_cache[ $cache_key ];
     261        }
    227262
    228263        // if excluded or already using CDN hostname
    229264        if ( self::is_excluded( $file_url ) || stripos( $file_url, $cdn_hostname ) !== false ) {
    230             return $file_url;
     265            self::remember_rewrite_cache( $cache_key, $file_url );
     266            return self::$rewrite_cache[ $cache_key ];
    231267        }
    232268
     
    234270        foreach ( $site_hostnames as $site_hostname ) {
    235271            if ( stripos( $file_url, '//' . $site_hostname ) !== false || stripos( $file_url, '\/\/' . $site_hostname ) !== false ) {
    236                 return substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) );
     272                self::remember_rewrite_cache(
     273                    $cache_key,
     274                    substr_replace( $file_url, $cdn_hostname, stripos( $file_url, $site_hostname ), strlen( $site_hostname ) )
     275                );
     276                return self::$rewrite_cache[ $cache_key ];
    237277            }
    238278        }
     
    242282            // rewrite relative URL (e.g. /wp-content/uploads/example.jpg)
    243283            if ( strpos( $file_url, '//' ) !== 0 && strpos( $file_url, '/' ) === 0 ) {
    244                 return '//' . $cdn_hostname . $file_url;
     284                self::remember_rewrite_cache( $cache_key, '//' . $cdn_hostname . $file_url );
     285                return self::$rewrite_cache[ $cache_key ];
    245286            }
    246287
    247288            // rewrite escaped relative URL (e.g. \/wp-content\/uploads\/example.jpg)
    248289            if ( strpos( $file_url, '\/\/' ) !== 0 && strpos( $file_url, '\/' ) === 0 ) {
    249                 return '\/\/' . $cdn_hostname . $file_url;
    250             }
    251         }
    252 
    253         return $file_url;
     290                self::remember_rewrite_cache( $cache_key, '\/\/' . $cdn_hostname . $file_url );
     291                return self::$rewrite_cache[ $cache_key ];
     292            }
     293        }
     294
     295        self::remember_rewrite_cache( $cache_key, $file_url );
     296
     297        return self::$rewrite_cache[ $cache_key ];
    254298    }
    255299
     
    274318        $contents = apply_filters( 'cdntr_contents_before_rewrite', $contents );
    275319
    276         $included_file_extensions_regex = quotemeta( implode( '|', explode( PHP_EOL, self::$settings['included_file_extensions'] ) ) );
     320        $included_file_extensions_regex = self::$included_file_extensions_regex;
     321        if ( empty( $included_file_extensions_regex ) ) {
     322            return $contents;
     323        }
    277324
    278325        $urls_regex = '#(?:(?:[\"\'\s=>,;]|url\()\K|^)[^\"\'\s(=>,;]+(' . $included_file_extensions_regex . ')(\?[^\/?\\\"\'\s)>,]+)?(?:(?=\/?[?\\\"\'\s)>,&])|$)#i';
     
    282329        return $rewritten_contents;
    283330    }
     331
     332    /**
     333     * Keep rewrite cache bounded to prevent excessive memory usage.
     334     *
     335     * @since   2.0.9
     336     * @change  2.0.9
     337     *
     338     * @param   string  $key    cache key
     339     * @param   string  $value  cache value
     340     */
     341    private static function remember_rewrite_cache( $key, $value ) {
     342
     343        if ( count( self::$rewrite_cache ) >= self::$rewrite_cache_limit ) {
     344            array_shift( self::$rewrite_cache );
     345        }
     346
     347        self::$rewrite_cache[ $key ] = $value;
     348    }
    284349}
  • cdntr/trunk/readme.txt

    r3131495 r3467539  
    33Tags: cdn, content delivery network, content distribution network
    44Tested up to: 6.6.1
    5 Stable tag: 1.2.2
     5Stable tag: 1.2.5
    66Requires at least: 5.7
    77Requires PHP: 7.2
     
    3434*Endpoints:
    3535*Purge All Cache: https://cdn.com.tr/api/purgeAll
     36*Purge Selected Paths: https://cdn.com.tr/api/purge
    3637*Check Account Status: https://cdn.com.tr/api/checkAccount
    3738*Privacy and Terms
     
    4344* [CDNTR](https://cdn.com.tr)
    4445
    45 == Screenshots ==
     46== Changelog ==
    4647
    47 1. screenshot-1.png
     48= 1.2.5 =
     49* Added async purge mode support for runtime-triggered invalidations
     50* Added coalesced purge queue with short window to reduce duplicate API calls
     51* Added lightweight purge telemetry counters for operational diagnostics
     52* Added purge mode/coalescing filters (`cdntr_purge_mode`, `cdntr_purge_coalesce_window`)
    4853
    49 == Changelog ==
     54= 1.2.4 =
     55* Added built-in term change purge hooks (create/edit/delete) with focused taxonomy support
     56* Added built-in sitewide purge hooks for customizer and theme switch events
     57* Added short purge burst throttling for noisy event streams to reduce duplicate API calls
     58* Reduced need for site-specific CDNTR auto-purge mu-plugins by moving common logic into core plugin
     59
     60= 1.2.3 =
     61* Security hardening for path-based purge normalization (control-char stripping, traversal-like path guard, length bound)
     62* Safer taxonomy purge handling by skipping invalid term links
     63* Improved delete-flow purge coverage by collecting related post/category/shop paths before deletion
    5064
    5165= 1.2.2 =
     
    5468= 1.2.1 =
    5569* Minor security updates
     70
     71
Note: See TracChangeset for help on using the changeset viewer.