PluginProbe
Slash Edit: Admin Shortcuts to Edit Posts and Pages Faster / 1.3.2
Slash Edit: Admin Shortcuts to Edit Posts and Pages Faster v1.3.2
trunk 1.0.0 1.1.0 1.1.1 1.2.0 1.3.0 1.3.2
slash-edit / slash-edit.php

slash-edit.php in Slash Edit: Admin Shortcuts to Edit Posts and Pages Faster 1.3.2, at slash-edit.php

608 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Slash Edit
4 * Plugin URI: https://mediaron.com
5 * Description: Edit your posts or pages with a simple "/edit" at the end.
6 * Author: Ronald Huereca
7 * Version: 1.3.2
8 * Requires at least: 6.5
9 * Author URI: https://mediaron.com
10 * Contributors: ronalfy
11 *
12 * @package slash-edit
13 */
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Slash Edit for WordPress
21 *
22 * @package slash-edit
23 */
24 class Slash_Edit {
25 /**
26 * Singleton instance
27 *
28 * @var Slash_Edit
29 */
30 private static $instance = null;
31
32 /**
33 * Endpoint
34 *
35 * @var string
36 */
37 private $endpoint = 'edit';
38
39 /**
40 * Last rewrite version update
41 *
42 * @var string
43 */
44 private $last_rewrite_version_update = '1.2.7'; // Will increment any time I need to change rewrite rules.
45
46 /**
47 * Get the singleton instance
48 *
49 * @return Slash_Edit
50 */
51 public static function get_instance() {
52 if ( null === self::$instance ) {
53 self::$instance = new self();
54 }
55 return self::$instance;
56 } //end get_instance
57
58 /**
59 * Constructor
60 *
61 * @return void
62 */
63 private function __construct() {
64 add_action( 'init', array( $this, 'init' ), 20 );
65 add_action( 'template_redirect', array( 'Slash_Edit', 'maybe_redirect' ) );
66 add_filter( 'rewrite_rules_array', array( 'Slash_Edit', 'add_rewrite_rules' ) );
67 add_action( 'save_post', array( 'Slash_Edit', 'save_post' ) );
68 $this->endpoint = sanitize_title( apply_filters( 'slash_edit_endpoint', 'edit' ) );
69 add_action( 'admin_init', array( $this, 'maybe_redirect_from_admin' ) );
70 add_filter( 'query_vars', array( $this, 'add_query_vars' ) );
71 } //end constructor
72
73 /**
74 * Get the endpoint
75 *
76 * @return string
77 */
78 public function get_endpoint() {
79 return $this->endpoint;
80 }
81
82 /**
83 * Add query vars
84 *
85 * @param array $query_vars Query vars.
86 *
87 * @return array
88 */
89 public function add_query_vars( $query_vars ) {
90 $query_vars[] = 'author';
91 $query_vars[] = 'theme';
92 $query_vars[] = 'users';
93 $query_vars[] = 'authors';
94 $query_vars[] = 'site';
95 $query_vars[] = 'sites';
96 return $query_vars;
97 }
98
99 /**
100 * Activate the plugin
101 *
102 * @return void
103 */
104 public static function activate() {
105 update_option( 'slash_edit_install', 'true' );
106 }
107
108 /**
109 * Add rewrite rules
110 *
111 * @param array $rules Rewrite rules.
112 *
113 * @global WP_Rewrite $wp_rewrite
114 * @return array
115 */
116 public static function add_rewrite_rules( $rules ) {
117 /**
118 * WP_Rewrite $wp_rewrite WordPress rewrite object.
119 *
120 * This is not always set on init.
121 */
122 global $wp_rewrite;
123
124 if ( ! is_object( $wp_rewrite ) ) {
125 return $rules;
126 }
127 // Get taxonomies.
128 $taxonomies = get_taxonomies();
129 $blog_prefix = '';
130 $endpoint = self::get_instance()->get_endpoint();
131 if ( is_multisite() && ! is_subdomain_install() && is_main_site() ) { /* stolen from /wp-admin/options-permalink.php */
132 $blog_prefix = 'blog/';
133 }
134 $exclude = array(
135 'category',
136 'post_tag',
137 'nav_menu',
138 'link_category',
139 'post_format',
140 );
141 $tax_rules = array();
142 foreach ( $taxonomies as $key => $taxonomy ) {
143 if ( in_array( $key, $exclude, true ) ) {
144 continue;
145 }
146 $taxonomy_obj = get_taxonomy( $key );
147
148 if ( ! $taxonomy_obj || empty( $taxonomy_obj->rewrite ) ) {
149 continue;
150 }
151
152 $taxonomy_slug = $taxonomy_obj->rewrite['slug'] ?? $key;
153 $query_var = $taxonomy_obj->query_var ? $taxonomy_obj->query_var : $key;
154
155 $rule_structure = sprintf(
156 '%1$s%2$s/(.+?)/%3$s/?$',
157 preg_quote( $blog_prefix, '#' ),
158 preg_quote( $taxonomy_slug, '#' ),
159 preg_quote( $endpoint, '#' )
160 );
161
162 $endpoint_structure = sprintf(
163 'index.php?%1$s=$matches[1]&%2$s=1',
164 $query_var,
165 $endpoint
166 );
167
168 $tax_rules[ $rule_structure ] = $endpoint_structure;
169 }
170 $rules = $tax_rules + $rules; // Add tax rules to the beginning of the rules array for precedence.
171
172 // Add post type archive rules. This allows things like /faqs/edit, and will take you to the post type edit screen.
173 $post_types = get_post_types( array( 'has_archive' => true ) );
174 $archive_rules = array();
175 foreach ( $post_types as $post_type ) {
176 $post_type_obj = get_post_type_object( $post_type );
177
178 // Determine the archive slug.
179 $cpt_archive_slugs_to_add = array();
180 if ( is_string( $post_type_obj->has_archive ) ) {
181 $cpt_archive_slugs_to_add[] = sanitize_title( $post_type_obj->has_archive );
182 }
183 // This is the singular slug, but should act as an archive too if post name is ommitted.
184 if ( isset( $post_type_obj->rewrite['slug'] ) ) {
185 $cpt_archive_slugs_to_add[] = sanitize_title( $post_type_obj->rewrite['slug'] );
186 } else {
187 $cpt_archive_slugs_to_add[] = sanitize_title( $post_type_obj->name );
188 }
189
190 // Check if we need to add front.
191 $has_front = false;
192 if ( isset( $post_type_obj->rewrite['with_front'] ) && true === $post_type_obj->rewrite['with_front'] && true === $post_type_obj->rewrite['with_front'] ) {
193 $has_front = true;
194 }
195
196 if ( $has_front && ! empty( $wp_rewrite->front ) ) {
197 foreach ( $cpt_archive_slugs_to_add as $key => $archive_slug ) {
198 $cpt_archive_slugs_to_add[ $key ] = substr( $wp_rewrite->front, 1 ) . $archive_slug;
199 }
200 }
201
202 // Add the archive edit rules.
203 foreach ( $cpt_archive_slugs_to_add as $archive_slug ) {
204 $rule_structure = "{$blog_prefix}{$archive_slug}/{$endpoint}/?$";
205 $endpoint_structure = 'index.php?post_type=' . $post_type . '&' . $endpoint . '=1';
206 $archive_rules[ $rule_structure ] = $endpoint_structure;
207 }
208 }
209
210 // Add /author/edit to rewrites.
211 $archive_rules[ "author/{$endpoint}/?$" ] = 'index.php?author=$matches[1]&' . $endpoint . '=1';
212 $archive_rules[ "users/{$endpoint}/?$" ] = 'index.php?users=$matches[1]&' . $endpoint . '=1';
213 $archive_rules[ "authors/{$endpoint}/?$" ] = 'index.php?authors=$matches[1]&' . $endpoint . '=1';
214
215 // Add /site/edit to rewrites.
216 $archive_rules[ "site/{$endpoint}/?$" ] = 'index.php?site=$matches[1]&' . $endpoint . '=1';
217 $archive_rules[ "sites/{$endpoint}/?$" ] = 'index.php?sites=$matches[1]&' . $endpoint . '=1';
218
219 // Add /theme/edit to rewrites.
220 $archive_rules[ "theme/{$endpoint}/?$" ] = 'index.php?theme=$matches[1]&' . $endpoint . '=1';
221
222 // Merge archive rules at the beginning for precedence.
223 $rules = $archive_rules + $rules;
224 $add_frontpage_edit_rules = false;
225 if ( ! get_page_by_path( $endpoint ) ) {
226 $add_frontpage_edit_rules = true;
227 } else {
228 $page = get_page_by_path( $endpoint );
229 if ( is_a( $page, 'WP_Post' ) && 'publish' === $page->post_status ) {
230 $add_frontpage_edit_rules = true;
231 }
232 }
233 if ( $add_frontpage_edit_rules ) {
234 $edit_array_rule = array( "{$endpoint}/?$" => 'index.php?' . $endpoint . '=frontpage' );
235 $rules = $edit_array_rule + $rules;
236 }
237 return $rules;
238 }
239
240 /**
241 * Deactivate the plugin
242 *
243 * @return void
244 */
245 public static function deactivate() {
246 }
247
248 /**
249 * Initialize the plugin
250 *
251 * @return void
252 */
253 public function init() {
254 global $wp_rewrite;
255 $endpoint = self::get_instance()->get_endpoint();
256
257 // Determine if we need to flush rules for a new version of the plugin.
258 $version = get_option( 'slash_edit_version', '1.0.0' );
259 if ( $version !== $this->last_rewrite_version_update ) {
260 update_option( 'slash_edit_version', $this->last_rewrite_version_update );
261 flush_rewrite_rules( true );
262 }
263
264 // Delete rewrite rules if plugin is deactivated.
265 if ( isset( $_GET['action'] ) && 'deactive' === sanitize_text_field( wp_unslash( $_GET['action'] ) ) ) {
266 $plugin_basename = plugin_basename( __FILE__ );
267
268 // Let's see if we're being deactivated.
269 if ( isset( $_GET['plugin'] ) && sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) === $plugin_basename ) {
270 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
271 if ( wp_verify_nonce( $nonce, 'deactivate-plugin_' . $plugin_basename ) ) {
272 add_rewrite_endpoint( $endpoint, EP_NONE );
273 flush_rewrite_rules( false );
274 }
275 }
276 }
277
278 // Refresh rewrite rules if plugin is activated.
279 add_rewrite_endpoint( $endpoint, EP_PERMALINK | EP_PAGES | EP_CATEGORIES | EP_TAGS | EP_AUTHORS | EP_ALL_ARCHIVES ); // todo - adding EP_ATTACHMENT messes up EP_PERMALINK and EP_PAGES.
280 if ( get_option( 'slash_edit_install', 'false' ) === 'true' ) {
281 flush_rewrite_rules( false );
282 delete_option( 'slash_edit_install' );
283 }
284 } //end init
285
286 /**
287 * Maybe redirect from admin.
288 *
289 * @return void
290 */
291 public static function maybe_redirect_from_admin() {
292 if ( ! is_admin() ) {
293 return;
294 }
295 $action_name = 'slash_edit_redirect';
296 $maybe_action = sanitize_text_field( wp_unslash( filter_input( INPUT_GET, 'action', FILTER_SANITIZE_SPECIAL_CHARS ) ) );
297 if ( $action_name !== $maybe_action ) {
298 return;
299 }
300
301 $token = sanitize_key( wp_unslash( filter_input( INPUT_GET, 'token', FILTER_SANITIZE_SPECIAL_CHARS ) ) );
302 if ( ! $token ) {
303 return;
304 }
305 $edit_url = get_transient( 'slash_edit_token_' . $token );
306 if ( ! $edit_url ) {
307 wp_safe_redirect( esc_url_raw( site_url() ) );
308 exit;
309 }
310 delete_transient( 'slash_edit_token_' . $token );
311
312 /**
313 * Filter the capability check.
314 *
315 * @param string $capability_check The capability check.
316 * @param string $edit_url The edit URL.
317 * @return string
318 */
319 $capability_check = apply_filters( 'slash_edit_capability_check', 'edit_others_posts', $edit_url );
320
321 // If path contains `wp-admin/network`, check for `manage_network` capability.
322 if ( strpos( $edit_url, 'wp-admin/network' ) !== false ) {
323 if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
324 wp_die( __( 'You are not authorized to edit this network item.', 'slash-edit' ) );
325 }
326 }
327
328 /**
329 * Filter Can Edit override. If true, the user can edit the item regardless of the capability check.
330 *
331 * @param bool $can_edit Whether the user can override the capability check and edit the item.
332 * @param string $edit_url The edit URL.
333 * @return bool
334 */
335 $can_edit = apply_filters( 'slash_edit_can_edit', false, $edit_url );
336
337 if ( current_user_can( $capability_check ) || $can_edit ) {
338 wp_safe_redirect( esc_url_raw( $edit_url ) );
339 } else {
340 wp_die( __( 'You are not authorized to edit this item.', 'slash-edit' ) );
341 }
342 exit;
343 }
344
345 /**
346 * Check if the query has the endpoint.
347 *
348 * @param WP_Query $query The query.
349 * @return bool
350 */
351 protected static function has_endpoint( $query ) {
352 $endpoint = self::get_instance()->get_endpoint();
353 if ( isset( $query->query_vars[ $endpoint ] ) ) {
354 return true;
355 }
356
357 // The `/edit` may be hidden in $query->query['name'] or $query->query['pagename'] in format `/slug/edit`. Can also just be `edit`.
358 if ( isset( $query->query['name'] ) && preg_match( '/\/?' . $endpoint . '\/?$/', $query->query['name'] ) ) {
359 return true;
360 }
361
362 if ( isset( $query->query['pagename'] ) && preg_match( '/\/' . $endpoint . '\/?$/', $query->query['pagename'] ) ) {
363 return true;
364 }
365 return false;
366 }
367
368 /**
369 * Get the edit URL by path.
370 *
371 * @param string $path The path.
372 * @param string $post_type The post type.
373 * @return int 0 if no post found, otherwise the post ID.
374 */
375 protected static function get_id_by_path( $path, $post_type = '' ) {
376 $maybe_has_path = get_page_by_path( $path, OBJECT, array( $post_type ) );
377 if ( is_a( $maybe_has_path, 'WP_Post' ) ) {
378 return absint( $maybe_has_path->ID );
379 }
380 // Now do a double-check using wpdb. This duplicates get_page_by_path, but doesn't care about post parent.
381 global $wpdb;
382 $results = $wpdb->get_results(
383 $wpdb->prepare(
384 "SELECT ID, post_name, post_parent, post_type FROM $wpdb->posts WHERE post_name = %s AND post_type = %s AND post_status = 'publish' LIMIT 1
385 ",
386 esc_sql( $path ),
387 esc_sql( $post_type )
388 ),
389 ARRAY_A
390 );
391 if ( is_array( $results ) && count( $results ) > 0 ) {
392 return absint( $results[0]['ID'] );
393 }
394 return 0;
395 }
396
397 /**
398 * Maybe redirect.
399 *
400 * @return void
401 */
402 public static function maybe_redirect() {
403 global $wp_query;
404 if ( ! self::has_endpoint( $wp_query ) ) {
405 return;
406 }
407 $endpoint = self::get_instance()->get_endpoint();
408 $edit_url = false;
409 /**
410 * Post, page, attachment, or CPTs.
411 */
412 if ( is_attachment() || is_single() || is_page() || is_singular() ) {
413 // Get the post, page, or cpt id.
414 $post = get_queried_object();
415 $post_id = isset( $post->ID ) ? $post->ID : false;
416 if ( false === $post_id ) {
417 return;
418 }
419
420 // Build the url.
421 $edit_url = add_query_arg(
422 array(
423 'post' => absint( $post_id ),
424 'action' => 'edit',
425 ),
426 admin_url( 'post.php' )
427 );
428 } elseif ( is_author() ) { /* Author Page */
429 $user_data = get_queried_object();
430 if ( is_a( $user_data, 'WP_User' ) ) {
431 $user_id = $user_data->ID;
432 // Build the url.
433 $edit_url = add_query_arg(
434 array(
435 'user_id' => absint( $user_id ),
436 'action' => 'edit',
437 ),
438 admin_url( 'user-edit.php' )
439 );
440 }
441 } elseif ( is_category() || is_tag() || is_tax() ) {
442 $tax_data = get_queried_object();
443 if ( is_object( $tax_data ) && isset( $tax_data->term_id ) ) {
444 $term_id = $tax_data->term_id;
445 $taxonomy = $tax_data->taxonomy;
446
447 // Get taxonomy post types.
448 $post_types = get_post_types( array( 'taxonomies' => array( $taxonomy ) ) );
449 $post_type = current( array_keys( $post_types ) );
450 if ( count( $post_types ) > 1 ) {
451 $post_type = '';
452 }
453
454 // Build the url.
455 $edit_url = add_query_arg(
456 array(
457 'tag_ID' => absint( $term_id ),
458 'taxonomy' => $taxonomy,
459 'action' => 'edit',
460 'post_type' => $post_type,
461 ),
462 admin_url( 'edit-tags.php' )
463 );
464 }
465 } elseif ( is_post_type_archive() ) {
466 $post_type = sanitize_key( get_query_var( 'post_type' ) );
467 $post_type_obj = get_post_type_object( $post_type );
468 if ( null !== $post_type_obj ) {
469 $edit_url = add_query_arg(
470 array(
471 'post_type' => $post_type,
472 ),
473 admin_url( 'edit.php' )
474 );
475 }
476 } elseif ( isset( $wp_query->query['post_type'] ) && isset( $wp_query->query['name'] ) ) {
477 // We're in a singular post type or post type archive, but the endpoint has been appended to the name in the query.
478 $temp_post_slug = sanitize_title( str_replace( '/', '', $wp_query->query['name'] ) );
479 $current_post_slug = get_query_var( 'name' );
480
481 // Make sure both bad names match to ensure intentions.
482 if ( $temp_post_slug === $current_post_slug && 'edit' !== $temp_post_slug ) {
483 // Strip `/edit` out from the name. This should leave you with the post slug.
484 $new_post_slug = sanitize_title( preg_replace( '/\/' . $endpoint . '\/?$/', '/', $wp_query->query['name'] ) );
485 $new_post_type = sanitize_key( $wp_query->query['post_type'] );
486
487 // Try to get the post ID.
488 $maybe_new_post_id = self::get_id_by_path( $new_post_slug, $new_post_type );
489
490 // If post is valid and post type matches, build the edit URL.
491 if ( 0 !== $maybe_new_post_id && get_post( $maybe_new_post_id ) && get_post_type( $maybe_new_post_id ) === $new_post_type ) {
492 $edit_url = add_query_arg(
493 array(
494 'post' => absint( $maybe_new_post_id ),
495 'action' => 'edit',
496 ),
497 admin_url( 'post.php' )
498 );
499 }
500 } elseif ( 'edit' === $temp_post_slug ) {
501 // We're in a post type archive, but the endpoint has been appended to the name in the query.
502 $new_post_type = sanitize_key( $wp_query->query['post_type'] );
503 $edit_url = add_query_arg(
504 array(
505 'post_type' => $new_post_type,
506 ),
507 admin_url( 'edit.php' )
508 );
509 }
510 }
511 // Fail safe for home_url/edit/.
512 if ( false === $edit_url && 'page' === get_option( 'show_on_front' ) && get_option( 'page_on_front' ) && 'frontpage' === get_query_var( 'edit' ) ) {
513 // Build the url.
514 $edit_url = add_query_arg(
515 array(
516 'post' => get_option( 'page_on_front' ),
517 'action' => 'edit',
518 ),
519 admin_url( 'post.php' )
520 );
521 } elseif ( 'frontpage' === get_query_var( 'edit' ) ) {
522 // No front page set - so redirect back to homepage.
523 $edit_url = home_url();
524 } elseif ( ( isset( $wp_query->query['author'] ) || isset( $wp_query->query['users'] ) || isset( $wp_query->query['authors'] ) ) && get_query_var( 'edit' ) ) {
525 $edit_url = admin_url( 'users.php' );
526 } elseif ( isset( $wp_query->query['theme'] ) && get_query_var( 'edit' ) ) {
527 if ( wp_is_block_theme() ) {
528 $edit_url = admin_url( 'site-editor.php' );
529 } else {
530 $edit_url = admin_url( 'customize.php' );
531 }
532 } elseif ( isset( $wp_query->query['site'] ) && get_query_var( 'edit' ) ) {
533 if ( is_multisite() ) {
534 $site_id = get_current_blog_id();
535 $edit_url = add_query_arg(
536 array(
537 'id' => $site_id,
538 ),
539 network_admin_url( 'site-info.php' )
540 );
541 }
542 } elseif ( isset( $wp_query->query['sites'] ) && get_query_var( 'edit' ) ) {
543 if ( is_multisite() ) {
544 $edit_url = network_admin_url( 'sites.php' );
545 }
546 } elseif ( is_home() ) {
547 $edit_url = admin_url( 'options-reading.php' );
548 }
549
550 // Filter to rule them all.
551 $edit_url = apply_filters( 'slash_edit_url', $edit_url ); // Return false for no redirect.
552
553 // Return if nothing to redirect to.
554 if ( false === $edit_url ) {
555 return;
556 }
557
558 // Create token. Lasts 5 minutes.
559 $token = sanitize_key( wp_generate_password( 20, false ) );
560 set_transient( 'slash_edit_token_' . $token, esc_url_raw( $edit_url ), 5 * MINUTE_IN_SECONDS );
561
562 $redirect_url = add_query_arg(
563 array(
564 'token' => $token,
565 'action' => 'slash_edit_redirect',
566 ),
567 admin_url()
568 );
569
570 // Redirect to admin with token.
571 wp_safe_redirect( esc_url_raw( $redirect_url ) );
572 exit;
573 }
574
575 /**
576 * Update rewrite rules if a parent page with slug 'edit' is edited and/or update - This way if there is a page with path www.domain.com/edit/, the page has priority.
577 *
578 * @param int $post_id The post ID.
579 * @return void
580 */
581 public static function save_post( $post_id = 0 ) {
582 $endpoint = self::get_instance()->get_endpoint();
583 if ( wp_is_post_revision( $post_id ) ) {
584 return;
585 }
586 global $post;
587 if ( ! is_object( $post ) ) {
588 $maybe_post = get_post( $post_id );
589 }
590 if ( 0 === $maybe_post->post_parent && $endpoint === $maybe_post->post_name && 'page' === $post->post_type ) {
591 flush_rewrite_rules( false );
592 }
593 }
594 } //end class Slash_Edit
595
596 add_action( 'plugins_loaded', 'slash_edit_instantiate' );
597 /**
598 * Instantiate the Slash_Edit class
599 *
600 * @return void
601 */
602 function slash_edit_instantiate() { // phpcs:ignore
603 Slash_Edit::get_instance();
604 }
605
606 register_activation_hook( __FILE__, array( 'Slash_Edit', 'activate' ) );
607 register_deactivation_hook( __FILE__, array( 'Slash_Edit', 'deactivate' ) );
608