From 81a26f690026ba86c6874fb0b0a804ada675a8bc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 26 Oct 2016 08:07:30 +0000 Subject: [PATCH] Posts, Post Types: Add support for post type templates. WordPress has supported custom page templates for over 12 years, allowing developers to create various layouts for specific pages. While this feature is very helpful, it has always been limited to the 'page' post type and not was not available to other post types. By opening up the page template functionality to all post types, we continue to improve the template hierarchy's flexibility. In addition to the `Template Name` file header, the post types supported by a template can be specified using `Template Post Type: post, foo, bar`. When at least one template exists for a post type, the 'Post Attributes' meta box will be displayed in the back end, without the need to add post type support for `'page-attributes'`. 'Post Attributes' can be customized per post type using the `'attributes'` label when registering a post type. Props johnbillion, Mte90, dipesh.kakadiya, swissspidy. Fixes #18375. Built from https://develop.svn.wordpress.org/trunk@38951 git-svn-id: http://core.svn.wordpress.org/trunk@38894 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/edit-form-advanced.php | 5 +- .../includes/class-wp-posts-list-table.php | 39 +++--- wp-admin/includes/meta-boxes.php | 34 +++--- wp-admin/includes/template.php | 17 +-- wp-admin/includes/theme.php | 8 +- wp-includes/class-wp-post.php | 2 +- wp-includes/class-wp-theme.php | 87 ++++++++++---- wp-includes/post-template.php | 113 ++++++++++-------- wp-includes/post.php | 7 +- wp-includes/template.php | 19 ++- wp-includes/version.php | 2 +- 11 files changed, 203 insertions(+), 130 deletions(-) diff --git a/wp-admin/edit-form-advanced.php b/wp-admin/edit-form-advanced.php index 540f1edab2..73cb63b8e3 100644 --- a/wp-admin/edit-form-advanced.php +++ b/wp-admin/edit-form-advanced.php @@ -259,8 +259,9 @@ foreach ( get_object_taxonomies( $post ) as $tax_name ) { add_meta_box( $tax_meta_box_id, $label, $taxonomy->meta_box_cb, null, 'side', 'core', array( 'taxonomy' => $tax_name ) ); } -if ( post_type_supports($post_type, 'page-attributes') ) - add_meta_box('pageparentdiv', 'page' == $post_type ? __('Page Attributes') : __('Attributes'), 'page_attributes_meta_box', null, 'side', 'core'); +if ( post_type_supports( $post_type, 'page-attributes' ) || count( get_page_templates( null, $post_type ) ) > 0 ) { + add_meta_box( 'pageparentdiv', $post_type_object->labels->attributes, 'page_attributes_meta_box', null, 'side', 'core' ); +} if ( $thumbnail_support && current_user_can( 'upload_files' ) ) add_meta_box('postimagediv', esc_html( $post_type_object->labels->featured_image ), 'post_thumbnail_meta_box', null, 'side', 'low'); diff --git a/wp-admin/includes/class-wp-posts-list-table.php b/wp-admin/includes/class-wp-posts-list-table.php index 714a491d2e..d26ab2f11b 100644 --- a/wp-admin/includes/class-wp-posts-list-table.php +++ b/wp-admin/includes/class-wp-posts-list-table.php @@ -1528,31 +1528,28 @@ class WP_Posts_List_Table extends WP_List_Table { - post_type ) : - ?> - - - + post_type ) ) ) : ?> + + + diff --git a/wp-admin/includes/meta-boxes.php b/wp-admin/includes/meta-boxes.php index 48c5e37a53..13d463d2c9 100644 --- a/wp-admin/includes/meta-boxes.php +++ b/wp-admin/includes/meta-boxes.php @@ -788,8 +788,7 @@ function post_revisions_meta_box( $post ) { * @param object $post */ function page_attributes_meta_box($post) { - $post_type_object = get_post_type_object($post->post_type); - if ( $post_type_object->hierarchical ) { + if ( is_post_type_hierarchical( $post->post_type ) ) : $dropdown_args = array( 'post_type' => $post->post_type, 'exclude_tree' => $post->ID, @@ -812,16 +811,17 @@ function page_attributes_meta_box($post) { */ $dropdown_args = apply_filters( 'page_attributes_dropdown_pages_args', $dropdown_args, $post ); $pages = wp_dropdown_pages( $dropdown_args ); - if ( ! empty($pages) ) { + if ( ! empty($pages) ) : ?>

post_type && 0 != count( get_page_templates( $post ) ) && get_option( 'page_for_posts' ) != $post->ID ) { - $template = !empty($post->page_template) ? $post->page_template : false; + endif; // end empty pages check + endif; // end hierarchical check. + + if ( count( get_page_templates( $post ) ) > 0 && get_option( 'page_for_posts' ) != $post->ID ) : + $template = ! empty( $post->page_template ) ? $post->page_template : false; ?>

- - +post_type ); ?> - + +post_type, 'page-attributes' ) ) : ?>

-post_type && get_current_screen()->get_help_tabs() ) { ?> +post_type && get_current_screen()->get_help_tabs() ) : ?>

-' . mysql2date( 's', $post->post_date, false ) . '
' . esc_html( $post->post_password ) . '
'; - if ( $post_type_object->hierarchical ) + if ( $post_type_object->hierarchical ) { echo '
' . $post->post_parent . '
'; + } - if ( $post->post_type == 'page' ) - echo '
' . esc_html( get_post_meta( $post->ID, '_wp_page_template', true ) ) . '
'; + echo '
' . esc_html( $post->page_template ) . '
'; - if ( post_type_supports( $post->post_type, 'page-attributes' ) ) + if ( post_type_supports( $post->post_type, 'page-attributes' ) ) { echo ''; + } $taxonomy_names = get_object_taxonomies( $post->post_type ); foreach ( $taxonomy_names as $taxonomy_name) { @@ -761,11 +762,13 @@ function touch_time( $edit = 1, $for_post = 1, $tab_index = 0, $multi = 0 ) { * Print out option HTML elements for the page templates drop-down. * * @since 1.5.0 + * @since 4.7.0 Added the `$post_type` parameter. * - * @param string $default Optional. The template file name. Default empty. + * @param string $default Optional. The template file name. Default empty. + * @param string $post_type Optional. Post type to get templates for. Default 'post'. */ -function page_template_dropdown( $default = '' ) { - $templates = get_page_templates( get_post() ); +function page_template_dropdown( $default = '', $post_type = 'page' ) { + $templates = get_page_templates( null, $post_type ); ksort( $templates ); foreach ( array_keys( $templates ) as $template ) { $selected = selected( $default, $templates[ $template ], false ); diff --git a/wp-admin/includes/theme.php b/wp-admin/includes/theme.php index 4eb01ea996..b2cc0eb61c 100644 --- a/wp-admin/includes/theme.php +++ b/wp-admin/includes/theme.php @@ -102,12 +102,14 @@ function delete_theme($stylesheet, $redirect = '') { * Get the Page Templates available in this theme * * @since 1.5.0 + * @since 4.7.0 Added the `$post_type` parameter. * - * @param WP_Post|null $post Optional. The post being edited, provided for context. + * @param WP_Post|null $post Optional. The post being edited, provided for context. + * @param string $post_type Optional. Post type to get the templates for. Default 'page'. * @return array Key is the template name, value is the filename of the template */ -function get_page_templates( $post = null ) { - return array_flip( wp_get_theme()->get_page_templates( $post ) ); +function get_page_templates( $post = null, $post_type = 'page' ) { + return array_flip( wp_get_theme()->get_page_templates( $post, $post_type ) ); } /** diff --git a/wp-includes/class-wp-post.php b/wp-includes/class-wp-post.php index a21776f192..c966934899 100644 --- a/wp-includes/class-wp-post.php +++ b/wp-includes/class-wp-post.php @@ -254,7 +254,7 @@ final class WP_Post { return true; if ( 'page_template' == $key ) - return ( 'page' == $this->post_type ); + return true; if ( 'post_category' == $key ) return true; diff --git a/wp-includes/class-wp-theme.php b/wp-includes/class-wp-theme.php index 05211d8689..66f623ee90 100644 --- a/wp-includes/class-wp-theme.php +++ b/wp-includes/class-wp-theme.php @@ -538,7 +538,7 @@ final class WP_Theme implements ArrayAccess { * @since 3.4.0 * @access private * - * @param string $key Type of data to store (theme, screenshot, headers, page_templates) + * @param string $key Type of data to store (theme, screenshot, headers, post_templates) * @param string $data Data to store * @return bool Return value from wp_cache_add() */ @@ -554,7 +554,7 @@ final class WP_Theme implements ArrayAccess { * @since 3.4.0 * @access private * - * @param string $key Type of data to retrieve (theme, screenshot, headers, page_templates) + * @param string $key Type of data to retrieve (theme, screenshot, headers, post_templates) * @return mixed Retrieved data */ private function cache_get( $key ) { @@ -568,7 +568,7 @@ final class WP_Theme implements ArrayAccess { * @access public */ public function cache_delete() { - foreach ( array( 'theme', 'screenshot', 'headers', 'page_templates' ) as $key ) + foreach ( array( 'theme', 'screenshot', 'headers', 'post_templates' ) as $key ) wp_cache_delete( $key . '-' . $this->cache_hash, 'themes' ); $this->template = $this->textdomain_loaded = $this->theme_root_uri = $this->parent = $this->errors = $this->headers_sanitized = $this->name_translated = null; $this->headers = array(); @@ -1006,56 +1006,101 @@ final class WP_Theme implements ArrayAccess { } /** - * Returns the theme's page templates. + * Returns the theme's post templates. * - * @since 3.4.0 + * @since 4.7.0 * @access public * - * @param WP_Post|null $post Optional. The post being edited, provided for context. - * @return array Array of page templates, keyed by filename, with the value of the translated header name. + * @return array Array of page templates, keyed by filename and post type, + * with the value of the translated header name. */ - public function get_page_templates( $post = null ) { + public function get_post_templates() { // If you screw up your current theme and we invalidate your parent, most things still work. Let it slide. - if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) + if ( $this->errors() && $this->errors()->get_error_codes() !== array( 'theme_parent_invalid' ) ) { return array(); + } - $page_templates = $this->cache_get( 'page_templates' ); + $post_templates = $this->cache_get( 'post_templates' ); - if ( ! is_array( $page_templates ) ) { - $page_templates = array(); + if ( ! is_array( $post_templates ) ) { + $post_templates = array(); $files = (array) $this->get_files( 'php', 1 ); foreach ( $files as $file => $full_path ) { - if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) ) + if ( ! preg_match( '|Template Name:(.*)$|mi', file_get_contents( $full_path ), $header ) ) { continue; - $page_templates[ $file ] = _cleanup_header_comment( $header[1] ); + } + + $types = array( 'page' ); + if ( preg_match( '|Template Post Type:(.*)$|mi', file_get_contents( $full_path ), $type ) ) { + $types = explode( ',', _cleanup_header_comment( $type[1] ) ); + } + + foreach ( $types as $type ) { + $type = trim( $type ); + if ( ! isset( $post_templates[ $type ] ) ) { + $post_templates[ $type ] = array(); + } + + $post_templates[ $type ][ $file ] = _cleanup_header_comment( $header[1] ); + } } - $this->cache_add( 'page_templates', $page_templates ); + $this->cache_add( 'post_templates', $post_templates ); } if ( $this->load_textdomain() ) { - foreach ( $page_templates as &$page_template ) { - $page_template = $this->translate_header( 'Template Name', $page_template ); + foreach ( $post_templates as &$post_type ) { + foreach ( $post_type as &$post_template ) { + $post_template = $this->translate_header( 'Template Name', $post_template ); + } } } - if ( $this->parent() ) - $page_templates += $this->parent()->get_page_templates( $post ); + return $post_templates; + } + + /** + * Returns the theme's post templates for a given post type. + * + * @since 3.4.0 + * @since 4.7.0 Added the `$post_type` parameter. + * @access public + * + * @param WP_Post|null $post Optional. The post being edited, provided for context. + * @param string $post_type Optional. Post type to get the templates for. Default 'page'. + * If a post is provided, its post type is used. + * @return array Array of page templates, keyed by filename, with the value of the translated header name. + */ + public function get_page_templates( $post = null, $post_type = 'page' ) { + if ( $post ) { + $post_type = get_post_type( $post ); + } + + $post_templates = $this->get_post_templates(); + $post_templates = isset( $post_templates[ $post_type ] ) ? $post_templates[ $post_type ] : array(); + + if ( $this->parent() ) { + $post_templates += $this->parent()->get_page_templates( $post ); + } /** * Filters list of page templates for a theme. * + * The dynamic portion of the hook name, `$post_type`, refers to the post type. + * * @since 3.9.0 * @since 4.4.0 Converted to allow complete control over the `$page_templates` array. + * @since 4.7.0 Added the `$post_type` parameter. * - * @param array $page_templates Array of page templates. Keys are filenames, + * @param array $post_templates Array of page templates. Keys are filenames, * values are translated names. * @param WP_Theme $this The theme object. * @param WP_Post|null $post The post being edited, provided for context, or null. + * @param string $post_type Post type to get the templates for. */ - return (array) apply_filters( 'theme_page_templates', $page_templates, $this, $post ); + return (array) apply_filters( "theme_{$post_type}_templates", $post_templates, $this, $post, $post_type ); } /** diff --git a/wp-includes/post-template.php b/wp-includes/post-template.php index 9e2b3a09e5..ac8864334a 100644 --- a/wp-includes/post-template.php +++ b/wp-includes/post-template.php @@ -594,23 +594,40 @@ function get_body_class( $class = '' ) { if ( is_404() ) $classes[] = 'error404'; - if ( is_single() ) { + if ( is_singular() ) { $post_id = $wp_query->get_queried_object_id(); $post = $wp_query->get_queried_object(); + $post_type = $post->post_type; - $classes[] = 'single'; - if ( isset( $post->post_type ) ) { - $classes[] = 'single-' . sanitize_html_class($post->post_type, $post_id); - $classes[] = 'postid-' . $post_id; + if ( is_page_template() ) { + $classes[] = "{$post_type}-template"; - // Post Format - if ( post_type_supports( $post->post_type, 'post-formats' ) ) { - $post_format = get_post_format( $post->ID ); + $template_slug = get_page_template_slug( $post_id ); + $template_parts = explode( '/', $template_slug ); - if ( $post_format && !is_wp_error($post_format) ) - $classes[] = 'single-format-' . sanitize_html_class( $post_format ); - else - $classes[] = 'single-format-standard'; + foreach ( $template_parts as $part ) { + $classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( array( '.', '/' ), '-', basename( $part, '.php' ) ) ); + } + $classes[] = "{$post_type}-template-" . sanitize_html_class( str_replace( '.', '-', $template_slug ) ); + } else { + $classes[] = "{$post_type}-template-default"; + } + + if ( is_single() ) { + $classes[] = 'single'; + if ( isset( $post->post_type ) ) { + $classes[] = 'single-' . sanitize_html_class( $post->post_type, $post_id ); + $classes[] = 'postid-' . $post_id; + + // Post Format + if ( post_type_supports( $post->post_type, 'post-formats' ) ) { + $post_format = get_post_format( $post->ID ); + + if ( $post_format && !is_wp_error($post_format) ) + $classes[] = 'single-format-' . sanitize_html_class( $post_format ); + else + $classes[] = 'single-format-standard'; + } } } @@ -619,6 +636,23 @@ function get_body_class( $class = '' ) { $mime_prefix = array( 'application/', 'image/', 'text/', 'audio/', 'video/', 'music/' ); $classes[] = 'attachmentid-' . $post_id; $classes[] = 'attachment-' . str_replace( $mime_prefix, '', $mime_type ); + } elseif ( is_page() ) { + $classes[] = 'page'; + + $page_id = $wp_query->get_queried_object_id(); + + $post = get_post($page_id); + + $classes[] = 'page-id-' . $page_id; + + if ( get_pages( array( 'parent' => $page_id, 'number' => 1 ) ) ) { + $classes[] = 'page-parent'; + } + + if ( $post->post_parent ) { + $classes[] = 'page-child'; + $classes[] = 'parent-pageid-' . $post->post_parent; + } } } elseif ( is_archive() ) { if ( is_post_type_archive() ) { @@ -671,36 +705,6 @@ function get_body_class( $class = '' ) { $classes[] = 'term-' . $term->term_id; } } - } elseif ( is_page() ) { - $classes[] = 'page'; - - $page_id = $wp_query->get_queried_object_id(); - - $post = get_post($page_id); - - $classes[] = 'page-id-' . $page_id; - - if ( get_pages( array( 'parent' => $page_id, 'number' => 1 ) ) ) { - $classes[] = 'page-parent'; - } - - if ( $post->post_parent ) { - $classes[] = 'page-child'; - $classes[] = 'parent-pageid-' . $post->post_parent; - } - if ( is_page_template() ) { - $classes[] = 'page-template'; - - $template_slug = get_page_template_slug( $page_id ); - $template_parts = explode( '/', $template_slug ); - - foreach ( $template_parts as $part ) { - $classes[] = 'page-template-' . sanitize_html_class( str_replace( array( '.', '/' ), '-', basename( $part, '.php' ) ) ); - } - $classes[] = 'page-template-' . sanitize_html_class( str_replace( '.', '-', $template_slug ) ); - } else { - $classes[] = 'page-template-default'; - } } if ( is_user_logged_in() ) @@ -1621,14 +1625,12 @@ function get_the_password_form( $post = 0 ) { * * @since 2.5.0 * @since 4.2.0 The `$template` parameter was changed to also accept an array of page templates. + * @since 4.7.0 Now works with any post type, not just pages. * * @param string|array $template The specific template name or array of templates to match. * @return bool True on success, false on failure. */ function is_page_template( $template = '' ) { - if ( ! is_page() ) - return false; - $page_template = get_page_template_slug( get_queried_object_id() ); if ( empty( $template ) ) @@ -1649,21 +1651,28 @@ function is_page_template( $template = '' ) { } /** - * Get the specific template name for a page. + * Get the specific template name for a given post. * * @since 3.4.0 + * @since 4.7.0 Now works with any post type, not just pages. * - * @param int $post_id Optional. The page ID to check. Defaults to the current post, when used in the loop. + * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string|false Page template filename. Returns an empty string when the default page template - * is in use. Returns false if the post is not a page. + * is in use. Returns false if the post does not exist. */ -function get_page_template_slug( $post_id = null ) { - $post = get_post( $post_id ); - if ( ! $post || 'page' != $post->post_type ) +function get_page_template_slug( $post = null ) { + $post = get_post( $post ); + + if ( ! $post ) { return false; + } + $template = get_post_meta( $post->ID, '_wp_page_template', true ); - if ( ! $template || 'default' == $template ) + + if ( ! $template || 'default' == $template ) { return ''; + } + return $template; } diff --git a/wp-includes/post.php b/wp-includes/post.php index cfb941f8c8..89ddaaa711 100644 --- a/wp-includes/post.php +++ b/wp-includes/post.php @@ -66,6 +66,7 @@ function create_initial_post_types() { 'add_new' => _x( 'Add New', 'add new media' ), 'edit_item' => __( 'Edit Media' ), 'view_item' => __( 'View Attachment Page' ), + 'attributes' => __( 'Attachment Attributes' ), ), 'public' => true, 'show_ui' => true, @@ -1326,6 +1327,7 @@ function _post_type_meta_capabilities( $capabilities = null ) { * post types. Default is 'Parent Page:'. * - `all_items` - Label to signify all items in a submenu link. Default is 'All Posts' / 'All Pages'. * - `archives` - Label for archives in nav menus. Default is 'Post Archives' / 'Page Archives'. + * - `attributes` - Label for the attributes meta box. Default is 'Post Attributes' / 'Page Attributes'. * - `insert_into_item` - Label for the media frame button. Default is 'Insert into post' / 'Insert into page'. * - `uploaded_to_this_item` - Label for the media frame filter. Default is 'Uploaded to this post' / * 'Uploaded to this page'. @@ -1351,7 +1353,7 @@ function _post_type_meta_capabilities( $capabilities = null ) { * @since 4.4.0 Added the `insert_into_item`, `uploaded_to_this_item`, `filter_items_list`, * `items_list_navigation`, and `items_list` labels. * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object. - * @since 4.7.0 Added the `view_items` label. + * @since 4.7.0 Added the `view_items` and `attributes` labels. * * @access private * @@ -1374,6 +1376,7 @@ function get_post_type_labels( $post_type_object ) { 'parent_item_colon' => array( null, __('Parent Page:') ), 'all_items' => array( __( 'All Posts' ), __( 'All Pages' ) ), 'archives' => array( __( 'Post Archives' ), __( 'Page Archives' ) ), + 'attributes' => array( __( 'Post Attributes' ), __( 'Page Attributes' ) ), 'insert_into_item' => array( __( 'Insert into post' ), __( 'Insert into page' ) ), 'uploaded_to_this_item' => array( __( 'Uploaded to this post' ), __( 'Uploaded to this page' ) ), 'featured_image' => array( __( 'Featured Image' ), __( 'Featured Image' ) ), @@ -3393,7 +3396,7 @@ function wp_insert_post( $postarr, $wp_error = false ) { $post = get_post( $post_ID ); - if ( ! empty( $postarr['page_template'] ) && 'page' == $data['post_type'] ) { + if ( ! empty( $postarr['page_template'] ) ) { $post->page_template = $postarr['page_template']; $page_templates = wp_get_theme()->get_page_templates( $post ); if ( 'default' != $postarr['page_template'] && ! isset( $page_templates[ $postarr['page_template'] ] ) ) { diff --git a/wp-includes/template.php b/wp-includes/template.php index fd0476bae1..636cb9005c 100644 --- a/wp-includes/template.php +++ b/wp-includes/template.php @@ -449,15 +449,17 @@ function get_search_template() { * * The hierarchy for this template looks like: * - * 1. single-{post_type}-{post_name}.php - * 2. single-{post_type}.php - * 3. single.php + * 1. {Post Type Template}.php + * 2. single-{post_type}-{post_name}.php + * 3. single-{post_type}.php + * 4. single.php * * An example of this is: * - * 1. single-post-hello-world.php - * 2. single-post.php - * 3. single.php + * 1. templates/full-width.php + * 2. single-post-hello-world.php + * 3. single-post.php + * 4. single.php * * The template hierarchy is filterable via the {@see 'single_template_hierarchy'} hook. * The template path is filterable via the {@see 'single_template'} hook. @@ -466,6 +468,7 @@ function get_search_template() { * @since 4.4.0 `single-{post_type}-{post_name}.php` was added to the top of the template hierarchy. * @since 4.7.0 The decoded form of `single-{post_type}-{post_name}.php` was added to the top of the * template hierarchy when the post name contains multibyte characters. + * @since 4.7.0 {Post Type Template}.php was added to the top of the template hierarchy. * * @see get_query_template() * @@ -477,6 +480,10 @@ function get_single_template() { $templates = array(); if ( ! empty( $object->post_type ) ) { + $template = get_page_template_slug( $object ); + if ( $template && 0 === validate_file( $template ) ) { + $templates[] = $template; + } $name_decoded = urldecode( $object->post_name ); if ( $name_decoded !== $object->post_name ) { diff --git a/wp-includes/version.php b/wp-includes/version.php index 4d627bef1d..17798803f2 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -4,7 +4,7 @@ * * @global string $wp_version */ -$wp_version = '4.7-alpha-38950'; +$wp_version = '4.7-alpha-38951'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.