Skip to content
Open

Release #1364

Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 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
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
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 );
} );
} );
Loading
Loading