WP Rest API get post by slug with special characters (ex: !&') - wordpress

I have posts with slugs with special characters. One of them is the following:
http://localhost/wp-json/wp/v2/posts?slug=my-post!
Unfortunately, WP REST API not showing the content since it has (!) within the slug.
Is there any solution you guys would recommend?

I have found the solution (at least for my case), not sure if it'll work for you but may indicate you the way to go. We need to tap into the function used to sanitize the slugs, as I said in my comment, by default is wp_parse_slug_list. Looking at the code the function that actually sanitizes the slug is sanitize_title.
Looking at the source code, wp_parse_slug_list calls sanitize_title with only one argument, which means that the context used is save. This means that, for posts that were already saved without being sanitized by this function, the slug won't match and the post will be inaccessible through the API. The solution is to change the sanitizing function slightly by adding a filter:
add_filter('rest_post_collection_params', function($query_params) {
$query_params['slug']['sanitize_callback'] = 'sanitize_rest_api_slug';
return $query_params;
}, 20, 1);
function sanitize_rest_api_slug( $list ) {
if ( ! is_array( $list ) ) {
$list = preg_split( '/[\s,]+/', $list );
}
foreach ( $list as $key => $value ) {
$list[ $key ] = sanitize_title( $value, '', 'query' );
}
return array_unique( $list );
}
The filter is actually being applied on the function get_collection_params() on the class-wp-rest-posts-controller class, but if you look at the source code, the filter has a variable depending on the post_type, so if you have another special kind of posts defined (besides post) you need to add/change a filter for that kind as well.
I hope this helps somebody else like me, even if it's too late for your issue.

Related

How do I sort by a custom rule using WordPress's WPQuery class?

I'm using the PowerPack's 'Advanced Posts' widget Elementor addon to display a set of custom posts. It provides useful fields for setting the orderby preference, including title.
However, I want to further customize the sorting method. In this case, the post titles are all names of people, and I want to sort by last names alphabetically, which isn't an option in the field settings.
For example, this is what I have right now sorting by title:
Alex Xela
Edgar Allen
John Doe
But I want it to be sorted as such, by last names:
Edgar Allen
John Doe
Alex Xela
The documentation explains how to customize the query through code, as it seems to use WPquery API, and so far I can manipulate the orderby data of the query as such:
add_action( 'powerpack/query/provider-query', function( $query )
{
//this code gets called upon the query with query_id 'provider-query'
$query->set('orderby', 'title');
});
However, I cannot figure out how to make a custom orderby rule 'last_name' that I can replace 'title' with in the above code that filters by last name.
I was able to get some insight from the accepted answer for https://wordpress.stackexchange.com/questions/198610/how-to-filter-by-last-name-for-custom-post
However, that seems to be for manual use of the WPquery class, and I can't see how to implement it with respect to the PowerPack API's hook in the code I made above.
For example, with the top answer I mentioned above, they recommended the code to be something that in my particular case would be similar to
add_filter( 'powerpack/query/provider-query', function( $orderby, $query )
{
if( $get_order = $query->get( 'order' ) )
{
if( in_array( strtoupper( $get_order ), ['ASC', 'DESC'] ) )
{
global $wpdb;
$orderby = " SUBSTRING_INDEX( {$wpdb->posts}.post_title, ' ', -1 ) " . $get_order;
}
}
return $orderby;
}, PHP_INT_MAX, 2 );
But that results in a fatal error (it seems the hook API for 'powerpack/query/provider-query' does not accept the callback if it has more than just the $query object passed as its argument.

Custom Taxonomies and how to stay in them when the posts have multiple terms selected?

Wondering if anyone can help me think this one out?
Whilst in lock-down, I've been putting together a simple site to organise my collection of Retro Gaming Adverts covering systems from the Atari 2600 up to the N64. I've still got a few 1000 to add to the site (takes time) but i've come across an issue I'm not sure how to implement a fix for.
You can browse the adverts by system through their single post pages but if an advert covers multiple systems it messes up the previous and next posts link and will drop you into another system.
For Example: If you're using the next and previous post links to go through the "mega drive / genesis" section once you get to " Battletoads and Double Dragon " when you press the next post arrow this time you're suddenly going through adverts tagged as NES, due to the fact thats the first term associated with it.
See : https://www.retrogameads.com/system/mega-drive/ and click on the first advert, then keep pressing the next arrow and you'll see what i mean.
I guess I could post each advert multiple times for each system but I don't like the idea of that.
Anyone got any suggestions on how I could work out what Term the user was browsing and keep them in that one?
Bare in mind this site is a work in progress so the design is just something basic till i work out the best way to organise things.
Let me know your thoughts.
Update:
Current method for getting prev next posts...
<?php
$terms = get_the_terms( $post->ID, 'system' );
$i = 0;
$systems = array();
foreach ( $terms as $term ) {
$systems[$i] = $term->slug;
$i++;
}
$postlist_args = array(
'posts_per_page' => -1,
'post_type' => 'portfolio',
'system' => $systems[0],
'order' => 'ASC',
'orderby' => 'title'
);
$postlist = get_posts( $postlist_args );
$ids = array();
foreach ($postlist as $thepost) {
$ids[] = $thepost->ID;
}
$thisindex = array_search($post->ID, $ids);
$previd = $ids[$thisindex-1];
$nextid = $ids[$thisindex+1];
?>
<div class="prev_next">
<?php
if ( !empty($previd) ) {
echo '<div class="older"><a rel="prev" href="' . get_permalink($previd). '">‹</a></div>';
}
if ( !empty($nextid) ) {
echo '<div class="newer"><a rel="next" href="' . get_permalink($nextid). '">›</a></div>';
}
?>
When you click on an advert, you're essentially visiting the single page for that advert. At that moment, WP does not know/remember that the user only wants to see adverts from the system you clicked.
How are you rendering the previous/next links right now?
One possible solution could be to add a parameter to the URL and then take this into account in the PHP code when rendering the previous/next links. (/portfolio/advert-name/?system=mega-drive for example)
Edit: Of course the URL could also be made prettier... if you want to put in some extra work, you could register a permalink of the form /portfolio/mega-drive/advert-name/ for example. But this would require a bit more work.
Would that work? I think it would be the best solution considering the alternatives.
Update: For the actual implementation, the get_next_post_where and get_previous_post_where filters might prove to be very useful. You could make it so that it takes the system parameter into account for the previous/next links.
Another option: You could also set up a PHP session and remember the current system that way, but then you will require a PHP session, which is not a good thing, and it will also prevent you from using full page caching (performance optimization).
Yet another option would be to remember the current system client-side through a cookie. But in that case you have caching issues again unless you load the previous/next links through AJAX.
After reading the update on your question, I have the following notes:
system is not a valid argument and will be ignored on the get_posts() call.
the method you use to get the previous and next posts is very inefficient, because you query the whole database, and then you save everything in memory, and then you comb through the whole result set in memory, using a lot of unnecessary ram and CPU power
So how can we improve this?
Simple: Use the get_previous_post and get_next_post functions on the $post. It accepts three arguments. Here is the example for get_next_post (but get_previous_post is basically the same):
get_next_post( bool $in_same_term = false, array|string $excluded_terms = '', string $taxonomy = 'category' )
Now if you set $in_same_term to true then the next post will be of a post that shares at least one taxonomy term (system in our case).
You also have to set $taxonomy to system because that is the custom taxonomy.
And the other parameter is $excluded_terms. Too bad there is no $included_terms otherwise we could just put the system we want in there. But we can do it another way... We can filter out the current system (in the system GET parameter in our URL) and keep all other systems as systems to exclude.
So let's build what we need now.
global $post;
// What system did we come from?
$system = $_GET['system'] ?? null; // Set to null if nothing was specified (uses PHP's null coalescing operator available since PHP 7
// Exclude nothing by default
$excluded_term_ids = [];
// If we came here by clicking on a system, then exclude all other systems so the previous/next links will be of the same system only
if ($system) {
// Retrieve system terms for this post
$terms = get_the_terms( $post, 'system' );
// Filter the list of terms to remove the system we came from
// This way we can create a list of terms to exclude
$excluded_terms = array_filter( $terms, function ( $term ) use ( $system ) {
return $term->slug != $system;
} );
// The get_previous_post and get_next_post functions expect $excluded_terms to be an array of term IDs, so we must map the WP_Term objects into IDs
$excluded_term_ids = array_map( function ( $term ) {
return $term->term_id;
}, $excluded_terms );
}
// Retrieve previous and next post
$previous_post = $post->get_previous_post( true, $excluded_term_ids, 'system' );
$previous_post = $post->get_next_post( true, $excluded_term_ids, 'system' );
// Echo out the page links
// And don't forget to re-add the ?system= parameter to the URL
$url_suffix = $system ? ('?system=' . $system) : '';
if ( $previous_post ) {
echo '<div class="older"><a rel="prev" href="' . get_permalink($previous_post) . $url_suffix . '">‹</a></div>';
}
if ( $next_post ) {
echo '<div class="newer"><a rel="next" href="' . get_permalink($next_post) . $url_suffix . '">›</a></div>';
}
NOTE: Untested code.. So there might be a small bug somewhere.. Let me know if you encounter an issue with this code..
Important: On the page of a system, you must now also add '?system=current-system' to the URLs that you render.
Probably something like: echo get_permalink($advert) . '?system=' . $post->post_name (could be different depending on how your code is on that screen..)

Filtering WooCommerce Products by $_GET

What I need to be able to do is apply a value for an attribute in the URL, so only the products match that attribute are displayed. I don't want to do any widgets or other visible filters on the page.
For this I presume I would need to use one of the webhooks, and filtering out all products that are about to be displayed.
Can anyone advise which hook will be best in this case and a simple explanation on how the triggered function will return the new array of products?
Thanks in advance!
NB: I also want to query a custom attribute, which does not have any terms, just a straight key/value.
UPDATE 1
I'm playing with two techniques; one is very reliable, and that's basically to use:
if (!$product->attributes || $product->get_attribute( 'testKey' ) != $_GET["testKey"]) {
//return;
}
at the top of content-product.php, but of course WooCommerce will still say the original value for found_posts. Certainly not ideal.
I've come across that something like this should work in functions.php:
function testFilter($meta_query) {
$meta_query[] = array (
'key' => 'testKey',
'value' => 'testVal',
'compare' => '='
);
return $meta_query;
}
add_filter( 'woocommerce_product_query_meta_query', 'testFilter', 9 );
Except it doesn't, returns no results, doesn't matter if I use LIKE, EXISTS etc. Am I using it wrong?
UPDATE 2
I'm not going to say this is the answer, as this only seems to look for one value within a group of custom attributes, but this result has helped.
add_filter( 'wpv_filter_query', 'wpv_filter_color_attribute' );
function wpv_filter_color_attribute( $query_args) {
$tax_query = array();
$tax_query['taxonomy'] = 'pa_size';
$tax_query['field'] = 'term_id';
$tax_query['terms'] = $_GET['pa_size'];
$query_args['tax_query'] = array($tax_query);
return $query_args;
}
You should replace "pa_size" with your attribute taxonomy slug, also $_GET['pa_size'] with the right URL parameter.
You can filter products by attributes using above code. I have not tried this. But, this may help you.

Trying to get lowest priority number in WordPress

Hello I am trying to get lowest priority number from all the functions that hook into wp_head.
My code is this.
function wpcrown_wpcss_loaded() {
global $wp_filter;
$lowest_priority = max(array_keys($wp_filter['wp_head']));
add_action('wp_head', 'wpcrown_wpcss_head', $lowest_priority + 1);
$arr = $wp_filter['wp_head'];
}
add_action('wp_head', "wpcrown_wpcss_loaded");
But its shown error like this.
Warning: array_keys() expects parameter 1 to be array
Warning: max() [<a href='function.max'>function.max</a>]: When only one parameter is given, it must be an array in
Please help to solve this.
Thanks
As Benoti said $wp_filter is no longer an array so array_keys is throwing these warnings because expects an array.
If you want to add scripts / styles / whatever you have in wpcrown_wpcss_head at the end of the head (to give priority) this should work:
1) Comment this action:
// add_action('wp_head', "wpcrown_wpcss_loaded");
2) Use this one instead:
add_action('wp_head', "wpcrown_wpcss_head");
Hope it helps.
Be carefull on the use of $wp_filters as an array since WordPress 4.7,
Here is the guidelines for wp .4.,7
Make.wordpress that explain:
If your plugin directly accesses the $wp_filter global rather than using the public hooks API, you might run into compatibility issues.
Case 1: Directly setting callbacks in the $wp_filter array
$wp_filter['save_post'][10]['my_special_key'] = array( 'function' => 'my_callback_function', 'accepted_args' => 2 );
This will no longer work, and will cause a fatal error. $wp_filter['save_post'] is no longer a simple array. Rather, it is an object that implements the ArrayAccess interface.
You have two options to work around. The first (and preferred) method is to use the add_filter() or add_action() functions.
add_action( 'save_post', 'my_callback_function', 10, 2 );
If, for some reason, you absolutely cannot, you can still work around this.
if ( ! isset( $wp_filter['save_post'] ) ) {
$wp_filter['save_post'] = new WP_Hook();
}
$wp_filter[ 'save_post' ]->callbacks[10]['my_special_key'] = array( 'function' => 'my_callback_function', 'accepted_args' => 2 );
Case 2: Directly unsetting callbacks in the $wp_filter array
unset( $wp_filter['save_post'][10][ $my_callback_id ] );
This will fail for the same reason as case one. To work around, you can use the standard remove_filter() / remove_action() functions.
remove_action( 'save_post', 'my_callback_function', 10, 2 );
Or, if you absolutely must access the array directly:
if ( isset( $wp_filter[ 'save_post' ] ) ) {
unset( $wp_filter['save_post']->callbacks[10][ $my_callback_id ] );
}
Case 3: Checking if a hook is an array
if ( isset( $wp_filter['save_post'] ) && is_array( $wp_filter['save_post'] ) ) {
// do something
}
This will always return false. $wp_filter['save_post'] is a WP_Hook object. To check if a hook has callbacks, use has_action() or has_filter()
if ( has_action( 'save_post' ) ) {
// do something
}
Case 4: Moving the $wp_filter array pointer manually
If you’re calling next() or prev() on the $wp_filter array pointer to manually manage the order that callbacks are called in (or if you’re doing it to work around #17817), you will likely be unsuccessful. Use callback priorities, add_action() / add_filter(), and remove_action() / remove_filter() appropriately and let WordPress manage execution order.

What's the difference between global var post and get_post?

I use the_posts filter to add an object to each queried post. When access the added object, I get different result by using $post or get_post.
This is the code to attach the object to posts:
add_filter( 'the_posts', 'populate_posts_obj', 10,2 );
function populate_posts_obj( $posts, $query ){
if ( !count( $posts ) || !isset($query->query['post_type']) )
return $posts;
if( in_array( $query->query['post_type'], get_valid_grade_types())){
foreach ( $posts as $post ) {
if ( $obj = new Gradebook( $post->ID ) )
$post->gradebook = $obj;
}
}
return $posts;
}
Then, access the obj via $post, sometimes get the obj, sometimes not (even when it's the same post):
function get_the_gradebook(){
global $post;
return isset($post->gradebook) ? $post->gradebook : null;
}
Access the obj via get_post(), always get the obj:
function get_the_gradebook(){
global $post;
$p = get_post($post->ID);
return isset($p->gradebook) ? $p->gradebook : null;
}
I can just use the get_post() version, but it would be useful if I know why the difference.
Additional info:
If you ask the reason I attach an obj to each post, I think WordPress may take care of the caching process at the first place. Then, other caching plugins can work on my obj as if working on standard WP posts.
Lets explain you with a little bit pseudo code. I am trying to be broad with my approach so that my answer is relevant to StackOverflow however I still don't know how many down votes I may be receiving for this.
The simple difference is $post is a variable and get_post() is a method that means you can expect a different output from get_post() due to several dependencies however $post will only change when you explicitly do that.
Lets assume something like this
function get_post() {
return rand(0, 5);
}
$post = get_post(); /* lets assume random
value that was generated
this time was "2" */
Now each time you call get_post() its value be keep changing however the value of $post is always 2.
Coming back to the context of wordpress, $post is set using get_post() within the Loop and corresponds to the object referring to default post ID for current URL where as get_post() will take post ID as an input and return the post object.
$post is what WordPress considers to be the current "post" (post/page/custom post type) and can quite often end up giving you data you didn't quite expect. This is especially true if you perform WP_Query's in your template or have a template that uses data from several "posts".
By using get_post() with the ID you want the data from, you can be assured that you are getting the data you really want.

Resources