--- a/jquery-archive-list-widget/assets/public/main.asset.php
+++ b/jquery-archive-list-widget/assets/public/main.asset.php
@@ -1 +0,0 @@
-<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => '98829dd444d1a2599b83');
--- a/jquery-archive-list-widget/build/index.asset.php
+++ b/jquery-archive-list-widget/build/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => 'b60bb6b41da71dd2fe96');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n'), 'version' => '44bdc6c75951c365267c');
--- a/jquery-archive-list-widget/build/view.asset.php
+++ b/jquery-archive-list-widget/build/view.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '4f7c367cbae28c505951');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wp-api-fetch', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => '7871f0af1a79545d1614');
--- a/jquery-archive-list-widget/classes/class-jq-archive-list-datasource.php
+++ b/jquery-archive-list-widget/classes/class-jq-archive-list-datasource.php
@@ -56,7 +56,7 @@
protected function build_sql_join(): string {
global $wpdb;
$join = '';
- if ($this->has_filtering_categories() || $this->only_show_cur_category()) {
+ if ($this->has_included_categories() || $this->only_show_cur_category()) {
$join = sprintf(' LEFT JOIN %s ON(%s.ID = %s.object_id)',
$wpdb->term_relationships, $wpdb->posts, $wpdb->term_relationships
);
@@ -77,6 +77,24 @@
}
/**
+ * Check if user selected categories for inclusion.
+ *
+ * @return bool
+ */
+ private function has_included_categories(): bool {
+ return !empty($this->config['included']);
+ }
+
+ /**
+ * Check if user selected categories for exclusion.
+ *
+ * @return bool
+ */
+ private function has_excluded_categories(): bool {
+ return !empty($this->config['excluded']);
+ }
+
+ /**
* Returns if the option to show only current categories was selected and current page is a category page.
*
* @return bool
@@ -110,14 +128,42 @@
$where_parts[] = 'MONTH(post_date) = %d';
$prepare_args[] = $month;
}
- $ids_key = !empty($this->config['included']) ? 'included' : (!empty($this->config['excluded']) ? 'excluded' : null);
- if ($ids_key) {
- $ids = is_array($this->config[$ids_key]) ? $this->config[$ids_key] : explode(',', $this->config[$ids_key]);
- $ids = array_map('intval', $ids);
- $placeholders = implode(', ', array_fill(0, count($ids), '%d'));
- $operator = $ids_key === 'included' ? 'IN' : 'NOT IN';
- $where_parts[] = sprintf('%s.term_id %s (%s)', $wpdb->term_taxonomy, $operator, $placeholders);
- $prepare_args = array_merge($prepare_args, $ids);
+ if ($this->has_included_categories()) {
+ $ids = is_array($this->config['included']) ? $this->config['included'] : explode(',', $this->config['included']);
+ $ids = array_values(array_filter(array_map('intval', $ids), static function ($id) {
+ return $id > 0;
+ }));
+
+ if (!empty($ids)) {
+ $placeholders = implode(', ', array_fill(0, count($ids), '%d'));
+ $where_parts[] = sprintf('%s.term_id IN (%s)', $wpdb->term_taxonomy, $placeholders);
+ $prepare_args = array_merge($prepare_args, $ids);
+ }
+ }
+
+ if ($this->has_excluded_categories()) {
+ $ids = is_array($this->config['excluded']) ? $this->config['excluded'] : explode(',', $this->config['excluded']);
+ $ids = array_values(array_filter(array_map('intval', $ids), static function ($id) {
+ return $id > 0;
+ }));
+
+ if (!empty($ids)) {
+ $placeholders = implode(', ', array_fill(0, count($ids), '%d'));
+ $where_parts[] = sprintf(
+ '%s.ID NOT IN (
+ SELECT tr.object_id
+ FROM %s tr
+ INNER JOIN %s tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
+ WHERE tt.taxonomy = %%s AND tt.term_id IN (%s)
+ )',
+ $wpdb->posts,
+ $wpdb->term_relationships,
+ $wpdb->term_taxonomy,
+ $placeholders
+ );
+ $prepare_args[] = 'category';
+ $prepare_args = array_merge($prepare_args, $ids);
+ }
}
if ($this->only_show_cur_category()) {
$query_cat = get_query_var('cat');
@@ -130,7 +176,7 @@
$prepare_args = array_merge($prepare_args, $categories_ids);
}
}
- if ($this->has_filtering_categories() || $this->only_show_cur_category()) {
+ if ($this->has_included_categories() || $this->only_show_cur_category()) {
$where_parts[] = $wpdb->term_taxonomy . '.taxonomy=%s';
$prepare_args[] = 'category';
}
--- a/jquery-archive-list-widget/classes/class-jq-archive-list-widget.php
+++ b/jquery-archive-list-widget/classes/class-jq-archive-list-widget.php
@@ -37,7 +37,7 @@
public function __construct() {
parent::__construct( 'jal_widget', 'JS Archive List Widget (Legacy)', [
'classname' => 'widget_archive widget_jaw_widget',
- 'description' => __( 'A widget for displaying an archive list with some effects.', 'jalw_i18n' ),
+ 'description' => __( 'A widget for displaying an archive list with some effects.', 'jquery-archive-list-widget' ),
'show_instance_in_rest' => true,
] );
}
@@ -61,7 +61,7 @@
//add_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $self, 'hide_jal_widget'] );
if ( function_exists( 'load_plugin_textdomain' ) ) {
- load_plugin_textdomain( 'jalw_i18n', null, basename( dirname( __FILE__ ) ) . '/languages' );
+ load_plugin_textdomain( 'jquery-archive-list-widget', null, basename( dirname( __FILE__ ) ) . '/languages' );
load_default_textdomain();
}
@@ -155,7 +155,7 @@
);
if ( count( $years ) < 1 ) {
- $html .= '<li>' . __( 'There are no post to show.', 'jalw_i18n' ) . '</li>';
+ $html .= '<li>' . __( 'There are no post to show.', 'jquery-archive-list-widget' ) . '</li>';
} else {
$html .= $this->html_years( $years );
}
@@ -445,7 +445,7 @@
$include_or_excluded = $instance['include-or-exclude'] ?? 'exclude';
?>
<dl>
- <dt><strong><?php _e( 'Title', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Title', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<input
name="<?php echo $this->get_field_name( 'title' ); ?>"
@@ -453,14 +453,14 @@
value="<?php echo esc_attr( $instance['title'] ); ?>"
/>
</dd>
- <dt><strong><?php _e( 'Trigger Symbol', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Trigger Symbol', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<select
id="<?php echo $this->get_field_id( 'symbol' ); ?>"
name="<?php echo $this->get_field_name( 'symbol' ); ?>"
>
<option value="0" <?php selected( $instance['symbol'], 0 ); ?> >
- <?php _e( 'Empty Space', 'jalw_i18n' ); ?>
+ <?php _e( 'Empty Space', 'jquery-archive-list-widget' ); ?>
</option>
<option value="1" <?php selected( $instance['symbol'], 1 ); ?> >
► ▼
@@ -473,24 +473,24 @@
</option>
</select>
</dd>
- <dt><strong><?php _e( 'Effect', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Effect', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<select
id="<?php echo $this->get_field_id( 'effect' ); ?>"
name="<?php echo $this->get_field_name( 'effect' ); ?>"
>
<option value="none" <?php selected( $instance['effect'], '' ); ?>>
- <?php _e( 'None', 'jalw_i18n' ); ?>
+ <?php _e( 'None', 'jquery-archive-list-widget' ); ?>
</option>
<option value="slide" <?php selected( $instance['effect'], 'slide' ); ?> >
- <?php _e( 'Slide( Accordion )', 'jalw_i18n' ); ?>
+ <?php _e( 'Slide( Accordion )', 'jquery-archive-list-widget' ); ?>
</option>
<option value="fade" <?php selected( $instance['effect'], 'fade' ); ?> >
- <?php _e( 'Fade', 'jalw_i18n' ); ?>
+ <?php _e( 'Fade', 'jquery-archive-list-widget' ); ?>
</option>
</select>
</dd>
- <dt><strong><?php _e( 'Month Format', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Month Format', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<select
id="<?php echo $this->get_field_id( 'month_format' ); ?>"
@@ -498,46 +498,46 @@
>
<option
value="full" <?php selected( $instance['month_format'], 'full' ); ?> >
- <?php _e( 'Full Name( January )', 'jalw_i18n' ); ?>
+ <?php _e( 'Full Name( January )', 'jquery-archive-list-widget' ); ?>
</option>
<option
value="short" <?php selected( $instance['month_format'], 'short' ); ?> >
- <?php _e( 'Short Name( Jan )', 'jalw_i18n' ); ?>
+ <?php _e( 'Short Name( Jan )', 'jquery-archive-list-widget' ); ?>
</option>
<option
value="number" <?php selected( $instance['month_format'], 'number' ); ?> >
- <?php _e( 'Number( 01 )', 'jalw_i18n' ); ?>
+ <?php _e( 'Number( 01 )', 'jquery-archive-list-widget' ); ?>
</option>
</select>
</dd>
- <dt><strong><?php _e( 'Expand', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Expand', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<select
id="<?php echo $this->get_field_id( 'expand' ); ?>"
name="<?php echo $this->get_field_name( 'expand' ); ?>"
>
<option value="" <?php selected( $instance['expand'], '' ); ?>>
- <?php _e( 'None', 'jalw_i18n' ); ?>
+ <?php _e( 'None', 'jquery-archive-list-widget' ); ?>
</option>
<option value="all" <?php selected( $instance['expand'], 'all' ); ?> >
- <?php _e( 'All', 'jalw_i18n' ); ?>
+ <?php _e( 'All', 'jquery-archive-list-widget' ); ?>
</option>
<option
value="current" <?php selected( $instance['expand'], 'current' ); ?> >
- <?php _e( 'Current or post date', 'jalw_i18n' ); ?>
+ <?php _e( 'Current or post date', 'jquery-archive-list-widget' ); ?>
</option>
<option
value="current_post" <?php selected( $instance['expand'], 'current_post' ); ?> >
- <?php _e( 'Only post date', 'jalw_i18n' ); ?>
+ <?php _e( 'Only post date', 'jquery-archive-list-widget' ); ?>
</option>
<option
value="current_date" <?php selected( $instance['expand'], 'current_date' ); ?> >
- <?php _e( 'Only current date', 'jalw_i18n' ); ?>
+ <?php _e( 'Only current date', 'jquery-archive-list-widget' ); ?>
</option>
</select>
</dd>
- <dt><strong><?php _e( 'Extra options', 'jalw_i18n' ); ?></strong></dt>
+ <dt><strong><?php _e( 'Extra options', 'jquery-archive-list-widget' ); ?></strong></dt>
<dd>
<input
id="<?php echo $this->get_field_id( 'showcount' ); ?>"
@@ -546,7 +546,7 @@
value="1"
/>
<label for="<?php echo $this->get_field_id( 'showcount' ); ?>">
- <?php _e( 'Show number of posts', 'jalw_i18n' ); ?>
+ <?php _e( 'Show number of posts', 'jquery-archive-list-widget' ); ?>
</label>
</dd>
<dd>
@@ -557,7 +557,7 @@
value="1"
/>
<label for="<?php echo $this->get_field_id( 'showpost' ); ?>">
- <?php _e( 'Show posts under months', 'jalw_i18n' ); ?>
+ <?php _e( 'Show posts under months', 'jquery-archive-list-widget' ); ?>
</label>
</dd>
<dd>
@@ -568,7 +568,7 @@
value="1"
/>
<label for="<?php echo $this->get_field_id( 'onlycategory' ); ?>">
- <?php _e( 'Show only post from selected category in a category page', 'jalw_i18n' ); ?>
+ <?php _e( 'Show only post from selected category in a category page', 'jquery-archive-list-widget' ); ?>
</label>
</dd>
<dd>
@@ -579,7 +579,7 @@
value="1" <?php echo $instance['only_sym_link'] ? 'checked="checked"' : ''; ?>
/>
<label for="<?php echo $this->get_field_id( 'only_sym_link' ); ?>">
- <?php _e( 'Only expand / reduce by clicking the symbol', 'jalw_i18n' ); ?>
+ <?php _e( 'Only expand / reduce by clicking the symbol', 'jquery-archive-list-widget' ); ?>
</label>
</dd>
<dd>
@@ -590,7 +590,7 @@
value="1" <?php echo $instance['accordion'] ? 'checked="checked"' : ''; ?>
/>
<label for="<?php echo $this->get_field_id( 'accordion' ); ?>">
- <?php _e( 'Only expand one at a the same time (accordion effect)', 'jalw_i18n' ); ?>
+ <?php _e( 'Only expand one at a the same time (accordion effect)', 'jquery-archive-list-widget' ); ?>
</label>
</dd>
<dt class="jaw-include-or-exclude">
@@ -608,7 +608,7 @@
<?php checked( $include_or_excluded, 'include' ); ?>
/>
<label for="<?php echo $this->get_field_id( 'include-or-exclude' ); ?>-include">
- <?php _e( 'Include the following categories', 'jalw_i18n' ); ?>
+ <?php _e( 'Include the following categories', 'jquery-archive-list-widget' ); ?>
</label><br>
<input
id="<?php echo $this->get_field_id( 'include-or-exclude' ); ?>-exclude"
@@ -620,14 +620,14 @@
<?php checked( $include_or_excluded, 'exclude' ) ?>
/>
<label for="<?php echo $this->get_field_id( 'include-or-exclude' ); ?>-exclude">
- <?php _e( 'Exclude the following categories', 'jalw_i18n' ); ?>
+ <?php _e( 'Exclude the following categories', 'jquery-archive-list-widget' ); ?>
</label>
<dl>
<dt
class="jaw-exclude <?php echo $this->get_field_id( 'excluded' ) ?>"
style="display:<?php echo $includeCategories ? 'none' : 'block'; ?>"
>
- <strong><?php _e( 'Exclude categories', 'jalw_i18n' ); ?></strong>
+ <strong><?php _e( 'Exclude categories', 'jquery-archive-list-widget' ); ?></strong>
</dt>
<dd
class="jaw-exclude <?php echo $this->get_field_id( 'excluded' ) ?>"
@@ -644,7 +644,7 @@
class="jaw-include <?php echo $this->get_field_id( 'included' ) ?>"
style="display:<?php echo ! $includeCategories ? 'none' : 'block'; ?>"
>
- <strong><?php _e( 'Include categories', 'jalw_i18n' ); ?></strong>
+ <strong><?php _e( 'Include categories', 'jquery-archive-list-widget' ); ?></strong>
</dt>
<dd
class="jaw-include <?php echo $this->get_field_id( 'included' ) ?>"
--- a/jquery-archive-list-widget/classes/class-js-archive-list-settings.php
+++ b/jquery-archive-list-widget/classes/class-js-archive-list-settings.php
@@ -3,16 +3,56 @@
class Js_Archive_List_Settings {
private $config;
+ public function get_config() {
+ return $this->config;
+ }
+
public function __construct( $settings ) {
$this->config = $settings;
- if ( is_string( $settings['included'] ) ) {
- $this->config['included'] = unserialize( $settings['included'] );
- }
+ $this->config['included'] = self::normalize_category_ids( $settings['included'] ?? [] );
+ $this->config['excluded'] = self::normalize_category_ids( $settings['excluded'] ?? [] );
+ }
- if ( is_string( $settings['excluded'] ) ) {
- $this->config['excluded'] = unserialize( $settings['excluded'] );
+ /**
+ * Parses category IDs from array/serialized array while rejecting unsafe payloads.
+ *
+ * @param mixed $raw_ids Value from DB or shortcode attributes.
+ *
+ * @return array<int>
+ */
+ private static function normalize_category_ids( $raw_ids ) {
+ $ids = [];
+
+ if ( is_array( $raw_ids ) ) {
+ $ids = $raw_ids;
+ } elseif ( is_string( $raw_ids ) ) {
+ $trimmed = trim( $raw_ids );
+
+ if ( '' !== $trimmed && is_serialized( $trimmed ) ) {
+ $parsed = unserialize( $trimmed, [ 'allowed_classes' => false ] );
+
+ if ( false !== $parsed || 'b:0;' === $trimmed ) {
+ $ids = is_array( $parsed ) ? $parsed : [];
+ }
+ }
}
+
+ $normalized_ids = array_values(
+ array_unique(
+ array_filter(
+ array_map(
+ 'intval',
+ array_filter( $ids, 'is_scalar' )
+ ),
+ static function ( $id ) {
+ return $id > 0;
+ }
+ )
+ )
+ );
+
+ return $normalized_ids;
}
/**
@@ -57,6 +97,7 @@
*/
public static function translateDbSettingsToInternal( &$config ) {
$jalwSettings = new self( $config );
+ $config = $jalwSettings->get_config();
$symbols = $jalwSettings->symbols();
if ( empty( $config['ex_sym'] ) && $config['ex_sym'] !== "" ) {
--- a/jquery-archive-list-widget/jquery-archive-list-widget.php
+++ b/jquery-archive-list-widget/jquery-archive-list-widget.php
@@ -3,13 +3,16 @@
Plugin Name: JS Archive List
Plugin URI: http://skatox.com/blog/jquery-archive-list-widget/
Description: A widget for displaying an archive list with some effects.
- Version: 6.1.7
+ Version: 6.2.0
+ Requires at least: 4.7
+ Requires PHP: 7.4
Author: Miguel Angel Useche Castro
Author URI: https://migueluseche.com/
- Text Domain: jalw_i18n
+ Text Domain: jquery-archive-list-widget
Domain Path: /languages
- License: GPL2
- Copyleft 2009-2026 Miguel Angel Useche Castro (email : migueluseche@skatox.com)
+ License: GPLv2
+ License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ Copyleft 2009-2024 Miguel Angel Useche Castro (email : migueluseche@skatox.com)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -35,7 +38,7 @@
define( 'JAL_BASE_URL', plugin_dir_url( __FILE__ ) );
}
if ( ! defined( 'JAL_VERSION' ) ) {
- define( 'JAL_VERSION', '6.1.7' );
+ define( 'JAL_VERSION', '6.2.0' );
}
require_once( 'admin/class-jaw-walker-category-checklist.php' );
@@ -59,7 +62,7 @@
);
wp_set_script_translations(
- 'jalw_i18n-script', 'jalw_i18n',
+ 'jquery-archive-list-widget-script', 'jquery-archive-list-widget',
plugin_dir_path( __FILE__ ) . 'languages'
);
}
--- a/jquery-archive-list-widget/tests/phpunit/LegacyCategoryFilteringSqlTest.php
+++ b/jquery-archive-list-widget/tests/phpunit/LegacyCategoryFilteringSqlTest.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Tests SQL category filtering generated for legacy datasource mode.
+ *
+ * @package js-archive-list
+ */
+
+use PHPUnitFrameworkTestCase;
+
+class Jalw_Testable_DataSource extends JQ_Archive_List_DataSource {
+ public function exposed_build_sql_where( ?int $year = null, ?int $month = null ): array {
+ return $this->build_sql_where( $year, $month );
+ }
+
+ public function exposed_build_sql_join(): string {
+ return $this->build_sql_join();
+ }
+}
+
+class LegacyCategoryFilteringSqlTest extends TestCase {
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ global $wpdb;
+ $wpdb = (object) [
+ 'posts' => 'wp_posts',
+ 'term_relationships' => 'wp_term_relationships',
+ 'term_taxonomy' => 'wp_term_taxonomy',
+ ];
+ }
+
+ public function test_excluded_categories_use_post_level_subquery() {
+ $data_source = new Jalw_Testable_DataSource(
+ [
+ 'type' => 'post',
+ 'excluded' => [ 3, 7 ],
+ ],
+ true
+ );
+
+ [ $where, $args ] = $data_source->exposed_build_sql_where();
+
+ $this->assertStringContainsString( 'wp_posts.ID NOT IN (', $where );
+ $this->assertStringContainsString( 'SELECT tr.object_id', $where );
+ $this->assertStringNotContainsString( 'wp_term_taxonomy.term_id NOT IN', $where );
+ $this->assertSame( [ '', 'post', 'publish', 'category', 3, 7 ], $args );
+ $this->assertSame( '', $data_source->exposed_build_sql_join() );
+ }
+
+ public function test_included_categories_keep_join_based_filtering() {
+ $data_source = new Jalw_Testable_DataSource(
+ [
+ 'type' => 'post',
+ 'included' => [ 2, 5 ],
+ ],
+ true
+ );
+
+ [ $where, $args ] = $data_source->exposed_build_sql_where();
+ $join = $data_source->exposed_build_sql_join();
+
+ $this->assertStringContainsString( 'wp_term_taxonomy.term_id IN (%d, %d)', $where );
+ $this->assertStringContainsString( 'wp_term_taxonomy.taxonomy=%s', $where );
+ $this->assertStringContainsString( 'LEFT JOIN wp_term_relationships', $join );
+ $this->assertStringContainsString( 'LEFT JOIN wp_term_taxonomy', $join );
+ $this->assertSame( [ '', 'post', 'publish', 2, 5, 'category' ], $args );
+ }
+}
--- a/jquery-archive-list-widget/tests/phpunit/ShortcodeDeserializationSecurityTest.php
+++ b/jquery-archive-list-widget/tests/phpunit/ShortcodeDeserializationSecurityTest.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Tests for secure category ID deserialization.
+ *
+ * @package js-archive-list
+ */
+
+use PHPUnitFrameworkTestCase;
+use PHPUnitFrameworkAttributesDataProvider;
+
+class Jalw_Test_Attack_Object {
+ public function __wakeup() {
+ $GLOBALS['jalw_attack_wakeup_triggered'] = true;
+ }
+}
+
+class ShortcodeDeserializationSecurityTest extends TestCase {
+
+ protected function setUp(): void {
+ parent::setUp();
+ $GLOBALS['jalw_attack_wakeup_triggered'] = false;
+ }
+
+ #[DataProvider( 'legacy_serialized_included_provider' )]
+ public function test_accepts_legacy_serialized_arrays_of_ids( $included, $expected ) {
+ $config = $this->build_config( [
+ 'included' => $included,
+ ] );
+
+ Js_Archive_List_Settings::translateDbSettingsToInternal( $config );
+
+ $this->assertSame( $expected, $config['included'] );
+ }
+
+ public static function legacy_serialized_included_provider() {
+ yield 'ints and numeric strings with duplicates and invalid ids' => [
+ 'a:5:{i:0;i:5;i:1;s:1:"8";i:2;i:0;i:3;s:1:"5";i:4;s:2:"-1";}',
+ [ 5, 8 ],
+ ];
+
+ yield 'serialized empty array' => [
+ 'a:0:{}',
+ [],
+ ];
+ }
+
+ #[DataProvider( 'blocked_object_payload_provider' )]
+ public function test_blocks_object_deserialization_payloads( $included, $expected ) {
+ $config = $this->build_config( [
+ 'included' => $included,
+ ] );
+
+ Js_Archive_List_Settings::translateDbSettingsToInternal( $config );
+
+ $this->assertSame( $expected, $config['included'] );
+ $this->assertFalse( $GLOBALS['jalw_attack_wakeup_triggered'] );
+ }
+
+ public static function blocked_object_payload_provider() {
+ yield 'serialized array with object payload' => [
+ serialize( [ 7, new Jalw_Test_Attack_Object() ] ),
+ [ 7 ],
+ ];
+
+ yield 'serialized object only' => [
+ serialize( new Jalw_Test_Attack_Object() ),
+ [],
+ ];
+ }
+
+ #[DataProvider( 'invalid_string_provider' )]
+ public function test_ignores_non_serialized_string_input( $included, $excluded ) {
+ $config = $this->build_config( [
+ 'included' => $included,
+ 'excluded' => $excluded,
+ ] );
+
+ Js_Archive_List_Settings::translateDbSettingsToInternal( $config );
+
+ $this->assertSame( [], $config['included'] );
+ $this->assertSame( [], $config['excluded'] );
+ }
+
+ public static function invalid_string_provider() {
+ yield 'comma-separated ids' => [
+ '10,11,12',
+ 'not-serialized',
+ ];
+
+ yield 'free text' => [
+ 'this is not serialized',
+ 'neither is this',
+ ];
+ }
+
+ #[DataProvider( 'array_normalization_provider' )]
+ public function test_normalizes_array_payloads( $included, $excluded, $expected_included, $expected_excluded ) {
+ $config = $this->build_config( [
+ 'included' => $included,
+ 'excluded' => $excluded,
+ ] );
+
+ Js_Archive_List_Settings::translateDbSettingsToInternal( $config );
+
+ $this->assertSame( $expected_included, $config['included'] );
+ $this->assertSame( $expected_excluded, $config['excluded'] );
+ }
+
+ public static function array_normalization_provider() {
+ yield 'mixed scalars and duplicates' => [
+ [ '4', 4, -3, 0, 'abc', 9 ],
+ [ '2', 2, 3 ],
+ [ 4, 9 ],
+ [ 2, 3 ],
+ ];
+
+ yield 'booleans and float strings' => [
+ [ true, false, '6.8', '0012' ],
+ [ 1.2, '0', '-2', '15' ],
+ [ 1, 6, 12 ],
+ [ 1, 15 ],
+ ];
+ }
+
+ private function build_config( $overrides = [] ) {
+ return array_merge(
+ [
+ 'symbol' => 1,
+ 'ex_sym' => '',
+ 'con_sym' => '',
+ 'included' => [],
+ 'excluded' => [],
+ ],
+ $overrides
+ );
+ }
+}
--- a/jquery-archive-list-widget/tests/phpunit/bootstrap.php
+++ b/jquery-archive-list-widget/tests/phpunit/bootstrap.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * Lightweight test bootstrap for pure PHPUnit unit tests.
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ define( 'ABSPATH', dirname( __DIR__, 2 ) . '/' );
+}
+
+if ( ! function_exists( 'is_serialized' ) ) {
+ /**
+ * Minimal equivalent of WordPress is_serialized() for this test suite.
+ *
+ * @param mixed $data Serialized candidate.
+ *
+ * @return bool
+ */
+ function is_serialized( $data ) {
+ if ( ! is_string( $data ) ) {
+ return false;
+ }
+
+ $data = trim( $data );
+
+ if ( 'N;' === $data ) {
+ return true;
+ }
+
+ if ( strlen( $data ) < 4 || ':' !== $data[1] ) {
+ return false;
+ }
+
+ $lastc = substr( $data, -1 );
+ if ( ';' !== $lastc && '}' !== $lastc ) {
+ return false;
+ }
+
+ $token = $data[0];
+ switch ( $token ) {
+ case 's':
+ return preg_match( '/^s:[0-9]+:".*";$/s', $data ) === 1;
+ case 'a':
+ case 'O':
+ case 'E':
+ return preg_match( '/^' . $token . ':[0-9]+:/s', $data ) === 1;
+ case 'b':
+ case 'i':
+ case 'd':
+ return preg_match( '/^' . $token . ':[0-9.E-]+;$/', $data ) === 1;
+ }
+
+ return false;
+ }
+}
+
+if ( ! function_exists( 'apply_filters' ) ) {
+ /**
+ * Minimal apply_filters stub for unit tests.
+ *
+ * @param string $hook_name Hook name.
+ * @param mixed $value Value to filter.
+ *
+ * @return mixed
+ */
+ function apply_filters( $hook_name, $value ) {
+ return $value;
+ }
+}
+
+if ( ! function_exists( 'get_query_var' ) ) {
+ /**
+ * Minimal get_query_var stub for unit tests.
+ *
+ * @param string $name Query var name.
+ *
+ * @return mixed
+ */
+ function get_query_var( $name ) {
+ return null;
+ }
+}
+
+if ( ! function_exists( 'is_category' ) ) {
+ /**
+ * Minimal is_category stub for unit tests.
+ *
+ * @return bool
+ */
+ function is_category() {
+ return false;
+ }
+}
+
+require dirname( __DIR__, 2 ) . '/classes/class-js-archive-list-settings.php';
+require dirname( __DIR__, 2 ) . '/classes/class-jq-archive-list-datasource.php';