diff --git a/src/wp-includes/query.php b/src/wp-includes/query.php index 7b85e2ed63..cf7b80f772 100644 --- a/src/wp-includes/query.php +++ b/src/wp-includes/query.php @@ -2199,6 +2199,97 @@ class WP_Query { return $search_orderby; } + /** + * If the passed orderby value is allowed, convert the alias to a + * properly-prefixed orderby value. + * + * @since 4.0.0 + * @access protected + * + * @global wpdb $wpdb WordPress database access abstraction object. + * + * @param string $orderby Alias for the field to order by. + * @return string|bool Table-prefixed value to used in the ORDER clause. False otherwise. + */ + protected function parse_orderby( $orderby ) { + global $wpdb; + + // Used to filter values. + $allowed_keys = array( + 'post_name', 'post_author', 'post_date', 'post_title', 'post_modified', + 'post_parent', 'post_type', 'name', 'author', 'date', 'title', 'modified', + 'parent', 'type', 'ID', 'menu_order', 'comment_count', 'rand', + ); + + $meta_key = $this->get( 'meta_key' ); + if ( ! empty( $meta_key ) ) { + $allowed_keys[] = $meta_key; + $allowed_keys[] = 'meta_value'; + $allowed_keys[] = 'meta_value_num'; + } + + if ( ! in_array( $orderby, $allowed_keys ) ) { + return false; + } + + switch ( $orderby ) { + case 'post_name': + case 'post_author': + case 'post_date': + case 'post_title': + case 'post_modified': + case 'post_parent': + case 'post_type': + case 'ID': + case 'menu_order': + case 'comment_count': + $orderby = "$wpdb->posts.{$orderby}"; + break; + case 'rand': + $orderby = 'RAND()'; + break; + case $meta_key: + case 'meta_value': + $type = $this->get( 'meta_type' ); + if ( ! empty( $type ) ) { + $meta_type = $this->meta_query->get_cast_for_type( $type ); + $orderby = "CAST($wpdb->postmeta.meta_value AS {$meta_type})"; + } else { + $orderby = "$wpdb->postmeta.meta_value"; + } + break; + case 'meta_value_num': + $orderby = "$wpdb->postmeta.meta_value+0"; + break; + default: + $orderby = "$wpdb->posts.post_" . $orderby; + break; + } + + return $orderby; + } + + /** + * Parse an 'order' query variable and cast it to ASC or DESC as necessary. + * + * @since 4.0.0 + * @access protected + * + * @param string $order The 'order' query variable. + * @return string The sanitized 'order' query variable. + */ + protected function parse_order( $order ) { + if ( ! is_string( $order ) || empty( $order ) ) { + return 'DESC'; + } + + if ( 'ASC' === strtoupper( $order ) ) { + return 'ASC'; + } else { + return 'DESC'; + } + } + /** * Sets the 404 property and saves whether query is feed. * @@ -2702,12 +2793,23 @@ class WP_Query { $where .= $search . $whichauthor . $whichmimetype; - if ( empty($q['order']) || ((strtoupper($q['order']) != 'ASC') && (strtoupper($q['order']) != 'DESC')) ) + if ( ! isset( $q['order'] ) ) { $q['order'] = 'DESC'; + } else { + $q['order'] = $this->parse_order( $q['order'] ); + } - // Order by - if ( empty($q['orderby']) ) { - $orderby = "$wpdb->posts.post_date " . $q['order']; + // Order by. + if ( empty( $q['orderby'] ) ) { + /* + * Boolean false or empty array blanks out ORDER BY, + * while leaving the value unset or otherwise empty sets the default. + */ + if ( isset( $q['orderby'] ) && ( is_array( $q['orderby'] ) || false === $q['orderby'] ) ) { + $orderby = ''; + } else { + $orderby = "$wpdb->posts.post_date " . $q['order']; + } } elseif ( 'none' == $q['orderby'] ) { $orderby = ''; } elseif ( $q['orderby'] == 'post__in' && ! empty( $post__in ) ) { @@ -2715,59 +2817,41 @@ class WP_Query { } elseif ( $q['orderby'] == 'post_parent__in' && ! empty( $post_parent__in ) ) { $orderby = "FIELD( {$wpdb->posts}.post_parent, $post_parent__in )"; } else { - // Used to filter values - $allowed_keys = array( 'name', 'author', 'date', 'title', 'modified', 'menu_order', 'parent', 'ID', 'rand', 'comment_count', 'type' ); - if ( !empty($q['meta_key']) ) { - $allowed_keys[] = $q['meta_key']; - $allowed_keys[] = 'meta_value'; - $allowed_keys[] = 'meta_value_num'; - } - $q['orderby'] = urldecode($q['orderby']); - $q['orderby'] = addslashes_gpc($q['orderby']); - $orderby_array = array(); - foreach ( explode( ' ', $q['orderby'] ) as $i => $orderby ) { - // Only allow certain values for safety - if ( ! in_array($orderby, $allowed_keys) ) - continue; + if ( is_array( $q['orderby'] ) ) { + foreach ( $q['orderby'] as $_orderby => $order ) { + $orderby = addslashes_gpc( urldecode( $_orderby ) ); + $parsed = $this->parse_orderby( $orderby ); - switch ( $orderby ) { - case 'menu_order': - $orderby = "$wpdb->posts.menu_order"; - break; - case 'ID': - $orderby = "$wpdb->posts.ID"; - break; - case 'rand': - $orderby = 'RAND()'; - break; - case $q['meta_key']: - case 'meta_value': - if ( isset( $q['meta_type'] ) ) { - $meta_type = $this->meta_query->get_cast_for_type( $q['meta_type'] ); - $orderby = "CAST($wpdb->postmeta.meta_value AS {$meta_type})"; - } else { - $orderby = "$wpdb->postmeta.meta_value"; - } - break; - case 'meta_value_num': - $orderby = "$wpdb->postmeta.meta_value+0"; - break; - case 'comment_count': - $orderby = "$wpdb->posts.comment_count"; - break; - default: - $orderby = "$wpdb->posts.post_" . $orderby; + if ( ! $parsed ) { + continue; + } + + $orderby_array[] = $parsed . ' ' . $this->parse_order( $order ); } + $orderby = implode( ', ', $orderby_array ); - $orderby_array[] = $orderby; + } else { + $q['orderby'] = urldecode( $q['orderby'] ); + $q['orderby'] = addslashes_gpc( $q['orderby'] ); + + foreach ( explode( ' ', $q['orderby'] ) as $i => $orderby ) { + $parsed = $this->parse_orderby( $orderby ); + // Only allow certain values for safety. + if ( ! $parsed ) { + continue; + } + + $orderby_array[] = $parsed; + } + $orderby = implode( ' ' . $q['order'] . ', ', $orderby_array ); + + if ( empty( $orderby ) ) { + $orderby = "$wpdb->posts.post_date ".$q['order']; + } else { + $orderby .= " {$q['order']}"; + } } - $orderby = implode( ' ' . $q['order'] . ', ', $orderby_array ); - - if ( empty( $orderby ) ) - $orderby = "$wpdb->posts.post_date ".$q['order']; - else - $orderby .= " {$q['order']}"; } // Order search results by relevance only when another "orderby" is not specified in the query. diff --git a/tests/phpunit/tests/post/query.php b/tests/phpunit/tests/post/query.php index 37dcdc7517..004e710f25 100644 --- a/tests/phpunit/tests/post/query.php +++ b/tests/phpunit/tests/post/query.php @@ -761,4 +761,74 @@ class Tests_Post_Query extends WP_UnitTestCase { $q3 = new WP_Query( array( 'post_status' => array( 'any', 'auto-draft' ) ) ); $this->assertNotContains( "post_status <> 'auto-draft'", $q3->request ); } + + /** + * + * @ticket 17065 + */ + function test_orderby_array() { + global $wpdb; + + $q1 = new WP_Query( array( + 'orderby' => array( + 'type' => 'DESC', + 'name' => 'ASC' + ) + ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_type DESC, $wpdb->posts.post_name ASC", + $q1->request + ); + + $q2 = new WP_Query( array( 'orderby' => array() ) ); + $this->assertNotContains( 'ORDER BY', $q2->request ); + $this->assertNotContains( 'ORDER', $q2->request ); + + $q3 = new WP_Query( array( 'post_type' => 'post' ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_date DESC", + $q3->request + ); + + $q4 = new WP_Query( array( 'post_type' => 'post' ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_date DESC", + $q4->request + ); + } + + /** + * + * @ticket 17065 + */ + function test_order() { + global $wpdb; + + $q1 = new WP_Query( array( + 'orderby' => array( + 'post_type' => 'foo' + ) + ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_type DESC", + $q1->request + ); + + $q2 = new WP_Query( array( + 'orderby' => 'title', + 'order' => 'foo' + ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_title DESC", + $q2->request + ); + + $q3 = new WP_Query( array( + 'order' => 'asc' + ) ); + $this->assertContains( + "ORDER BY $wpdb->posts.post_date ASC", + $q3->request + ); + } } \ No newline at end of file