diff --git a/.editorconfig b/.editorconfig index 79207a40cb9326b8c6b8c958fa864b5345f94e68..0fcdf7fd4fc5705384ea40f93be21e0fc85ac557 100755 --- a/.editorconfig +++ b/.editorconfig @@ -17,6 +17,3 @@ indent_size = 4 [{.jshintrc,*.json,*.yml}] indent_style = space indent_size = 2 - -[{*.txt,wp-config-sample.php}] -end_of_line = crlf diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 40c53b76bbfcdb3b614b4ff21a13d2a52de56d50..a4d480111b33701c1f5cd7785d3f506e883a7b21 100755 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ variables: # Configure mysql service (https://hub.docker.com/_/mysql/) MYSQL_DATABASE: wordpress_tests MYSQL_ROOT_PASSWORD: mysql + WP_VERSION: latest cache: paths: @@ -10,7 +11,7 @@ cache: before_script: # Set up WordPress tests - - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql latest true + - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql $WP_VERSION true # PHPUnit - | @@ -20,10 +21,43 @@ before_script: composer global require "phpunit/phpunit=4.8.*" fi - # Install PHPCS and WPCS - - composer global require automattic/vipwpcs - - composer global require phpcompatibility/phpcompatibility-wp - - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp +PHPunit:PHP5.3:MySQL: + stage: test + variables: + WP_VERSION: 5.1.1 + image: containers.ethitter.com:443/docker/images/php:5.3 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP5.6:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:5.6 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.0:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.0 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.1:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.1 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit PHPunit:PHP7.2:MySQL: stage: test @@ -32,9 +66,7 @@ PHPunit:PHP7.2:MySQL: - mysql:5.6 script: - find . -type "f" -iname "*.php" | xargs -L "1" php -l - - phpcs -n - phpunit - allow_failure: true PHPunit:PHP7.3:MySQL: stage: test @@ -43,13 +75,21 @@ PHPunit:PHP7.3:MySQL: - mysql:5.6 script: - find . -type "f" -iname "*.php" | xargs -L "1" php -l - - phpcs -n - phpunit - allow_failure: true + +PHPCS: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.3 + before_script: + - composer global require automattic/vipwpcs + - composer global require phpcompatibility/phpcompatibility-wp + - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp + script: + - phpcs -n PluginSVN: stage: deploy - image: containers.ethitter.com:443/docker/images/php:7.3 + image: containers.ethitter.com:443/docker/wp-org-plugin-deploy:latest before_script: - curl -o ./bin/deploy.sh https://git-cdn.e15r.co/open-source/wp-org-plugin-deploy/raw/master/scripts/deploy.sh - chmod +x ./bin/deploy.sh diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 7dfcb692f269238decac45ae18a1e6a510736005..6555ad3378423a8fc1a3f1466bb3ee1848986b61 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -26,7 +26,7 @@ <!-- Rules: WordPress Coding Standards --> <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards --> <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties --> - <config name="minimum_supported_wp_version" value="3.2.1"/> + <config name="minimum_supported_wp_version" value="4.4"/> <rule ref="WordPress" /> <rule ref="WordPressVIPMinimum" /> <rule ref="WordPress-VIP-Go" /> diff --git a/README.md b/README.md index a9166e400919ee66468366d3d72dda5e6a2bc899..e33e80de4a8e784357978575eb1830680acfb9c3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Tags:** seo, meta tags **Requires at least:** 4.4 **Tested up to:** 5.2 -**Stable tag:** 0.5 +**Stable tag:** 0.6 **License:** GPLv2 or later **License URI:** http://www.gnu.org/licenses/gpl-2.0.html @@ -30,8 +30,25 @@ For example, http://example.com/?p=123 becomes http://example.com/p/123/. No, shortlinks use the posts' IDs, so aren't available for modification. +### Why aren't redirects validated? ### + +Sites may use plugins that allow a post object's permalink to be set to an external URL, and this plugin is designed to respect those plugins. + +If you wish to validate the redirects issued by this plugin, you can use the `eth_simple_shortlinks_redirect_url` filter to apply `wp_validate_redirect()` to the destination URL. + +### After upgrading to 0.6, redirects stopped working ### + +Beginning with release 0.6, before performing a redirect, the plugin checks that the post type and post status are supported. Previously, these checks were only applied when overriding an object's shortlink. + +If, after upgrading, redirects stop working, use the `eth_simple_shortlinks_allowed_post_types` and `eth_simple_shortlinks_allowed_post_statuses` filters to permit additional types and statuses, or use the `eth_simple_shortlinks_verify_requested_post_support` filter to disable the supports checks. + ## Changelog ## +### 0.6 ### +* Introduce filters in redirection handling. +* Apply supported post-type and post-status checks before redirecting. +* Conform to WordPress VIP's Coding Standards. + ### 0.5 ### * Admin notices when permalinks won't support the plugin * Disable plugin functionality when permalink structure is incompatible @@ -39,3 +56,9 @@ No, shortlinks use the posts' IDs, so aren't available for modification. ### 0.4 ### * Initial release + +## Upgrade Notice ## + +### 0.6 ### + +Applies supported post-type and post-status checks before performing redirect. If, after upgrading, redirects stop working, see the "After upgrading to 0.6, redirects stopped working" section of the FAQ. diff --git a/eth-simple-shortlinks.php b/eth-simple-shortlinks.php index 381f07d0915d3884d4b50ff4aca5348c4495181a..22eda06b43046b71a242adb3dd7f04bc731b1670 100644 --- a/eth-simple-shortlinks.php +++ b/eth-simple-shortlinks.php @@ -1,298 +1,33 @@ <?php -/* -Plugin Name: ETH Simple Shortlinks -Plugin URI: https://ethitter.com/plugins/ -Description: Simple non-GET shortlinks using post IDs -Author: Erick Hitter -Version: 0.5 -Author URI: https://ethitter.com/ -Text Domain: eth_simple_shortlinks -Domain Path: /languages/ - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - -class ETH_Simple_Shortlinks { - /** - * PLUGIN SETUP - */ - - /** - * Singleton - */ - private static $instance = null; - - /** - * Instantiate singleton - */ - public static function get_instance() { - if ( ! is_a( self::$instance, __CLASS__ ) ) { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Dummy magic methods - */ - public function __clone() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ uh?', 'eth_simple_shortlinks' ), '0.1' ); } - public function __wakeup() { _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ uh?', 'eth_simple_shortlinks' ), '0.1' ); } - public function __call( $name = '', $args = array() ) { unset( $name, $args ); return null; } - - /** - * Class properties - */ - private $name = 'ETH Simple Shortlinks'; - private $slug = 'p'; - private $rewrite_rule = null; - private $rewrite_match = null; - private $qv = 'eth-shortlink'; - - private $plugin_supported = false; - - private $supported_post_types = array(); - private $supported_post_statuses = array(); - - /** - * Register plugin's setup action - */ - private function __construct() { - // Build rewrite parts using other class properties - $this->rewrite_rule = '^' . $this->slug . '/([\d]+)/?$'; - $this->rewrite_match = 'index.php?p=$matches[1]&' . $this->qv . '=1'; - - // Basic plugin actions - add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) ); - add_action( 'init', array( $this, 'action_init' ) ); - } - - /** - * Load plugin translations - */ - public function action_plugins_loaded() { - load_plugin_textdomain( 'eth_simple_shortlinks', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' ); - } - - /** - * Verify plugin is supported, then register its functionality - */ - public function action_init() { - global $wp_rewrite; - - // Plugin won't work if site doesn't use pretty permalinks - if ( empty( $wp_rewrite->permalink_structure ) ) { - add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) ); - } else { - $this->plugin_supported = true; - - // Admin notices - add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) ); - - // Register rewrite rule - add_rewrite_rule( $this->rewrite_rule, $this->rewrite_match, 'top' ); - - // Request handling - add_action( 'wp_loaded', array( $this, 'filter_support' ) ); - add_filter( 'query_vars', array( $this, 'filter_query_vars' ) ); - add_action( 'parse_request', array( $this, 'action_parse_request' ) ); - - // Shortlink overrides - add_filter( 'get_shortlink', array( $this, 'filter_get_shortlink' ), 10, 2 ); - add_action( 'admin_head-edit.php', array( $this, 'add_admin_header_assets' ) ); - add_filter( 'post_row_actions', array( $this, 'filter_row_actions' ), 10, 2 ); - add_filter( 'page_row_actions', array( $this, 'filter_row_actions' ), 10, 2 ); - } - } - - /** - * Display admin notices if plugin's requirements aren't met - */ - public function action_add_admin_notices() { - // Notices are only relevant if current user can get to the Permalinks and Plugins options screens - if ( ! current_user_can( 'manage_options') || ! current_user_can( 'activate_plugins' ) ) { - return; - } - - // Build notices - $message = ''; - - if ( $this->plugin_supported ) { - // Check option for the plugin's rule - // The `$wp_rewrite` global will include it in `extra_rules_top` even though it hasn't been saved to the DB, and therefore isn't really active. - $rewrites = get_option( 'rewrite_rules' ); - - if ( is_array( $rewrites ) && ! array_key_exists( $this->rewrite_rule, $rewrites ) ) { - $message = sprintf( __( 'Please visit the <a href="%1$s">Permalinks</a> settings page to refresh your permalinks. Doing so will add the rules this plugin requires.', 'eth_simple_shortlinks' ), admin_url( 'options-permalink.php' ) ); - } - } else { - $message = sprintf( __( 'Please enable <a href="%1$s">pretty permalinks</a>, otherwise disable this plugin as it is not compatible with "Plain" permalinks.', 'eth_simple_shortlinks' ), admin_url( 'options-permalink.php' ) ); - } - - // Display a notice if one exists - if ( ! empty( $message ) ) { - $message = sprintf( __( '<strong>%1$s</strong>: %2$s', 'eth_simple_shortlinks' ), $this->name, $message ); - - ?><div class="error"> - <p><?php echo $message; ?></p> - </div><?php - } - } - - /** - * PLUGIN FUNCTIONALITY - */ - - /** - * Allow filtering of supported statuses and types - */ - public function filter_support() { - $this->supported_post_statuses = apply_filters( 'eth_simple_shortlinks_allowed_post_statuses', array( 'publish', 'future' ) ); - $this->supported_post_types = apply_filters( 'eth_simple_shortlinks_allowed_post_types', array( 'post', 'page' ) ); - } - - /** - * Add custom query var to those permitted, so it can be detected at `parse_request` - */ - public function filter_query_vars( $qv ) { - $qv[] = $this->qv; - - return $qv; - } - - /** - * Catch this plugin's requests and issue redirects, otherwise WP will serve content at duplicate URLs - * - * Allows invalid post IDs fall through to WP's 404 handler, or anything else that might intercede - * - * URLs aren't validated in case plugins filter permalinks to point to external URLs - */ - public function action_parse_request( $request ) { - if ( isset( $request->query_vars[ $this->qv ] ) ) { - $dest = get_permalink( $request->query_vars[ 'p' ] ); - - if ( $dest ) { - wp_redirect( $dest, 301 ); - exit; - } - } - } - - /** - * Override shortlinks with this plugin's format - */ - public function filter_get_shortlink( $shortlink, $id ) { - if ( empty( $id ) ) { - $_p = get_post(); - - if ( ! empty( $_p->ID ) ) { - $id = $_p->ID; - } - } - - if ( empty( $id ) ) { - return $shortlink; - } - - if ( ! $this->is_supported_post_status( get_post_status( $id ) ) ) { - return $shortlink; - } - - if ( ! $this->is_supported_post_type( get_post_type( $id ) ) ) { - return $shortlink; - } - - return $this->get_shortlink( $id ); - } - - /** - * Header assets for shortlink in row actions - */ - public function add_admin_header_assets() { - global $typenow; - if ( ! $this->is_supported_post_type( $typenow ) ) { - return; - } - - ?> - <script type="text/javascript"> - - jQuery( document ) .ready( function( $ ) { - $( '.row-actions .shortlink a' ).click( function( e ) { - e.preventDefault(); - - prompt( 'URL:', $( this ).attr('href') ); - } ); - } ); - </script> - <?php - } - - /** - * Provide the shortlink in row actions for easy access - */ - public function filter_row_actions( $actions, $post ) { - if ( ! $this->is_supported_post_type( get_post_type( $post ) ) || ! $this->is_supported_post_status( get_post_status( $post ) ) ) { - return $actions; - } - - $actions['shortlink'] = '<a href="' . esc_js( $this->get_shortlink( $post->ID ) ) . '">' . __( 'Shortlink', 'eth_simple_shortlinks' ) . '</a>'; - - return $actions; - } - - /** - * Check if given post type is supported - */ - private function is_supported_post_type( $type ) { - return in_array( $type, $this->supported_post_types ); - } - - /** - * Check if given post status is supported - */ - private function is_supported_post_status( $status ) { - return in_array( $status, $this->supported_post_statuses ); - } - - /** - * Utility method for building permlink - */ - public function get_shortlink( $post_id ) { - // Use Core's default when this plugin can't build a link - if ( ! $this->plugin_supported ) { - return wp_get_shortlink( $post_id ); - } - - return user_trailingslashit( home_url( sprintf( '%s/%d', $this->slug, $post_id ) ) ); - } -} - /** - * One instance to rule them all + * Load ETH Simple Shortlinks. + * + * @package ETH_Simple_Shortlinks */ -ETH_Simple_Shortlinks::get_instance(); /** - * Shortcut for using the shortlink outside of this plugin's considerations + * Plugin Name: ETH Simple Shortlinks + * Plugin URI: https://ethitter.com/plugins/ + * Description: Simple non-GET shortlinks using post IDs + * Author: Erick Hitter + * Version: 0.6 + * Author URI: https://ethitter.com/ + * Text Domain: eth_simple_shortlinks + * Domain Path: /languages/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -function eth_simple_shortlinks_get( $post_id ) { - if ( ! did_action( 'wp_loaded' ) ) { - _doing_it_wrong( __FUNCTION__, __( 'Shortlinks cannot be generated until after <code>wp_loaded</code>; this ensures that all post types are registered.', 'eth_simple_shortlinks' ), '0.3' ); - return false; - } - return ETH_Simple_Shortlinks::get_instance()->get_shortlink( $post_id ); -} +require_once __DIR__ . '/inc/class-eth-simple-shortlinks.php'; diff --git a/inc/class-eth-simple-shortlinks.php b/inc/class-eth-simple-shortlinks.php new file mode 100644 index 0000000000000000000000000000000000000000..9dbb12da2a015947d3aa4af4e06aa31ff3afa539 --- /dev/null +++ b/inc/class-eth-simple-shortlinks.php @@ -0,0 +1,491 @@ +<?php +/** + * Plugin functionality. + * + * @package ETH_Simple_Shortlinks + */ + +/** + * Class ETH_Simple_Shortlinks. + */ +class ETH_Simple_Shortlinks { + /** + * PLUGIN SETUP + */ + + /** + * Singleton + * + * @var self + */ + private static $instance; + + /** + * Instantiate singleton + */ + public static function get_instance() { + if ( ! is_a( self::$instance, __CLASS__ ) ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Dummy magic method + */ + public function __clone() { + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ uh?', 'eth_simple_shortlinks' ), '0.1' ); + } + + /** + * Dummy magic method + */ + public function __wakeup() { + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ uh?', 'eth_simple_shortlinks' ), '0.1' ); + } + + /** + * Dummy magic method + * + * @param string $name Method name. + * @param array $args Method arguments. + * @return null + */ + public function __call( $name = '', $args = array() ) { + return null; + } + + /** + * Plugin name. + * + * @var string + */ + private $name = 'ETH Simple Shortlinks'; + + /** + * Rewrite slug. + * + * @var string + */ + private $slug = 'p'; + + /** + * Rewrite rule. + * + * @var string + */ + private $rewrite_rule; + + /** + * Rewrite rule match. + * + * @var string + */ + private $rewrite_match; + + /** + * Query variable. + * + * @var string + */ + private $qv = 'eth-shortlink'; + + /** + * Is environment ready? + * + * @var bool + */ + private $plugin_supported = false; + + /** + * Post types supported by the plugin. + * + * @var array + */ + private $supported_post_types = array(); + + /** + * Post statuses allowed to redirect to. + * + * @var array + */ + private $supported_post_statuses = array(); + + /** + * Register plugin's setup action. + */ + private function __construct() { + // Build rewrite parts using other class properties. + $this->rewrite_rule = '^' . $this->slug . '/([\d]+)/?$'; + $this->rewrite_match = 'index.php?p=$matches[1]&' . $this->qv . '=1'; + + // Basic plugin actions. + add_action( 'plugins_loaded', array( $this, 'action_plugins_loaded' ) ); + add_action( 'init', array( $this, 'action_init' ) ); + } + + /** + * Load plugin translations. + */ + public function action_plugins_loaded() { + load_plugin_textdomain( + 'eth_simple_shortlinks', + false, + dirname( plugin_basename( __FILE__ ) ) . '/languages/' + ); + } + + /** + * Verify plugin is supported, then register its functionality. + */ + public function action_init() { + global $wp_rewrite; + + // Plugin won't work if site doesn't use pretty permalinks. + if ( empty( $wp_rewrite->permalink_structure ) ) { + add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) ); + return; + } + + $this->plugin_supported = true; + + // Admin notices. + add_action( 'admin_notices', array( $this, 'action_add_admin_notices' ) ); + + // Register rewrite rule. + add_rewrite_rule( $this->rewrite_rule, $this->rewrite_match, 'top' ); + + // Request handling. + add_action( 'wp_loaded', array( $this, 'filter_support' ) ); + add_filter( 'query_vars', array( $this, 'filter_query_vars' ) ); + add_action( 'parse_request', array( $this, 'action_parse_request' ) ); + + // Shortlink overrides. + add_filter( 'get_shortlink', array( $this, 'filter_get_shortlink' ), 10, 2 ); + add_action( 'admin_head-edit.php', array( $this, 'add_admin_header_assets' ) ); + add_filter( 'post_row_actions', array( $this, 'filter_row_actions' ), 10, 2 ); + add_filter( 'page_row_actions', array( $this, 'filter_row_actions' ), 10, 2 ); + } + + /** + * Display admin notices if plugin's requirements aren't met. + */ + public function action_add_admin_notices() { + // Notices are only relevant if current user can get to the Permalinks and Plugins options screens. + if ( ! current_user_can( 'manage_options' ) || ! current_user_can( 'activate_plugins' ) ) { + return; + } + + // Build notices. + $message = ''; + + if ( $this->plugin_supported ) { + // Check option for the plugin's rule. + // The `$wp_rewrite` global will include it in `extra_rules_top` even though it hasn't been saved to the DB, and therefore isn't really active. + $rewrites = get_option( 'rewrite_rules' ); + + if ( is_array( $rewrites ) && ! array_key_exists( $this->rewrite_rule, $rewrites ) ) { + $message = sprintf( + /* translators: 1: URL of permalink options page. */ + __( + 'Please visit the <a href="%1$s">Permalinks</a> settings page to refresh your permalinks. Doing so will add the rules this plugin requires.', + 'eth_simple_shortlinks' + ), + admin_url( 'options-permalink.php' ) + ); + } + } else { + $message = sprintf( + /* translators: 1: URL of permalink options page. */ + __( + 'Please enable <a href="%1$s">pretty permalinks</a>, otherwise disable this plugin as it is not compatible with "Plain" permalinks.', + 'eth_simple_shortlinks' + ), + admin_url( 'options-permalink.php' ) + ); + } + + // Display a notice if one exists. + if ( ! empty( $message ) ) { + $message = sprintf( + /* translators: 1: Plugin name, 2: Notice text. */ + __( + '<strong>%1$s</strong>: %2$s', + 'eth_simple_shortlinks' + ), + $this->name, + $message + ); + + ?> + <div class="error"> + <p><?php echo wp_kses_post( $message ); ?></p> + </div> + <?php + } + } + + /** + * PLUGIN FUNCTIONALITY + */ + + /** + * Allow filtering of supported statuses and types. + */ + public function filter_support() { + /** + * Filters supported post statuses. + * + * @param array $statuses Supported statuses. + */ + $this->supported_post_statuses = apply_filters( 'eth_simple_shortlinks_allowed_post_statuses', array( 'publish', 'future' ) ); + + /** + * Filters supported post types. + * + * @param array $types Post types. + */ + $this->supported_post_types = apply_filters( 'eth_simple_shortlinks_allowed_post_types', array( 'post', 'page' ) ); + } + + /** + * Add custom query var to those permitted, so it can be detected at `parse_request`. + * + * @param array $qv Registered query vars. + * @return array + */ + public function filter_query_vars( $qv ) { + $qv[] = $this->qv; + + return $qv; + } + + /** + * Catch this plugin's requests and issue redirects, otherwise WP + * will serve content at duplicate URLs. + * + * Allows invalid post IDs fall through to WP's 404 handler, or + * anything else that might intercede. + * + * @param WP $request WP object. + */ + public function action_parse_request( $request ) { + $redirect = $this->get_redirect_for_request( $request ); + + if ( null === $redirect ) { + return; + } + + /** + * URLs aren't validated in case plugins filter permalinks to point to external URLs. + * + * If validation is desired, hook into the `eth_simple_shortlinks_redirect_url` filter. + */ + // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + wp_redirect( $redirect->destination, $redirect->status_code ); + exit; + } + + /** + * Parse a given WP object for its redirect destination. + * + * @param WP $request WP object. + * @return \stdClass|null + */ + public function get_redirect_for_request( $request ) { + if ( ! isset( $request->query_vars[ $this->qv ], $request->query_vars['p'] ) ) { + return null; + } + + $post_object = get_post( $request->query_vars['p'] ); + + if ( ! $post_object instanceof WP_Post ) { + return null; + } + + /** + * Filters if post type and status should be validated. + * + * @since 0.6 + * + * @param bool $validate Perform validation. + * @param WP_Post $post_object Post being redirected to. + * @param WP $request WP object. + */ + if ( + apply_filters( 'eth_simple_shortlinks_verify_requested_post_support', true, $post_object, $request ) && + ( + ! $this->is_supported_post_type( $post_object->post_type ) || + ! $this->is_supported_post_status( $post_object->post_status ) + ) + ) { + return null; + } + + /** + * Filters the redirect URL. + * + * @since 0.6 + * + * @param string $destination Redirect destination. + * @param WP_Post $post_object Post being redirected to. + * @param WP $request WP object. + */ + $destination = apply_filters( 'eth_simple_shortlinks_redirect_url', get_permalink( $post_object ), $post_object, $request ); + + /** + * Filters the redirect status code. + * + * @since 0.6 + * + * @param int $status_code Redirect status code. + * @param string $destination Redirect destination. + * @param WP_Post $post_object Post being redirected to. + * @param WP $request WP object. + */ + $status_code = (int) apply_filters( 'eth_simple_shortlinks_redirect_status', 301, $destination, $post_object, $request ); + + if ( empty( $destination ) || empty( $status_code ) ) { + return null; + } + + return (object) compact( 'destination', 'status_code' ); + } + + /** + * Override shortlinks with this plugin's format. + * + * @param string $shortlink Short URL. + * @param int $id Post ID. + * @return string + */ + public function filter_get_shortlink( $shortlink, $id ) { + if ( empty( $id ) ) { + $_p = get_post(); + + if ( ! empty( $_p->ID ) ) { + $id = $_p->ID; + } + } + + if ( empty( $id ) ) { + return $shortlink; + } + + if ( ! $this->is_supported_post_status( get_post_status( $id ) ) ) { + return $shortlink; + } + + if ( ! $this->is_supported_post_type( get_post_type( $id ) ) ) { + return $shortlink; + } + + return $this->get_shortlink( $id ); + } + + /** + * Header assets for shortlink in row actions. + */ + public function add_admin_header_assets() { + global $typenow; + if ( ! $this->is_supported_post_type( $typenow ) ) { + return; + } + + ?> + <script type="text/javascript"> + + jQuery( document ) .ready( function( $ ) { + $( '.row-actions .shortlink a' ).click( function( e ) { + e.preventDefault(); + + prompt( 'URL:', $( this ).attr('href') ); + } ); + } ); + </script> + <?php + } + + /** + * Provide the shortlink in row actions for easy access. + * + * @param array $actions Row actions. + * @param WP_Post $post Post object. + * @return array + */ + public function filter_row_actions( $actions, $post ) { + if ( ! $this->is_supported_post_type( get_post_type( $post ) ) || ! $this->is_supported_post_status( get_post_status( $post ) ) ) { + return $actions; + } + + $actions['shortlink'] = '<a href="' . esc_js( $this->get_shortlink( $post->ID ) ) . '">' . __( 'Shortlink', 'eth_simple_shortlinks' ) . '</a>'; + + return $actions; + } + + /** + * Check if given post type is supported. + * + * @param string $type Post type. + * @return bool + */ + private function is_supported_post_type( $type ) { + return in_array( $type, $this->supported_post_types, true ); + } + + /** + * Check if given post status is supported. + * + * @param string $status Post status. + * @return bool + */ + private function is_supported_post_status( $status ) { + return in_array( $status, $this->supported_post_statuses, true ); + } + + /** + * Utility method for building permalink. + * + * @param int $post_id Post ID. + * @return string + */ + public function get_shortlink( $post_id ) { + // Use Core's default when this plugin can't build a link. + if ( ! $this->plugin_supported ) { + return wp_get_shortlink( $post_id ); + } + + return user_trailingslashit( home_url( sprintf( '%s/%d', $this->slug, $post_id ) ) ); + } +} + +/** + * One instance to rule them all + */ +ETH_Simple_Shortlinks::get_instance(); + +/** + * Shortcut for using the shortlink outside of this plugin's considerations. + * + * @param int $post_id Post ID. + * @return string + */ +function eth_simple_shortlinks_get( $post_id ) { + if ( ! did_action( 'wp_loaded' ) ) { + _doing_it_wrong( + __FUNCTION__, + wp_kses_post( + __( + 'Shortlinks cannot be generated until after <code>wp_loaded</code>; this ensures that all post types are registered.', + 'eth_simple_shortlinks' + ) + ), + '0.3' + ); + + return false; + } + + return ETH_Simple_Shortlinks::get_instance()->get_shortlink( $post_id ); +} diff --git a/languages/eth-simple-shortlinks.pot b/languages/eth-simple-shortlinks.pot index bf9b3a5f7cec79430b85db643333b98b4e8b850a..a5b77c8aff7dbd20fdc0b8ef7b0e7c6b142710bb 100644 --- a/languages/eth-simple-shortlinks.pot +++ b/languages/eth-simple-shortlinks.pot @@ -2,10 +2,10 @@ # This file is distributed under the same license as the ETH Simple Shortlinks package. msgid "" msgstr "" -"Project-Id-Version: ETH Simple Shortlinks 0.5\n" +"Project-Id-Version: ETH Simple Shortlinks 0.6\n" "Report-Msgid-Bugs-To: " "https://wordpress.org/support/plugin/eth-simple-shortlinks\n" -"POT-Creation-Date: 2019-04-14 04:31:11+00:00\n" +"POT-Creation-Date: 2019-05-12 23:21:03+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -25,31 +25,35 @@ msgstr "" "X-Poedit-Bookmarks: \n" "X-Textdomain-Support: yes\n" -#: eth-simple-shortlinks.php:51 eth-simple-shortlinks.php:52 +#: inc/class-eth-simple-shortlinks.php:38 +#: inc/class-eth-simple-shortlinks.php:45 msgid "Cheatin’ uh?" msgstr "" -#: eth-simple-shortlinks.php:138 +#: inc/class-eth-simple-shortlinks.php:191 +#. translators: 1: URL of permalink options page. msgid "" "Please visit the <a href=\"%1$s\">Permalinks</a> settings page to refresh " "your permalinks. Doing so will add the rules this plugin requires." msgstr "" -#: eth-simple-shortlinks.php:141 +#: inc/class-eth-simple-shortlinks.php:201 +#. translators: 1: URL of permalink options page. msgid "" "Please enable <a href=\"%1$s\">pretty permalinks</a>, otherwise disable " "this plugin as it is not compatible with \"Plain\" permalinks." msgstr "" -#: eth-simple-shortlinks.php:146 +#: inc/class-eth-simple-shortlinks.php:213 +#. translators: 1: Plugin name, 2: Notice text. msgid "<strong>%1$s</strong>: %2$s" msgstr "" -#: eth-simple-shortlinks.php:251 +#: inc/class-eth-simple-shortlinks.php:422 msgid "Shortlink" msgstr "" -#: eth-simple-shortlinks.php:293 +#: inc/class-eth-simple-shortlinks.php:479 msgid "" "Shortlinks cannot be generated until after <code>wp_loaded</code>; this " "ensures that all post types are registered." diff --git a/readme.txt b/readme.txt index 5c47f052e0e907f0fb4f4d5fcd4156b2397399d0..42ca0fee77b1857e91599de6ecc5286445d15185 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Donate link: https://ethitter.com/donate/ Tags: seo, meta tags Requires at least: 4.4 Tested up to: 5.2 -Stable tag: 0.5 +Stable tag: 0.6 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -30,8 +30,25 @@ For example, http://example.com/?p=123 becomes http://example.com/p/123/. No, shortlinks use the posts' IDs, so aren't available for modification. += Why aren't redirects validated? = + +Sites may use plugins that allow a post object's permalink to be set to an external URL, and this plugin is designed to respect those plugins. + +If you wish to validate the redirects issued by this plugin, you can use the `eth_simple_shortlinks_redirect_url` filter to apply `wp_validate_redirect()` to the destination URL. + += After upgrading to 0.6, redirects stopped working = + +Beginning with release 0.6, before performing a redirect, the plugin checks that the post type and post status are supported. Previously, these checks were only applied when overriding an object's shortlink. + +If, after upgrading, redirects stop working, use the `eth_simple_shortlinks_allowed_post_types` and `eth_simple_shortlinks_allowed_post_statuses` filters to permit additional types and statuses, or use the `eth_simple_shortlinks_verify_requested_post_support` filter to disable the supports checks. + == Changelog == += 0.6 = +* Introduce filters in redirection handling. +* Apply supported post-type and post-status checks before redirecting. +* Conform to WordPress VIP's Coding Standards. + = 0.5 = * Admin notices when permalinks won't support the plugin * Disable plugin functionality when permalink structure is incompatible @@ -39,3 +56,9 @@ No, shortlinks use the posts' IDs, so aren't available for modification. = 0.4 = * Initial release + +== Upgrade Notice == + += 0.6 = + +Applies supported post-type and post-status checks before performing redirect. If, after upgrading, redirects stop working, see the "After upgrading to 0.6, redirects stopped working" section of the FAQ. diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f40e77271f4e5330b21b07efd0341ac3fb7abf57..64e9b989474adbc19f6ff29207308a53fa076f5a 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -23,6 +23,9 @@ require_once $_tests_dir . '/includes/functions.php'; * Manually load the plugin being tested. */ function _manually_load_plugin() { + // Plugin requires a permalink structure to operate. + _set_default_permalink_structure_for_tests(); + require dirname( dirname( __FILE__ ) ) . '/eth-simple-shortlinks.php'; } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); diff --git a/tests/test-plugin.php b/tests/test-plugin.php new file mode 100755 index 0000000000000000000000000000000000000000..1252510bfd9fc6b468c0a92554b6a0aa6f12d750 --- /dev/null +++ b/tests/test-plugin.php @@ -0,0 +1,81 @@ +<?php +/** + * Class PluginTest + * + * @package ETH_Simple_Shortlinks + */ + +/** + * Plugin test case. + */ +class PluginTest extends WP_UnitTestCase { + /** + * Post ID for published tests. + * + * @var int + */ + protected static $post_id_published; + + /** + * Post ID for draft tests. + * + * @var int + */ + protected static $post_id_draft; + + /** + * Create a post to test with. + */ + public function setUp() { + parent::setUp(); + + static::$post_id_published = $this->factory->post->create(); + static::$post_id_draft = $this->factory->post->create( + array( + 'post_status' => 'draft', + ) + ); + } + + /** + * Test shortlink overrides. + */ + public function test_shortlink_filters() { + $expected_published = user_trailingslashit( home_url( 'p/' . static::$post_id_published ) ); + $expected_draft = add_query_arg( 'p', static::$post_id_draft, user_trailingslashit( home_url() ) ); + + $this->assertEquals( $expected_published, wp_get_shortlink( static::$post_id_published ), 'Failed to assert that published post has a simple shortlink.' ); + $this->assertEquals( $expected_draft, wp_get_shortlink( static::$post_id_draft ), 'Failed to assert that draft post did not have its shortlink modified.' ); + } + + /** + * Test redirect parsing for supported post. + */ + public function test_published_post_redirect() { + $fake_request = new \stdClass(); + $fake_request->query_vars = array( + 'p' => static::$post_id_published, + 'eth-shortlink' => true, + ); + + $redirect = ETH_Simple_Shortlinks::get_instance()->get_redirect_for_request( $fake_request ); + + $this->assertEquals( get_permalink( static::$post_id_published ), $redirect->destination, 'Failed to assert that redirect destination is post\'s permalink.' ); + $this->assertEquals( 301, $redirect->status_code, 'Failed to assert that redirect status code is that for a permanent redirect.' ); + } + + /** + * Test redirect parsing for unsupported post. + */ + public function test_draft_post_redirect() { + $fake_request = new \stdClass(); + $fake_request->query_vars = array( + 'p' => static::$post_id_draft, + 'eth-shortlink' => true, + ); + + $redirect = ETH_Simple_Shortlinks::get_instance()->get_redirect_for_request( $fake_request ); + + $this->assertNull( $redirect, 'Failed to assert that redirect is not generated for unsupported post status.' ); + } +} diff --git a/tests/test-sample.php b/tests/test-sample.php deleted file mode 100755 index d2da28996e7c1ae676df62c9fd21fc2459aa7e15..0000000000000000000000000000000000000000 --- a/tests/test-sample.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Class SampleTest - * - * @package ETH_Simple_Shortlinks - */ - -/** - * Sample test case. - */ -class SampleTest extends WP_UnitTestCase { - - /** - * A single example test. - */ - public function test_sample() { - // Replace this with some actual testing code. - $this->assertTrue( true ); - } -}