PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.0
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
ablocks / addons / link-guard / resolver.php

resolver.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.13.0, at addons/link-guard/resolver.php

198 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ABlocksLinkGuard;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit; // Exit if accessed directly.
7 }
8
9 /**
10 * Maps an `href` to the post it points at.
11 *
12 * `url_to_postid()` understands every permalink structure, custom post type
13 * and page hierarchy, but it answers the question "can the *current visitor*
14 * see this?" rather than "which post is this?". A scheduled post resolves for
15 * an editor and resolves to 0 for everyone else, because `WP_Query` drops
16 * unpublished singular results after fetching them. A cache built on that
17 * answer would depend on who happened to load the page first.
18 *
19 * The fetch happens before the drop, and `posts_results` fires in between. So
20 * the resolver listens there for the duration of one `url_to_postid()` call and
21 * keeps the post it saw, which makes the answer the same for every viewer.
22 */
23 class Resolver {
24
25 /**
26 * Post seen by the singular query during the current resolve() call.
27 *
28 * @var int
29 */
30 private static $captured = 0;
31
32 /**
33 * Slug the singular query asked for during the current resolve() call.
34 *
35 * @var string
36 */
37 private static $slug = '';
38
39 /**
40 * Reduce an href to a canonical same-site URL, or '' when it cannot be a
41 * link to a post (another site, an anchor, mailto:, an upload, wp-admin...).
42 *
43 * The result is also the cache key, so two spellings of the same link —
44 * relative and absolute, with or without `www.`, a `#section` or a
45 * tracking query string — collapse to one entry.
46 *
47 * @param string $href Raw attribute value.
48 * @return string
49 */
50 public static function normalize( $href ) {
51 $href = trim( (string) $href );
52
53 if ( '' === $href || '#' === $href[0] ) {
54 return '';
55 }
56
57 if ( 0 === strpos( $href, '//' ) ) {
58 $href = 'https:' . $href;
59 }
60
61 $parts = wp_parse_url( $href );
62 if ( false === $parts ) {
63 return '';
64 }
65
66 if ( isset( $parts['scheme'] ) && ! in_array( strtolower( $parts['scheme'] ), [ 'http', 'https' ], true ) ) {
67 return '';
68 }
69
70 $home = wp_parse_url( home_url() );
71
72 if ( isset( $parts['host'] ) ) {
73 if ( self::bare_host( $parts['host'] ) !== self::bare_host( isset( $home['host'] ) ? $home['host'] : '' ) ) {
74 return '';
75 }
76 } elseif ( empty( $parts['path'] ) || '/' !== $parts['path'][0] ) {
77 // Document-relative links ("next-post/", "?p=3") depend on the page
78 // they sit on; guessing their target could hide a working link.
79 return '';
80 }
81
82 $path = isset( $parts['path'] ) ? $parts['path'] : '/';
83 $home_path = isset( $home['path'] ) ? rtrim( $home['path'], '/' ) : '';
84
85 if ( '' !== $home_path && 0 !== strpos( $path, $home_path ) ) {
86 return '';
87 }
88
89 $relative = '/' . ltrim( substr( $path, strlen( $home_path ) ), '/' );
90
91 if ( preg_match( '#^/(?:wp-admin|wp-content|wp-includes|wp-json)(?:/|$)|^/wp-[a-z-]+\.php#i', $relative ) ) {
92 return '';
93 }
94
95 // A file: an upload, a sitemap, a download. Never a post.
96 if ( preg_match( '#\.(?!html?$|php$)[a-z0-9]{2,5}$#i', $relative ) ) {
97 return '';
98 }
99
100 // Keep the query string only when it is what identifies the post.
101 $query = '';
102 if ( ! empty( $parts['query'] ) && preg_match( '#(?:^|&)(p|page_id|preview_id)=(\d+)#', $parts['query'], $match ) ) {
103 $query = '?' . ( 'page_id' === $match[1] ? 'page_id' : 'p' ) . '=' . $match[2];
104 }
105
106 return untrailingslashit( home_url( $relative ) ) . $query;
107 }
108
109 /**
110 * Post ID a normalized URL points at, whatever that post's status. 0 when
111 * the URL is not a single post (an archive, a custom route, nothing).
112 *
113 * @param string $url Output of normalize().
114 * @return int
115 */
116 public static function resolve( $url ) {
117 if ( preg_match( '#\?(?:p|page_id)=(\d+)$#', $url, $match ) ) {
118 return (int) $match[1];
119 }
120
121 self::$captured = 0;
122 self::$slug = '';
123 add_action( 'parse_query', [ __CLASS__, 'capture_slug' ], PHP_INT_MAX );
124 add_filter( 'posts_results', [ __CLASS__, 'capture' ], PHP_INT_MAX, 2 );
125
126 try {
127 $id = (int) url_to_postid( trailingslashit( $url ) );
128 } finally {
129 remove_action( 'parse_query', [ __CLASS__, 'capture_slug' ], PHP_INT_MAX );
130 remove_filter( 'posts_results', [ __CLASS__, 'capture' ], PHP_INT_MAX );
131 }
132
133 if ( $id || self::$captured ) {
134 return $id ? $id : self::$captured;
135 }
136
137 return self::trashed( self::$slug );
138 }
139
140 /**
141 * Trashing a post renames its slug to `{slug}__trashed`, so its old URL
142 * stops matching anything and would otherwise look like a link to nowhere.
143 *
144 * @param string $slug Slug the URL asked for.
145 * @return int
146 */
147 private static function trashed( $slug ) {
148 if ( '' === $slug ) {
149 return 0;
150 }
151
152 $ids = get_posts(
153 [
154 'name' => $slug . '__trashed',
155 'post_type' => 'any',
156 'post_status' => 'trash',
157 'fields' => 'ids',
158 'posts_per_page' => 1,
159 'suppress_filters' => true,
160 'no_found_rows' => true,
161 ]
162 );
163
164 return $ids ? (int) $ids[0] : 0;
165 }
166
167 /**
168 * @internal Hooked only for the duration of resolve().
169 *
170 * @param \WP_Query $query The query url_to_postid() is about to run.
171 */
172 public static function capture_slug( $query ) {
173 if ( '' !== self::$slug || ! $query->is_singular() ) {
174 return;
175 }
176 $slug = $query->get( 'name' ) ? $query->get( 'name' ) : $query->get( 'pagename' );
177 self::$slug = $slug ? sanitize_title( basename( $slug ) ) : '';
178 }
179
180 /**
181 * @internal Hooked only for the duration of resolve().
182 *
183 * @param \WP_Post[] $posts Raw query results.
184 * @param \WP_Query $query The query.
185 * @return \WP_Post[]
186 */
187 public static function capture( $posts, $query ) {
188 if ( ! self::$captured && ! empty( $posts ) && $query->is_singular() ) {
189 self::$captured = (int) ( is_object( $posts[0] ) ? $posts[0]->ID : $posts[0] );
190 }
191 return $posts;
192 }
193
194 private static function bare_host( $host ) {
195 return preg_replace( '#^www\.#', '', strtolower( $host ) );
196 }
197 }
198