) { unset( $post_type_meta_caps[ $cap ] ); } } /** * Unregisters the post type meta box if a custom callback was specified. * * @since 4.6.0 */ public function unregister_meta_boxes() { if ( $this->register_meta_box_cb ) { remove_action( 'add_meta_boxes_' . $this->name, $this->register_meta_box_cb, 10 ); } } /** * Removes the post type from all taxonomies. * * @since 4.6.0 */ public function unregister_taxonomies() { foreach ( get_object_taxonomies( $this->name ) as $taxonomy ) { unregister_taxonomy_for_object_type( $taxonomy, $this->name ); } } /** * Removes the future post hook action for the post type. * * @since 4.6.0 */ public function remove_hooks() { remove_action( 'future_' . $this->name, '_future_post_hook', 5 ); } /** * Gets the REST API controller for this post type. * * Will only instantiate the controller class once per request. * * @since 5.3.0 * * @return WP_REST_Controller|null The controller instance, or null if the post type * is set not to show in rest. */ public function get_rest_controller() { if ( ! $this->show_in_rest ) { return null; } $class = $this->rest_controller_class ? $this->rest_controller_class : WP_REST_Posts_Controller::class; if ( ! class_exists( $class ) ) { return null; } if ( ! is_subclass_of( $class, WP_REST_Controller::class ) ) { return null; } if ( ! $this->rest_controller ) { $this->rest_controller = new $class( $this->name ); } if ( ! ( $this->rest_controller instanceof $class ) ) { return null; } return $this->rest_controller; } /** * Returns the default labels for post types. * * @since 6.0.0 * * @return (string|null)[][] The default labels for post types. */ public static function get_default_labels() { if ( ! empty( self::$default_labels ) ) { return self::$default_labels; } self::$default_labels = array( 'name' => array( _x( 'Posts', 'post type general name' ), _x( 'Pages', 'post type general name' ) ), 'singular_name' => array( _x( 'Post', 'post type singular name' ), _x( 'Page', 'post type singular name' ) ), 'add_new' => array( _x( 'Add New', 'post' ), _x( 'Add New', 'page' ) ), 'add_new_item' => array( __( 'Add New Post' ), __( 'Add New Page' ) ), 'edit_item' => array( __( 'Edit Post' ), __( 'Edit Page' ) ), 'new_item' => array( __( 'New Post' ), __( 'New Page' ) ), 'view_item' => array( __( 'View Post' ), __( 'View Page' ) ), 'view_items' => array( __( 'View Posts' ), __( 'View Pages' ) ), 'search_items' => array( __( 'Search Posts' ), __( 'Search Pages' ) ), 'not_found' => array( __( 'No posts found.' ), __( 'No pages found.' ) ), 'not_found_in_trash' => array( __( 'No posts found in Trash.' ), __( 'No pages found in Trash.' ) ), '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( _x( 'Featured image', 'post' ), _x( 'Featured image', 'page' ) ), 'set_featured_image' => array( _x( 'Set featured image', 'post' ), _x( 'Set featured image', 'page' ) ), 'remove_featured_image' => array( _x( 'Remove featured image', 'post' ), _x( 'Remove featured image', 'page' ) ), 'use_featured_image' => array( _x( 'Use as featured image', 'post' ), _x( 'Use as featured image', 'page' ) ), 'filter_items_list' => array( __( 'Filter posts list' ), __( 'Filter pages list' ) ), 'filter_by_date' => array( __( 'Filter by date' ), __( 'Filter by date' ) ), 'items_list_navigation' => array( __( 'Posts list navigation' ), __( 'Pages list navigation' ) ), 'items_list' => array( __( 'Posts list' ), __( 'Pages list' ) ), 'item_published' => array( __( 'Post published.' ), __( 'Page published.' ) ), 'item_published_privately' => array( __( 'Post published privately.' ), __( 'Page published privately.' ) ), 'item_reverted_to_draft' => array( __( 'Post reverted to draft.' ), __( 'Page reverted to draft.' ) ), 'item_trashed' => array( __( 'Post trashed.' ), __( 'Page trashed.' ) ), 'item_scheduled' => array( __( 'Post scheduled.' ), __( 'Page scheduled.' ) ), 'item_updated' => array( __( 'Post updated.' ), __( 'Page updated.' ) ), 'item_link' => array( _x( 'Post Link', 'navigation link block title' ), _x( 'Page Link', 'navigation link block title' ), ), 'item_link_description' => array( _x( 'A link to a post.', 'navigation link block description' ), _x( 'A link to a page.', 'navigation link block description' ), ), ); return self::$default_labels; } /** * Resets the cache for the default labels. * * @since 6.0.0 */ public static function reset_default_labels() { self::$default_labels = array(); } } gment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_clauses() { /* * $queries are passed by reference to get_sql_for_query() for recursion. * To keep $this->queries unaltered, pass a copy. */ $queries = $this->queries; $sql = $this->get_sql_for_query( $queries ); if ( ! empty( $sql['where'] ) ) { $sql['where'] = ' AND ' . $sql['where']; } return $sql; } /** * Generates SQL clauses for a single query array. * * If nested subqueries are found, this method recurses the tree to * produce the properly nested SQL. * * @since 4.1.0 * * @param array $query Query to parse (passed by reference). * @param int $depth Optional. Number of tree levels deep we currently are. * Used to calculate indentation. Default 0. * @return string[] { * Array containing JOIN and WHERE SQL clauses to append to a single query array. * * @type string $join SQL fragment to append to the main JOIN clause. * @type string $where SQL fragment to append to the main WHERE clause. * } */ protected function get_sql_for_query( &$query, $depth = 0 ) { $sql_chunks = array( 'join' => array(), 'where' => array(), ); $sql = array( 'join' => '', 'where' => '', ); $indent = ''; for ( $i = 0; $i < $depth; $i++ ) { $indent .= ' '; } foreach ( $query as $key => &$clause ) { if ( 'relation' === $key ) { $relation = $query['relation']; } elseif ( is_array( $clause ) ) { // This is a first-order clause. if ( $this->is_first_order_clause( $clause ) ) { $clause_sql = $this->get_sql_for_clause( $clause, $query ); $where_count = count( $clause_sql['where'] ); if ( ! $where_count ) { $sql_chunks['where'][] = ''; } elseif ( 1 === $where_count ) { $sql_chunks['where'][] = $clause_sql['where'][0]; } else { $sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )'; } $sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] ); // This is a subquery, so we recurse. } else { $clause_sql = $this->get_sql_for_query( $clause, $depth + 1 ); $sql_chunks['where'][] = $clause_sql['where']; $sql_chunks['join'][] = $clause_sql['join']; } } } // Filter to remove empties. $sql_chunks['join'] = array_filter( $sql_chunks['join'] ); $sql_chunks['where'] = array_filter( $sql_chunks['where'] ); if ( empty( $relation ) ) { $relation = 'AND'; } // Filter duplicate JOIN clauses and combine into a single string. if ( ! empty( $sql_chunks['join'] ) ) { $sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) ); } // Generate a single WHERE clause with proper brackets and indentation. if ( ! empty( $sql_chunks['where'] ) ) { $sql['where'] = '( ' . "\n " . $indent . implode( ' ' . "\n " . $indent . $relation . ' ' . "\n " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')'; } return $sql; } /** * Generates SQL JOIN and WHERE clauses for a "first-order" query clause. * * @since 4.1.0 * * @global wpdb $wpdb The WordPress database abstraction object. * * @param array $clause Query clause (passed by reference). * @param array $parent_query Parent query array. * @return array { * Array containing JOIN and WHERE SQL clauses to append to a first-order query. * * @type string[] $join Array of SQL fragments to append to the main JOIN clause. * @type string[] $where Array of SQL fragments to append to the main WHERE clause. * } */ public function get_sql_for_clause( &$clause, $parent_query ) { global $wpdb; $sql = array( 'where' => array(), 'join' => array(), ); $join = ''; $where = ''; $this->clean_query( $clause ); if ( is_wp_error( $clause ) ) { return self::$no_results; } $terms = $clause['terms']; $operator = strtoupper( $clause['operator'] ); if ( 'IN' === $operator ) { if ( empty( $terms ) ) { return self::$no_results; } $terms = implode( ',', $terms ); /* * Before creating another table join, see if this clause has a * sibling with an existing join that can be shared. */ $alias = $this->find_compatible_table_alias( $clause, $parent_query ); if ( false === $alias ) { $i = count( $this->table_aliases ); $alias = $i ? 'tt' . $i : $wpdb->term_relationships; // Store the alias as part of a flat array to build future iterators. $this->table_aliases[] = $alias; // Store the alias with this clause, so later siblings can use it. $clause['alias'] = $alias; $join .= " LEFT JOIN $wpdb->term_relationships"; $join .= $i ? " AS $alias" : ''; $join .= " ON ($this->primary_table.$this->primary_id_column = $alias.object_id)"; } $where = "$alias.term_taxonomy_id $operator ($terms)"; } elseif ( 'NOT IN' === $operator ) { if ( empty( $terms ) ) { return $sql; } $terms = implode( ',', $terms ); $where = "$this->primary_table.$this->primary_id_column NOT IN ( SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id IN ($terms) )"; } elseif ( 'AND' === $operator ) { if ( empty( $terms ) ) { return $sql; } $num_terms = count( $terms ); $terms = implode( ',', $terms ); $where = "( SELECT COUNT(1) FROM $wpdb->term_relationships WHERE term_taxonomy_id IN ($terms) AND object_id = $this->primary_table.$this->primary_id_column ) = $num_terms"; } elseif ( 'NOT EXISTS' === $operator || 'EXISTS' === $operator ) { $where = $wpdb->prepare( "$operator ( SELECT 1 FROM $wpdb->term_relationships INNER JOIN $wpdb->term_taxonomy ON $wpdb->term_taxonomy.term_taxonomy_id = $wpdb->term_relationships.term_taxonomy_id WHERE $wpdb->term_taxonomy.taxonomy = %s AND $wpdb->term_relationships.object_id = $this->primary_table.$this->primary_id_column )", $clause['taxonomy'] ); } $sql['join'][] = $join; $sql['where'][] = $where; return $sql; } /** * Identifies an existing table alias that is compatible with the current query clause. * * We avoid unnecessary table joins by allowing each clause to look for * an existing table alias that is compatible with the query that it * needs to perform. * * An existing alias is compatible if (a) it is a sibling of `$clause` * (ie, it's under the scope of the same relation), and (b) the combination * of operator and relation between the clauses allows for a shared table * join. In the case of WP_Tax_Query, this only applies to 'IN' * clauses that are connected by the relation 'OR'. * * @since 4.1.0 * * @param array $clause Query clause. * @param array $parent_query Parent query of $clause. * @return string|false Table alias if found, otherwise false. */ protected function find_compatible_table_alias( $clause, $parent_query ) { $alias = false; // Sanity check. Only IN queries use the JOIN syntax. if ( ! isset( $clause['operator'] ) || 'IN' !== $clause['operator'] ) { return $alias; } // Since we're only checking IN queries, we're only concerned with OR relations. if ( ! isset( $parent_query['relation'] ) || 'OR' !== $parent_query['relation'] ) { return $alias; } $compatible_operators = array( 'IN' ); foreach ( $parent_query as $sibling ) { if ( ! is_array( $sibling ) || ! $this->is_first_order_clause( $sibling ) ) { continue; } if ( empty( $sibling['alias'] ) || empty( $sibling['operator'] ) ) { continue; } // The sibling must both have compatible operator to share its alias. if ( in_array( strtoupper( $sibling['operator'] ), $compatible_operators, true ) ) { $alias = preg_replace( '/\W/', '_', $sibling['alias'] ); break; } } return $alias; } /** * Validates a single query. * * @since 3.2.0 * * @param array $query The single query. Passed by reference. */ private function clean_query( &$query ) { if ( empty( $query['taxonomy'] ) ) { if ( 'term_taxonomy_id' !== $query['field'] ) { $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); return; } // So long as there are shared terms, 'include_children' requires that a taxonomy is set. $query['include_children'] = false; } elseif ( ! taxonomy_exists( $query['taxonomy'] ) ) { $query = new WP_Error( 'invalid_taxonomy', __( 'Invalid taxonomy.' ) ); return; } if ( 'slug' === $query['field'] || 'name' === $query['field'] ) { $query['terms'] = array_unique( (array) $query['terms'] ); } else { $query['terms'] = wp_parse_id_list( $query['terms'] ); } if ( is_taxonomy_hierarchical( $query['taxonomy'] ) && $query['include_children'] ) { $this->transform_query( $query, 'term_id' ); if ( is_wp_error( $query ) ) { return; } $children = array(); foreach ( $query['terms'] as $term ) { $children = array_merge( $children, get_term_children( $term, $query['taxonomy'] ) ); $children[] = $term; } $query['terms'] = $children; } $this->transform_query( $query, 'term_taxonomy_id' ); } /** * Transforms a single query, from one field to another. * * Operates on the `$query` object by reference. In the case of error, * `$query` is converted to a WP_Error object. * * @since 3.2.0 * * @param array $query The single query. Passed by reference. * @param string $resulting_field The resulting field. Accepts 'slug', 'name', 'term_taxonomy_id', * or 'term_id'. Default 'term_id'. */ public function transform_query( &$query, $resulting_field ) { if ( empty( $query['terms'] ) ) { return; } if ( $query['field'] === $resulting_field ) { return; } $resulting_field = sanitize_key( $resulting_field ); // Empty 'terms' always results in a null transformation. $terms = array_filter( $query['terms'] ); if ( empty( $terms ) ) { $query['terms'] = array(); $query['field'] = $resulting_field; return; } $args = array( 'get' => 'all', 'number' => 0, 'taxonomy' => $query['taxonomy'], 'update_term_meta_cache' => false, 'orderby' => 'none', ); // Term query parameter name depends on the 'field' being searched on. switch ( $query['field'] ) { case 'slug': $args['slug'] = $terms; break; case 'name': $args['name'] = $terms; break; case 'term_taxonomy_id': $args['term_taxonomy_id'] = $terms; break; default: $args['include'] = wp_parse_id_list( $terms ); break; } if ( ! is_taxonomy_hierarchical( $query['taxonomy'] ) ) { $args['number'] = count( $terms ); } $term_query = new WP_Term_Query(); $term_list = $term_query->query( $args ); if ( is_wp_error( $term_list ) ) { $query = $term_list; return; } if ( 'AND' === $query['operator'] && count( $term_list ) < count( $query['terms'] ) ) { $query = new WP_Error( 'inexistent_terms', __( 'Inexistent terms.' ) ); return; } $query['terms'] = wp_list_pluck( $term_list, $resulting_field ); $query['field'] = $resulting_field; } }