WP_REST_Server::CREATABLE, 'callback' => array( $this, 'rest_update' ), 'permission_callback' => static function () { return current_user_can( 'manage_options' ); }, 'args' => array( 'key' => array( 'required' => true, 'type' => 'string', ), ), ) ); } /** * The presets, in the order the core screen lists them. * * Structures are stored without the leading slash WordPress adds itself, * matching what `options-permalink.php` writes. * * @return array key => structure. */ private static function presets() { return array( 'plain' => '', 'day-and-name' => '/%year%/%monthnum%/%day%/%postname%/', 'month-and-name' => '/%year%/%monthnum%/%postname%/', 'numeric' => '/archives/%post_id%', 'post-name' => '/%postname%/', ); } /** * Human labels for the presets. * * @return array key => label. */ private static function labels() { return array( 'plain' => __( 'Plain', 'oorank' ), 'day-and-name' => __( 'Day and name', 'oorank' ), 'month-and-name' => __( 'Month and name', 'oorank' ), 'numeric' => __( 'Numeric', 'oorank' ), 'post-name' => __( 'Post name', 'oorank' ), ); } /** * What a URL looks like under each structure. * * Built the way the core screen builds it — real home URL, today's date, * a sample slug — so the preview is the address the owner will actually * see rather than a template full of percent signs. * * @param string $structure Permalink structure. * @return string Example URL. */ private static function preview( $structure ) { $home = home_url( '/' ); if ( '' === $structure ) { return $home . '?p=123'; } $path = str_replace( array( '%year%', '%monthnum%', '%day%', '%postname%', '%post_id%' ), array( gmdate( 'Y' ), gmdate( 'm' ), gmdate( 'd' ), 'sample-post', '123' ), $structure ); return $home . ltrim( $path, '/' ); } /** * Everything the admin app needs to render the choice. * * A custom structure the site already runs is appended as its own option * so the list always contains the current setting; selecting anything * else is then an explicit choice to abandon it. * * @return array{current:string, structure:string, pretty:bool, choices:array} */ public static function status() { $structure = (string) get_option( 'permalink_structure', '' ); $presets = self::presets(); $labels = self::labels(); $current = array_search( $structure, $presets, true ); $choices = array(); foreach ( $presets as $key => $preset ) { $choices[] = array( 'key' => $key, 'label' => $labels[ $key ], 'structure' => $preset, 'preview' => self::preview( $preset ), ); } if ( false === $current ) { $current = 'custom'; $choices[] = array( 'key' => 'custom', 'label' => __( 'Custom structure', 'oorank' ), 'structure' => $structure, 'preview' => self::preview( $structure ), ); } return array( 'current' => $current, 'structure' => $structure, 'pretty' => '' !== $structure, 'choices' => $choices, ); } /** * POST /permalinks handler. * * `flush_rules( true )` is the hard flush: it rewrites .htaccess on Apache * where the file is writable, which is what makes the virtual endpoints * answer without a second visit to Settings → Permalinks. Where it is not * writable the rules still land in the database and WordPress prints its * own instructions on the core screen, which is why the drawer keeps a * link to it. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function rest_update( WP_REST_Request $request ) { $key = sanitize_key( (string) $request->get_param( 'key' ) ); $presets = self::presets(); if ( ! isset( $presets[ $key ] ) ) { return new WP_Error( 'oorank_bad_permalink', __( 'That is not one of the permalink structures Oorank can set. Use the WordPress permalink screen for a custom structure.', 'oorank' ), array( 'status' => 400 ) ); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/misc.php'; global $wp_rewrite; $wp_rewrite->set_permalink_structure( $presets[ $key ] ); $wp_rewrite->flush_rules( true ); return rest_ensure_response( array( 'saved' => true, 'permalinks' => self::status(), ) ); } }