Skip to content
Merged
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
5 changes: 2 additions & 3 deletions assets/src/dashboard/parts/connected/Toasts.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { filter } from 'lodash';
import { SnackbarList } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

export default function() {
const notices = useSelect(
( select ) => select( noticesStore ).getNotices(),
( select ) => select( 'core/notices' ).getNotices(),
[]
);

const { removeNotice } = useDispatch( noticesStore );
const { removeNotice } = useDispatch( 'core/notices' );

const snackbarNotices = filter( notices, {
type: 'snackbar'
Expand Down
64 changes: 42 additions & 22 deletions assets/src/dashboard/parts/connected/settings/Compression.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BaseControl,
Button,
Icon,
RadioControl,
RangeControl,
ToggleControl,

Expand Down Expand Up @@ -173,32 +174,51 @@ const Compression = ({
}
}, [ showSample ]);

const compressionModes = [
{
className: 'bg-blue-500',
label: optimoleDashboardApp.strings.options_strings.compression_mode_speed_optimized,
value: 'speed_optimized'
},
{
label: optimoleDashboardApp.strings.options_strings.compression_mode_quality_optimized,
value: 'quality_optimized'
},
{
label: optimoleDashboardApp.strings.options_strings.compression_mode_custom,
value: 'custom'
}
];

const compressionModeLabel = (
<div className="mb-4">
<strong>{optimoleDashboardApp.strings.options_strings.compression_mode}</strong>
</div>
);

return (
<>

<ToggleGroupControl className=" w-4/6"
aria-label={ optimoleDashboardApp.strings.options_strings.compression_mode }
onChange={ value => transformCompressionMode( value ) }
value={ compressionMode }
label={ (
<div className="mb-4">
<strong>{optimoleDashboardApp.strings.options_strings.compression_mode}</strong>
</div>
) }
>
<ToggleGroupControlOption className="bg-blue-500"
label={ optimoleDashboardApp.strings.options_strings.compression_mode_speed_optimized }
value="speed_optimized"
{ /* ToggleGroupControl ships with WordPress 5.9. */ }
{ ToggleGroupControl ? (
<ToggleGroupControl className=" w-4/6"
aria-label={ optimoleDashboardApp.strings.options_strings.compression_mode }
onChange={ value => transformCompressionMode( value ) }
value={ compressionMode }
label={ compressionModeLabel }
>
{ compressionModes.map( mode => (
<ToggleGroupControlOption key={ mode.value } { ...mode } />
) ) }
</ToggleGroupControl>
) : (
<RadioControl
label={ compressionModeLabel }
selected={ compressionMode }
options={ compressionModes }
onChange={ value => transformCompressionMode( value ) }
/>
<ToggleGroupControlOption
label={ optimoleDashboardApp.strings.options_strings.compression_mode_quality_optimized }
value="quality_optimized"
/>
<ToggleGroupControlOption
label={ optimoleDashboardApp.strings.options_strings.compression_mode_custom }
value="custom"
/>
</ToggleGroupControl>
) }
{'speed_optimized' === compressionMode && (
<p className="text-sm text-gray-600 mb-20">
{optimoleDashboardApp.strings.options_strings.compression_mode_speed_optimized_desc}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* The compression mode control on WordPress versions whose @wordpress/components has no
* ToggleGroupControl (below 5.9).
*/
import { act } from 'react-dom/test-utils';
import { createRoot } from 'react-dom/client';

jest.mock( '@wordpress/components', () => ({
BaseControl: ({ children }) => children,
Button: ({ children }) => children,
Icon: () => null,
RangeControl: () => null,
ToggleControl: () => null,
RadioControl: ({ options, selected, onChange }) => options.map( option => (
<input
key={ option.value }
type="radio"
value={ option.value }
checked={ selected === option.value }
onChange={ () => onChange( option.value ) }
/>
) )
}) );
jest.mock( '@wordpress/data', () => ({
useSelect: () => ({ sampleImages: {}, isLoading: false })
}) );
jest.mock( '@wordpress/icons', () => ({ rotateRight: null }) );
jest.mock( 'react-compare-image', () => () => null );
jest.mock( '../../../../utils/api', () => ({ sampleRate: jest.fn() }) );
jest.mock( '../../../components/ProgressBar', () => () => null );

global.IS_REACT_ACT_ENVIRONMENT = true;
global.optimoleDashboardApp = {
strings: {
options_strings: new Proxy({}, { get: ( target, name ) => name })
}
};

const Compression = require( '../Compression' ).default;

describe( 'Compression', () => {
let container;
let root;

beforeEach( () => {
container = document.createElement( 'div' );
document.body.appendChild( container );
root = createRoot( container );
});

afterEach( () => {
act( () => root.unmount() );
container.remove();
});

test( 'offers the three compression modes without ToggleGroupControl', () => {
const setSettings = jest.fn();

act( () => {
root.render(
<Compression
settings={ { compression_mode: 'quality_optimized' } }
setSettings={ setSettings }
setCanSave={ jest.fn() }
showSample={ false }
isSampleLoading={ false }
setIsSampleLoading={ jest.fn() }
/>
);
});

const radios = container.querySelectorAll( 'input[type="radio"]' );
expect( Array.from( radios, radio => radio.value ) ).toEqual([ 'speed_optimized', 'quality_optimized', 'custom' ]);
expect( container.querySelector( 'input:checked' ).value ).toBe( 'quality_optimized' );

act( () => radios[0].click() );

expect( setSettings ).toHaveBeenCalledWith( expect.objectContaining({
compression_mode: 'speed_optimized',
retina_images: 'disabled'
}) );
});
});
16 changes: 12 additions & 4 deletions assets/src/dashboard/utils/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ const mockActions = {};

jest.mock( '@wordpress/api-fetch', () => jest.fn() );
jest.mock( '@wordpress/data', () => ({
dispatch: () => new Proxy({}, {

// Like the default registry, an unregistered store name resolves to null.
dispatch: store => [ 'optimole', 'core/notices' ].includes( store ) ? new Proxy({}, {
get: ( target, name ) => {
if ( ! mockActions[ name ]) {
mockActions[ name ] = jest.fn();
}
return mockActions[ name ];
}
}),
}) : null,
select: () => ({
getOptimizedImages: () => [],
getQueryArgs: () => ({}),
Expand All @@ -24,8 +26,8 @@ jest.mock( '@wordpress/data', () => ({
}) );
jest.mock( '@wordpress/url', () => ({ addQueryArgs: url => url }) );

// Provided by WordPress at runtime, not installed.
jest.mock( '@wordpress/notices', () => ({ store: 'core/notices' }), { virtual: true });
// Provided by WordPress at runtime, not installed; before 5.7 it has no `store` export.
jest.mock( '@wordpress/notices', () => ({}), { virtual: true });
jest.mock( '../helpers', () => ({ toggleDashboardSidebarSubmenu: jest.fn() }) );

global.optimoleDashboardApp = {
Expand All @@ -37,6 +39,12 @@ global.jQuery = { ajax: jest.fn() };
const apiFetch = require( '@wordpress/api-fetch' );
const { callSync } = require( '../api' );

describe( 'api module', () => {
test( 'loads when @wordpress/notices has no store export', () => {
expect( () => jest.isolateModules( () => require( '../api' ) ) ).not.toThrow();
});
});

const flushPromises = () => new Promise( resolve => setTimeout( resolve, 0 ) );

describe( 'callSync', () => {
Expand Down
6 changes: 4 additions & 2 deletions assets/src/dashboard/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
} from '@wordpress/data';

import { addQueryArgs } from '@wordpress/url';
import { store as noticesStore } from '@wordpress/notices';

// Registers the core/notices store; its `store` export only exists from WordPress 5.7.
import '@wordpress/notices';
import { toggleDashboardSidebarSubmenu } from './helpers';

const {
Expand Down Expand Up @@ -52,7 +54,7 @@ const {
getSiteSettings
} = select( 'optimole' );

const { createNotice } = dispatch( noticesStore );
const { createNotice } = dispatch( 'core/notices' );


export const sendOnboardingImages = ( data = {}) => {
Expand Down
11 changes: 8 additions & 3 deletions assets/src/widget/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createRoot } from '@wordpress/element';
import { createRoot, render } from '@wordpress/element';
import App from './App';
import './style.scss';

const root = createRoot( document.getElementById( 'optimole-dashboard-widget-root' ) );
const container = document.getElementById( 'optimole-dashboard-widget-root' );

root.render( <App /> );
// createRoot ships with WordPress 6.2 (React 18).
if ( createRoot ) {
createRoot( container ).render( <App /> );
} else {
render( <App />, container );
}
41 changes: 20 additions & 21 deletions inc/media_rename/attachment_db_renamer.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ private function get_columns( $table ) {
$text_columns = [];

// Get table information
$results = $wpdb->get_results( $wpdb->prepare( 'DESCRIBE %i', $table ) );
$results = $wpdb->get_results( 'DESCRIBE ' . esc_sql( $this->quote_identifier( $table ) ) );

if ( ! empty( $results ) ) {
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
Expand All @@ -132,6 +132,19 @@ private function get_columns( $table ) {
return [ $primary_keys, $text_columns ];
}

/**
* Quote a table or column name for use in SQL.
*
* Replaces the %i placeholder, which wpdb::prepare() only supports from WordPress 6.2.
*
* @param string $identifier Table or column name.
*
* @return string The backtick-quoted identifier.
*/
private function quote_identifier( string $identifier ): string {
return '`' . str_replace( '`', '``', $identifier ) . '`';
}

/**
* Check if column is text type
*
Expand Down Expand Up @@ -166,12 +179,8 @@ private function process_column( $table, $column, $primary_keys, $old_url, $new_

// Check for serialized data
$has_serialized = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(%i) FROM %i WHERE %i REGEXP '^[aiO]:[1-9]' LIMIT 1",
$column,
$table,
$column
)
'SELECT COUNT(' . esc_sql( $this->quote_identifier( $column ) ) . ') FROM ' . esc_sql( $this->quote_identifier( $table ) ) .
' WHERE ' . esc_sql( $this->quote_identifier( $column ) ) . " REGEXP '^[aiO]:[1-9]' LIMIT 1"
);

// Process with PHP if serialized data is found
Expand Down Expand Up @@ -256,9 +265,7 @@ private function sql_handle_column( $table, $column, $primary_keys, $old_url, $n
// Get rows with regular URLs
$rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM %i WHERE %i LIKE %s',
$table,
$column,
'SELECT * FROM ' . esc_sql( $this->quote_identifier( $table ) ) . ' WHERE ' . esc_sql( $this->quote_identifier( $column ) ) . ' LIKE %s',
'%' . $wpdb->esc_like( $base_url ) . '%'
)
);
Comment thread
pirate-bot marked this conversation as resolved.
Resolved
Expand All @@ -269,9 +276,7 @@ private function sql_handle_column( $table, $column, $primary_keys, $old_url, $n
// Get rows with JSON-escaped URLs
$json_rows = $wpdb->get_results(
$wpdb->prepare(
'SELECT * FROM %i WHERE %i LIKE %s',
$table,
$column,
'SELECT * FROM ' . esc_sql( $this->quote_identifier( $table ) ) . ' WHERE ' . esc_sql( $this->quote_identifier( $column ) ) . ' LIKE %s',
'%' . $wpdb->esc_like( $json_base_url ) . '%'
)
);
Expand Down Expand Up @@ -330,17 +335,14 @@ private function php_handle_column( $table, $column, $primary_keys, $old_url, $n
// Build the query and allow processing with multiple primary keys.
$query = 'SELECT ';
foreach ( $primary_keys as $key => $value ) {
$query .= $wpdb->prepare( '%i, ', $value );
$query .= $this->quote_identifier( $value ) . ', ';
}
$query .= '%i FROM %i WHERE %i LIKE %s';
$query .= $this->quote_identifier( $column ) . ' FROM ' . $this->quote_identifier( $table ) . ' WHERE ' . $this->quote_identifier( $column ) . ' LIKE %s';

// Get the rows that need updating - first for regular URLs
$rows = $wpdb->get_results(
$wpdb->prepare(
$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$column,
$table,
$column,
'%' . $wpdb->esc_like( $base_url ) . '%'
)
);
Expand All @@ -349,9 +351,6 @@ private function php_handle_column( $table, $column, $primary_keys, $old_url, $n
$json_rows = $wpdb->get_results(
$wpdb->prepare(
$query, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$column,
$table,
$column,
'%' . $wpdb->esc_like( $json_base_url ) . '%'
)
);
Expand Down
22 changes: 21 additions & 1 deletion inc/video_player.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,31 @@ function ( $key, $value ) {
// Alignment and custom classes already come from block supports, so no attribute is forwarded here.
return sprintf(
'<div %s><optimole-video-player %s></optimole-video-player></div>',
get_block_wrapper_attributes(),
function_exists( 'get_block_wrapper_attributes' ) ? get_block_wrapper_attributes() : $this->get_legacy_wrapper_attributes( $attributes ),
implode( ' ', $tag_attributes ),
);
}

/**
* Build the wrapper class attribute on WordPress versions without get_block_wrapper_attributes() (below 5.6).
*
* @param array<string, mixed> $attributes The block attributes.
* @return string The wrapper class attribute.
*/
private function get_legacy_wrapper_attributes( array $attributes ): string {
$classes = [ 'wp-block-optimole-video-player' ];

if ( isset( $attributes['align'] ) && in_array( $attributes['align'], [ 'left', 'center', 'right', 'wide', 'full' ], true ) ) {
$classes[] = 'align' . $attributes['align'];
}

if ( ! empty( $attributes['className'] ) && is_string( $attributes['className'] ) ) {
$classes[] = $attributes['className'];
}

return 'class="' . esc_attr( implode( ' ', $classes ) ) . '"';
}

/**
* Get the localization strings.
*
Expand Down
Loading
Loading