Skip to content
Open

Release #1364

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
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
12 changes: 6 additions & 6 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
69 changes: 69 additions & 0 deletions tests/e2e/specs/usage-logger.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* WordPress dependencies
*/
const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );

/**
* Internal dependencies
*/
const { deleteAllCharts } = require( '../utils/common' );

/**
* Regression tests for https://github.com/Codeinwp/visualizer/issues/1359
*
* A published chart whose `visualizer-settings` meta is a string (instead of
* the sanitized settings array) crashed `Visualizer_Module_Setup::getUsage()`
* with a PHP 8 TypeError, aborting the whole SDK usage collection request.
* The logger must tolerate such charts and still report the others.
*/
test.describe( 'Usage logger', () => {
let corruptedId;
let manualId;

test.beforeAll( async ( { requestUtils } ) => {
// The assertions below count charts, so start from a clean library.
await deleteAllCharts( requestUtils );

// A chart whose settings meta is a corrupted string value.
const corrupted = await requestUtils.rest( {
method: 'POST',
path: '/wp/v2/visualizer',
data: { title: 'Corrupted settings chart', status: 'publish' },
} );
corruptedId = corrupted.id;
await requestUtils.rest( {
method: 'POST',
path: `/visualizer-e2e/v1/chart-settings/${ corruptedId }`,
data: { settings: 'corrupted string settings' },
} );

// A healthy chart with a manual configuration, which must still be counted.
const manual = await requestUtils.rest( {
method: 'POST',
path: '/wp/v2/visualizer',
data: { title: 'Manual config chart', status: 'publish' },
} );
manualId = manual.id;
await requestUtils.rest( {
method: 'POST',
path: `/visualizer-e2e/v1/chart-settings/${ manualId }`,
data: { settings: { manual: '{"colors": ["#000"]}' } },
} );
} );

test.afterAll( async ( { requestUtils } ) => {
for ( const id of [ corruptedId, manualId ] ) {
if ( id ) {
await requestUtils.rest( { method: 'DELETE', path: `/wp/v2/visualizer/${ id }`, params: { force: true } } );
}
}
} );

test( 'survives a chart whose settings meta is a string', async ( { requestUtils } ) => {
// Before the fix this request died with a TypeError (HTTP 500).
const usage = await requestUtils.rest( { method: 'GET', path: '/visualizer-e2e/v1/usage' } );

expect( usage.manual_config ).toBe( 1 );
expect( Object.values( usage.types ).reduce( ( a, b ) => a + b, 0 ) ).toBe( 2 );
} );
} );
14 changes: 14 additions & 0 deletions tests/test-chart-data-permissions.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@
*/
class Test_Visualizer_Chart_Data_Permissions extends WP_UnitTestCase {

/**
* A newly created REST chart has no settings metadata yet.
*/
public function test_rest_can_create_chart_without_settings_metadata() {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
$request = new WP_REST_Request( 'POST', '/wp/v2/visualizer' );
$request->set_param( 'title', 'Empty chart' );
$request->set_param( 'status', 'publish' );
$response = rest_get_server()->dispatch( $request );
$this->assertSame( 201, $response->get_status() );
$data = $response->get_data();
$this->assertIsArray( $data['chart_data']['visualizer-settings'] );
}

/**
* Create a chart owned by the given user.
*
Expand Down
Loading
Loading