| @@ -4,10 +4,8 @@ | ||
| 4 | 4 | * |
| 5 | 5 | * @package gutenberg |
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | -add_action( 'admin_menu', 'gutenberg_register_media_editor_admin_page' ); | |
| 9 | - | |
| 10 | 8 | /** |
| 11 | 9 | * Registers a hidden wp-admin page for direct media editor deep links. |
| 12 | 10 | */ |
| 13 | 11 | function gutenberg_register_media_editor_admin_page() { |
| @@ -22,20 +20,137 @@ | ||
| 22 | 20 | 'gutenberg_media_editor_wp_admin_render_page' |
| 23 | 21 | ); |
| 24 | 22 | |
| 25 | 23 | if ( $hook_suffix ) { |
| 26 | - // Hidden pages do not resolve a title from a visible menu item, so set | |
| 27 | - // one before admin-header.php formats the page title. | |
| 28 | - add_action( "load-$hook_suffix", 'gutenberg_media_editor_wp_admin_set_title' ); | |
| 24 | + add_action( "load-$hook_suffix", 'gutenberg_media_editor_wp_admin_prepare_screen' ); | |
| 29 | 25 | } |
| 30 | 26 | } |
| 31 | 27 | |
| 32 | 28 | /** |
| 33 | - * Sets the admin page title before wp-admin/admin-header.php renders. | |
| 29 | + * Prepares the admin chrome before wp-admin/admin-header.php renders. | |
| 34 | 30 | * |
| 35 | - * @global string $title The admin page title. | |
| 31 | + * @global string $title The admin page title. | |
| 32 | + * @global string $parent_file The current top-level menu item. | |
| 33 | + * @global string $submenu_file The current submenu item. | |
| 36 | 34 | */ |
| 37 | -function gutenberg_media_editor_wp_admin_set_title() { | |
| 38 | - global $title; | |
| 35 | +function gutenberg_media_editor_wp_admin_prepare_screen() { | |
| 36 | + global $title, $parent_file, $submenu_file; | |
| 39 | 37 | |
| 38 | + // Hidden pages do not resolve a title from a visible menu item, so set one | |
| 39 | + // before admin-header.php formats the page title. | |
| 40 | 40 | $title = __( 'Edit media', 'gutenberg' ); |
| 41 | + | |
| 42 | + /* | |
| 43 | + * Take the page out of the hidden `''` submenu bucket it was registered in. | |
| 44 | + * Left in place, get_admin_page_parent() matches it there and resets | |
| 45 | + * $parent_file to '' — and it does so *after* the `parent_file` filter runs, | |
| 46 | + * so filtering cannot win. With no match it preserves a non-empty | |
| 47 | + * $parent_file instead. | |
| 48 | + * | |
| 49 | + * Safe at this point: `load-` fires after the capability check in admin.php, | |
| 50 | + * which needs the page registered, and before the menu is rendered. | |
| 51 | + */ | |
| 52 | + remove_submenu_page( '', 'media-editor-wp-admin' ); | |
| 53 | + | |
| 54 | + // Match the classic Edit Media screen, which the media editor stands in for: | |
| 55 | + // Media expanded and current, Library the current submenu item. | |
| 56 | + $parent_file = 'upload.php'; | |
| 57 | + $submenu_file = 'upload.php'; | |
| 41 | 58 | } |
| 59 | + | |
| 60 | +add_action( 'admin_menu', 'gutenberg_register_media_editor_admin_page' ); | |
| 61 | + | |
| 62 | +/** | |
| 63 | + * Builds the media editor URL for an attachment. | |
| 64 | + * | |
| 65 | + * The router reads its internal path from the `p` query argument, so the | |
| 66 | + * attachment id travels as `p=/media-editor/<id>` rather than as a query | |
| 67 | + * argument of its own. | |
| 68 | + * | |
| 69 | + * @param int $post_id Attachment ID. | |
| 70 | + * @param string $separator Argument separator. `&` when the URL is | |
| 71 | + * destined for HTML output, `&` otherwise. | |
| 72 | + * @return string The media editor URL. | |
| 73 | + */ | |
| 74 | +function gutenberg_media_editor_get_url( $post_id, $separator = '&' ) { | |
| 75 | + return admin_url( | |
| 76 | + 'admin.php?page=media-editor-wp-admin' . $separator . 'p=' . rawurlencode( '/media-editor/' . (int) $post_id ) | |
| 77 | + ); | |
| 78 | +} | |
| 79 | + | |
| 80 | +/** | |
| 81 | + * Points every "edit this attachment" link at the media editor. | |
| 82 | + * | |
| 83 | + * Filtering here rather than redirecting means the Media Library list table, | |
| 84 | + * the media modal's "Edit more details" and "Edit Image" links, and the admin | |
| 85 | + * bar all navigate straight to the media editor with no redirect hop. | |
| 86 | + * | |
| 87 | + * @param string|null $link The edit link, or null when the user cannot edit the post. | |
| 88 | + * @param int $post_id Post ID. | |
| 89 | + * @param string $context The link context. `display` expects an HTML-escaped separator. | |
| 90 | + * @return string|null The filtered edit link. | |
| 91 | + */ | |
| 92 | +function gutenberg_media_editor_filter_edit_post_link( $link, $post_id, $context ) { | |
| 93 | + // A null link means the user lacks the capability. Leave that alone. | |
| 94 | + if ( ! $link || 'attachment' !== get_post_type( $post_id ) ) { | |
| 95 | + return $link; | |
| 96 | + } | |
| 97 | + | |
| 98 | + // `edit_post` is enough for the classic screen, but the media editor page | |
| 99 | + // requires `upload_files`. A contributor can own an attachment and edit it | |
| 100 | + // without being able to upload, so pointing them at the media editor would | |
| 101 | + // send them to a screen they cannot open. | |
| 102 | + if ( ! current_user_can( 'upload_files' ) ) { | |
| 103 | + return $link; | |
| 104 | + } | |
| 105 | + | |
| 106 | + return gutenberg_media_editor_get_url( $post_id, 'display' === $context ? '&' : '&' ); | |
| 107 | +} | |
| 108 | + | |
| 109 | +add_filter( 'get_edit_post_link', 'gutenberg_media_editor_filter_edit_post_link', 10, 3 ); | |
| 110 | + | |
| 111 | +/** | |
| 112 | + * Redirects the classic Edit Media screen to the media editor. | |
| 113 | + * | |
| 114 | + * The `get_edit_post_link` filter above covers links rendered by WordPress, | |
| 115 | + * but bookmarks, hand-typed URLs, and hard-coded links still reach post.php. | |
| 116 | + * This catches those. It deliberately bails on the failure paths so that | |
| 117 | + * WordPress keeps rendering its own error messages instead of sending people | |
| 118 | + * to a screen that would only fail again. | |
| 119 | + */ | |
| 120 | +function gutenberg_media_editor_redirect_classic_screen() { | |
| 121 | + // GET renders the form. POST is the editattachment/editpost save handler, | |
| 122 | + // which must run untouched. | |
| 123 | + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) ) { | |
| 124 | + return; | |
| 125 | + } | |
| 126 | + | |
| 127 | + // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reading the same unauthenticated query args post.php itself reads to decide what to render. | |
| 128 | + if ( ! isset( $_GET['action'] ) || 'edit' !== $_GET['action'] ) { | |
| 129 | + return; | |
| 130 | + } | |
| 131 | + | |
| 132 | + // Guard against `?post[]=1`, where an array casts to int 1 without warning. | |
| 133 | + $post_id = isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ? (int) $_GET['post'] : 0; | |
| 134 | + // phpcs:enable WordPress.Security.NonceVerification.Recommended | |
| 135 | + | |
| 136 | + if ( ! $post_id || 'attachment' !== get_post_type( $post_id ) ) { | |
| 137 | + return; | |
| 138 | + } | |
| 139 | + | |
| 140 | + // Let post.php own these: it already renders the appropriate wp_die(). | |
| 141 | + if ( ! current_user_can( 'edit_post', $post_id ) || 'trash' === get_post_status( $post_id ) ) { | |
| 142 | + return; | |
| 143 | + } | |
| 144 | + | |
| 145 | + // The media editor page requires `upload_files`, which `edit_post` does not | |
| 146 | + // imply. Without this, a contributor who owns an attachment would be | |
| 147 | + // redirected off a screen that works onto one that refuses them. | |
| 148 | + if ( ! current_user_can( 'upload_files' ) ) { | |
| 149 | + return; | |
| 150 | + } | |
| 151 | + | |
| 152 | + wp_safe_redirect( gutenberg_media_editor_get_url( $post_id ) ); | |
| 153 | + exit; | |
| 154 | +} | |
| 155 | + | |
| 156 | +add_action( 'load-post.php', 'gutenberg_media_editor_redirect_classic_screen' ); | |