Wordpress: Automatically change URLs in the_content section - php

The solution from here isn't solving our problem.
I have already a solution to change all links in a field form in our theme. I am using different arrays for every network like $example_network_1 $example_network_2 with a PHP foreach for each affiliate network.
Now I need a solution to use this same arrays for the content of a WordPress post.
This solution is working for one network, but it caused a 404e Error for YouTube videos. We put the URL from a YouTube video and WordPress generates automatically an embedded video. With the following code we get a 404 error iframe or something like this.
We need a solution for more than one network.
I am very thankful for every help!
$example_network_1 = array(
array('shop'=>'shop1.com','id'=>'11111'),
array('shop'=>'shop2.co.uk','id'=>'11112'),
);
$example_network_2 = array(
array('shop'=>'shop-x1.com','id'=>'11413'),
array('shop'=>'shop-x2.net','id'=>'11212'),
);
add_filter( 'the_content', 'wpso_change_urls' ) ;
function wpso_found_urls( $matches ) {
global $example_network_1,$example_network_2;
foreach( $example_network_1 as $rule ) {
if (strpos($matches[0], $rule['shop']) !== false) {
$raw_url = trim( $matches[0], '"' ) ;
return '"https://www.network-x.com/click/'. $rule['id'].'lorem_lorem='.rawurlencode($raw_url ) . '"';
}
/*
foreach( $example_network_2 as $rule ) {
if (strpos($matches[0], $rule['shop']) !== false) {
$raw_url = trim( $matches[0], '"' ) ;
return '"https://www.network-y.com/click/'. $rule['id'].'lorem='.rawurlencode($raw_url ) . '"';
}
*/
}
}
function wpso_change_urls( $content ) {
global $example_network_1,example_network_2;
return preg_replace_callback( '/"+(http|https)(\:\/\/\S*'. $example_network_1 ['shop'] .'\S*")/', 'wpso_found_urls', $content ) ;
// return preg_replace_callback( '/"+(http|https)(\:\/\/\S*'. $example_network_2 ['shop'] .'\S*")/', 'wpso_found_urls', $content ) ;
}

autoembed is hooked at the_content with priority 8 on wp-includes/class-wp-embed.php:39
Try to lower the priority of the the_content filter so that the URL replacement happens before the embed, something like this:
add_filter( 'the_content', function ( $content ) {
/*
* Here, we define the replacements for each site in the network.
* '1' = main blog
* '2' = site 2 in the network, and so on
*
* To add more sites, just add the key number '3', etc
*/
$network_replacements = [
'1' => [
[ 'shop' => 'shop1.com', 'id' => '11111' ],
[ 'shop' => 'shop2.co.uk', 'id' => '11112' ],
],
'2' => [
[ 'shop' => 'shop-x1.com', 'id' => '11413' ],
[ 'shop' => 'shop-x2.net', 'id' => '11212' ],
]
];
// Early bail: Current blog ID does not have replacements defined
if ( ! array_key_exists( get_current_blog_id(), $network_replacements ) ) {
return $content;
}
$replacements = $network_replacements[ get_current_blog_id() ];
return preg_replace_callback( '/"+(http|https)(\:\/\/\S*' . $replacements['shop'] . '\S*")/', function( $matches ) use ( $replacements ) {
foreach ( $replacements as $rule ) {
if ( strpos( $matches[0], $rule['shop'] ) !== false ) {
$raw_url = trim( $matches[0], '"' );
return '"https://www.network-x.com/click/' . $rule['id'] . 'lorem_lorem=' . rawurlencode( $raw_url ) . '"';
}
}
}, $content );
}, 1, 1 );
This is not a copy and paste solution, but should get you going. You might need to tweak your "preg_replace_callback" code, but you said it was working so I just left it is it was.

If preventing the wp auto-embed works, then just add this line to your theme functions.php
remove_filter( 'the_content', array( $GLOBALS['wp_embed'], 'autoembed' ), 8 );

I wrote solution without test. Your code is hard to test without your site but I think that problem is with regex. Callback is hard to debugging. My version below.
First step, change your structure. I suspect that domains are unique. One dimensional array is more useful.
$domains = array(
'shop1.com'=>'11111',
'shop2.co.uk'=>'11112',
'shop-x1.com'=>'11413',
'shop-x2.net'=>'11212',
);
Next:
$dangerouschars = array(
'.'=>'\.',
);
function wpso_change_urls( $content ) {
global $domains,$dangerouschars;
foreach($domains as $domain=>$id){
$escapedDomain = str_replace(array_keys($dangerouschars),array_values($dangerouschars), $domain);
if (preg_match_all('/=\s*"(\s*https?:\/\/(www\.)?'.$escapedDomain.'[^"]+)\s*"\s+/mi', $content, $matches)){
// $matches[0] - ="https://example.com"
// $matches[1] - https://example.com
for($i = 0; $i<count($matches[0]); $i++){
$matchedUrl = $matches[1][$i];
$url = rawurlencode($matchedUrl);
//is very important to replace with ="..."
$content = str_replace($matches[0][$i], "=\"https://www.network-x.com/click/{$id}lorem_lorem={$url}\" ", $content);
}
}
}
return $content;
}
Example script

Related

Why Wordpress post_name is returning multiple values?

I'm trying to get the current page slug by using get_post_field( 'post_name', get_post() ), however, this is returning multiple values.
My code looks like this (I'm writing this in a custom plugin):
function prefix_filter_query( $query_string, $grid_id, $action ) {
// If the content is not filtered on first render.
if ( 'render' === $action && empty( $query_string ) ) {
$slug = get_post_field( 'post_name', get_post() );
$query_string = [
'categories' => [ $slug ]
];
_error_log($slug);
}
return $query_string;
}
function _error_log ($value) {
error_log(print_r($value, true), 3, __DIR__ . '/log.txt');
error_log("\r\n\r\n", 3, __DIR__ . '/log.txt');
}
add_filter( 'wp_grid_builder/facet/query_string', 'prefix_filter_query', 10, 3 );
The log is showing first the current page (here a category, like 'hoodies'), and then the homepage slug of my website, like this :
hoodies
home
I understood that home was showed because I set the home page of my website to be the static default homepage. I tried to disable it and see if it solved my issue but then the second value returned by the log was just an empty space:
hoodies
I want to get only hoodies and I don't understand why there's a second value, whether it is home or an empty value.
To give a bit of context, I'm using a filter plugin for products in an e-commerce website and the plugin is giving a built-in function to filter the content before it is rendered. https://docs.wpgridbuilder.com/resources/filter-facet-query-string/
Another interesting fact, in our example, hoodies will successfully filter the grid of items to show only hoodies but the query in the URL will be ?_categories=home.
Solution found 28/12/2021
I got an answer from the plugin support (WP Grid Builder) and the issue was that my code was incompatible with the Ajax requests made by WP Grid Builder. Here's the solution I was provided:
add_filter(
'wp_grid_builder/facet/query_string',
function( $query_string, $grid_id, $action ) {
global $post;
if ( 'render' === $action && empty( $query_string ) ) {
$referer = wp_get_referer();
$post_id = wp_doing_ajax() ? url_to_postid( $referer ) : $post->ID;
$post_slug = get_post_field( 'post_name', $post_id );
$query_string = [
'categories' => [ $post_slug ],
];
}
return $query_string;
},
10,
3
);
Are you just trying to get the current page slug ? You could go through the server request uri $_SERVER['REQUEST_URI']:
PHP >= 8.0.0
<?php
/**
* Retrieve the current page slug.
*
* #return String The current page slug.
*/
if ( ! function_exists( 'get_the_current_slug' ) ) {
function get_the_current_slug() {
$url = $_SERVER['REQUEST_URI'];
if ( str_contains( $url, '?' ) ) {
$url = substr( $url, 0, strpos( $url, '?' ) );
};
$slugs = ( str_ends_with( $url, '/' ) ? explode( '/', substr( $url, 1, -1 ) ) : explode( '/', substr( $url, 1 ) ) );
return end( $slugs );
};
};
PHP < 8.0.0 (eg: 7.x.x)
<?php
/**
* Checks if a string ends with a given substring.
*
* #param String $haystack The string to search in.
* #param String $needle The substring to search for in the haystack.
*
* #return Integer < 0 if haystack from position offset is less than needle, > 0 if it is greater than needle, and 0 if they are equal. If offset is equal to (prior to PHP 7.2.18, 7.3.5) or greater than the length of haystack, or the length is set and is less than 0, substr_compare() prints a warning and returns false.
*
* #see https://www.php.net/manual/en/function.substr-compare.php
*/
if ( ! function_exists( 'startsWith' ) ) {
function startsWith( $haystack, $needle ) {
return substr_compare( $haystack, $needle, 0, strlen( $needle ) ) === 0;
};
};
/**
* Retrieve the current page slug.
*
* #return String The current page slug.
*/
if ( ! function_exists( 'get_the_current_slug' ) ) {
function get_the_current_slug() {
$url = $_SERVER['REQUEST_URI'];
if ( strpos( $url, '?' ) !== false ) {
$url = substr( $url, 0, strpos( $url, '?' ) );
};
$slugs = ( startsWith( $url, '/' ) ? explode( '/', substr( $url, 1, -1 ) ) : explode( '/', substr( $url, 1 ) ) );
return end( $slugs );
};
};
On the front end:
<?php
echo get_the_current_slug();

Wordpress - Using a recursive fn to render the data from an API

I am setting up a new Wordpress site that utilizes the Spoonacular API.
I want to use a recursive function to render 50 items from the API, but I cannot figure out why my code is not calling the function again.
The result I am expecting to get, is that the 'report.txt' file that is created will display a constantly updating number up to the specified per_page number '50'.
The 'report.txt' file only displays the default number '1' instead of incrementing.
I am not sure if the function is being called again or not.
Any help is greatly appreciated. Thank you.
functions.php
<?php
$apiKey = '123456789';
function get_ingredients_from_api() {
$file = get_stylesheet_directory() . '/report.txt';
$current_ingredient = ( ! empty( $_POST[ 'current_ingredient' ] ) ) ? $_POST[ 'current_ingredient' ] : 1 ;
$ingredients = [];
$results = wp_remote_retrieve_body(
wp_remote_get('https://api.spoonacular.com/food/ingredients/autocomplete?query=appl&number=' . $current_ingredient . '&per_page=50&apiKey=' . $apiKey)
);
file_put_contents( $file, "Current Ingredient: " . $current_ingredient. "\n\n", FILE_APPEND);
$results = json_decode($results);
if ( ! is_array( $results ) || empty( $results ) ) {
return false;
}
$current_ingredient = $current_ingredient + 1;
wp_remote_post( admin_url('admin-ajax.php?action=get_ingredients_from_api'), [
'blocking' => false,
'sslverify' => false,
'body' => [
'current_ingredient' => $current_ingredient
]
]);
}
add_action('wp_ajax_nopriv_get_ingredients_from_api', 'get_ingredients_from_api');
add_action('wp_ajax_get_ingredients_from_api', 'get_ingredients_from_api');
?>
I realized that commenting out or removing the if statement from the code allows the function to run perfectly. I'm not sure how to keep the if statement and have the function run yet.

Wordpress Invalid Range In Character Class After Update

I have a problem about Visual Compser Backend not showing any elements, it only shows the code in the classic mode editor.
See Screenshot
Warning: preg_match_all(): Compilation failed: invalid range in character class at offset 11 in /home/customer/www/******.com/public_html/wp-content/plugins/js_composer/include/autoload/hook-vc-grid.php on line 162
This is line 162
preg_match_all( "/$pattern/", $post->post_content, $found ); // fetch only needed shortcodes
```public function gridSavePostSettingsId( array $settings, $post_id, $post ) {
$pattern = $this->getShortcodeRegexForId();
preg_match_all( "/$pattern/", $post->post_content, $found ); // fetch only needed shortcodes
$settings['vc_grid_id'] = array();
if ( is_array( $found ) && ! empty( $found[0] ) ) {
$to_save = array();
if ( isset( $found[1] ) && is_array( $found[1] ) ) {
foreach ( $found[1] as $key => $parse_able ) {
if ( empty( $parse_able ) || '[' !== $parse_able ) {
$id_pattern = '/' . $this->grid_id_unique_name . '\:([\w-_]+)/';
$id_value = $found[4][ $key ];
preg_match( $id_pattern, $id_value, $id_matches );
if ( ! empty( $id_matches ) ) {
$id_to_save = $id_matches[1];
// why we need to check if shortcode is parse able?
// 1: if it is escaped it must not be displayed (parsed)
// 2: so if 1 is true it must not be saved in database meta
$shortcode_tag = $found[2][ $key ];
$shortcode_atts_string = $found[3][ $key ];
/** #var $atts array */
$atts = shortcode_parse_atts( $shortcode_atts_string );
$content = $found[6][ $key ];
$data = array(
'tag' => $shortcode_tag,
'atts' => $atts,
'content' => $content,
);
$to_save[ $id_to_save ] = $data;
}
}
}
}
if ( ! empty( $to_save ) ) {
$settings['vc_grid_id'] = array( 'shortcodes' => $to_save );
}
}
return $settings;
}```
I tried disabling all plug-ins and rollback wordpress version but still the same.
Is this problem related to the warning above? I know very little about coding. Please point me in the right direction.
Thank you!
Exactly same problem back-end mode didn't show any elements and /hook-vc-grid.php on line 162 error. " [\w-_] is wrong, it must be [\w-] " this helps only dissappear error but not solution for not showing elements.
Edit: php 7.1 works. It solved elements thing also.
Thank you guys
All hyphen '-' characters (that are not used as a range character) should be escaped '\-' as of PHP 7.3 so that it will NOT be treated anymore as a range character. Therefor the following WordPress function needs to be changed this way to make this expression work the same again:
private function getShortcodeRegexForId() {
return '\\[' // Opening bracket
. '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
. '([\\w\-_]+)' // 2: Shortcode name
. '(?![\\w\-])' // Not followed by word character or hyphen

How do I apply_filters to my custom function that is executed by add_action?

I have a custom function in my parent theme that hooks into the Wordpress admin_head, doing stuff with allowed domains which I define in the function. This part works perfectly.
But, I want to be able to add additional allowed domains to that function from the child theme (in other words, without modifying the original function in the parent theme). I thought that apply_filters may be a good solution but it doesn't seem to pass the additional domains in. What am I doing wrong? Or is there a better way to accomplish this?
This demonstrates the issue I'm trying to resolve:
function custom_function( $additional_domains ) {
$allowed_domains = array(
'domain1.com',
'domain2.com',
'domain3.com',
);
if ( $additional_domains ) {
array_push( $allowed_domains, $additional_domains );
}
print_r( $allowed_domains );
}
add_action('admin_head', 'custom_function');
function send_domains_to_custom_function( $domains ) {
return $domains;
}
add_filter( 'custom_function', 'send_domains_to_custom_function', 10, 1 );
$add_these_domains = array(
'domain4.com',
'domain5.com',
);
apply_filters( 'custom_function', $add_these_domains );
This is the result of the previous code:
Array
(
[0] => domain1.com
[1] => domain2.com
[2] => domain3.com
)
But this is the result I'm wanting:
Array
(
[0] => domain1.com
[1] => domain2.com
[2] => domain3.com
[3] => domain4.com
[3] => domain5.com
)
Update: Final full working solution
Thanks to #melvin, and to provide clarity for anyone else who may find this later, here's what I ended up with:
Parent theme function:
function custom_function( $additional_domains ) {
$allowed_domains = array(
'domain1.com',
'domain2.com',
'domain3.com',
);
$additional_domains = apply_filters( 'add_to_allowed_domains', $additional_domains );
if ( !empty($additional_domains) ) {
$allowed_domains = array_merge( $allowed_domains, $additional_domains );
}
print_r( $allowed_domains );
}
add_action('admin_head', 'custom_function');
Child theme function & filter:
add_filter('add_to_allowed_domains','add_to_domains_fn');
function add_to_domains_fn($domains){
$domains= array('domain4.com','domain5.com');
return $domains;
}
The parent theme function still works with the original allowed domains defined within it. If the child theme includes add_to_domains_fn() and the filter, it adds the additional domains in as expected.
See the following function from store-front theme of wordpress
function storefront_header_styles() {
$is_header_image = get_header_image();
$header_bg_image = '';
if ( $is_header_image ) {
$header_bg_image = 'url(' . esc_url( $is_header_image ) . ')';
}
$styles = array();
if ( '' !== $header_bg_image ) {
$styles['background-image'] = $header_bg_image;
}
$styles = apply_filters( 'storefront_header_styles', $styles );
foreach ( $styles as $style => $value ) {
echo esc_attr( $style . ': ' . $value . '; ' );
}
}
Have you seen how apply_filters is applied inorder to use the variable $styles?
$styles = apply_filters( 'storefront_header_styles', $styles );
Either the theme should put a filter in the position or you should manually add an apply_filter to the theme.
#UPDATE
I don't really get what code is from theme function and child theme. Assuming i have understood correctly, you need something as follows
function custom_function( $additional_domains ) {
$allowed_domains = array(
'domain1.com',
'domain2.com',
'domain3.com',
);
$allowed_domains = apply_filters( 'add_to_allowed_domains', $allowed_domains );
if ( $additional_domains ) {
array_push( $allowed_domains, $additional_domains );
}
print_r( $allowed_domains );
}
add_action('admin_head', 'custom_function');
After changing the function as the above, you can use
add_filter('add_to_allowed_domains','add_to_domains_fn');
function add_to_domains_fn($domains){
$domains= array('domain4.com','domain5.com');
return $domains;
}
N.B : Since the function in the parent theme doesn't have any filters there, adding apply_filters manually is not recommended. Because the changes get overridden on next theme update. So you can ask theme developer to add a filter there

How to exclude specific post from wp functions.php code?

I used this code to show responsive ad from adsense after 5th paragraph.
Adding Ads After First And Second Paragraph of WordPress Post
This is the code I use on my site:
function prefix_insert_after_paragraph2( $ads, $content ) {
if ( ! is_array( $ads ) ) {
return $content;
}
$closing_p = '</p>';
$paragraphs = explode( $closing_p, $content );
foreach ($paragraphs as $index => $paragraph) {
if ( trim( $paragraph ) ) {
$paragraphs[$index] .= $closing_p;
}
$n = $index + 1;
if ( isset( $ads[ $n ] ) ) {
$paragraphs[$index] .= $ads[ $n ];
}
}
return implode( '', $paragraphs );
}
add_filter( 'the_content', 'prefix_insert_post_ads' );
function prefix_insert_post_ads( $content ) {
if ( is_single() && ! is_admin() ) {
$content = prefix_insert_after_paragraph2( array(
// The format is: '{PARAGRAPH_NUMBER}' => 'AD_CODE',
'5' => '<div>Ad code after FIRST paragraph goes here</div>',
), $content );
}
return $content;
}
I would like to exclude this code only for specific posts. How can I add the proper code to be able to exclude this function for post id=280?
Thank you.
Why do you want to exclude this function inside functions.php?
I think it's much easier to do it inside the post loop.
For example, to exclude the POST ID 280, if you're inside the page.php, or similar you can do this:
global $post;
if ($post->ID == 280){ remove_filter( 'the_content', 'prefix_insert_post_ads' ); }
This way you have the 'add_filter' on your functions.php file, and if you find the exception inside the post (ID=280), you remove the filter.
Just U have to check the current post_id. E.g. :
function prefix_insert_post_ads( $content ) {
global $post;
$post_id = $post->ID;
$post_ids_excluded = [280,....]; // excluded posts ids
if ( in_array($post_id,$post_ids_excluded) ){
return $content;
}
/*
... the same code .....
*/
}

Categories