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 );
}
}
+
}