diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f1d68ac --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "humanmade/wp-permastructure", + "description": "Post type rewrite features for WordPress", + "type": "wordpress-plugin", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Robert O'Rourke", + "email": "rob@humanmade.com" + } + ], + "require": {} +} diff --git a/wp-permastructure.php b/wp-permastructure.php index 0040a7b..5c0eaf2 100644 --- a/wp-permastructure.php +++ b/wp-permastructure.php @@ -3,7 +3,7 @@ Plugin Name: WP Permastructure Plugin URI: https://github.com/interconnectit/wp-permastructure Description: Adds the ability to define permalink structures for any custom post type using rewrite tags. -Version: 1.4.3 +Version: 1.6.1 Author: Robert O'Rourke Author URI: http://interconnectit.com License: GPLv2 or later @@ -19,9 +19,9 @@ * eg: * * register_post_type( 'my_type', array( - * ... - * 'rewrite' => array( 'permastruct' => '/%custom_taxonomy%/%author%/%postname%/' ), - * ... + * ... + * 'rewrite' => array( 'permastruct' => '/%custom_taxonomy%/%author%/%postname%/' ), + * ... * ) ); * * Alternatively you can set the permalink structure from the permalinks settings page @@ -31,6 +31,8 @@ /** * Changelog * + * 1.6: Sort permastructs by length for specifity, allow filtering whether to expand hierarchical terms + * 1.5: Ensure rewrites prefixed with taxonomies are checked before the taxonomies themselves * 1.4: Handles sample filters without the need for the get post call * 1.3: Fixed permalink sanitisation, was truncating the third placeholder for some reason * 1.2: Fixed attachment URL rewrites, fixed edge case where permastruct is %postname% only @@ -38,8 +40,6 @@ * 1.0: Initial import */ -if ( ! class_exists( 'wp_permastructure' ) ) { - add_action( 'init', array( 'wp_permastructure', 'instance' ), 0 ); class wp_permastructure { @@ -66,8 +66,9 @@ class wp_permastructure { * @return void */ public static function instance() { - null === self :: $instance AND self :: $instance = new self; - return self :: $instance; + null === self::$instance AND self::$instance = new self; + + return self::$instance; } @@ -109,7 +110,7 @@ public function admin_init() { 'permalink' ); - foreach( get_post_types( array( '_builtin' => false, 'public' => true ), 'objects' ) as $type ) { + foreach ( get_post_types( array( '_builtin' => false, 'public' => true ), 'objects' ) as $type ) { $id = $type->name . '_permalink_structure'; register_setting( 'permalink', $id, array( $this, 'sanitize_permalink' ) ); @@ -145,20 +146,21 @@ public function permalink_field( $args ) { * @return string Sanitised permalink structure */ public function sanitize_permalink( $permalink ) { - if ( ! empty( $permalink ) && ! preg_match( '/%(post_id|postname)%/', $permalink ) ) + if ( ! empty( $permalink ) && ! preg_match( '/%(post_id|postname)%/', $permalink ) ) { add_settings_error( 'permalink_structure', 10, __( 'Permalink structures must contain at least the %post_id% or %postname%.' ) ); + } $filtered = wp_check_invalid_utf8( $permalink ); - if ( strpos($filtered, '<') !== false ) { + if ( strpos( $filtered, '<' ) !== false ) { $filtered = wp_pre_kses_less_than( $filtered ); // This will strip extra whitespace for us. $filtered = wp_strip_all_tags( $filtered, true ); } else { - $filtered = trim( preg_replace('/[\r\n\t ]+/', ' ', $filtered) ); + $filtered = trim( preg_replace( '/[\r\n\t ]+/', ' ', $filtered ) ); } - return preg_replace( '/[^a-zA-Z0-9\/\%_-]*/', '', $filtered ); + return preg_replace( '/[^a-zA-Z0-9\/\%\._-]*/', '', $filtered ); } @@ -173,8 +175,9 @@ public function add_permastructs( $rules ) { global $wp_rewrite; // restore endpoints - if ( empty( $wp_rewrite->endpoints ) && ! empty( $this->endpoints ) ) + if ( empty( $wp_rewrite->endpoints ) && ! empty( $this->endpoints ) ) { $wp_rewrite->endpoints = $this->endpoints; + } $permastruct = $wp_rewrite->permalink_structure; $permastructs = array( $permastruct => array( 'post' ) ); @@ -183,45 +186,59 @@ public function add_permastructs( $rules ) { $wp_rewrite->use_verbose_page_rules = false; // get permastructs foreach custom post type and group any that use the same struct - foreach( get_post_types( array( '_builtin' => false, 'public' => true ), 'objects' ) as $type ) { + foreach ( get_post_types( array( '_builtin' => false, 'public' => true ), 'objects' ) as $type ) { // add/override the custom permalink structure if set in options $post_type_permastruct = get_option( $type->name . '_permalink_structure' ); if ( $post_type_permastruct && ! empty( $post_type_permastruct ) ) { - if ( ! is_array( $type->rewrite ) ) + if ( ! is_array( $type->rewrite ) ) { $type->rewrite = array(); + } $type->rewrite[ 'permastruct' ] = $post_type_permastruct; } // check we have a custom permalink structure - if ( ! is_array( $type->rewrite ) || ! isset( $type->rewrite[ 'permastruct' ] ) ) + if ( ! is_array( $type->rewrite ) || ! isset( $type->rewrite[ 'permastruct' ] ) ) { continue; + } // remove default struct rules - add_filter( $type->name . '_rewrite_rules', create_function( '$rules', 'return array();' ), 11 ); + add_filter( $type->name . '_rewrite_rules', function( $rules ) { + return array(); + }, 11 ); - if ( ! isset( $permastructs[ $type->rewrite[ 'permastruct' ] ] ) ) + if ( ! isset( $permastructs[ $type->rewrite[ 'permastruct' ] ] ) ) { $permastructs[ $type->rewrite[ 'permastruct' ] ] = array(); + } $permastructs[ $type->rewrite[ 'permastruct' ] ][] = $type->name; } + // Sort permastructs by longest to shortest, e.g. higher specificity to lower. + uksort( $permastructs, function ( $a, $b ) { + return mb_strlen( $b ) <=> mb_strlen( $a ); + } ); + $rules = array(); // add our permastructs scoped to the post types - overwriting any keys that already exist - foreach( $permastructs as $struct => $post_types ) { + foreach ( $permastructs as $struct => $post_types ) { // if a struct is %postname% only then we need page rules first - if not found wp tries again with later rules - if ( preg_match( '/^\/?%postname%\/?$/', $struct ) ) + if ( preg_match( '/^\/?%postname%\/?$/', $struct ) ) { $wp_rewrite->use_verbose_page_rules = true; + } // get rewrite rules without walking dirs $post_type_rules_temp = $wp_rewrite->generate_rewrite_rules( $struct, EP_PERMALINK, false, true, false, false, true ); - foreach( $post_type_rules_temp as $regex => $query ) { + foreach ( $post_type_rules_temp as $regex => $query ) { if ( preg_match( '/(&|\?)(cpage|attachment|p|name|pagename)=/', $query ) ) { - $post_type_query = ( count( $post_types ) < 2 ? '&post_type=' . $post_types[ 0 ] : '&post_type[]=' . join( '&post_type[]=', array_unique( $post_types ) ) ); + $post_type_query = ( count( $post_types ) < 2 ? '&post_type=' . $post_types[0] : '&post_type[]=' . join( '&post_type[]=', array_unique( $post_types ) ) ); $rules[ $regex ] = $query . ( preg_match( '/(&|\?)(attachment|pagename)=/', $query ) ? '' : $post_type_query ); - } else + // Ensure permalinks that match a custom taxonomy path don't get swallowed. + $wp_rewrite->extra_rules_top[ $regex ] = $rules[ $regex ]; + } else { unset( $rules[ $regex ] ); + } } } @@ -234,22 +251,23 @@ public function add_permastructs( $rules ) { * Generic version of standard permalink parsing function. Adds support for * custom taxonomies as well as the standard %author% etc... * - * @param string $post_link The post URL - * @param WP_Post $post The post object - * @param bool $leavename Passed to pre_post_link filter - * @param bool $sample Used in admin if generating an example permalink + * @param string $post_link The post URL + * @param WP_Post $post The post object + * @param bool $leavename Passed to pre_post_link filter + * @param bool $sample Used in admin if generating an example permalink * * @return string The parsed permalink */ public function parse_permalinks( $post_link, $post, $leavename, $sample = false ) { - // Yoast Sitemap plug-in doesn't pass a WP_Post object causing a fatal, so we'll check for it and return. - if ( !is_a( $post, 'WP_Post' ) ) { - return $post_link; - } + // Yoast Sitemap plug-in doesn't pass a WP_Post object causing a fatal, so we'll check for it and return. + if ( ! is_a( $post, 'WP_Post' ) ) { + return $post_link; + } // Make a stupid request and we'll do nothing. - if ( !post_type_exists( $post->post_type ) ) + if ( ! post_type_exists( $post->post_type ) ) { return $post_link; + } $rewritecode = array( '%year%', @@ -258,25 +276,31 @@ public function parse_permalinks( $post_link, $post, $leavename, $sample = false '%hour%', '%minute%', '%second%', - $leavename? '' : '%postname%', + $leavename ? '' : '%postname%', '%post_id%', '%author%', - $leavename? '' : '%pagename%', + $leavename ? '' : '%pagename%', ); $taxonomies = get_object_taxonomies( $post->post_type ); - foreach( $taxonomies as $taxonomy ) + foreach ( $taxonomies as $taxonomy ) { $rewritecode[] = '%' . $taxonomy . '%'; + } - if ( is_object($post) && isset($post->filter) && 'sample' == $post->filter ) + if ( is_object( $post ) && isset( $post->filter ) && 'sample' == $post->filter ) { $sample = true; + } $post_type = get_post_type_object( $post->post_type ); $permastruct = get_option( $post_type->name . '_permalink_structure' ); + if ( empty( $permastruct ) && $post->post_type === 'post' ) { + $permastruct = get_option( 'permalink_structure' ); + } + // prefer option over default - if ( $permastruct && ! empty( $permastruct ) ) { + if ( ! empty( $permastruct ) ) { $permalink = $permastruct; } elseif ( isset( $post_type->rewrite[ 'permastruct' ] ) && ! empty( $post_type->rewrite[ 'permastruct' ] ) ) { $permalink = $post_type->rewrite[ 'permastruct' ]; @@ -284,61 +308,86 @@ public function parse_permalinks( $post_link, $post, $leavename, $sample = false return $post_link; } - $permalink = apply_filters('pre_post_link', $permalink, $post, $leavename); + $permalink = apply_filters( 'pre_post_link', $permalink, $post, $leavename ); - if ( '' != $permalink && !in_array($post->post_status, array('draft', 'pending', 'auto-draft')) ) { - $unixtime = strtotime($post->post_date); + if ( + '' !== $permalink + && ! wp_force_plain_post_permalink( $post, $sample ) + ) { + $unixtime = strtotime( $post->post_date ); // add ability to use any taxonomies in post type permastruct $replace_terms = array(); - foreach( $taxonomies as $taxonomy ) { + foreach ( $taxonomies as $taxonomy ) { $term = ''; $taxonomy_object = get_taxonomy( $taxonomy ); - if ( strpos($permalink, '%'. $taxonomy .'%') !== false ) { + if ( strpos( $permalink, '%' . $taxonomy . '%' ) !== false ) { $terms = get_the_terms( $post->ID, $taxonomy ); - if ( $terms ) { - usort($terms, '_usort_terms_by_ID'); // order by ID - $term = $terms[0]->slug; - if ( $taxonomy_object->hierarchical && $parent = $terms[0]->parent ) - $term = get_term_parents($parent, $taxonomy, false, '/', true) . $term; + if ( $terms && ! is_wp_error( $terms ) ) { + // order by term_id ASC. + $terms = wp_list_sort( $terms, 'term_id', 'ASC' ); + + /** + * Filter the term that gets used in the `$tax` permalink token. + * + * @param WP_Term $term The `$tax` term to use in the permalink. + * @param array $terms Array of all `$tax` terms associated with the post. + * @param WP_Post $post The post in question. + */ + $term_object = apply_filters( "post_link_{$taxonomy}", reset( $terms ), $terms, $post ); + + /** + * Filter whether to expand child term paths. + * + * @param bool $allow_hierarchy Whether to expand child terms to include term parents. + * @param WP_Term $term Current term object selected. + * @param WP_Post $post The post in question. + */ + $allow_hierarchy = apply_filters( "post_link_{$taxonomy}_allow_hierarchy", $taxonomy_object->hierarchical, $term_object, $post ); + + $term = $term_object->slug; + if ( $allow_hierarchy && $parent = $term_object->parent ) { + $term = get_term_parents( $parent, $taxonomy, false, '/', true ) . $term; + } } // show default category in permalinks, without // having to assign it explicitly - if ( empty( $term ) && $taxonomy == 'category' ) { + if ( empty( $term ) && $taxonomy === 'category' ) { $default_category = get_category( get_option( 'default_category' ) ); $term = is_wp_error( $default_category ) ? '' : $default_category->slug; } - } $replace_terms[ $taxonomy ] = $term; } $author = ''; - if ( strpos($permalink, '%author%') !== false ) { - $authordata = get_userdata($post->post_author); + if ( strpos( $permalink, '%author%' ) !== false ) { + $authordata = get_userdata( $post->post_author ); $author = $authordata->user_nicename; } - $date = explode(" ",date('Y m d H i s', $unixtime)); + $date = explode( " ", date( 'Y m d H i s', $unixtime ) ); $rewritereplace = - array( - $date[0], - $date[1], - $date[2], - $date[3], - $date[4], - $date[5], - $post->post_name, - $post->ID, - $author, - $post->post_name, - ); - foreach( $taxonomies as $taxonomy ) + array( + $date[ 0 ], + $date[ 1 ], + $date[ 2 ], + $date[ 3 ], + $date[ 4 ], + $date[ 5 ], + $post->post_name, + $post->ID, + $author, + $post->post_name, + ); + foreach ( $taxonomies as $taxonomy ) { $rewritereplace[] = $replace_terms[ $taxonomy ]; - $permalink = home_url( str_replace($rewritecode, $rewritereplace, $permalink) ); - $permalink = user_trailingslashit($permalink, 'single'); - } else { // if they're not using the fancy permalink option - $permalink = home_url('?p=' . $post->ID); + } + $permalink = home_url( str_replace( $rewritecode, $rewritereplace, $permalink ) ); + $permalink = user_trailingslashit( $permalink, 'single' ); + } else { + // If they're not using the fancy permalink option or it's forced to be plain. + $permalink = $post_link; } return $permalink; @@ -346,41 +395,51 @@ public function parse_permalinks( $post_link, $post, $leavename, $sample = false } -} - if ( ! function_exists( 'get_term_parents' ) ) { /** * Retrieve term parents with separator. * - * @param int $id Term ID. + * @param int $id Term ID. * @param string $taxonomy The taxonomy the term belongs to. - * @param bool $link Optional, default is false. Whether to format with link. + * @param bool $link Optional, default is false. Whether to format with link. * @param string $separator Optional, default is '/'. How to separate categories. - * @param bool $nicename Optional, default is false. Whether to use nice name for display. - * @param array $visited Optional. Already linked to categories to prevent duplicates. + * @param bool $nicename Optional, default is false. Whether to use nice name for display. + * @param array $visited Optional. Already linked to categories to prevent duplicates. + * * @return string */ - function get_term_parents( $id, $taxonomy, $link = false, $separator = '/', $nicename = false, $visited = array() ) { + function get_term_parents( + $id, + $taxonomy, + $link = false, + $separator = '/', + $nicename = false, + $visited = array() + ) { $chain = ''; $parent = get_term( $id, $taxonomy ); - if ( is_wp_error( $parent ) ) + if ( is_wp_error( $parent ) ) { return $parent; + } - if ( $nicename ) + if ( $nicename ) { $name = $parent->slug; - else + } else { $name = $parent->cat_name; + } - if ( $parent->parent && ( $parent->parent != $parent->term_id ) && !in_array( $parent->parent, $visited ) ) { + if ( $parent->parent && ( $parent->parent != $parent->term_id ) && ! in_array( $parent->parent, $visited ) ) { $visited[] = $parent->parent; $chain .= get_term_parents( $parent->parent, $taxonomy, $link, $separator, $nicename, $visited ); } - if ( $link ) - $chain .= 'name ) ) . '">'.$name.'' . $separator; - else - $chain .= $name.$separator; + if ( $link ) { + $chain .= '' . $name . '' . $separator; + } else { + $chain .= $name . $separator; + } + return $chain; } @@ -401,28 +460,31 @@ function enable_permalinks_settings() { // save hook for permalinks page if ( isset( $_POST['permalink_structure'] ) || isset( $_POST['category_base'] ) ) { - check_admin_referer('update-permalink'); + check_admin_referer( 'update-permalink' ); $option_page = 'permalink'; $capability = 'manage_options'; $capability = apply_filters( "option_page_capability_{$option_page}", $capability ); - if ( !current_user_can( $capability ) ) - wp_die(__('Cheatin’ uh?')); + if ( ! current_user_can( $capability ) ) { + wp_die( __( 'Cheatin’ uh?' ) ); + } // get extra permalink options $options = $new_whitelist_options[ $option_page ]; if ( $options ) { foreach ( $options as $option ) { - $option = trim($option); + $option = trim( $option ); $value = null; - if ( isset($_POST[$option]) ) - $value = $_POST[$option]; - if ( !is_array($value) ) - $value = trim($value); - $value = stripslashes_deep($value); + if ( isset( $_POST[ $option ] ) ) { + $value = $_POST[ $option ]; + } + if ( ! is_array( $value ) ) { + $value = trim( $value ); + } + $value = stripslashes_deep( $value ); update_option( $option, $value ); } } @@ -430,7 +492,8 @@ function enable_permalinks_settings() { /** * Handle settings errors */ - set_transient('settings_errors', get_settings_errors(), 30); + set_transient( 'settings_errors', get_settings_errors(), 30 ); } } + }