I am working on a website that is more of a search directory and haven't been able to find a clear answer on how to automatically create pretty links from long URL's with multiple query parameters (Wordpress).
For example, if the link looks like this: website.com/search/search-results/?address=98101,+seattle,+washington&contractor=plumber&latitude=1234&longitude=9876&filter=20&order=distance
Is there a way to make it look like this: website.com/search/search-results/98101/seattle/washington/plumber/
automatically without having to go through every link and the new pretty link will show the same page as the page with all of the query parameters?
Thanks in advance, I've been trying to figure this out all day and it's not my strong point...
Really interesting and quite technical question.
The following is not production ready. It is a working proof of concept.
I'm just pondering here. You could create a new Table (see Creating Tables with Plugins with a requested_url key and a shortened_url value. The whole system would be based on that approach.
First we creates a custom table in the database, if it doesn’t already exist. This table will be used to store our urls.
<?php
add_action( 'after_switch_theme', function () {
global $wpdb;
$table_name = $wpdb->prefix . 'wpso74035985';
$charset_collate = $wpdb->get_charset_collate();
$create_ddl = "CREATE TABLE IF NOT EXISTS $table_name (
id mediumint(9) NOT NULL AUTO_INCREMENT,
requested_url varchar(55) DEFAULT '' NOT NULL,
shortened_url varchar(55) DEFAULT '' NOT NULL,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
maybe_create_table( $table_name, $create_ddl );
} );
We could then retrieve the requested url by parsing the $_SERVER['QUERY_STRING'] and verifying the url integrity through get_query_var() as it only retrieves public query variables that are recognized by WP_Query.
We would then search the table for the requested url, and if it doesn't already exist, set a new key pair value.
<?php
add_action( 'pre_get_posts', function ( $wp_query ) {
if ( ! is_admin() && $wp_query->is_main_query() ) {
if ( $wp_query->is_search() ) {
global $wp;
parse_str( filter_input( INPUT_SERVER, 'QUERY_STRING', FILTER_SANITIZE_STRING ), $variables );
$buffer = array();
foreach ( $variables as $variable => $value ) {
if ( get_query_var( $variable ) ) {
array_push( $buffer, array( $variable => $value ) );
};
};
$buffer = array_reduce( $buffer, 'array_merge', array() );
$requested_url = esc_url_raw( add_query_arg( $buffer, home_url( $wp->request ) ) );
global $wpdb;
$table_name = $wpdb->prefix . 'wpso74035985';
$results = $wpdb->get_results(
"SELECT * FROM $table_name"
);
$needle_key = array_search( $requested_url, array_column( $results, 'requested_url' ) );
if ( $needle_key === false ) {
$shortened_url = str_shuffle( base64_encode( random_bytes( 100 ) ) );
$shortened_url = preg_replace( '/\W/s', '', $shortened_url );
$shortened_url = substr( $shortened_url, 0, 7 );
$wpdb->insert(
$table_name,
array(
'requested_url' => sanitize_url( $requested_url ),
'shortened_url' => sanitize_url( home_url( $shortened_url ) ),
)
);
};
};
};
} );
There is no real science here, the $shortened_url is just a 7 bytes alpha numerical string (Why 7? Bit.ly a famous shortener is using 7 characters).
Through the template_redirect hook, we could check the shortened url against our table and redirect based on that.
<?php
add_action( 'template_redirect', function () {
if ( is_404() ) {
global $wp;
$shortened_url = home_url( $wp->request );
global $wpdb;
$table_name = $wpdb->prefix . 'shortener';
$results = $wpdb->get_results(
"SELECT * FROM $table_name"
);
$needle_key = array_search( $shortened_url, array_column( $results, 'shortened_url' ) );
if ( $needle_key !== false ) {
$location = $results[$needle_key]->requested_url;
wp_safe_redirect( $location );
exit();
};
};
} );
As we're checking for a 404 through is_404() first (pretty much limiting the number of empty request), you should also include a 404.php to your root.
On the front end you can access your table through
<?php
global $wpdb;
$table_name = $wpdb->prefix . 'wpso74035985';
$results = $wpdb->get_results(
"SELECT * FROM $table_name"
);
var_dump( $results );
Now, a few thing to understand before you go to production with this, even tho we're generating a pseudo random string through random_bytes() and base64_encode() they're not actually fully randomized, so you could end up with a problem at some point where two shortened url are the same. You could do a while loop and constantly check if that shortened url already exist before using it.
You would also want to restrict the search query as much as you can to improve performance on the long term.
You might want also to add an expiry date.
Related
I have a site that has tens of thousands of orders, which I need to compare the billing and customer/user emails and show a flag if they don't match. One of the stipulations is that I'm unable to add any metadata to the orders. So my solution is to just add a custom column, and compare the emails on the fly when the orders list is rendered. That works just fine.
add_filter( 'manage_edit-shop_order_columns', 'mismatched_orders_column' );
function mismatched_orders_column( $columns ) {
$columns['mismatched'] = 'Mismatched';
return $columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'mismatched_orders_column_data' );
function mismatched_orders_column_data( $column ) {
global $post;
if ( 'mismatched' === $column ) {
$order = new WC_Order( $post->ID );
$customer = $order->get_user();
$result = '';
$billing_email = strtolower ( $order->get_billing_email() );
$customer_email = '';
if ($customer) $customer_email = strtolower ( $customer->user_email );
if ( $customer && ( $billing_email != $customer_email ) ) {
$result = '<span class="mismatched-order" title="Possible order mismatch">Yes</span>';
}
echo $result;
}
}
My issue is when trying to add sorting. Because I'm not accessing any post metadata, I don't have any easy data to sort via the main query. My solution here was originally to hook into pre_get_posts, grab all the orders in a new WP_Query, then loop through them and add the ones that had mismatched emails to an array for use in post__in.
This works/worked fine on my small dev site, but throws fatal memory errors when trying to loop over any more than about 8 or 9 thousand posts (out of a total of 30-40 thousand). Increasing memory isn't really an option.
add_filter( 'manage_edit-shop_order_sortable_columns', 'mismatched_orders_column_sortable');
function mismatched_orders_column_sortable( $columns ) {
$columns['mismatched'] = 'mismatched';
return $columns;
}
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
function mismatched_emails_posts_orderby( $query ) {
if( ! is_admin() || ! $query->is_main_query() ) {
return;
}
//Remove the pre_get_posts hook so we don't get stuck in a loop
add_action( 'pre_get_posts', 'mismatched_emails_posts_orderby' );
//Make sure we're only looking at our custom column
if ( 'mismatched' === $query->get( 'orderby') ) {
//Set our initial array for 'post__in'
$mismatched = array();
$orders_list = get_posts(array(
'post_type' => 'shop_order',
'posts_per_page' => -1,
'post_status' => 'any',
'fields' => 'ids'
));
//And here is our problem
foreach( $orders_list as $order_post ) :
//Get our order and customer/user object
$order_object = new WC_Order( $order_post );
$customer = $order_object->get_user();
//Check that billing and customer emails don't match, and also that we're not dealing with a guest order
if ( ( $order_object->get_billing_email() != $customer->user_email ) && $order_object->get_user() != false ) {
$mismatched[] = $order_post;
}
endforeach; wp_reset_postdata();
$query->set( 'post__in', $mismatched );
}
}
I would seriously appreciate any insight into how I could either reduce the expense of the query I'm trying to run, or an alternate approach. Again, just for clarification, adding metadata to the orders isn't an option.
Thanks!
I need a function to check if post_content already exist in database.
Wordpress built in function post_exists() checks by post post_title.
I need to check by post_content regardless of the post_title.
There such a function exist?
How can I solve this?
Thank you for your help
It looks like a small variation on post_exists() should work. Create a function like this in your child theme's functions.php, and then use it instead of post_exists():
function post_exists_by_content($content) {
global $wpdb;
$post_content = wp_unslash( sanitize_post_field( 'post_content', $content, 0, 'db' ) );
$query = "SELECT ID FROM $wpdb->posts WHERE 1=1";
$args = array();
if ( !empty ( $content ) ) {
$query .= ' AND post_content = %s';
$args[] = $post_content;
}
if ( !empty ( $args ) )
return (int) $wpdb->get_var( $wpdb->prepare($query, $args) );
return 0;
}
I need a "get products A, B and C for $xxx" special offer, products A, B and C must be available on their own, and the bundle is a special offer accessible through a coupon code.
On a marketing page hosting outside my site, I would like a button leading to my site that carries a query string like ?add-to-cart=244,249,200 so that once on my site, all bundle products are already added to the cart (instead of adding them one by one which sounds unacceptably tedious).
If not possible, then at least I'd like a landing page on my site with a single button adding all bundle products to cart at once.
I couldn't find working solutions googling around (here's one example). Any suggestion?
After some research I found that DsgnWrks wrote a hook that does exactly this. For your convenience, and in case the blog goes offline, I bluntly copied his code to this answer:
function woocommerce_maybe_add_multiple_products_to_cart( $url = false ) {
// Make sure WC is installed, and add-to-cart qauery arg exists, and contains at least one comma.
if ( ! class_exists( 'WC_Form_Handler' ) || empty( $_REQUEST['add-to-cart'] ) || false === strpos( $_REQUEST['add-to-cart'], ',' ) ) {
return;
}
// Remove WooCommerce's hook, as it's useless (doesn't handle multiple products).
remove_action( 'wp_loaded', array( 'WC_Form_Handler', 'add_to_cart_action' ), 20 );
$product_ids = explode( ',', $_REQUEST['add-to-cart'] );
$count = count( $product_ids );
$number = 0;
foreach ( $product_ids as $id_and_quantity ) {
// Check for quantities defined in curie notation (<product_id>:<product_quantity>)
// https://dsgnwrks.pro/snippets/woocommerce-allow-adding-multiple-products-to-the-cart-via-the-add-to-cart-query-string/#comment-12236
$id_and_quantity = explode( ':', $id_and_quantity );
$product_id = $id_and_quantity[0];
$_REQUEST['quantity'] = ! empty( $id_and_quantity[1] ) ? absint( $id_and_quantity[1] ) : 1;
if ( ++$number === $count ) {
// Ok, final item, let's send it back to woocommerce's add_to_cart_action method for handling.
$_REQUEST['add-to-cart'] = $product_id;
return WC_Form_Handler::add_to_cart_action( $url );
}
$product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( $product_id ) );
$was_added_to_cart = false;
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
continue;
}
$add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart );
// Variable product handling
if ( 'variable' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_variable', $product_id );
// Grouped Products
} elseif ( 'grouped' === $add_to_cart_handler ) {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_grouped', $product_id );
// Custom Handler
} elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ){
do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url );
// Simple Products
} else {
woo_hack_invoke_private_method( 'WC_Form_Handler', 'add_to_cart_handler_simple', $product_id );
}
}
}
// Fire before the WC_Form_Handler::add_to_cart_action callback.
add_action( 'wp_loaded', 'woocommerce_maybe_add_multiple_products_to_cart', 15 );
/**
* Invoke class private method
*
* #since 0.1.0
*
* #param string $class_name
* #param string $methodName
*
* #return mixed
*/
function woo_hack_invoke_private_method( $class_name, $methodName ) {
if ( version_compare( phpversion(), '5.3', '<' ) ) {
throw new Exception( 'PHP version does not support ReflectionClass::setAccessible()', __LINE__ );
}
$args = func_get_args();
unset( $args[0], $args[1] );
$reflection = new ReflectionClass( $class_name );
$method = $reflection->getMethod( $methodName );
$method->setAccessible( true );
$args = array_merge( array( $class_name ), $args );
return call_user_func_array( array( $method, 'invoke' ), $args );
}
It works just like you'd expect, by providing a comma separated list of products. It even works with quantities using ?add-to-cart=63833:2,221916:4
I was, and am still looking for a 'pure' solution that allows to add multiple products to the cart without having to install a plugin or add custom actions. But for many, the above might be an appropriate solution
I've added a couple of custom field columns to our Woocommerce orders list in the admin of WordPress using the methods below, but the sort is not working....
add_filter( 'manage_edit-shop_order_columns', 'my_wc_columns' );
function my_wc_columns($columns){
$new_columns = (is_array($columns)) ? $columns : array();
unset( $new_columns['order_actions'] );
$new_columns['program_id'] = 'Program';
$new_columns['constituent_id'] = 'Constituent ID';
$new_columns['order_actions'] = $columns['order_actions'];
return $new_columns;
}
add_action( 'manage_shop_order_posts_custom_column', 'my_wc_column_values', 2 );
function my_wc_column_values($column){
global $post;
if ( $column == 'program_id' ) {
$program = get_post_meta( $post->ID, '_program_id', true );
$program_title = get_the_title($program);
$column_val = (isset($program) && $program>0 ? $program_title : 'All');
echo '<span>' . my_programs_get_name( $column_val ) . ' (' . $program . ')</span>';
}
if ( $column == 'constituent_id' ) {
$consid = get_post_meta( $post->ID, 'constituent_id', true );
$column_val = (isset($consid) && $consid != "") ? $consid : "";
echo '<span>' . $column_val . '</span>';
}
}
// Make column sortable
add_filter( "manage_edit-shop_order_sortable_columns", 'my_wc_column_sort' );
function my_wc_column_sort( $columns ) {
$custom = array(
'program_id' => '_program_id',
'constituent_id' => 'constituent_id',
);
return wp_parse_args( $custom, $columns );
}
I expected to have an issue perhaps with the program name, since it is an id that needs to be translated via a custom function to a name, but neither column is sorting properly. The records change order after clicking their column titles, but I cannot tell how the sort is being done. The program is not sorting on name or ID and both are seem random but consistent. Keep in mind both fields are custom fields that may or may not have a value defined. How can I make this sortable?
Here's a good tutorial on custom sortable columns. After you register the column, you need to handle the actual sorting. Sadly, that part doesn't happen automagically. Untested, but adapted from the above tutorial:
add_action( 'pre_get_posts', 'manage_wp_posts_be_qe_pre_get_posts', 1 );
function manage_wp_posts_be_qe_pre_get_posts( $query ) {
/**
* We only want our code to run in the main WP query
* AND if an orderby query variable is designated.
*/
if ( $query->is_main_query() && ( $orderby = $query->get( 'orderby' ) ) ) {
switch( $orderby ) {
// If we're ordering by 'program_id'
case 'program_id':
// set our query's meta_key, which is used for custom fields
$query->set( 'meta_key', '_program_id' );
/**
* Tell the query to order by our custom field/meta_key's
* value
*
* If your meta value are numbers, change 'meta_value'
* to 'meta_value_num'.
*/
$query->set( 'orderby', 'meta_value' );
break;
}
}
}
I am using this code which is partially working for changing the profile url everywhere in buddypress and wordpress from “http:/mywebsite/user/username” to “http:/mywebsite/user/userid”
function _bp_core_get_user_domain($domain, $user_id, $user_nicename = false, $user_login = false) {
if ( empty( $user_id ) ){
return;
}
if( isset($user_nicename) ){
$user_nicename = bp_core_get_username($user_id);
}
$after_domain = bp_get_members_root_slug() . '/' . $user_id;
$domain = trailingslashit( bp_get_root_domain() . '/' . $after_domain );
$domain = apply_filters( 'bp_core_get_user_domain_pre_cache', $domain, $user_id, $user_nicename, $user_login );
if ( !empty( $domain ) ) {
wp_cache_set( 'bp_user_domain_' . $user_id, $domain, 'bp' );
}
return $domain;
}
add_filter('bp_core_get_user_domain', '_bp_core_get_user_domain', 10, 4);
function _bp_core_get_userid($userid, $username){
if(is_numeric($username)){
$aux = get_userdata( $username );
if( get_userdata( $username ) )
$userid = $username;
}
return $userid;
}
add_filter('bp_core_get_userid', '_bp_core_get_userid', 10, 2);
function _bp_get_activity_parent_content($content){
global $bp;
$user = get_user_by('slug', $bp->displayed_user->fullname); // 'slug' - user_nicename
return preg_replace('/href=\"(.*?)\"/is', 'href="'.bp_core_get_user_domain($user->ID, $bp->displayed_user->fullname).'"', $content);
}
add_filter( 'bp_get_activity_parent_content','_bp_get_activity_parent_content', 10, 1 );
add_filter('bp_core_get_userid_from_nicename', '_bp_core_get_userid', 10, 2);
It is working perfectly for me at the moment BUT not on this little place (see picture):
http://i.imgur.com/4dX0RUB.png
– url change of the author of an activity starting-message is not working in both groups activities and personnal activities
url change of the author of an activity REPLY is working
I don’t know if I am explaining very well what issue I have got but I hope you will understand.
Thank you for your answers
PS : thanks to aSeptik from StackExchange for the code
It's impossible to do that on a fly gracefully. BuddyPress Activity component is developed in such a way, that those text with the user link in activity stream (for site-wide, personal and groups) is stored directly in the database as an action. Just take a look at wp_bp_activity.action field in your DB.
So you should filter and preg_replace it as well. I guess you know that you are penalting yourself with rendering speed loss.