PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
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 / public / class-metasync-hidden-post.php

class-metasync-hidden-post.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.25, at public/class-metasync-hidden-post.php

483 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 class MetaSyncHiddenPostManager
5 {
6 # Define static properties for the hidden post
7 private $post_title; # Title of the hidden post
8 private $post_content; # Default content
9 public $image_url; # Featured image URL
10 private $hidden_post_type; # Non-existent post type to hide it
11
12 # Initialize hooks on WordPress init
13 public function init()
14 {
15 # Get the last execution timestamp
16 $metasync_data = Metasync::get_option('general');
17
18 # Check if 24 hours have passed
19 if (empty($metasync_data['last_run_time']) || (time() - $metasync_data['last_run_time']) > 604800) {
20
21 $this->create_hidden_post(); #create/show post for crawling
22 }
23 }
24
25 public function __construct()
26 {
27 $this->post_title = 'Please do not delete this Metasync test Post'; # Initialize post_title
28 $this->post_content = 'This post, is used by the ' . Metasync::get_effective_plugin_name() . ' plugin, please do not delete.'; # Initialize content
29 $this->hidden_post_type = 'metasync_post_type'; # Initialize Non-existent post type to hide it later
30
31 }
32
33 /**
34 * Upload a local image to WordPress media library and return the URL
35 */
36 private function upload_local_image($file_path)
37 {
38 if (!file_exists($file_path)) {
39 return ''; # Return empty if file doesn't exist
40 }
41
42 require_once ABSPATH . 'wp-admin/includes/file.php';
43 require_once ABSPATH . 'wp-admin/includes/media.php';
44 require_once ABSPATH . 'wp-admin/includes/image.php';
45
46 # Create a temporary copy to prevent deletion
47 $tmp_file_path = wp_tempnam($file_path);
48 copy($file_path, $tmp_file_path);
49
50 # Upload the copied file
51 $file_array = [
52 'name' => basename($file_path),
53 'tmp_name' => $tmp_file_path, # Use the copied file
54 ];
55
56 $attachment_id = media_handle_sideload($file_array, 0);
57
58 # Clean up the temp file
59 if (file_exists($tmp_file_path)) {
60 @unlink($tmp_file_path); # Delete only the temp copy, not the original file
61 }
62
63 if (is_wp_error($attachment_id)) {
64 return ''; # Return empty if upload fails
65 }
66
67 # Return the URL of the uploaded image
68 return wp_get_attachment_url($attachment_id);
69 }
70
71
72 /**
73 * Get Image url after load
74 */
75
76 public function get_image_url_Data()
77 {
78
79 # Get the latest uploaded image from the media library
80 $latest_image = get_posts([
81 'post_type' => 'attachment', # Fetch only media attachments
82 'post_mime_type' => 'image', # Ensure it's an image
83 'posts_per_page' => 1, # Limit to 1 result
84 'orderby' => 'date', # Get the latest one
85 'order' => 'DESC', # Sort in descending order
86 ]);
87
88
89 # Get local image path
90 $local_image_path = plugin_dir_path(dirname(__FILE__)) . 'assets/banner-1544x500.png';
91
92 # Check if an image exists and retrieve its URL
93 $this->image_url = (!empty($latest_image)) ? wp_get_attachment_url($latest_image[0]->ID) : $this->upload_local_image($local_image_path); # Use default if no image found
94
95
96 }
97
98 /**
99 * Create a hidden post programmatically if it doesn't exist
100 */
101
102 public function create_hidden_post()
103 {
104
105 # Check if the post already exists by title
106 global $wpdb;
107
108 # prepare query
109 $query = $wpdb->prepare(
110 "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND (post_type = 'post' OR post_type = 'metasync_post_type') LIMIT 1",
111 $this->post_title
112 );
113
114
115 # Get the Data
116 $existing_post = $wpdb->get_var($query);
117
118 $this->get_image_url_Data();
119 if ($existing_post) {
120
121 #show post
122 $this->show_post_completely($existing_post);
123
124 # Update Post meta for Divi
125 $existing_meta = get_post_meta($existing_post, '_et_pb_built_for_post_type', true);
126
127 if ($existing_meta !== '') {
128 # If meta key exists, update it
129 update_post_meta($existing_post, '_et_pb_built_for_post_type', 'post');
130 } else {
131 # If meta key does not exist, add it
132 add_post_meta($existing_post, '_et_pb_built_for_post_type', 'post', true);
133 }
134 # Check if title and image exist in post content and fix if needed
135 $this->check_and_fix_post_content($existing_post);
136
137 $this->check_and_fix_page_content($existing_post);
138
139 # Hide the post completely by changing its post type
140 $this->hide_post_completely($existing_post);
141 return $existing_post;
142 }
143
144 $prepare_content['post_content'] = $this->post_content;
145
146 # Pass the Parameter
147 if (defined('METASYNC_VERSION')) {
148 $version = METASYNC_VERSION;
149 } else {
150 $version = '1.0.0';
151 }
152 $plugin_name = 'metasync';
153
154 # Call the MetaSync REST API Class
155 $content = new Metasync_Rest_Api($plugin_name,$version);
156 $builderData = $content->metasync_upload_post_content($prepare_content);
157
158 # Insert as draft so wp_insert_post does not fire transition_post_status
159 # with 'publish'. Jetpack Social, Zapier, RSS, and similar auto-share
160 # plugins listen to that hook and would broadcast this internal test post.
161 $post_id = wp_insert_post([
162 'post_title' => $this->post_title,
163 'post_content' => $builderData['content'] ? $builderData['content'] : $prepare_content,
164 'post_status' => 'draft',
165 'post_type' => 'post',
166 ]);
167
168 if (is_wp_error($post_id)) {
169 return false;
170 }
171
172 # Flip to publish via direct DB write — bypasses transition_post_status
173 global $wpdb;
174 $wpdb->update(
175 $wpdb->posts,
176 ['post_status' => 'publish'],
177 ['ID' => $post_id]
178 );
179 clean_post_cache($post_id);
180
181 $post_meta = array();
182
183 # Update Post meta for Page Builder to work
184 if(isset($builderData['elementor_meta_data'])){
185 $post_meta = array_merge($post_meta,$builderData['elementor_meta_data']);
186 }else if(isset($builderData['divi_meta_data'])){
187 $post_meta = array_merge($post_meta,$builderData['divi_meta_data']);
188 $post_meta['_et_pb_ab_current_shortcode']='[et_pb_split_track id="'.$post_id.'" /]';
189 $post_meta['_et_pb_use_builder']='on'; # Turn on the page Builder
190 $post_meta['_et_pb_built_for_post_type']='post';
191 }
192
193 # Add custom fields to posts and pages
194 foreach ($post_meta as $key => $value) {
195 # Add Post Meta
196 add_post_meta($post_id, $key, $value, true);
197
198 }
199
200 # check if the feature image is empty
201 if (!empty($this->image_url)) {
202
203 # Set a featured image for the post
204 $this->set_featured_image($post_id, $this->image_url);
205 }
206
207
208 if (isset( $builderData['elementor_meta_data']) && did_action( 'elementor/loaded' )) {
209 # Clear Elementor cache for the specified post ID
210 \Elementor\Plugin::instance()->files_manager->clear_cache();
211
212 }
213
214 # Check if title and image exist in post content and fix if needed
215 $this->check_and_fix_post_content($post_id);
216
217 $this->check_and_fix_page_content($post_id);
218
219 # Hide the post completely by changing its post type
220 $this->hide_post_completely($post_id);
221
222 return $post_id;
223 }
224
225
226 /**
227 * Change the post type to page and check the
228 */
229 private function check_and_fix_page_content($post_id)
230 {
231 # Switch to page type via direct DB write — bypasses transition_post_status
232 global $wpdb;
233 $wpdb->update(
234 $wpdb->posts,
235 [
236 'post_type' => 'page',
237 'post_status' => 'publish',
238 ],
239 ['ID' => $post_id]
240 );
241 clean_post_cache($post_id);
242
243 # Update Post meta for Divi
244 $existing_meta = get_post_meta($post_id, '_et_pb_built_for_post_type', true);
245
246 if ($existing_meta !== '') {
247 # If meta key exists, update it
248 update_post_meta($post_id, '_et_pb_built_for_post_type', 'page');
249 } else {
250 # If meta key does not exist, add it
251 add_post_meta($post_id, '_et_pb_built_for_post_type', 'page', true);
252 }
253
254 # Check the page has the feature image and the Page title
255 $this->check_and_fix_post_content($post_id,'page');
256 }
257
258 /**
259 * Attach a featured image to the post if missing
260 */
261 private function set_featured_image($post_id, $image_url)
262 {
263 require_once ABSPATH . 'wp-admin/includes/image.php';
264 require_once ABSPATH . 'wp-admin/includes/file.php';
265 require_once ABSPATH . 'wp-admin/includes/media.php';
266
267 # Download and attach the image to the post
268 $media_id = media_sideload_image($image_url, $post_id, null, 'id');
269
270 if (!is_wp_error($media_id)) {
271 set_post_thumbnail($post_id, $media_id); # Set as featured image
272 }
273 }
274
275 /**
276 * Check if the post content contains the title and featured image, add them if missing
277 * add post title and post feature image on the content
278 */
279 private function check_and_fix_post_content($post_id,$post_type='post')
280 {
281 if (!class_exists('DOMDocument')) {
282 error_log('MetaSync: skipping check_and_fix_post_content — DOMDocument (php-dom extension) is not available on this server.');
283 return false;
284 }
285
286 # Get the post title
287 $post_title = get_the_title($post_id);
288
289 # Get the Metasync Option
290 $metasyncData = Metasync::get_option();
291 # Prepare HTML content to prepend if title or image is missing
292 $prepend_content = '';
293
294 # Get the featured image URL of the post
295 $featured_image_url = get_the_post_thumbnail_url($post_id, 'full');
296
297 # Check for [.webp,.jpg,.png] pattern
298 if (!empty($featured_image_url)) {
299 $featured_image_url = preg_replace('/\.[^.]+$/', '', $featured_image_url);
300 }
301
302 # Get the preview URL of the post
303 $post_url = get_permalink($post_id);
304
305 $probe_result = $this->fetch_post_html($post_url);
306 $html = $probe_result['body'];
307 $http_code = $probe_result['status'];
308
309 # Only trust the probe when we verifiably fetched the test post.
310 # A cache 404, bot challenge, or security-plugin block used to parse as
311 # "title not found" and flip title_in_headings to false, which made every
312 # synced post get an extra <h1> prepended (duplicate H1 on the live page).
313 # "please do not delete" appears in both the test post title and body.
314 if (empty($html) || $http_code !== 200 || stripos($html, 'please do not delete') === false) {
315
316 # Inconclusive fetch: keep the previously detected values and record
317 # the run so the probe is not retried on every request.
318 $metasyncData['general']['last_run_time'] = time();
319 Metasync::set_option($metasyncData);
320 return false;
321 }
322
323 # Load the HTML content into DOMDocument
324 $dom = new DOMDocument();
325 libxml_use_internal_errors(true); # Suppress HTML parsing errors
326 $dom->loadHTML($html); # Parse the HTML content
327 libxml_clear_errors(); # Clear any errors
328
329 # Use XPath to search for specific elements in the HTML
330 $xpath = new DOMXPath($dom);
331
332 # Query all heading tags (h1 - h6) and img tags
333 $elements = $xpath->query("//h1 | //h2 | //h3 | //h4 | //h5 | //h6 | //img");
334
335 # Flags to check if title or image is present
336 $title_in_headings = false;
337 $image_in_content = false;
338
339 # Arrays to store found headings and images
340 $headings = [];
341 $image_tags = [];
342
343 # Loop through all queried elements
344 foreach ($elements as $element) {
345 if (in_array($element->nodeName, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])) {
346 # Process heading tags
347 $heading_text = trim($element->textContent); # Get the text content of the heading
348 $headings[] = [
349 'tag' => $element->nodeName, # Store the tag type (h1, h2, etc.)
350 'text' => $heading_text # Store the heading text
351 ];
352
353 # Check if the post title is present in any heading
354 if (stripos($heading_text, $post_title) !== false) {
355 $title_in_headings = true;
356 }
357 } elseif ($element->nodeName === 'img') {
358 # Process image tags
359 $img_src = $element->getAttribute("src"); # Get the image source URL
360 $image_tags[] = $img_src; # Store the image URL
361
362 # Check if the featured image is already present in content
363 if (!empty($featured_image_url) && stripos($img_src, $featured_image_url) !== false) {
364 $image_in_content = true;
365 }
366 }
367 }
368
369
370 # If the post title is found title_in_headings to true
371 if ($title_in_headings) {
372
373
374 # Update Metasync data to indicate the title is already there
375 $metasyncData['general']['title_in_headings'][$post_type] = true;
376 } else {
377
378 # Mark that the title was not there.
379 $metasyncData['general']['title_in_headings'][$post_type] = false;
380 }
381
382 # If the featured image is found image_in_content to true or make it true if there is no feature image
383 if ($image_in_content || empty($featured_image_url)) {
384
385 # Update Metasync data to indicate the image is already there
386 $metasyncData['general']['image_in_content'][$post_type] = true;
387 } else {
388
389 # Mark that the image was not there.
390 $metasyncData['general']['image_in_content'][$post_type] = false;
391 }
392
393 # Update the timestamp
394 $metasyncData['general']['last_run_time'] = time();
395
396 # Save the updated Metasync options
397 Metasync::set_option($metasyncData);
398
399 # Return the content that needs to be prepended
400 return [
401 'content' => $prepend_content
402 ];
403 }
404
405 /**
406 * Fetch the hidden test post for content detection.
407 *
408 * @param string $post_url Hidden test post URL.
409 * @return array{body: string|false, status: int}
410 */
411 protected function fetch_post_html($post_url)
412 {
413 $ch = curl_init();
414 curl_setopt($ch, CURLOPT_URL, $post_url); # Set the URL to fetch
415 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); # Return the response as a string
416 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); # Follow redirects if any
417 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); # Bound connection establishment
418 curl_setopt($ch, CURLOPT_TIMEOUT, 15); # Bound the complete request
419 curl_setopt($ch, CURLOPT_MAXREDIRS, 5); # Bound redirect chains
420 $html = curl_exec($ch); # Execute the request and store the HTML response
421 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); # Capture the HTTP status code
422 curl_close($ch); # Close the cURL session
423
424 return [
425 'body' => $html,
426 'status' => (int) $http_code,
427 ];
428 }
429
430 /**
431 * Change post type to a post type to hide it from admin
432 */
433 private function show_post_completely($post_id)
434 {
435 global $wpdb;
436
437 # Single direct DB write for both type + status — bypasses
438 # transition_post_status so auto-share plugins never see this post
439 $wpdb->update(
440 $wpdb->posts,
441 [
442 'post_type' => 'post',
443 'post_status' => 'publish',
444 ],
445 ['ID' => $post_id]
446 );
447 clean_post_cache($post_id);
448 }
449 /**
450 * Change post type to a non-existent type to hide it from admin
451 */
452 private function hide_post_completely($post_id)
453 {
454 global $wpdb;
455
456 # Single direct DB write — bypasses transition_post_status on the
457 # publish→draft transition (some plugins hook that direction too)
458 $wpdb->update(
459 $wpdb->posts,
460 [
461 'post_status' => 'draft',
462 'post_type' => 'metasync_post_type',
463 ],
464 ['ID' => $post_id]
465 );
466 clean_post_cache($post_id);
467 }
468
469 /**
470 * Prevent deletion of the hidden post
471 */
472 public function prevent_post_deletion($post_id)
473 {
474 global $wpdb;
475 $post = get_post($post_id);
476
477 // Check if post exists before accessing its properties
478 if ($post && $post->post_type === $this->hidden_post_type) {
479 } // wp_die('This post cannot be deleted.'); # Block deletion
480
481 }
482 }
483