diff --git a/assets/css/msls.css b/assets/css/msls.css index 1a5af2ab5..853106066 100644 --- a/assets/css/msls.css +++ b/assets/css/msls.css @@ -1 +1 @@ -div#msls.postbox label{margin-right:6px}div#msls.postbox input.msls_title,div#msls.postbox select{width:100%}select.msls-translations{width:226px}#msls.postbox .inside li{display:flex;align-items:center}#msls.postbox .inside li label{display:flex}#msls.postbox .inside li input.msls_title,#msls.postbox .inside li select{flex-grow:1}#msls-content-import .button-primary{margin:1em auto}.flag-icon{width:1.3333em!important;height:1em!important;vertical-align:middle;overflow:hidden;line-height:1!important;color:transparent}.msls-icon-wrapper{display:inline-flex;justify-content:center;align-items:center;text-align:center}.msls-icon-wrapper.flag{min-width:36px}.msls-icon-wrapper.label{min-width:48px}label .msls-icon-wrapper{text-align:left}#wpadminbar * .language-badge,#wpadminbar .language-badge,.language-badge{display:inline-block;min-width:32px;height:auto;padding:4px 6px;white-space:nowrap;font-size:10px;line-height:1;text-align:center;background-color:currentColor;border-radius:9px;user-select:none}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span,.language-badge>span{display:inline-block;vertical-align:top;margin:0 1px;font-size:10px;font-weight:600;line-height:1;text-transform:uppercase;color:#fff;text-align:center}#wpadminbar * .language-badge>span:nth-child(2),#wpadminbar .language-badge>span:nth-child(2),.language-badge>span:nth-child(2){opacity:.5}.column-mslscol .language-badge{margin:0 1px!important}.column-mslscol{width:56px}#wpadminbar * .language-badge,#wpadminbar .language-badge{position:relative;top:-1px;padding-top:3px;padding-bottom:3px;background-color:transparent;border:1px currentColor solid}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span{color:currentColor} \ No newline at end of file +div#msls.postbox label{margin-right:6px}div#msls.postbox input.msls_title,div#msls.postbox select{width:100%}select.msls-translations{width:226px}#msls.postbox .inside li{display:flex;align-items:center}#msls.postbox .inside li label{display:flex}#msls.postbox .inside li input.msls_title,#msls.postbox .inside li select{flex-grow:1}#msls-content-import .button-primary{margin:1em auto}.flag-icon{width:1.3333em!important;height:1em!important;vertical-align:middle;overflow:hidden;line-height:1!important;color:transparent}.msls-icon-wrapper{display:inline-flex;justify-content:center;align-items:center;text-align:center}.msls-icon-wrapper.flag{min-width:36px}.msls-icon-wrapper.label{min-width:48px}label .msls-icon-wrapper{text-align:left}#wpadminbar * .language-badge,#wpadminbar .language-badge,.language-badge{display:inline-block;min-width:32px;height:auto;padding:4px 6px;white-space:nowrap;font-size:10px;line-height:1;text-align:center;background-color:currentColor;border-radius:9px;user-select:none}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span,.language-badge>span{display:inline-block;vertical-align:top;margin:0 1px;font-size:10px;font-weight:600;line-height:1;text-transform:uppercase;color:#fff;text-align:center}#wpadminbar * .language-badge>span:nth-child(2),#wpadminbar .language-badge>span:nth-child(2),.language-badge>span:nth-child(2){opacity:.5}.column-mslscol .language-badge{margin:0 1px!important}.column-mslscol{width:56px}#wpadminbar * .language-badge,#wpadminbar .language-badge{position:relative;top:-1px;padding-top:3px;padding-bottom:3px;background-color:transparent;border:1px currentColor solid}#wpadminbar * .language-badge>span,#wpadminbar .language-badge>span{color:currentColor}.msls-quick-create{background:none;border:none;padding:0;margin:0;cursor:pointer;color:inherit;font:inherit;line-height:inherit}.msls-quick-create.msls-loading .dashicons{animation:msls-spin 1s linear infinite}@keyframes msls-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}} \ No newline at end of file diff --git a/assets/js/msls-quick-create.js b/assets/js/msls-quick-create.js new file mode 100644 index 000000000..736fd9d5d --- /dev/null +++ b/assets/js/msls-quick-create.js @@ -0,0 +1 @@ +jQuery(document).ready(function($){$(document).on("click",".msls-quick-create",function(){var $button=$(this);if($button.hasClass("msls-loading")){return}$button.addClass("msls-loading");$button.find(".dashicons").removeClass("dashicons-plus").addClass("dashicons-update");wp.apiFetch({path:"/msls/v1/create-translation",method:"POST",data:{source_post_id:parseInt($button.data("source-post-id"),10),source_blog_id:parseInt($button.data("source-blog-id"),10),target_blog_id:parseInt($button.data("target-blog-id"),10)}}).then(function(response){var $link=$("").attr("href",response.edit_url).attr("title",$button.attr("title").replace(/Create/,"Edit")).html($button.html());$link.find(".dashicons").removeClass("dashicons-update dashicons-plus").addClass("dashicons-edit");$button.replaceWith($link);var $container=$link.closest("li");if(!$container.length){return}var $hiddenInput=$container.find('input[type="hidden"][name^="msls_input_"]');if($hiddenInput.length){$hiddenInput.val(response.post_id)}var $select=$container.find('select[name^="msls_input_"]');if($select.length){$select.append($("%3$s ', esc_attr( $title ), esc_url( $href ), $this->get_icon() ); } + /** + * @return bool + */ + protected function should_quick_create(): bool { + return null !== $this->id + && null !== $this->origin_language + && msls_options()->activate_quick_create; + } + + /** + * @return string + */ + protected function get_quick_create_a(): string { + $collection = msls_blog_collection(); + $source_blog_id = $collection->get_blog_id( $this->origin_language ); + $target_blog_id = get_current_blog_id(); + + /* translators: %s: blog name */ + $format = __( 'Create a new translation in the %s-blog', 'multisite-language-switcher' ); + $title = sprintf( $format, $this->language ); + + return sprintf( + ' ', + esc_attr( $title ), + $target_blog_id, + $this->id, + $source_blog_id, + $this->get_icon() + ); + } + /** * Get icon as html-tag * diff --git a/includes/MslsOptions.php b/includes/MslsOptions.php index ba0727b5b..286b0956f 100644 --- a/includes/MslsOptions.php +++ b/includes/MslsOptions.php @@ -14,6 +14,7 @@ * @package Msls * @property bool $activate_autocomplete * @property bool $activate_content_import + * @property bool $activate_quick_create * @property bool $output_current_blog * @property bool $only_with_translation * @property int $content_priority diff --git a/includes/MslsPlugin.php b/includes/MslsPlugin.php index 670ca6398..365e47f5e 100644 --- a/includes/MslsPlugin.php +++ b/includes/MslsPlugin.php @@ -44,6 +44,7 @@ public static function init(): void { add_action( 'admin_enqueue_scripts', array( $obj, 'custom_enqueue' ) ); add_action( 'wp_enqueue_scripts', array( $obj, 'custom_enqueue' ) ); + add_action( 'rest_api_init', array( MslsRestApi::class, 'init' ) ); add_action( 'init', array( MslsAdminBar::class, 'init' ) ); add_action( 'init', array( MslsBlock::class, 'init' ) ); add_action( 'init', array( MslsShortCode::class, 'init' ) ); @@ -124,6 +125,10 @@ public function custom_enqueue(): void { if ( $this->options->activate_autocomplete ) { wp_enqueue_script( 'msls-autocomplete', self::plugins_url( "$folder/msls.js" ), array( 'jquery-ui-autocomplete' ), $ver, array( 'in_footer' => true ) ); } + + if ( $this->options->activate_quick_create ) { + wp_enqueue_script( 'msls-quick-create', self::plugins_url( "$folder/msls-quick-create.js" ), array( 'jquery', 'wp-api-fetch' ), $ver, array( 'in_footer' => true ) ); + } } /** diff --git a/includes/MslsRestApi.php b/includes/MslsRestApi.php new file mode 100644 index 000000000..17d7002a9 --- /dev/null +++ b/includes/MslsRestApi.php @@ -0,0 +1,364 @@ + \WP_REST_Server::CREATABLE, + 'callback' => array( new self(), 'create_translation' ), + 'permission_callback' => array( new self(), 'check_permission' ), + 'args' => self::get_route_args(), + ) + ); + + add_filter( 'msls_quick_create_post_data', array( self::class, 'prefix_source_language' ), 10, 4 ); + } + + /** + * @return array> + */ + private static function get_route_args(): array { + return array( + 'source_post_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + 'source_blog_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + 'target_blog_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ); + } + + /** + * @param \WP_REST_Request $request + * + * @return bool + */ + public function check_permission( \WP_REST_Request $request ): bool { + $source_blog_id = (int) $request->get_param( 'source_blog_id' ); + $source_post_id = (int) $request->get_param( 'source_post_id' ); + $target_blog_id = (int) $request->get_param( 'target_blog_id' ); + + switch_to_blog( $source_blog_id ); + $can_read = current_user_can( 'read_post', $source_post_id ); + restore_current_blog(); + + if ( ! $can_read ) { + return false; + } + + switch_to_blog( $target_blog_id ); + $can_edit = current_user_can( 'edit_posts' ); + restore_current_blog(); + + return $can_edit; + } + + /** + * @param \WP_REST_Request $request + * + * @return \WP_REST_Response|\WP_Error + */ + public function create_translation( \WP_REST_Request $request ) { + $source_post_id = (int) $request->get_param( 'source_post_id' ); + $source_blog_id = (int) $request->get_param( 'source_blog_id' ); + $target_blog_id = (int) $request->get_param( 'target_blog_id' ); + + switch_to_blog( $source_blog_id ); + $source_post = get_post( $source_post_id ); + restore_current_blog(); + + if ( ! $source_post instanceof \WP_Post ) { + return new \WP_Error( + 'msls_source_not_found', + __( 'Source post not found.', 'multisite-language-switcher' ), + array( 'status' => 404 ) + ); + } + + $target_lang = MslsBlogCollection::get_blog_language( $target_blog_id ); + + $post_data = $this->prepare_post_data( $source_post ); + $post_data = $this->prepare_taxonomies( $source_post, $source_blog_id, $target_blog_id, $target_lang, $post_data ); + + /** + * Filters the post data before creating the translation. + * + * @param array $post_data The post data for wp_insert_post. + * @param \WP_Post $source_post The source post object. + * @param int $source_blog_id The source blog ID. + * @param int $target_blog_id The target blog ID. + * + * @since TBD + */ + $post_data = apply_filters( 'msls_quick_create_post_data', $post_data, $source_post, $source_blog_id, $target_blog_id ); + + switch_to_blog( $target_blog_id ); + + if ( ! post_type_exists( $post_data['post_type'] ) ) { + restore_current_blog(); + + return new \WP_Error( + 'msls_target_post_type_not_found', + __( 'Post type does not exist on the target blog.', 'multisite-language-switcher' ), + array( 'status' => 400 ) + ); + } + + $new_post_id = wp_insert_post( $post_data, true ); + + if ( is_wp_error( $new_post_id ) ) { + restore_current_blog(); + + return $new_post_id; + } + + $this->assign_taxonomies( $post_data, $new_post_id ); + + /** + * Fires after the translation post is created on the target blog. + * + * @param int $new_post_id The new post ID. + * @param \WP_Post $source_post The source post object. + * @param int $source_blog_id The source blog ID. + * @param int $target_blog_id The target blog ID. + * + * @since TBD + */ + do_action( 'msls_quick_create_after_insert', $new_post_id, $source_post, $source_blog_id, $target_blog_id ); + + $edit_url = get_edit_post_link( $new_post_id, 'raw' ); + restore_current_blog(); + + $this->establish_link( $source_post_id, $source_blog_id, $new_post_id, $target_blog_id ); + + $response_data = array( + 'post_id' => $new_post_id, + 'edit_url' => $edit_url, + ); + + /** + * Filters the REST response data after creating a translation. + * + * @param array $response_data The response data. + * @param int $new_post_id The new post ID. + * @param \WP_Post $source_post The source post object. + * @param int $source_blog_id The source blog ID. + * @param int $target_blog_id The target blog ID. + * + * @since TBD + */ + $response_data = apply_filters( + 'msls_quick_create_response', + $response_data, + $new_post_id, + $source_post, + $source_blog_id, + $target_blog_id + ); + + return new \WP_REST_Response( $response_data, 201 ); + } + + /** + * @param \WP_Post $source_post + * + * @return array + */ + protected function prepare_post_data( \WP_Post $source_post ): array { + return array( + 'post_type' => $source_post->post_type, + 'post_status' => 'draft', + 'post_title' => $source_post->post_title, + 'post_content' => $source_post->post_content, + ); + } + + /** + * Prefixes post title and content with the source language code. + * + * Registered as a filter callback on msls_quick_create_post_data. + * Can be removed via remove_filter() to disable the prefix. + * + * @param array $post_data + * @param \WP_Post $source_post + * @param int $source_blog_id + * @param int $target_blog_id + * + * @return array + */ + public static function prefix_source_language( array $post_data, \WP_Post $source_post, int $source_blog_id, int $target_blog_id ): array { + $lang_code = substr( MslsBlogCollection::get_blog_language( $source_blog_id ), 0, 2 ); + + $post_data['post_title'] = sprintf( + /* translators: 1: language code, 2: original post title */ + __( 'From %1$s: %2$s', 'multisite-language-switcher' ), + $lang_code, + $post_data['post_title'] + ); + + return $post_data; + } + + /** + * @param \WP_Post $source_post + * @param int $source_blog_id + * @param int $target_blog_id + * @param string $target_lang + * @param array $post_data + * + * @return array + */ + protected function prepare_taxonomies( + \WP_Post $source_post, + int $source_blog_id, + int $target_blog_id, + string $target_lang, + array $post_data + ): array { + switch_to_blog( $source_blog_id ); + + $taxonomies = get_object_taxonomies( $source_post->post_type ); + $tax_input = array(); + + foreach ( $taxonomies as $taxonomy ) { + $terms = wp_get_object_terms( $source_post->ID, $taxonomy, array( 'fields' => 'ids' ) ); + + if ( is_wp_error( $terms ) || empty( $terms ) ) { + continue; + } + + $mapped_terms = array(); + foreach ( $terms as $term_id ) { + /** @var MslsOptionsTax $term_options */ + $term_options = MslsOptionsTax::create( $term_id ); + + if ( $term_options->has_value( $target_lang ) ) { + $mapped_terms[] = (int) $term_options->$target_lang; + } + } + + if ( ! empty( $mapped_terms ) ) { + $tax_input[ $taxonomy ] = $mapped_terms; + } + } + + restore_current_blog(); + + $post_data['_msls_tax_input'] = $tax_input; + + /** + * Filters the mapped taxonomy terms before assigning them. + * + * @param array $tax_input Taxonomy => term IDs mapping. + * @param \WP_Post $source_post The source post object. + * @param int $source_blog_id The source blog ID. + * @param int $target_blog_id The target blog ID. + * + * @since TBD + */ + $post_data['_msls_tax_input'] = apply_filters( + 'msls_quick_create_tax_input', + $post_data['_msls_tax_input'], + $source_post, + $source_blog_id, + $target_blog_id + ); + + return $post_data; + } + + /** + * @param array $post_data + * @param int $new_post_id + */ + protected function assign_taxonomies( array $post_data, int $new_post_id ): void { + if ( empty( $post_data['_msls_tax_input'] ) ) { + return; + } + + foreach ( $post_data['_msls_tax_input'] as $taxonomy => $term_ids ) { + wp_set_object_terms( $new_post_id, $term_ids, $taxonomy ); + } + } + + /** + * @param int $source_post_id + * @param int $source_blog_id + * @param int $new_post_id + * @param int $target_blog_id + */ + protected function establish_link( + int $source_post_id, + int $source_blog_id, + int $new_post_id, + int $target_blog_id + ): void { + $collection = msls_blog_collection(); + $source_lang = MslsBlogCollection::get_blog_language( $source_blog_id ); + $target_lang = MslsBlogCollection::get_blog_language( $target_blog_id ); + + // Read existing links from the source post + switch_to_blog( $source_blog_id ); + $source_options = new MslsOptionsPost( $source_post_id ); + $existing_links = $source_options->get_arr(); + restore_current_blog(); + + // Build a complete link map: all existing links + source + target + $link_map = $existing_links; + $link_map[ $source_lang ] = $source_post_id; + $link_map[ $target_lang ] = $new_post_id; + + // Update every blog in the link map + foreach ( $link_map as $lang => $post_id ) { + if ( empty( $post_id ) ) { + continue; + } + + $blog_id = $collection->get_blog_id( $lang ); + + if ( null === $blog_id ) { + continue; + } + + switch_to_blog( $blog_id ); + + $options = new MslsOptionsPost( $post_id ); + $save_data = $link_map; + unset( $save_data[ $lang ] ); + + $options->save( $save_data ); + + restore_current_blog(); + } + } +} diff --git a/package.json b/package.json index 435318dd6..abab51ea5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "scripts": { - "uglify": "uglifyjs src/msls.js > assets/js/msls.js", + "uglify": "uglifyjs src/msls.js > assets/js/msls.js && uglifyjs src/msls-quick-create.js > assets/js/msls-quick-create.js", "less": "lessc assets/css/msls.less assets/css/msls.css --clean-css=\"--s1 --advanced\"", "build-msls-block": "wp-scripts build --webpack-src-dir=src/msls-widget-block --output-path=assets/js/msls-widget-block", "build": "npm run uglify && npm run less && npm run build-msls-block", diff --git a/src/msls-quick-create.js b/src/msls-quick-create.js new file mode 100644 index 000000000..36771c710 --- /dev/null +++ b/src/msls-quick-create.js @@ -0,0 +1,63 @@ +jQuery( document ).ready( + function ( $ ) { + $( document ).on( + 'click', + '.msls-quick-create', + function () { + var $button = $( this ); + + if ( $button.hasClass( 'msls-loading' ) ) { + return; + } + + $button.addClass( 'msls-loading' ); + $button.find( '.dashicons' ).removeClass( 'dashicons-plus' ).addClass( 'dashicons-update' ); + + wp.apiFetch( + { + path: '/msls/v1/create-translation', + method: 'POST', + data: { + source_post_id: parseInt( $button.data( 'source-post-id' ), 10 ), + source_blog_id: parseInt( $button.data( 'source-blog-id' ), 10 ), + target_blog_id: parseInt( $button.data( 'target-blog-id' ), 10 ) + } + } + ).then( + function ( response ) { + var $link = $( '' ) + .attr( 'href', response.edit_url ) + .attr( 'title', $button.attr( 'title' ).replace( /Create/, 'Edit' ) ) + .html( $button.html() ); + + $link.find( '.dashicons' ).removeClass( 'dashicons-update dashicons-plus' ).addClass( 'dashicons-edit' ); + + $button.replaceWith( $link ); + + var $container = $link.closest( 'li' ); + if ( ! $container.length ) { + return; + } + + var $hiddenInput = $container.find( 'input[type="hidden"][name^="msls_input_"]' ); + if ( $hiddenInput.length ) { + $hiddenInput.val( response.post_id ); + } + + var $select = $container.find( 'select[name^="msls_input_"]' ); + if ( $select.length ) { + $select.append( + $( '  '; diff --git a/tests/phpunit/TestMslsPlugin.php b/tests/phpunit/TestMslsPlugin.php index bec240d96..9534af9cb 100644 --- a/tests/phpunit/TestMslsPlugin.php +++ b/tests/phpunit/TestMslsPlugin.php @@ -39,6 +39,22 @@ function test_admin_menu_with_autocomplete(): void { $test->custom_enqueue(); } + function test_admin_menu_with_quick_create(): void { + Functions\expect( 'is_admin_bar_showing' )->once()->andReturnTrue(); + Functions\expect( 'wp_enqueue_style' )->twice(); + Functions\expect( 'plugins_url' )->times( 3 )->andReturn( 'https://msls.co/wp-content/plugins' ); + Functions\expect( 'wp_enqueue_script' )->once(); + + $options = \Mockery::mock( MslsOptions::class ); + + $options->activate_quick_create = true; + + $test = new MslsPlugin( $options ); + + $this->expectNotToPerformAssertions(); + $test->custom_enqueue(); + } + function test_admin_menu_admin_bar_not_showing(): void { Functions\expect( 'is_admin_bar_showing' )->once()->andReturnFalse(); diff --git a/tests/phpunit/TestMslsRestApi.php b/tests/phpunit/TestMslsRestApi.php new file mode 100644 index 000000000..0b195ada6 --- /dev/null +++ b/tests/phpunit/TestMslsRestApi.php @@ -0,0 +1,168 @@ +data = $data; $this->status = $status; } public function get_data() { return $this->data; } public function get_status() { return $this->status; } }' ); + } + + if ( ! class_exists( \WP_Error::class ) ) { + // phpcs:ignore + eval( 'class WP_Error { protected $code; protected $message; protected $data; public function __construct( $code = "", $message = "", $data = "" ) { $this->code = $code; $this->message = $message; $this->data = $data; } public function get_error_code() { return $this->code; } }' ); + } + } + + public function test_check_permission_returns_true(): void { + $request = \Mockery::mock( \WP_REST_Request::class ); + $request->shouldReceive( 'get_param' )->with( 'source_blog_id' )->andReturn( 1 ); + $request->shouldReceive( 'get_param' )->with( 'source_post_id' )->andReturn( 10 ); + $request->shouldReceive( 'get_param' )->with( 'target_blog_id' )->andReturn( 2 ); + + Functions\expect( 'switch_to_blog' )->twice(); + Functions\expect( 'current_user_can' )->once()->with( 'read_post', 10 )->andReturn( true ); + Functions\expect( 'current_user_can' )->once()->with( 'edit_posts' )->andReturn( true ); + Functions\expect( 'restore_current_blog' )->twice(); + + $api = new MslsRestApi(); + $this->assertTrue( $api->check_permission( $request ) ); + } + + public function test_check_permission_no_read_access(): void { + $request = \Mockery::mock( \WP_REST_Request::class ); + $request->shouldReceive( 'get_param' )->with( 'source_blog_id' )->andReturn( 1 ); + $request->shouldReceive( 'get_param' )->with( 'source_post_id' )->andReturn( 10 ); + $request->shouldReceive( 'get_param' )->with( 'target_blog_id' )->andReturn( 2 ); + + Functions\expect( 'switch_to_blog' )->once()->with( 1 ); + Functions\expect( 'current_user_can' )->once()->with( 'read_post', 10 )->andReturn( false ); + Functions\expect( 'restore_current_blog' )->once(); + + $api = new MslsRestApi(); + $this->assertFalse( $api->check_permission( $request ) ); + } + + public function test_check_permission_no_edit_access(): void { + $request = \Mockery::mock( \WP_REST_Request::class ); + $request->shouldReceive( 'get_param' )->with( 'source_blog_id' )->andReturn( 1 ); + $request->shouldReceive( 'get_param' )->with( 'source_post_id' )->andReturn( 10 ); + $request->shouldReceive( 'get_param' )->with( 'target_blog_id' )->andReturn( 2 ); + + Functions\expect( 'switch_to_blog' )->twice(); + Functions\expect( 'current_user_can' )->once()->with( 'read_post', 10 )->andReturn( true ); + Functions\expect( 'current_user_can' )->once()->with( 'edit_posts' )->andReturn( false ); + Functions\expect( 'restore_current_blog' )->twice(); + + $api = new MslsRestApi(); + $this->assertFalse( $api->check_permission( $request ) ); + } + + public function test_create_translation_source_not_found(): void { + $request = \Mockery::mock( \WP_REST_Request::class ); + $request->shouldReceive( 'get_param' )->with( 'source_post_id' )->andReturn( 999 ); + $request->shouldReceive( 'get_param' )->with( 'source_blog_id' )->andReturn( 1 ); + $request->shouldReceive( 'get_param' )->with( 'target_blog_id' )->andReturn( 2 ); + + Functions\expect( 'switch_to_blog' )->once()->with( 1 ); + Functions\expect( 'get_post' )->once()->with( 999 )->andReturn( null ); + Functions\expect( 'restore_current_blog' )->once(); + + $api = new MslsRestApi(); + $result = $api->create_translation( $request ); + + $this->assertInstanceOf( \WP_Error::class, $result ); + } + + public function test_create_translation_success(): void { + $source_post = \Mockery::mock( \WP_Post::class ); + $source_post->ID = 10; + $source_post->post_title = 'Hello World'; + $source_post->post_content = 'Some content'; + $source_post->post_type = 'post'; + + $request = \Mockery::mock( \WP_REST_Request::class ); + $request->shouldReceive( 'get_param' )->with( 'source_post_id' )->andReturn( 10 ); + $request->shouldReceive( 'get_param' )->with( 'source_blog_id' )->andReturn( 1 ); + $request->shouldReceive( 'get_param' )->with( 'target_blog_id' )->andReturn( 2 ); + + Functions\expect( 'switch_to_blog' )->times( 5 ); + Functions\expect( 'restore_current_blog' )->times( 5 ); + Functions\expect( 'get_post' )->once()->with( 10 )->andReturn( $source_post ); + + Functions\expect( 'get_object_taxonomies' )->once()->with( 'post' )->andReturn( array() ); + Functions\expect( 'post_type_exists' )->once()->with( 'post' )->andReturn( true ); + Functions\expect( 'wp_insert_post' )->once()->andReturn( 42 ); + Functions\expect( 'get_edit_post_link' )->once()->with( 42, 'raw' )->andReturn( 'https://example.tld/wp-admin/post.php?post=42&action=edit' ); + + Functions\expect( 'get_option' )->andReturn( array() ); + Functions\expect( 'add_option' )->andReturn( true ); + Functions\expect( 'delete_option' )->andReturn( true ); + + $collection = \Mockery::mock( MslsBlogCollection::class ); + $collection->shouldReceive( 'get_blog_id' )->andReturn( 1, 2 ); + + Functions\expect( 'msls_blog_collection' )->andReturn( $collection ); + Functions\expect( 'get_blog_option' )->andReturn( 'en_US' ); + + Functions\expect( 'apply_filters' )->andReturnUsing( + function ( $hook, $value ) { + return $value; + } + ); + + Functions\expect( 'do_action' )->andReturnNull(); + + $api = new MslsRestApi(); + $result = $api->create_translation( $request ); + + $this->assertInstanceOf( \WP_REST_Response::class, $result ); + $this->assertEquals( 201, $result->get_status() ); + + $data = $result->get_data(); + $this->assertEquals( 42, $data['post_id'] ); + $this->assertEquals( 'https://example.tld/wp-admin/post.php?post=42&action=edit', $data['edit_url'] ); + } + + public function test_prefix_source_language(): void { + Functions\expect( 'get_blog_option' )->once()->andReturn( 'de_DE' ); + + $source_post = \Mockery::mock( \WP_Post::class ); + $source_post->post_title = 'Hallo Welt'; + $source_post->post_content = 'Inhalt'; + + $post_data = array( + 'post_title' => 'Hallo Welt', + 'post_content' => 'Inhalt', + ); + + $result = MslsRestApi::prefix_source_language( $post_data, $source_post, 1, 2 ); + + $this->assertEquals( 'From de: Hallo Welt', $result['post_title'] ); + $this->assertEquals( 'Inhalt', $result['post_content'] ); + } + + public function test_prefix_source_language_is_removable(): void { + $this->assertTrue( + method_exists( MslsRestApi::class, 'prefix_source_language' ), + 'prefix_source_language must be a public static method for use with remove_filter' + ); + + $reflection = new \ReflectionMethod( MslsRestApi::class, 'prefix_source_language' ); + $this->assertTrue( $reflection->isPublic() ); + $this->assertTrue( $reflection->isStatic() ); + } +}