PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.6
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.6
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / redirections / class-metasync-auto-redirect.php

class-metasync-auto-redirect.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.6, at redirections/class-metasync-auto-redirect.php

278 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Automatic Redirect on Slug Change
5 *
6 * Creates 301 redirects automatically when post/page URL slugs are changed.
7 * This preserves SEO link equity when content URLs are modified.
8 *
9 * @link https://searchatlas.com
10 * @since 1.0.0
11 * @package Metasync
12 * @subpackage Metasync/redirections
13 * @author Engineering Team <[email protected]>
14 */
15
16 // Abort if this file is accessed directly.
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 class Metasync_Auto_Redirect
22 {
23 /**
24 * Reference to the redirection database class
25 *
26 * @var Metasync_Redirection_Database
27 */
28 private $db_redirection;
29
30 /**
31 * Post types to monitor for slug changes
32 *
33 * @var array
34 */
35 private $monitored_post_types = array('post', 'page');
36
37 /**
38 * Constructor
39 *
40 * @param Metasync_Redirection_Database $db_redirection Database class reference
41 */
42 public function __construct($db_redirection)
43 {
44 $this->db_redirection = $db_redirection;
45 }
46
47 /**
48 * Initialize hooks
49 */
50 public function init()
51 {
52 add_action('post_updated', array($this, 'check_slug_change'), 10, 3);
53 add_action('admin_notices', array($this, 'display_auto_redirect_notice'));
54 }
55
56 /**
57 * Get supported post types
58 *
59 * @return array
60 */
61 private function get_supported_post_types()
62 {
63 return apply_filters('metasync_auto_redirect_post_types', $this->monitored_post_types);
64 }
65
66 /**
67 * Check if slug changed and create redirect if needed
68 *
69 * @param int $post_id Post ID
70 * @param WP_Post $post_after Post object after update
71 * @param WP_Post $post_before Post object before update
72 */
73 public function check_slug_change($post_id, $post_after, $post_before)
74 {
75 // Skip autosaves
76 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
77 return;
78 }
79
80 // Skip revisions
81 if (wp_is_post_revision($post_id)) {
82 return;
83 }
84
85 // Only check supported post types
86 if (!in_array($post_after->post_type, $this->get_supported_post_types(), true)) {
87 return;
88 }
89
90 // Only create redirects for published posts
91 if ($post_after->post_status !== 'publish') {
92 return;
93 }
94
95 // Skip if the post wasn't published before (new publish)
96 if ($post_before->post_status !== 'publish') {
97 return;
98 }
99
100 // Check if the slug actually changed
101 if ($post_before->post_name === $post_after->post_name) {
102 return;
103 }
104
105 // Skip if slug is empty
106 if (empty($post_before->post_name) || empty($post_after->post_name)) {
107 return;
108 }
109
110 // Build the old and new URLs
111 $old_url = $this->build_relative_url($post_before);
112 $new_url = $this->build_relative_url($post_after);
113
114 // Skip if URLs are the same
115 if ($old_url === $new_url) {
116 return;
117 }
118
119 // Create the redirect
120 $this->create_redirect($old_url, $new_url, $post_id);
121 }
122
123 /**
124 * Build relative URL for a post
125 *
126 * @param WP_Post $post Post object
127 * @return string Relative URL path
128 */
129 private function build_relative_url($post)
130 {
131 $permalink = get_permalink($post);
132 $parsed = wp_parse_url($permalink);
133 $path = isset($parsed['path']) ? $parsed['path'] : '/';
134 return $path;
135 }
136
137 /**
138 * Create a 301 redirect from old URL to new URL
139 *
140 * @param string $old_url Old URL path
141 * @param string $new_url New URL path
142 * @param int $post_id Post ID (for reference)
143 * @return bool Success or failure
144 */
145 private function create_redirect($old_url, $new_url, $post_id)
146 {
147 // Check for duplicate redirects
148 $existing = $this->redirect_exists($old_url);
149 if ($existing !== false) {
150 // Update existing redirect instead of creating duplicate
151 $this->update_existing_redirect($existing, $new_url);
152 return true;
153 }
154
155 // Loop detection — silently skip and log if creating this redirect would produce a chain back to $old_url
156 if (!class_exists('Metasync_Redirection')) {
157 require_once dirname(__FILE__) . '/class-metasync-redirection.php';
158 }
159 $db_ref = $this->db_redirection;
160 $redirection_helper = new Metasync_Redirection($db_ref);
161 $loop_chain = array();
162 if ($redirection_helper->would_create_loop($old_url, $new_url, $loop_chain)) {
163 error_log('[MetaSync] Auto-redirect skipped: loop detected — ' . implode(' → ', $loop_chain));
164 return false;
165 }
166
167 // Prepare redirect data
168 $redirect_data = array(
169 'sources_from' => serialize(array($old_url => 'exact')),
170 'url_redirect_to' => $new_url,
171 'http_code' => 301,
172 'hits_count' => 0,
173 'status' => 'active',
174 'pattern_type' => 'exact',
175 'regex_pattern' => null,
176 'description' => sprintf(
177 'Auto-created: Slug change for post #%d on %s',
178 $post_id,
179 current_time('Y-m-d H:i:s')
180 ),
181 'created_at' => current_time('mysql'),
182 'updated_at' => current_time('mysql'),
183 );
184
185 // Add the redirect
186 $result = $this->db_redirection->add($redirect_data);
187
188 if ($result) {
189 // Set admin notice
190 set_transient('metasync_auto_redirect_notice', array(
191 'old_url' => $old_url,
192 'new_url' => $new_url,
193 'post_id' => $post_id,
194 ), 60);
195
196 do_action('metasync_auto_redirect_created', $old_url, $new_url, $post_id);
197 }
198
199 return $result !== false;
200 }
201
202 /**
203 * Check if a redirect already exists for the given source URL
204 *
205 * @param string $source_url Source URL to check
206 * @return object|false False if not found, redirect object if found
207 */
208 private function redirect_exists($source_url)
209 {
210 $all_redirects = $this->db_redirection->getAllActiveRecords();
211
212 if (empty($all_redirects)) {
213 return false;
214 }
215
216 foreach ($all_redirects as $redirect) {
217 $sources = unserialize($redirect->sources_from, ['allowed_classes' => false]); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
218 if (is_array($sources) && isset($sources[$source_url])) {
219 return $redirect;
220 }
221 }
222
223 return false;
224 }
225
226 /**
227 * Update an existing redirect's destination
228 *
229 * @param object $existing Existing redirect object
230 * @param string $new_destination New destination URL
231 * @return bool Success or failure
232 */
233 private function update_existing_redirect($existing, $new_destination)
234 {
235 $updated_description = $existing->description . sprintf(
236 "\nUpdated destination on %s",
237 current_time('Y-m-d H:i:s')
238 );
239
240 $this->db_redirection->update(
241 array(
242 'url_redirect_to' => $new_destination,
243 'description' => $updated_description,
244 ),
245 $existing->id
246 );
247
248 return true;
249 }
250
251 /**
252 * Display admin notice after auto-redirect creation
253 */
254 public function display_auto_redirect_notice()
255 {
256 $notice = get_transient('metasync_auto_redirect_notice');
257
258 if (!$notice) {
259 return;
260 }
261
262 delete_transient('metasync_auto_redirect_notice');
263
264 $message = sprintf(
265 'A 301 redirect was automatically created from %s to %s because the URL slug was changed.',
266 '<code>' . esc_html($notice['old_url']) . '</code>',
267 '<code>' . esc_html($notice['new_url']) . '</code>'
268 );
269
270 echo '<div class="notice notice-success is-dismissible">';
271 echo '<p><strong>MetaSync Auto-Redirect:</strong> ' . wp_kses_post($message) . '</p>';
272 echo '</div>';
273 }
274 }
275
276
277
278