PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / PermalinkResolver.php

PermalinkResolver.php in 404 Solution trunk, at includes/core/PermalinkResolver.php

201 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Resolves stored "ID|TYPE" permalink references into live arrays of
10 * {id, type, score, link, title, status} by querying the WordPress entity
11 * (post, term, attachment, etc.) the reference points at.
12 *
13 * Extracted from ABJ_404_Solution_Functions per design-audit-2026-06-02
14 * M201 (Functions.php grab-bag split). This is a translation/lookup
15 * concern, not a string/url utility.
16 */
17 class ABJ_404_Solution_PermalinkResolver {
18
19 /** Turns ID|TYPE, SCORE into an array with id, type, score, link, and title.
20 *
21 * @param string $idAndType e.g. 15|POST is a page ID of 15 and a type POST.
22 * @param int|float $linkScore
23 * @param string $rowType if this is "image" then wp_get_attachment_image_src() is used.
24 * @param array<string, mixed>|null $options in case an external URL is used.
25 * @return array<string, mixed> an array with id, type, score, link, and title.
26 */
27 static function permalinkInfoToArray($idAndType, $linkScore, $rowType = null, $options = null) {
28 if ($idAndType == null) {
29 return array('score' => -999);
30 }
31
32 $meta = explode("|", $idAndType);
33 $permalink = array(
34 'id' => $meta[0],
35 // Handle malformed data that doesn't contain a pipe separator
36 'type' => isset($meta[1]) ? $meta[1] : '',
37 'score' => $linkScore,
38 'status' => 'unknown',
39 'link' => 'dunno',
40 );
41
42 /** @var int $idInt */
43 $idInt = (int)$permalink['id'];
44 // Use strict comparison to avoid null/false == 0 issues with type coercion
45 // Cast to int for comparison since ABJ404_TYPE_* constants are integers
46 $typeInt = is_numeric($permalink['type']) ? (int)$permalink['type'] : -1;
47
48 self::resolveByType($permalink, $typeInt, $idInt, $rowType, $options);
49
50 if ($permalink['status'] === false) {
51 $permalink['status'] = 'trash';
52 }
53
54 // Decode anything that might be encoded to support utf8 characters
55 $sanitizer = abj_service('sanitizer');
56 $linkVal = is_string($permalink['link']) ? $permalink['link']
57 : (is_scalar($permalink['link']) ? (string)$permalink['link'] : '');
58 $permalink['link'] = $sanitizer->normalizeUrlString($linkVal);
59 $titleVal = (array_key_exists('title', $permalink) && is_string($permalink['title']))
60 ? $permalink['title'] : '';
61 $permalink['title'] = $sanitizer->normalizeUrlString($titleVal);
62
63 return $permalink;
64 }
65
66 /**
67 * Fill link/title/status on $permalink for the recognized $typeInt.
68 *
69 * @param array<string, mixed> $permalink (by-ref) mutated with link/title/status
70 * @param int $typeInt one of the ABJ404_TYPE_* integer constants, or -1 for unrecognized
71 * @param int $idInt
72 * @param string|null $rowType
73 * @param array<string, mixed>|null $options
74 * @return void
75 */
76 private static function resolveByType(array &$permalink, $typeInt, $idInt, $rowType, $options) {
77 if ($typeInt === ABJ404_TYPE_POST) {
78 self::fillPost($permalink, $idInt, $rowType);
79 } else if ($typeInt === ABJ404_TYPE_TAG) {
80 self::fillTag($permalink, $idInt);
81 } else if ($typeInt === ABJ404_TYPE_CAT) {
82 self::fillCategory($permalink, $idInt);
83 } else if ($typeInt === ABJ404_TYPE_HOME) {
84 // Ask for the home page's real URL, path included, and pin exactly
85 // one trailing delimiter on whatever comes back.
86 //
87 // Callers append the 404ing request's own comment-page/query part
88 // to this link (PluginLogicPageOrdering::buildFinalRedirectDestination),
89 // so the link has to name the home page on its own. get_home_url()
90 // with no path usually returns the site root WITHOUT a trailing
91 // slash ("https://site.com", "https://site.com/blog"), and that
92 // append then produces a URL whose path is not the home page:
93 // "https://site.com/blog?page=1" asks the server for a file named
94 // "blog", which bounces straight back to "/blog/?page=1" and 404s
95 // again (support report 282).
96 //
97 // The '/' path argument is not enough on its own. WordPress never
98 // normalizes the `home` option (sanitize_option('home') only runs
99 // sanitize_url()), and the `home_url` filter runs last, so
100 // WP_HOME, a multisite mapping, or a multilingual plugin can hand
101 // back a URL that already ends in a slash. get_home_url(null, '/')
102 // would then return "https://site.com//", whose path is likewise
103 // not the home page. rtrim + '/' is idempotent, so the delimiter
104 // is pinned regardless of what the option or the filter returns.
105 $homeUrl = get_home_url(null, '/');
106 $permalink['link'] = rtrim(is_string($homeUrl) ? $homeUrl : '', '/') . '/';
107 $permalink['title'] = get_bloginfo('name');
108 $permalink['status'] = 'published';
109 } else if ($typeInt === ABJ404_TYPE_EXTERNAL) {
110 self::fillExternal($permalink, $options);
111 } else if ($typeInt === ABJ404_TYPE_404_DISPLAYED) {
112 $permalink['link'] = '404';
113 $permalink['status'] = 'published';
114 } else {
115 // A corrupt/unrecognized redirect row (e.g. empty or garbage type
116 // from a damaged DB record) is a tolerable data condition: the
117 // resolver simply can't resolve it, so the row is left in its
118 // unresolvable 'unknown'/'dunno' state for the caller to skip.
119 // Per Defensive Coding Philosophy #8, bad-data the plugin can
120 // function past is logged at WARNING level -- not errorMessage(),
121 // which fires a production telemetry report / admin error notice.
122 $logger = abj_service('logging');
123 $logger->warn("Unrecognized permalink type: " .
124 wp_kses_post((string)json_encode($permalink)));
125 }
126 }
127
128 /**
129 * @param array<string, mixed> $permalink (by-ref)
130 * @param int $idInt
131 * @param string|null $rowType
132 * @return void
133 */
134 private static function fillPost(array &$permalink, $idInt, $rowType) {
135 if ($rowType == 'image') {
136 $imageURL = wp_get_attachment_image_src($idInt, "attached-image");
137 $permalink['link'] = is_array($imageURL) ? $imageURL[0] : '';
138 } else {
139 $permalink['link'] = get_permalink($idInt);
140 }
141 $permalink['title'] = get_the_title($idInt);
142 $permalink['status'] = get_post_status($idInt);
143 }
144
145 /**
146 * @param array<string, mixed> $permalink (by-ref)
147 * @param int $idInt
148 * @return void
149 */
150 private static function fillTag(array &$permalink, $idInt) {
151 $permalink['link'] = get_tag_link($idInt);
152 $tag = get_term($idInt);
153 if (is_object($tag) && !is_wp_error($tag)) {
154 $permalink['title'] = $tag->name;
155 } else {
156 $permalink['title'] = $permalink['link'];
157 }
158 $permalink['status'] = ($permalink['title'] == null || $permalink['title'] == '') ? 'trash' : 'published';
159 }
160
161 /**
162 * Uses get_term_link() instead of get_category_link() to support
163 * custom taxonomies like WooCommerce product_cat.
164 *
165 * @param array<string, mixed> $permalink (by-ref)
166 * @param int $idInt
167 * @return void
168 */
169 private static function fillCategory(array &$permalink, $idInt) {
170 $catTerm = get_term($idInt);
171 if (is_object($catTerm) && !is_wp_error($catTerm)) {
172 $termLink = get_term_link($catTerm);
173 $permalink['link'] = is_wp_error($termLink) ? get_category_link($idInt) : $termLink;
174 $permalink['title'] = $catTerm->name;
175 } else {
176 $permalink['link'] = get_category_link($idInt);
177 $permalink['title'] = $permalink['link'];
178 }
179 $permalink['status'] = ($permalink['title'] == null || $permalink['title'] == '') ? 'trash' : 'published';
180 }
181
182 /**
183 * @param array<string, mixed> $permalink (by-ref)
184 * @param array<string, mixed>|null $options
185 * @return void
186 */
187 private static function fillExternal(array &$permalink, $options) {
188 $permalink['link'] = $permalink['id'];
189 if ($permalink['link'] == ABJ404_TYPE_EXTERNAL) {
190 if ($options == null) {
191 $options = abj_service('options_repository')->getOptions();
192 }
193 $urlDestination = (array_key_exists('dest404pageURL', $options) &&
194 isset($options['dest404pageURL']) ? $options['dest404pageURL'] :
195 'External URL not found in options ABJ404 Solution Error');
196 $permalink['link'] = $urlDestination;
197 }
198 $permalink['status'] = 'published';
199 }
200 }
201