'rank_math_title', 'meta_description' => 'rank_math_description', 'focus_keyword' => 'rank_math_focus_keyword', ); } if ( defined( 'WPSEO_VERSION' ) ) { return array( 'meta_title' => '_yoast_wpseo_title', 'meta_description' => '_yoast_wpseo_metadesc', 'focus_keyword' => '_yoast_wpseo_focuskw', ); } return null; } /** Register unified fields on all REST-enabled post types. */ function wiloai_register_fields() { $map = wiloai_meta_map(); if ( ! $map ) { return; } $args = array( 'get_callback' => 'wiloai_get_field', 'update_callback' => 'wiloai_update_field', 'schema' => array( 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ); foreach ( get_post_types( array( 'show_in_rest' => true ), 'names' ) as $post_type ) { foreach ( array_keys( $map ) as $field ) { register_rest_field( $post_type, $field, $args ); } } } add_action( 'rest_api_init', 'wiloai_register_fields' ); /** * Read a field. The focus keyword is private to users who can edit the post. * * @param array $prepared_post Prepared post data. * @param string $field REST field name. * @return string */ function wiloai_get_field( $prepared_post, $field ) { $map = wiloai_meta_map(); $post_id = isset( $prepared_post['id'] ) ? (int) $prepared_post['id'] : 0; if ( ! $map || ! $post_id || ! isset( $map[ $field ] ) ) { return ''; } if ( 'focus_keyword' === $field && ! current_user_can( 'edit_post', $post_id ) ) { return ''; } return (string) get_post_meta( $post_id, $map[ $field ], true ); } /** * Validate and write a field after checking access to this specific post. * * @param mixed $value Incoming value. * @param WP_Post $post Target post. * @param string $field REST field name. * @return true|WP_Error */ function wiloai_update_field( $value, $post, $field ) { if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'wiloai_forbidden', __( 'You cannot edit SEO metadata for this post.', 'wiloai' ), array( 'status' => rest_authorization_required_code() ) ); } $map = wiloai_meta_map(); if ( ! $map || ! isset( $map[ $field ] ) ) { return new WP_Error( 'wiloai_unsupported_field', __( 'No supported SEO field is available.', 'wiloai' ), array( 'status' => 400 ) ); } if ( ! is_string( $value ) ) { return new WP_Error( 'wiloai_invalid_value', __( 'SEO metadata must be a string.', 'wiloai' ), array( 'status' => 400 ) ); } $value = 'meta_description' === $field ? sanitize_textarea_field( $value ) : sanitize_text_field( $value ); $stored = update_post_meta( $post->ID, $map[ $field ], wp_slash( $value ) ); if ( false === $stored ) { // SEO providers may normalize values further (for example Yoast removes line breaks). $expected = sanitize_meta( $map[ $field ], $value, 'post', get_post_type( $post ) ); if ( (string) get_post_meta( $post->ID, $map[ $field ], true ) !== $expected ) { return new WP_Error( 'wiloai_write_failed', __( 'SEO metadata could not be saved.', 'wiloai' ), array( 'status' => 500 ) ); } } return true; } /** Register the administrator-only backup route. */ function wiloai_register_backup_route() { register_rest_route( 'wiloai/v1', '/updraft-backup', array( 'methods' => 'POST', 'callback' => 'wiloai_queue_database_backup', 'permission_callback' => 'wiloai_backup_permission', ) ); } add_action( 'rest_api_init', 'wiloai_register_backup_route' ); /** * Check whether the authenticated user may request a backup. * * @return bool */ function wiloai_backup_permission() { return current_user_can( 'manage_options' ); } /** * Release only the lock value owned by this request, not a newer lock. * * @param int $expires Lock expiry timestamp. * @return void */ function wiloai_release_backup_lock( $expires ) { global $wpdb; // Atomic compare-and-delete prevents competing requests removing a newer lock. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic lock operation; option caches are explicitly invalidated below. $deleted = $wpdb->delete( $wpdb->options, array( 'option_name' => 'wiloai_backup_lock', 'option_value' => (string) $expires, ), array( '%s', '%s' ) ); if ( $deleted ) { wp_cache_delete( 'wiloai_backup_lock', 'options' ); wp_cache_delete( 'notoptions', 'options' ); } } /** * Queue one database backup; a 202 response is not confirmation of completion. * * @return WP_REST_Response|WP_Error */ function wiloai_queue_database_backup() { if ( ! has_action( 'updraft_backupnow_backup_database' ) ) { return new WP_Error( 'wiloai_updraft_unavailable', __( 'UpdraftPlus database backup support is not active.', 'wiloai' ), array( 'status' => 503 ) ); } if ( wp_next_scheduled( 'wiloai_run_database_backup' ) ) { return new WP_Error( 'wiloai_backup_pending', __( 'A database backup is already queued.', 'wiloai' ), array( 'status' => 429 ) ); } $previous = (int) get_option( 'wiloai_backup_lock', 0 ); if ( $previous && $previous <= time() ) { wiloai_release_backup_lock( $previous ); } $expires = time() + 10 * MINUTE_IN_SECONDS; if ( ! add_option( 'wiloai_backup_lock', $expires, '', false ) ) { return new WP_Error( 'wiloai_backup_rate_limited', __( 'Wait ten minutes between database backup requests.', 'wiloai' ), array( 'status' => 429 ) ); } $scheduled = wp_schedule_single_event( time(), 'wiloai_run_database_backup', array(), true ); if ( is_wp_error( $scheduled ) || ! $scheduled ) { wiloai_release_backup_lock( $expires ); return new WP_Error( 'wiloai_backup_schedule_failed', __( 'The database backup could not be queued.', 'wiloai' ), array( 'status' => 500 ) ); } return new WP_REST_Response( array( 'status' => 'queued', 'backup_type' => 'database', ), 202 ); } /** Run a queued request using UpdraftPlus's own database-only backup hook. */ function wiloai_run_database_backup() { if ( has_action( 'updraft_backupnow_backup_database' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- This hook belongs to UpdraftPlus. do_action( 'updraft_backupnow_backup_database', array( 'nocloud' => 0 ) ); } } add_action( 'wiloai_run_database_backup', 'wiloai_run_database_backup' ); /** Remove pending requests and the cooldown when WiloAI is deactivated. */ function wiloai_deactivate() { wp_clear_scheduled_hook( 'wiloai_run_database_backup' ); delete_option( 'wiloai_backup_lock' ); } register_deactivation_hook( __FILE__, 'wiloai_deactivate' );