\d+)/seo', array( array( 'methods' => 'GET', 'callback' => array( $this, 'get' ), 'permission_callback' => array( RestModule::class, 'require_manage_woocommerce' ), ), array( 'methods' => 'PUT', 'callback' => array( $this, 'put' ), 'permission_callback' => array( RestModule::class, 'require_manage_woocommerce' ), ), ) ); } /** * @return \WP_REST_Response|\WP_Error */ public function get( \WP_REST_Request $request ) { $id = self::product_id( $request ); if ( is_wp_error( $id ) ) { return $id; } return new \WP_REST_Response( self::read( $id ) ); } /** * @return \WP_REST_Response|\WP_Error */ public function put( \WP_REST_Request $request ) { $id = self::product_id( $request ); if ( is_wp_error( $id ) ) { return $id; } $body = $request->get_json_params(); if ( ! self::has_valid_keys( $body ) ) { return self::invalid( __( 'Send a JSON object with title and/or description.', 'cortexcommerce' ) ); } $values = array(); foreach ( self::FIELDS as $field ) { if ( ! array_key_exists( $field, $body ) ) { $values[ $field ] = null; continue; } if ( ! is_string( $body[ $field ] ) ) { /* translators: %s: field name. */ return self::invalid( sprintf( __( '%s must be a string.', 'cortexcommerce' ), $field ) ); } $values[ $field ] = trim( sanitize_text_field( $body[ $field ] ) ); } SeoAdapters::detect()->set( $id, $values['title'], $values['description'] ); return new \WP_REST_Response( self::read( $id ) ); } /** * True when $body decodes to an object whose keys are a subset of FIELDS. * * Takes the decoded body as `mixed` (rather than checking `is_array()` on the return value of * `get_json_params()` directly) so PHPStan doesn't flag the check as dead code: the WordPress stub * declares that method `@return array`, which doesn't account for the `null` WordPress core can * actually hand back for a literal JSON `null` body. * * @param mixed $body Decoded JSON body. */ private static function has_valid_keys( $body ): bool { return is_array( $body ) && array() !== $body && array() === array_diff( array_keys( $body ), self::FIELDS ); } /** * @return array{title: string, description: string, source: string} */ private static function read( int $id ): array { $adapter = SeoAdapters::detect(); $fields = $adapter->get( $id ); return array( 'title' => $fields['title'], 'description' => $fields['description'], 'source' => $adapter->id(), ); } /** * @return int|\WP_Error */ private static function product_id( \WP_REST_Request $request ) { $id = (int) $request['id']; $post = get_post( $id ); if ( ! $post instanceof \WP_Post || 'product' !== $post->post_type || 'trash' === $post->post_status ) { return new \WP_Error( 'cortexcommerce_not_found', __( 'Unknown product.', 'cortexcommerce' ), array( 'status' => 404 ) ); } return $id; } private static function invalid( string $message ): \WP_Error { return new \WP_Error( 'cortexcommerce_invalid_seo', $message, array( 'status' => 400 ) ); } }