From d08bb3a510c964ea822eff653de670ec8c022d7f Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:32:27 +0530 Subject: [PATCH 01/13] Returns boolean output based on result of transformation --- assets/js/editor/transform/ClassicBlockTransformer.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/js/editor/transform/ClassicBlockTransformer.js b/assets/js/editor/transform/ClassicBlockTransformer.js index 6cac53b..c40ca6d 100644 --- a/assets/js/editor/transform/ClassicBlockTransformer.js +++ b/assets/js/editor/transform/ClassicBlockTransformer.js @@ -10,6 +10,7 @@ class ClassicBlockTransformer { */ constructor() { this.wp = window.wp; + this.didTransform = false; } /** @@ -23,6 +24,8 @@ class ClassicBlockTransformer { /* Currently set to do 3 levels of recursion */ this.convertBlocks(blocks, 1, 3); } + + return this.didTransform; } /** @@ -65,6 +68,8 @@ class ClassicBlockTransformer { this.wp.data .dispatch('core/block-editor') .replaceBlocks(block.clientId, this.blockHandler(block)); + + this.didTransform = true; } else if (block.innerBlocks && block.innerBlocks.length > 0) { this.convertBlocks(block.innerBlocks); } From 89a194b192cc90a18860e2043e61fe295e6e3a13 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:33:12 +0530 Subject: [PATCH 02/13] Adds bulk migration browser client --- assets/js/editor/transform/MigrationClient.js | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 assets/js/editor/transform/MigrationClient.js diff --git a/assets/js/editor/transform/MigrationClient.js b/assets/js/editor/transform/MigrationClient.js new file mode 100644 index 0000000..7cc5aa3 --- /dev/null +++ b/assets/js/editor/transform/MigrationClient.js @@ -0,0 +1,104 @@ +const { wp, location } = window; + +/** + * MigrationClient provides the client-side support for the BE MigrationAgent. + */ +class MigrationClient { + /** + * Initializes the client with the specified config settings. + * + * @param {object} config The convert to blocks config + */ + constructor(config) { + this.config = config; + this.saved = false; + this.didNext = false; + } + + /** + * Saves the curent post by manually dispatching savePost. + */ + save() { + // don't rerun after save + if (this.saved) { + return; + } + + this.saved = true; + + const { dispatch, subscribe } = wp.data; + const editor = dispatch('core/editor'); + + subscribe(this.didSave.bind(this)); + editor.savePost(); + } + + /** + * On Post save, runs the next post migration. + */ + didSave() { + const { select } = wp.data; + const isSavingPost = select('core/editor').isSavingPost(); + const isAutosavingPost = select('core/editor').isAutosavingPost(); + + if (isAutosavingPost && !isSavingPost) { + return; + } + + if (this.hasNext()) { + this.next(); + } + } + + /** + * Checks if there is a post in the queue. + * + * @returns {boolean} True or false if next is present. + */ + hasNext() { + if (this.didNext) { + return false; + } + + if (!this.hasNextConfig()) { + return false; + } + + return this.config.agent.next; + } + + /** + * Navigates to the next post to migrate. + */ + next() { + if (!this.hasNextConfig()) { + return; + } + + this.didNext = true; + location.href = this.config.agent.next; + } + + /** + * Checks if the next migration post data is present in config. + * + * @returns {boolean} True or false if next config is present + */ + hasNextConfig() { + if (!this.config) { + return false; + } + + if (!this.config.agent) { + return false; + } + + if (!this.config.agent.next) { + return false; + } + + return true; + } +} + +export default MigrationClient; From f87a8edf98465da80be5c1515a5d0957dab24975 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:33:40 +0530 Subject: [PATCH 03/13] Adds bulk migration agent --- includes/ConvertToBlocks/MigrationAgent.php | 217 ++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 includes/ConvertToBlocks/MigrationAgent.php diff --git a/includes/ConvertToBlocks/MigrationAgent.php b/includes/ConvertToBlocks/MigrationAgent.php new file mode 100644 index 0000000..0430122 --- /dev/null +++ b/includes/ConvertToBlocks/MigrationAgent.php @@ -0,0 +1,217 @@ +has_ctb_client_param() ) { + return; + } + + if ( ! $this->is_running() ) { + return; + } + + wp_localize_script( + 'convert_to_blocks_editor', + 'convert_to_blocks_agent', + [ + 'agent' => [ + 'next' => $this->next(), + ], + ] + ); + } + + /** + * Always register since this check needs to be happen later in lifecycle. + * + * @return bool + */ + public function can_register() { + return true; + } + + /** + * Starts the batch conversion + * + * @param array $opts Optional opts. + * @return bool + */ + public function start( $opts = [] ) { + $posts_to_update = $this->get_posts_to_update( $opts ); + + if ( empty( $posts_to_update ) ) { + return false; + } + + update_option( 'ctb_running', 1 ); + update_option( 'ctb_posts_to_update', $posts_to_update ); + update_option( 'ctb_cursor', -1 ); + + return $this->next(); + } + + /** + * Stops the batch conversion if running and resets the previous session. + * + * @return bool + */ + public function stop() { + update_option( 'ctb_running', 0 ); + update_option( 'ctb_posts_to_update', [] ); + update_option( 'ctb_cursor', -1 ); + } + + /** + * Returns the current status of the batch conversion. + * + * @return array + */ + public function get_status() { + $running = get_option( 'ctb_running' ); + $posts_to_update = get_option( 'ctb_posts_to_update' ); + + if ( empty( $posts_to_update ) ) { + $posts_to_update = []; + } + + $total = count( $posts_to_update ); + $cursor = get_option( 'ctb_cursor' ); + + if ( $total > 0 ) { + $progress = round( ( $cursor + 1 ) / $total * 100 ); + } else { + $progress = 0; + } + + return [ + 'running' => $running, + 'cursor' => $cursor, + 'total' => $total, + 'progress' => $progress, + 'active' => $this->get_client_link( $posts_to_update[ $cursor ] ?? 0 ), + ]; + } + + /** + * Returns a boolean based on whether a migration is currently running. + * + * @return bool + */ + public function is_running() { + $running = get_option( 'ctb_running' ); + return ! empty( $running ); + } + + /** + * Updates the progress cursor to jump to the next post in the queue. + */ + public function next() { + $posts_to_update = get_option( 'ctb_posts_to_update' ); + $total = count( $posts_to_update ); + $cursor = get_option( 'ctb_cursor' ); + + if ( $cursor + 1 < $total ) { + $next_cursor = ++$cursor; + update_option( 'ctb_cursor', $next_cursor ); + + return $this->get_client_link( $posts_to_update[ $next_cursor ] ); + } elseif ( $cursor + 1 === $total ) { + update_option( 'ctb_running', 0 ); + return false; + } else { + return false; + } + } + + /** + * Returns the next post URL to migrate. + * + * @param int $post_id The next post id. + * @return string + */ + public function get_client_link( $post_id ) { + if ( empty( $post_id ) ) { + return ''; + } + + $edit_post_link = admin_url( 'post.php' ); + + $args = [ + 'post' => $post_id, + 'action' => 'edit', + 'ctb_client' => $post_id, + ]; + + return add_query_arg( $args, $edit_post_link ); + } + + /** + * Returns the list of post ids that need to be migrated. + * + * @param array $opts Optional opts + * @return array + */ + public function get_posts_to_update( $opts = [] ) { + if ( ! empty( $opts['post_type'] ) ) { + $post_type = explode( ',', $opts['post_type'] ); + $post_type = array_filter( $post_type ); + + if ( empty( $post_type ) ) { + $post_type = [ 'post', 'page' ]; + } + } else { + $post_type = [ 'post', 'page' ]; + } + + $query_params = [ + 'post_type' => $post_type, + 'post_status' => 'publish', + 'fields' => 'ids', + 'posts_per_page' => -1, + 'ignore_sticky_posts' => true, + ]; + + if ( ! empty( $opts['only'] ) ) { + $post_in = explode( ',', $opts['only'] ); + $post_in = array_map( 'intval', $post_in ); + $post_in = array_filter( $post_in ); + + $query_params['post__in'] = $post_in; + } + + $query = new \WP_Query( $query_params ); + $posts = $query->posts; + + return $posts; + } + + /** + * Returns a boolean based on whether the current url has the ctb_client + * editor parameter + * + * @return bool + */ + public function has_ctb_client_param() { + // phpcs:disable + $ctb_client = sanitize_text_field( isset( $_GET['ctb_client'] ) ? $_GET['ctb_client'] : '' ) ; + // phpcs:enable + $ctb_client = intval( $ctb_client ); + + return ! empty( $ctb_client ); + } + +} From 40171011bfa12625a914d4b07d8e9cfda17be067 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:33:59 +0530 Subject: [PATCH 04/13] Adds bulk migration WP CLI command --- includes/ConvertToBlocks/MigrationCommand.php | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 includes/ConvertToBlocks/MigrationCommand.php diff --git a/includes/ConvertToBlocks/MigrationCommand.php b/includes/ConvertToBlocks/MigrationCommand.php new file mode 100644 index 0000000..695b495 --- /dev/null +++ b/includes/ConvertToBlocks/MigrationCommand.php @@ -0,0 +1,124 @@ +] + * : Optional comma delimited list of post types to migrate. Defaults to post,page + * + * [] + * : Optional comma delimited list of post ids to migrate. + * + * @synopsis [--post_type=] + * @synopsis [--only=] + */ + public function start( $args = [], $opts = [] ) { + $agent = new MigrationAgent(); + $delay = 5; // 5 second delay between each tick + + if ( $agent->is_running() ) { + \WP_CLI::error( 'Please stop the currently running migration first.' ); + } + + $result = $agent->start( $opts ); + + if ( empty( $result ) ) { + \WP_CLI::error( 'No posts to migrate.' ); + } + + $status = $agent->get_status( $opts ); + + if ( ! $status['running'] ) { + \WP_CLI::error( 'Failed to start migration.' ); + } + + \WP_CLI::log( 'Migration started.' ); + \WP_CLI::log( 'Please open the following URL in a browser to start the migration agent.' ); + \WP_CLI::line( '' ); + \WP_CLI::log( $result ); + \WP_CLI::line( '' ); + + $total = $status['total']; + + $message = "Converting $total Posts ..."; + $progress_bar = \WP_CLI\Utils\make_progress_bar( $message, $total ); + $progress_bar->tick(); + + $prev_progress = -1; + + while ( true ) { + $status = $agent->get_status(); + + if ( ! $status['running'] ) { + break; + } + + $progress = $status['progress']; + + if ( $progress !== $prev_progress ) { + $progress_bar->tick( $progress - $prev_progress ); + $prev_progress = $progress; + } + + sleep( $delay ); + + // required as we need to reload options that the browser client is updating + wp_cache_delete( 'alloptions', 'options' ); + } + + $progress_bar->finish(); + + \WP_CLI::success( 'Migration finished successfully.' ); + + // cleanup the options used during migration + $agent->stop(); + } + + /** + * Stops the currently running migration if active. + */ + public function stop( $args = [], $opts = [] ) { + $agent = new MigrationAgent(); + + if ( ! $agent->is_running() ) { + \WP_CLI::warning( 'No migrations are currently running' ); + return; + } + + $agent->stop( $opts ); + + \WP_CLI::success( 'Migration stopped successfully' ); + } + + /** + * Prints the status of the currently running migration. + */ + public function status( $args = [], $opts = [] ) { + $agent = new MigrationAgent(); + $status = $agent->get_status( $opts ); + + if ( ! $status['running'] ) { + \WP_CLI::log( 'No migrations are currently running.' ); + return; + } + + \WP_CLI::log( 'Migration is currently running ...' ); + \WP_CLI::log( $status['progress'] . ' [' . ( $status['cursor'] + 1 ) . '/' . $status['total'] . ']' ); + \WP_CLI::log( 'Active: ' . $status['active'] ); + } + +} From ccad5ab13491b5f60653a6a2d4f511331f1323bf Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:35:22 +0530 Subject: [PATCH 05/13] Adds migration support to main plugin --- includes/ConvertToBlocks/Plugin.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/ConvertToBlocks/Plugin.php b/includes/ConvertToBlocks/Plugin.php index 446be68..fe06557 100644 --- a/includes/ConvertToBlocks/Plugin.php +++ b/includes/ConvertToBlocks/Plugin.php @@ -116,6 +116,7 @@ public function init_admin() { new RevisionSupport(), new ClassicEditorSupport(), new Assets(), + new MigrationAgent(), ] ); } @@ -124,6 +125,7 @@ public function init_admin() { * Initializes the Plugin WP CLI commands */ public function init_commands() { + \WP_CLI::add_command( 'convert-to-blocks', '\ConvertToBlocks\MigrationCommand' ); } /* Helpers */ From 245da61a573ab0c70fc6922d74ac0a5b2399f568 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:36:32 +0530 Subject: [PATCH 06/13] Adds migration agent to plugin js --- assets/js/editor/editor.js | 52 ++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/assets/js/editor/editor.js b/assets/js/editor/editor.js index 95c9de7..187b6a3 100644 --- a/assets/js/editor/editor.js +++ b/assets/js/editor/editor.js @@ -1,25 +1,28 @@ import ClassicBlockTransformer from './transform/ClassicBlockTransformer'; +import MigrationClient from './transform/MigrationClient'; + +let loaded = false; /** - * ConnectToBlocksSupport connects the JS implementation of - * Connect to Blocks to Gutenberg JS. + * ConvertToBlocksSupport connects the JS implementation of + * Convert to Blocks to Gutenberg JS. */ -class ConnectToBlocksEditorSupport { +class ConvertToBlocksEditorSupport { /** - * Returns the singleton instance of ConnectToBlocksEditorSupport. + * Returns the singleton instance of ConvertToBlocksEditorSupport. * - * @returns {ConnectToBlocksEditorSupport} + * @returns {ConvertToBlocksEditorSupport} */ static getInstance() { if (!this.instance) { - this.instance = new ConnectToBlocksEditorSupport(); + this.instance = new ConvertToBlocksEditorSupport(); } return this.instance; } /** - * Activates the ConnectToBlocksEditorSupport + * Activates the ConvertToBlocksEditorSupport */ enable() { document.addEventListener('DOMContentLoaded', this.didBlockEditorLoad.bind(this)); @@ -38,14 +41,43 @@ class ConnectToBlocksEditorSupport { registerPlugin('convert-to-blocks', { render: () => { - transformer.execute(); + // Don't render more than once, to avoid triggering multiple migrations + if (loaded) { + return null; + } + + loaded = true; + + // This delay allows Gutenberg to initialize legacy content into freeform blocks + setTimeout(() => { + const result = transformer.execute(); + const config = window.convert_to_blocks_agent || false; + + // if no migration config, then ignore this request + if (!config) { + return null; + } + + const client = new MigrationClient(config); + + // if no blocks transformed, then we can jump to the next post + if (!result) { + client.next(); + return null; + } + + client.save(); + + return null; + }, 500); + return null; }, }); } } -const support = ConnectToBlocksEditorSupport.getInstance(); +const support = ConvertToBlocksEditorSupport.getInstance(); support.enable(); -export default ConnectToBlocksEditorSupport; +export default ConvertToBlocksEditorSupport; From f82e5717ca6ea455106026864664e6740c66f0eb Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:39:12 +0530 Subject: [PATCH 07/13] Adds jsdoc --- assets/js/editor/transform/ClassicBlockTransformer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/js/editor/transform/ClassicBlockTransformer.js b/assets/js/editor/transform/ClassicBlockTransformer.js index c40ca6d..af6d68a 100644 --- a/assets/js/editor/transform/ClassicBlockTransformer.js +++ b/assets/js/editor/transform/ClassicBlockTransformer.js @@ -15,6 +15,8 @@ class ClassicBlockTransformer { /** * Runs the Classic to Gutenberg Block transform on the current document. + * + * @returns {boolean} The result of the transformation. */ execute() { const coreEditor = this.wp.data.select('core/block-editor'); From 481376c01322fc3a0d0adc6c2a4750a67f37f008 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:40:13 +0530 Subject: [PATCH 08/13] Fixes return statement --- includes/ConvertToBlocks/MigrationAgent.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/ConvertToBlocks/MigrationAgent.php b/includes/ConvertToBlocks/MigrationAgent.php index 0430122..2a40648 100644 --- a/includes/ConvertToBlocks/MigrationAgent.php +++ b/includes/ConvertToBlocks/MigrationAgent.php @@ -67,7 +67,6 @@ public function start( $opts = [] ) { /** * Stops the batch conversion if running and resets the previous session. * - * @return bool */ public function stop() { update_option( 'ctb_running', 0 ); From dd9d4b03809e9b86e12c754bfaa94bd9f1bbb2ee Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Wed, 8 Jun 2022 18:42:27 +0530 Subject: [PATCH 09/13] Fixes linter warnings --- includes/ConvertToBlocks/MigrationAgent.php | 1 - includes/ConvertToBlocks/MigrationCommand.php | 9 +++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/includes/ConvertToBlocks/MigrationAgent.php b/includes/ConvertToBlocks/MigrationAgent.php index 2a40648..a5c319e 100644 --- a/includes/ConvertToBlocks/MigrationAgent.php +++ b/includes/ConvertToBlocks/MigrationAgent.php @@ -66,7 +66,6 @@ public function start( $opts = [] ) { /** * Stops the batch conversion if running and resets the previous session. - * */ public function stop() { update_option( 'ctb_running', 0 ); diff --git a/includes/ConvertToBlocks/MigrationCommand.php b/includes/ConvertToBlocks/MigrationCommand.php index 695b495..d0a857a 100644 --- a/includes/ConvertToBlocks/MigrationCommand.php +++ b/includes/ConvertToBlocks/MigrationCommand.php @@ -25,6 +25,9 @@ class MigrationCommand extends \WP_CLI_Command { * * @synopsis [--post_type=] * @synopsis [--only=] + * + * @param array $args The command args + * @param array $opts The command opts */ public function start( $args = [], $opts = [] ) { $agent = new MigrationAgent(); @@ -90,6 +93,9 @@ public function start( $args = [], $opts = [] ) { /** * Stops the currently running migration if active. + * + * @param array $args The command args + * @param array $opts The command opts */ public function stop( $args = [], $opts = [] ) { $agent = new MigrationAgent(); @@ -106,6 +112,9 @@ public function stop( $args = [], $opts = [] ) { /** * Prints the status of the currently running migration. + * + * @param array $args The command args + * @param array $opts The command opts */ public function status( $args = [], $opts = [] ) { $agent = new MigrationAgent(); From 11279041982eb10d53c516b65bc73a7385c7b355 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Fri, 24 Jun 2022 14:45:58 +0530 Subject: [PATCH 10/13] Improves progress bar --- includes/ConvertToBlocks/MigrationCommand.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/includes/ConvertToBlocks/MigrationCommand.php b/includes/ConvertToBlocks/MigrationCommand.php index d0a857a..af0cbbf 100644 --- a/includes/ConvertToBlocks/MigrationCommand.php +++ b/includes/ConvertToBlocks/MigrationCommand.php @@ -61,7 +61,8 @@ public function start( $args = [], $opts = [] ) { $progress_bar = \WP_CLI\Utils\make_progress_bar( $message, $total ); $progress_bar->tick(); - $prev_progress = -1; + $prev_progress = 0; + $ticks = 0; while ( true ) { $status = $agent->get_status(); @@ -72,12 +73,26 @@ public function start( $args = [], $opts = [] ) { $progress = $status['progress']; + // since the WP CLI progress bar can't tick upto a progress % we need to + // tick in steps upto the progress % of total if ( $progress !== $prev_progress ) { - $progress_bar->tick( $progress - $prev_progress ); + $required_ticks = floor( $progress / 100 * $total ); + + while ( $ticks < $required_ticks ) { + $progress_bar->tick(); + $ticks++; + } + $prev_progress = $progress; } - sleep( $delay ); + if ( $ticks < $total ) { + // sleeping helps reduce load on server + sleep( $delay ); + } else { + // don't need the full sleep delay on last tick + sleep( 1 ); + } // required as we need to reload options that the browser client is updating wp_cache_delete( 'alloptions', 'options' ); From 97052e242ce673e730968d7f1c18c67d072a9327 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Fri, 24 Jun 2022 14:55:57 +0530 Subject: [PATCH 11/13] Updates log message --- includes/ConvertToBlocks/MigrationCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/ConvertToBlocks/MigrationCommand.php b/includes/ConvertToBlocks/MigrationCommand.php index af0cbbf..f6b3571 100644 --- a/includes/ConvertToBlocks/MigrationCommand.php +++ b/includes/ConvertToBlocks/MigrationCommand.php @@ -49,7 +49,7 @@ public function start( $args = [], $opts = [] ) { \WP_CLI::error( 'Failed to start migration.' ); } - \WP_CLI::log( 'Migration started.' ); + \WP_CLI::log( 'Migration started...' ); \WP_CLI::log( 'Please open the following URL in a browser to start the migration agent.' ); \WP_CLI::line( '' ); \WP_CLI::log( $result ); From fa6d40697588440c53360f95077a259ab4dfb5d6 Mon Sep 17 00:00:00 2001 From: Darshan Sawardekar Date: Tue, 5 Jul 2022 13:12:31 +0530 Subject: [PATCH 12/13] Fixes linter warnings --- includes/ConvertToBlocks/MigrationCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/ConvertToBlocks/MigrationCommand.php b/includes/ConvertToBlocks/MigrationCommand.php index f6b3571..b7325b5 100644 --- a/includes/ConvertToBlocks/MigrationCommand.php +++ b/includes/ConvertToBlocks/MigrationCommand.php @@ -62,7 +62,7 @@ public function start( $args = [], $opts = [] ) { $progress_bar->tick(); $prev_progress = 0; - $ticks = 0; + $ticks = 0; while ( true ) { $status = $agent->get_status(); From b630da6653a73181b749735c659b6131162f283c Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Fri, 8 Jul 2022 09:50:53 -0600 Subject: [PATCH 13/13] Apply suggestions from code review Co-authored-by: Faisal Alvi --- includes/ConvertToBlocks/MigrationAgent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/ConvertToBlocks/MigrationAgent.php b/includes/ConvertToBlocks/MigrationAgent.php index a5c319e..7aed24f 100644 --- a/includes/ConvertToBlocks/MigrationAgent.php +++ b/includes/ConvertToBlocks/MigrationAgent.php @@ -205,7 +205,7 @@ public function get_posts_to_update( $opts = [] ) { */ public function has_ctb_client_param() { // phpcs:disable - $ctb_client = sanitize_text_field( isset( $_GET['ctb_client'] ) ? $_GET['ctb_client'] : '' ) ; + $ctb_client = sanitize_text_field( $_GET['ctb_client'] ?? '' ); // phpcs:enable $ctb_client = intval( $ctb_client );