Skip to content
Merged

Release #1364

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10cdb01
Sync branch [skip ci]
pirate-bot Jul 30, 2026
2c97125
Sync branch [skip ci]
pirate-bot Jul 30, 2026
1ac4288
fix: keep usage logger alive when chart settings meta is not an array
lucadobrescu Jul 31, 2026
a4de841
fix: guard per-key permission counts against malformed pro meta
lucadobrescu Jul 31, 2026
54aeb78
fix: skip pro charts whose nested permissions meta is not an array
lucadobrescu Aug 3, 2026
9c6cce5
chore(deps): bump codeinwp/themeisle-sdk from 3.3.57 to 3.3.58
dependabot[bot] Aug 3, 2026
adc8116
Merge pull request #1360 from Codeinwp/fix/1359-usage-logger-string-s…
selul Aug 4, 2026
0c428e4
Merge pull request #1362 from Codeinwp/dependabot/composer/developmen…
selul Aug 4, 2026
a6c9130
Sync branch [skip ci]
pirate-bot Aug 20, 2026
8a05dd1
Sync branch [skip ci]
pirate-bot Aug 31, 2026
14c6377
ci: skip PR-comment job on Dependabot PRs (no secrets, always fails)
selul Sep 2, 2026
d7ee33a
ci: revert Dependabot gate on PR-comment job (org Dependabot secret c…
selul Sep 2, 2026
150d730
fix: keep the unsafe-destination error for non-public IP literals on …
selul Sep 2, 2026
016faff
fix: load the block editor stylesheet inside the iframed editor canvas
selul Sep 2, 2026
af6c784
test(e2e): target the iframed editor canvas in Gutenberg specs
selul Sep 2, 2026
5d3235e
chore(deps): bump codeinwp/themeisle-sdk from 3.3.58 to 3.3.61 (#1371)
dependabot[bot] Sep 2, 2026
45e73d7
fix: avoid counting malformed chart series metadata (#1377)
selul Sep 10, 2026
4f57c01
chore(deps-dev): bump phpstan/phpstan from 2.2.6 to 2.2.13 (#1378)
dependabot[bot] Sep 11, 2026
f54c21c
Sanitized JSON data source headers to prevent XSS vulnerabilities (#1…
girishpanchal30 Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
strategy:
fail-fast: false
runs-on: ubuntu-22.04
env:
# PHP 7.4 images depend on the expired Debian Bullseye package index.
WP_ENV_PHP_VERSION: "8.2"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .wp-env.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"core": null,
"phpVersion": "7.4",
"phpVersion": "8.1",
"plugins": [
"."
],
Expand Down
20 changes: 20 additions & 0 deletions classes/Visualizer/Gutenberg/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,25 @@ public function enqueue_gutenberg_scripts() {
* Hook server side rendering into render callback
*/
public function register_block_type() {
$asset_path = VISUALIZER_ABSPATH . '/classes/Visualizer/Gutenberg/build/index.asset.php';
$version = $this->version;
if ( file_exists( $asset_path ) ) {
// @phpstan-ignore-next-line
$asset = require $asset_path;
$version = isset( $asset['version'] ) ? $asset['version'] : $version;
}
if ( ! wp_style_is( 'visualizer-datatables', 'registered' ) ) {
wp_register_style( 'visualizer-datatables', VISUALIZER_ABSURL . 'css/lib/datatables.min.css', array(), Visualizer_Plugin::VERSION );
}
if ( ! wp_style_is( 'visualizer-gutenberg-block', 'registered' ) ) {
wp_register_style( 'visualizer-gutenberg-block', VISUALIZER_ABSURL . 'classes/Visualizer/Gutenberg/build/style-index.css', array( 'visualizer-datatables' ), $version );
}
register_block_type(
'visualizer/chart', array(
// The editor_style registration is what gets the stylesheet into the
// iframed editor canvas; styles enqueued via enqueue_block_editor_assets
// only reach the parent document.
'editor_style' => 'visualizer-gutenberg-block',
'render_callback' => array( $this, 'gutenberg_block_callback' ),
'attributes' => array(
'id' => array(
Expand Down Expand Up @@ -297,6 +314,9 @@ public function get_visualizer_data( $post ) {

// faetch and update settings
$data['visualizer-settings'] = get_post_meta( $post_id, Visualizer_Plugin::CF_SETTINGS, true );
if ( ! is_array( $data['visualizer-settings'] ) ) {
$data['visualizer-settings'] = array();
}
if ( empty( $data['visualizer-settings']['pagination'] ) ) {
$data['visualizer-settings']['pageSize'] = '';
}
Expand Down
1 change: 0 additions & 1 deletion classes/Visualizer/Module/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ public function __construct( Visualizer_Plugin $plugin ) {
$this->_addFilter( 'media_view_strings', 'setupMediaViewStrings' );
$this->_addFilter( 'plugin_action_links', 'getPluginActionLinks', 10, 2 );
$this->_addFilter( 'plugin_row_meta', 'getPluginMetaLinks', 10, 2 );
$this->_addFilter( 'visualizer_logger_data', 'getLoggerData' );
$this->_addFilter( 'visualizer_feedback_review_trigger', 'feedbackReviewTrigger' );
$this->_addFilter( 'themeisle_sdk_blackfriday_data', 'add_black_friday_data' );

Expand Down
2 changes: 1 addition & 1 deletion classes/Visualizer/Module/Frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@ private function getChartData( $cache_key = '', $chart_id = 0 ) {
$series = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SERIES, true );
$is_woocommerce_report = get_post_meta( $chart->ID, Visualizer_Plugin::CF_IS_WOOCOMMERCE_SOURCE, true );

if ( isset( $settings['series'] ) && ! ( count( $settings['series'] ) - count( $series ) > 1 ) ) {
if ( isset( $settings['series'] ) && is_array( $settings['series'] ) && is_array( $series ) && ! ( count( $settings['series'] ) - count( $series ) > 1 ) ) {
$diff_total_series = abs( count( $settings['series'] ) - count( $series ) );
if ( $diff_total_series ) {
foreach ( range( 1, $diff_total_series ) as $k => $diff_series ) {
Expand Down
6 changes: 3 additions & 3 deletions classes/Visualizer/Module/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public function getUsage( $data, $meta_keys = array() ) {
$lib = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
$charts['library'][ $lib ] = isset( $charts['library'][ $lib ] ) ? $charts['library'][ $lib ] + 1 : 1;
$settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
if ( array_key_exists( 'manual', $settings ) && ! empty( $settings['manual'] ) ) {
if ( is_array( $settings ) && ! empty( $settings['manual'] ) ) {
$charts['manual_config'] = $charts['manual_config'] + 1;
}

Expand All @@ -124,15 +124,15 @@ public function getUsage( $data, $meta_keys = array() ) {

if ( Visualizer_Module::is_pro() ) {
$permissions = get_post_meta( $chart_id, Visualizer_Pro::CF_PERMISSIONS, true );
if ( empty( $permissions ) ) {
if ( ! is_array( $permissions ) || empty( $permissions['permissions'] ) || ! is_array( $permissions['permissions'] ) ) {
continue;
}
$permissions = $permissions['permissions'];
$customized = false;
foreach ( $default_perms as $key => $val ) {
if ( ! is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && $permissions[ $key ] !== $val ) {
$customized = true;
} elseif ( is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && count( $permissions[ $key ] ) !== count( $val ) ) {
} elseif ( is_array( $val ) && ! is_null( $val ) && isset( $permissions[ $key ] ) && is_array( $permissions[ $key ] ) && count( $permissions[ $key ] ) !== count( $val ) ) {
$customized = true;
}
}
Expand Down
9 changes: 9 additions & 0 deletions classes/Visualizer/Remote/Fetch.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,15 @@ private static function validate_url( $url, &$ips = array() ) {
$ips = array();
$validated_url = wp_http_validate_url( $url );
if ( false === $validated_url ) {
// WordPress 7.1+ rejects non-public IP literals inside wp_http_validate_url()
// itself; older cores let them through to our is_global_ip() check below. Keep
// the distinct "unsafe destination" error on every core version so callers can
// tell a policy block from a malformed URL.
$scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
$host = (string) wp_parse_url( $url, PHP_URL_HOST );
if ( in_array( $scheme, array( 'http', 'https' ), true ) && filter_var( $host, FILTER_VALIDATE_IP ) && ! self::is_global_ip( $host ) ) {
return new WP_Error( 'visualizer_unsafe_remote_url', 'The remote URL resolves to a non-public address.' );
}
return new WP_Error( 'visualizer_invalid_remote_url', 'The remote URL is not allowed.' );
}

Expand Down
19 changes: 12 additions & 7 deletions classes/Visualizer/Render/Layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,13 @@ public static function _renderJsonScreen( $args ) {
}
$methods = apply_filters( 'visualizer_json_request_methods', array( 'GET', 'POST' ) );

$auth = isset( $headers['auth'] ) ? $headers['auth'] : '';
$auth_username = is_array( $auth ) && isset( $auth['username'] ) ? $auth['username'] : '';
$auth_password = is_array( $auth ) && isset( $auth['password'] ) ? $auth['password'] : '';
$auth_string = is_string( $auth ) ? $auth : '';

// open the headers by default?
$headers_open = $headers && array_key_exists( 'auth', $headers ) && ( array_key_exists( 'username', $headers['auth'] ) && ! empty( $headers['auth']['username'] ) ) || ( ! empty( $headers['auth'] ) && is_string( $headers['auth'] ) );
$headers_open = ! empty( $auth_username ) || ! empty( $auth_string );
?>
<div id="visualizer-json-screen" style="display: none">
<div class="visualizer-json-form">
Expand Down Expand Up @@ -242,15 +247,15 @@ class="visualizer-input json-form-element"
type="text"
id="vz-import-json-username"
name="username"
value="<?php echo ( array_key_exists( 'auth', $headers ) && array_key_exists( 'username', $headers['auth'] ) ? $headers['auth']['username'] : '' ); ?>"
value="<?php echo esc_attr( $auth_username ); ?>"
placeholder="<?php esc_html_e( 'Username/Access Key', 'visualizer' ); ?>"
class="json-form-element">
&
<input
type="password"
id="vz-import-json-password"
name="password"
value="<?php echo ( array_key_exists( 'auth', $headers ) && array_key_exists( 'password', $headers['auth'] ) ? $headers['auth']['password'] : '' ); ?>"
value="<?php echo esc_attr( $auth_password ); ?>"
placeholder="<?php esc_html_e( 'Password/Secret Key', 'visualizer' ); ?>"
class="json-form-element">
</div>
Expand All @@ -266,15 +271,15 @@ class="json-form-element">
type="text"
id="vz-import-json-auth"
name="auth"
value="<?php echo ( array_key_exists( 'auth', $headers ) && is_string( $headers['auth'] ) ? $headers['auth'] : '' ); ?>"
value="<?php echo esc_attr( $auth_string ); ?>"
placeholder="<?php esc_html_e( 'e.g. SharedKey <AccountName>:<Signature>', 'visualizer' ); ?>"
class="visualizer-input json-form-element">
</div>
</div>
<div class="json-wizard-header">
<div class="field-title"><?php esc_html_e( 'Additional headers', 'visualizer' ); ?></div>
<div>
<textarea name="additional_headers" class="visualizer-input" placeholder="<?php esc_html_e( 'Key:Value, Key2:Value2,...', 'visualizer' ); ?>"><?php echo isset( $headers['additional_headers'] ) ? $headers['additional_headers'] : ''; ?></textarea>
<textarea name="additional_headers" class="visualizer-input" placeholder="<?php esc_html_e( 'Key:Value, Key2:Value2,...', 'visualizer' ); ?>"><?php echo esc_textarea( isset( $headers['additional_headers'] ) ? $headers['additional_headers'] : '' ); ?></textarea>
</div>
</div>
</div>
Expand All @@ -289,7 +294,7 @@ class="visualizer-input json-form-element">
<?php
if ( ! empty( $root ) ) {
?>
<option value="<?php echo esc_attr( $root ); ?>"><?php echo str_replace( Visualizer_Source_Json::TAG_SEPARATOR, Visualizer_Source_Json::TAG_SEPARATOR_VIEW, $root ); ?></option>
<option value="<?php echo esc_attr( $root ); ?>"><?php echo esc_html( str_replace( Visualizer_Source_Json::TAG_SEPARATOR, Visualizer_Source_Json::TAG_SEPARATOR_VIEW, $root ) ); ?></option>
<?php
}
?>
Expand All @@ -307,7 +312,7 @@ class="visualizer-input json-form-element">
<?php
if ( ! empty( $paging ) ) {
?>
<option value="<?php echo esc_attr( $paging ); ?>"><?php echo sprintf( 'Get results from the first %d pages using %s', apply_filters( 'visualizer_json_fetch_pages', 5, $url ), str_replace( Visualizer_Source_Json::TAG_SEPARATOR, Visualizer_Source_Json::TAG_SEPARATOR_VIEW, $paging ) ); ?></option>
<option value="<?php echo esc_attr( $paging ); ?>"><?php echo esc_html( sprintf( 'Get results from the first %d pages using %s', apply_filters( 'visualizer_json_fetch_pages', 5, $url ), str_replace( Visualizer_Source_Json::TAG_SEPARATOR, Visualizer_Source_Json::TAG_SEPARATOR_VIEW, $paging ) ) ); ?></option>
<?php
}
?>
Expand Down
20 changes: 10 additions & 10 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions tests/e2e/config/mu-plugins/plant-chart-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,21 @@ function () {
},
)
);

// Runs the SDK usage logger on demand, so specs can verify it
// tolerates whatever chart meta they planted (issue #1359).
register_rest_route(
'visualizer-e2e/v1',
'/usage',
array(
'methods' => 'GET',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'callback' => function () {
return apply_filters( 'visualizer_logger_data', array() );
},
)
);
}
);
55 changes: 30 additions & 25 deletions tests/e2e/specs/gutenberg-editor.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,37 @@ test.describe( 'Charts with Gutenberg Editor', () => {
page.setDefaultTimeout( 5000 );
} );

test('check available action on block creation', async ( { admin, editor, page } ) => {
test('check available action on block creation', async ( { admin, editor } ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'visualizer/chart'} );

// Check chart selection options are available.
await expect( page.getByText('Make a new chart or display') ).toBeVisible();
await expect( page.getByLabel('Editor content').locator('a') ).toBeVisible();
await expect( page.locator('div').filter({ hasText: /^Display an existing chart$/ }) ).toBeVisible();
// The block renders inside the editor canvas, which WordPress serves in
// an iframe, so every locator must go through editor.canvas.
await expect( editor.canvas.getByText('Make a new chart or display') ).toBeVisible();
await expect( editor.canvas.locator('a.visualizer-settings__content-option').filter({ hasText: 'Create a new chart' }) ).toBeVisible();
await expect( editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }) ).toBeVisible();
} );

test('new chart creation', async ( { admin, editor, page } ) => {
await admin.createNewPost();
await editor.insertBlock( { name: 'visualizer/chart'} );

await expect( page.getByText('Make a new chart or display') ).toBeVisible();
await expect( page.getByLabel('Editor content').locator('a') ).toBeVisible();
await expect( editor.canvas.getByText('Make a new chart or display') ).toBeVisible();
const createOption = editor.canvas.locator('a.visualizer-settings__content-option').filter({ hasText: 'Create a new chart' });
await expect( createOption ).toBeVisible();

await page.getByLabel('Editor content').locator('a').click({ force: true});
await createOption.click({ force: true });

// Create chart via popup.
await page.frameLocator('iframe').getByRole('button', { name: 'Next' }).click();
await page.frameLocator('iframe').getByRole('button', { name: 'Create Chart' }).click();
// Create chart via popup; target the wizard frame, not the canvas iframe.
const wizard = page.frameLocator('iframe[src*="visualizer-create-chart"]');
await wizard.getByRole('button', { name: 'Next' }).click();
await wizard.getByRole('button', { name: 'Create Chart' }).click();

await expect( page.getByRole('button', { name: 'Done' }) ).toBeVisible();
await page.getByRole('button', { name: 'Done' }).click();
await expect( editor.canvas.getByRole('button', { name: 'Done' }) ).toBeVisible();
await editor.canvas.getByRole('button', { name: 'Done' }).click();

await expect( page.locator('.wp-block-visualizer-chart').count() ).resolves.toBe( 1 );
await expect( page.getByRole('button', { name: 'Done' }) ).toBeHidden();
await expect( editor.canvas.locator('.wp-block-visualizer-chart') ).toHaveCount( 1 );
await expect( editor.canvas.getByRole('button', { name: 'Done' }) ).toBeHidden();

} );

Expand All @@ -55,15 +58,15 @@ test.describe( 'Charts with Gutenberg Editor', () => {
// Create a new post and insert the first available chart.
await admin.createNewPost();
await editor.insertBlock( { name: 'visualizer/chart'} );
await page.locator('div').filter({ hasText: /^Display an existing chart$/ }).click();
await page.locator('.visualizer-settings__charts-controls').first().click();
await editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }).click();
await editor.canvas.locator('.visualizer-settings__charts-controls').first().click();

// Check if it was inserted correctly then enter view mode for the block.
expect( page.getByLabel('Block: Visualizer Chart').getByText('Visualizer') ).not.toBeNull();
await page.getByRole('button', { name: 'Done' }).click();
await expect( editor.canvas.getByLabel('Block: Visualizer Chart') ).toBeVisible();
await editor.canvas.getByRole('button', { name: 'Done' }).click();

// Check if the Chart did not crash the editor.
expect( page.locator('.wp-block-visualizer-chart').count() ).resolves.toBe( 1 );
await expect( editor.canvas.locator('.wp-block-visualizer-chart') ).toHaveCount( 1 );
} );

test( 'check block Edit new button', async ( { admin, editor, page } ) => {
Expand All @@ -73,13 +76,15 @@ test.describe( 'Charts with Gutenberg Editor', () => {
await admin.createNewPost();
await editor.insertBlock( { name: 'visualizer/chart'} );

await page.locator('div').filter({ hasText: /^Display an existing chart$/ }).click();
await page.locator('.visualizer-settings__charts-controls').first().click();
await editor.canvas.locator('div').filter({ hasText: /^Display an existing chart$/ }).click();
await editor.canvas.locator('.visualizer-settings__charts-controls').first().click();

expect( page.getByLabel('Block: Visualizer Chart').getByText('Visualizer') ).not.toBeNull();
await expect( editor.canvas.getByLabel('Block: Visualizer Chart') ).toBeVisible();

await expect(page.getByRole('button', { name: 'Edit Chart' })).toBeVisible();
await page.getByRole('button', { name: 'Edit Chart' }).click();
// The Edit Chart button lives in the block toolbar / inspector, which
// render in the parent document, not the canvas iframe.
await expect( page.getByRole('button', { name: 'Edit Chart' }).first() ).toBeVisible();
await page.getByRole('button', { name: 'Edit Chart' }).first().click();
//await page.goto('http://localhost:8889/wp-admin/post.php?post=29&action=edit');
await expect(page.getByLabel('Visualizer', { exact: true }).locator('h1')).toContainText('Visualizer');
await page.getByRole('button', { name: 'Close dialog' }).click();
Expand Down
Loading
Loading