Let me explain WordPress' processing of a request, and a method to change WordPress' behaviour to accomplish your goals accordingly.
Parsing the request
When WordPress receives a request, it starts a process of dissecting the request and transforming it into a page. The core of this process begins when WordPress main query method WP::main()
is called. This function parses the query, as you correctly identified, in parse_request()
(in includes/class-wp.php
). There, WordPress tries to match the URL against one of the rewrite rules. When the URL is matched, it creates a query string of the URL parts and encodes these parts (everything between two slashes) using urlencode()
, to prevent special characters such as &
from messing up the query string. These encoded characters might have caused you to think that the problem resided there, but they're actually turned into their corresponding "real" characters when parsing the query string.
Running the query associated with the request
After WordPress has parsed the URL, it sets up the main query class, WP_Query
, which is done in the same main()
method of the WP
class. The beef of WP_Query
can be found in its get_posts()
method where all query arguments are parsed and sanitized and the actual SQL query is constructed (and, eventually, run).
In this method, on line 2730, the following code is executed:
$q['name'] = sanitize_title_for_query( $q['name'] );
This sanitizes the post for fetching it from the posts table. Outputting debug info inside the loop shows that this is where the problem resides: your post name, my-permalink~
, is transformed to my-permalink
, which is then used to fetch the post from the database.
The post title sanitization function
The function sanitize_title_for_query
calls sanitize_title
with the proper parameters, which proceeds to sanitize the title. Now the core of this function is applying the sanitize_title
filter:
$title = apply_filters( 'sanitize_title', $title, $raw_title, $context );
This filter has, in native WordPress, a single function attached to it: sanitize_title_with_dashes
. I've written an extensive overview of what this function does, which can be found here. In this function, the line that's causing your problem is
$title = preg_replace('/[^%a-z0-9 _-]/', '', $title);
This line strips all characters except for alphanumerical characters, spaces, hyphens and underscores.
Solving your problem
So, there is basically a single way to solve your problem: removing the sanitize_title_with_dashes
function from the filter and replacing it with your own function. This is actually not that difficult to do, but:
- When WordPress changes the internal process of sanitizing titles, this will have major effects on your website.
- Other plugins hooking into this filter might not correctly handle the new functionality.
Most importantly: WordPress uses the result of the sanitize_title
function directly in the SQL query by this line:
$where .= " AND $wpdb->posts.post_name = '" . $q['name'] . "'";
Should you ever consider changing the filter, be sure that you properly escape the title before it is used in the query!
Conclusion: solving your problem is not necessary as far as security is concerned, but should you want to do it, replace the sanitize_title_with_dashes
with your own functionality and pay attention to SQL escaping.
NB all file names and line numbers correspond with WordPress 4.4.2 files.