PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.5.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.5.23
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-opengraph.php

class-metasync-opengraph.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.5.23, at includes/class-metasync-opengraph.php

1,085 lines 43.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The Open Graph functionality of the plugin.
5 *
6 * @package MetaSync
7 * @subpackage MetaSync/includes
8 * @since 1.0.0
9 */
10
11 # Prevent direct access
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 /**
17 * Open Graph Tags Generator Class
18 *
19 * This class handles the generation and management of Open Graph and Twitter Card tags
20 * for WordPress posts and pages.
21 */
22 class Metasync_OpenGraph {
23
24 /**
25 * The ID of this plugin.
26 */
27 private $plugin_name;
28
29 /**
30 * The version of this plugin.
31 */
32 private $version;
33
34 /**
35 * Meta box ID
36 */
37 const META_BOX_ID = 'metasync_opengraph_meta_box';
38
39 /**
40 * Supported post types
41 */
42 private $supported_post_types = ['post', 'page'];
43
44 /**
45 * Initialize the class and set its properties.
46 */
47 public function __construct($plugin_name, $version) {
48 $this->plugin_name = $plugin_name;
49 $this->version = $version;
50 }
51
52 /**
53 * Register all hooks for this class
54 */
55 public function init() {
56 # Admin hooks
57 add_action('add_meta_boxes', [$this, 'add_meta_box']);
58 add_action('save_post', [$this, 'save_meta_box_data']);
59 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
60
61 # Alternative script loading for post edit screens
62 add_action('admin_print_scripts-post.php', [$this, 'force_enqueue_scripts']);
63 add_action('admin_print_scripts-post-new.php', [$this, 'force_enqueue_scripts']);
64
65 # Frontend hooks
66 add_action('wp_head', [$this, 'output_opengraph_tags'], 5);
67
68 # Update OpenGraph URL when post is published/updated
69 add_action('save_post', [$this, 'update_opengraph_url'], 20);
70
71 # Update OpenGraph URL when post permalink changes
72 add_action('post_updated', [$this, 'check_permalink_change'], 10, 3);
73
74 # Also check on transition_post_status for status changes
75 add_action('transition_post_status', [$this, 'check_status_change'], 10, 3);
76
77 # Check when post slug is updated via edit slug functionality
78 add_action('wp_ajax_sample-permalink', [$this, 'check_slug_change'], 5);
79
80 # AJAX hooks for preview
81 add_action('wp_ajax_metasync_og_preview', [$this, 'ajax_generate_preview']);
82 add_action('wp_ajax_nopriv_metasync_og_preview', [$this, 'ajax_generate_preview']);
83 }
84
85 /**
86 * Add the Open Graph meta box to post and page editors
87 */
88 public function add_meta_box() {
89 # Don't show meta box if user's role doesn't have plugin access
90 if (!Metasync::current_user_has_plugin_access()) {
91 return;
92 }
93
94 # Check if user has permission to edit posts
95 if (!current_user_can('edit_posts')) {
96 return;
97 }
98
99 # Meta title and description are always enabled by default
100 $general_settings = Metasync::get_option('general', []);
101
102 # Check if Social Media & Open Graph meta box is disabled
103 if (!empty($general_settings['disable_social_opengraph_metabox'])) {
104 return;
105 }
106
107 # Get supported post types (allow filtering)
108 $post_types = $this->get_supported_post_types();
109 $plugin_name = Metasync::get_effective_plugin_name();
110
111 foreach ($post_types as $post_type) {
112 add_meta_box(
113 self::META_BOX_ID,
114 sprintf(esc_html__('Social Media & Open Graph by %s', 'metasync'), $plugin_name),
115 [$this, 'render_meta_box'],
116 $post_type,
117 'normal',
118 'high'
119 );
120 }
121 }
122
123 /**
124 * Render the meta box content
125 */
126 public function render_meta_box($post) {
127 # Add nonce for security
128 wp_nonce_field('metasync_opengraph_nonce', 'metasync_opengraph_nonce');
129
130 # Get existing values
131 $og_enabled = get_post_meta($post->ID, '_metasync_og_enabled', true);
132 $og_title = get_post_meta($post->ID, '_metasync_og_title', true);
133 $og_description = get_post_meta($post->ID, '_metasync_og_description', true);
134 $og_image = get_post_meta($post->ID, '_metasync_og_image', true);
135 $og_url = get_post_meta($post->ID, '_metasync_og_url', true);
136 $og_type = get_post_meta($post->ID, '_metasync_og_type', true);
137
138 # Twitter Card fields
139 $twitter_card = get_post_meta($post->ID, '_metasync_twitter_card', true);
140 $twitter_site = get_post_meta($post->ID, '_metasync_twitter_site', true);
141 $twitter_title = get_post_meta($post->ID, '_metasync_twitter_title', true);
142 $twitter_description = get_post_meta($post->ID, '_metasync_twitter_description', true);
143 $twitter_image = get_post_meta($post->ID, '_metasync_twitter_image', true);
144 $twitter_image_alt = get_post_meta($post->ID, '_metasync_twitter_image_alt', true);
145
146 # Twitter App Card fields
147 $twitter_app_id_iphone = get_post_meta($post->ID, '_metasync_twitter_app_id_iphone', true);
148 $twitter_app_id_ipad = get_post_meta($post->ID, '_metasync_twitter_app_id_ipad', true);
149 $twitter_app_id_googleplay = get_post_meta($post->ID, '_metasync_twitter_app_id_googleplay', true);
150 $twitter_app_url_iphone = get_post_meta($post->ID, '_metasync_twitter_app_url_iphone', true);
151 $twitter_app_url_ipad = get_post_meta($post->ID, '_metasync_twitter_app_url_ipad', true);
152 $twitter_app_url_googleplay = get_post_meta($post->ID, '_metasync_twitter_app_url_googleplay', true);
153 $twitter_app_country = get_post_meta($post->ID, '_metasync_twitter_app_country', true);
154
155 # Twitter Player Card fields
156 $twitter_player = get_post_meta($post->ID, '_metasync_twitter_player', true);
157 $twitter_player_width = get_post_meta($post->ID, '_metasync_twitter_player_width', true);
158 $twitter_player_height = get_post_meta($post->ID, '_metasync_twitter_player_height', true);
159
160 # Set default values
161 # Note: Check for empty string specifically, not just empty(), since '0' is a valid value
162 if ($og_enabled === '') {
163 # For new posts, default to enabled
164 $og_enabled = '1';
165 }
166 if (empty($og_title)) {
167 $og_title = $post->post_title;
168 }
169 if (empty($og_description)) {
170 $og_description = $this->get_post_excerpt($post);
171 }
172 if (empty($og_url)) {
173 $og_url = $this->get_canonical_url($post);
174 }
175 if (empty($og_type)) {
176 $og_type = 'article';
177 }
178 if (empty($og_image)) {
179 $og_image = $this->get_featured_image_url($post->ID);
180 }
181
182 # Twitter defaults
183 if (empty($twitter_card)) {
184 $twitter_card = 'summary_large_image';
185 }
186 if (empty($twitter_title)) {
187 $twitter_title = $og_title;
188 }
189 if (empty($twitter_description)) {
190 $twitter_description = $og_description;
191 }
192 if (empty($twitter_image)) {
193 $twitter_image = $og_image;
194 }
195
196 # Include the meta box template
197 include plugin_dir_path(__FILE__) . '../admin/partials/metasync-opengraph-meta-box.php';
198 }
199
200 /**
201 * Save meta box data
202 */
203 public function save_meta_box_data($post_id) {
204 # Check if nonce is valid
205 if (!isset($_POST['metasync_opengraph_nonce']) ||
206 !wp_verify_nonce($_POST['metasync_opengraph_nonce'], 'metasync_opengraph_nonce')) {
207 return;
208 }
209
210 # Check if user has permission to edit
211 if (!current_user_can('edit_post', $post_id)) {
212 return;
213 }
214
215 # Check if this is an autosave
216 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
217 return;
218 }
219
220 # Handle the checkbox field separately (unchecked checkboxes don't send POST data)
221 # Meta title and description are always enabled by default
222 if (isset($_POST['_metasync_og_enabled'])) {
223 # User checked the box
224 update_post_meta($post_id, '_metasync_og_enabled', '1');
225 } else {
226 # User unchecked the box
227 update_post_meta($post_id, '_metasync_og_enabled', '0');
228 }
229
230 # Save Open Graph data (excluding the enabled field which is handled above)
231 $og_fields = [
232 '_metasync_og_title' => 'sanitize_text_field',
233 '_metasync_og_description' => 'sanitize_textarea_field',
234 '_metasync_og_image' => 'esc_url_raw',
235 '_metasync_og_url' => 'esc_url_raw',
236 '_metasync_og_type' => 'sanitize_text_field',
237 ];
238
239 # Save Twitter Card data
240 $twitter_fields = [
241 '_metasync_twitter_card' => 'sanitize_text_field',
242 '_metasync_twitter_site' => 'sanitize_text_field',
243 '_metasync_twitter_title' => 'sanitize_text_field',
244 '_metasync_twitter_description' => 'sanitize_textarea_field',
245 '_metasync_twitter_image' => 'esc_url_raw',
246 '_metasync_twitter_image_alt' => 'sanitize_text_field',
247 ];
248
249 # Save Twitter App Card data
250 $twitter_app_fields = [
251 '_metasync_twitter_app_id_iphone' => 'sanitize_text_field',
252 '_metasync_twitter_app_id_ipad' => 'sanitize_text_field',
253 '_metasync_twitter_app_id_googleplay' => 'sanitize_text_field',
254 '_metasync_twitter_app_url_iphone' => 'esc_url_raw',
255 '_metasync_twitter_app_url_ipad' => 'esc_url_raw',
256 '_metasync_twitter_app_url_googleplay' => 'esc_url_raw',
257 '_metasync_twitter_app_country' => 'sanitize_text_field',
258 ];
259
260 # Save Twitter Player Card data
261 $twitter_player_fields = [
262 '_metasync_twitter_player' => 'esc_url_raw',
263 '_metasync_twitter_player_width' => 'absint',
264 '_metasync_twitter_player_height' => 'absint',
265 ];
266
267 $all_fields = array_merge($og_fields, $twitter_fields, $twitter_app_fields, $twitter_player_fields);
268
269 foreach ($all_fields as $field => $sanitize_callback) {
270 if (isset($_POST[$field])) {
271 $value = call_user_func($sanitize_callback, $_POST[$field]);
272 update_post_meta($post_id, $field, $value);
273 }
274 }
275 }
276
277 /**
278 * Enqueue admin scripts and styles
279 */
280 public function enqueue_admin_scripts($hook) {
281 global $post_type;
282
283 # Only load on post edit screens for supported post types
284 if (!in_array($hook, ['post.php', 'post-new.php']) ||
285 !in_array($post_type, $this->supported_post_types)) {
286 return;
287 }
288
289 wp_enqueue_media();
290
291 wp_enqueue_script(
292 'metasync-opengraph-admin',
293 plugin_dir_url(__FILE__) . '../admin/js/metasync-opengraph.js',
294 ['jquery', 'wp-util'],
295 $this->version,
296 true
297 );
298
299 wp_enqueue_style(
300 'metasync-opengraph-admin',
301 plugin_dir_url(__FILE__) . '../admin/css/metasync-opengraph.css',
302 [],
303 $this->version
304 );
305
306 # Get the current post permalink for preview
307 global $post;
308 $current_permalink = '';
309 if ($post && $post->ID) {
310 $current_permalink = $this->get_canonical_url($post);
311 }
312
313 # Localize script for AJAX
314 wp_localize_script('metasync-opengraph-admin', 'metasync_og', [
315 'ajax_url' => admin_url('admin-ajax.php'),
316 'nonce' => wp_create_nonce('metasync_og_preview_nonce'),
317 'current_permalink' => $current_permalink,
318 'strings' => [
319 'select_image' => esc_html__('Select Image', 'metasync'),
320 'use_image' => esc_html__('Use This Image', 'metasync'),
321 'remove_image' => esc_html__('Remove Image', 'metasync'),
322 ]
323 ]);
324 }
325
326 /**
327 * Force enqueue scripts for post edit screens (backup method)
328 */
329 public function force_enqueue_scripts() {
330 global $post_type;
331
332 if (!in_array($post_type, $this->supported_post_types)) {
333 return;
334 }
335
336 # Check if already enqueued
337 if (wp_script_is('metasync-opengraph-admin', 'enqueued')) {
338 return;
339 }
340
341 wp_enqueue_media();
342 wp_enqueue_script(
343 'metasync-opengraph-admin',
344 plugin_dir_url(__FILE__) . '../admin/js/metasync-opengraph.js',
345 ['jquery', 'wp-util'],
346 $this->version,
347 true
348 );
349
350 wp_enqueue_style(
351 'metasync-opengraph-admin',
352 plugin_dir_url(__FILE__) . '../admin/css/metasync-opengraph.css',
353 [],
354 $this->version
355 );
356
357 # Get the current post permalink for preview
358 global $post;
359 $current_permalink = '';
360 if ($post && $post->ID) {
361 $current_permalink = $this->get_canonical_url($post);
362 }
363
364 wp_localize_script('metasync-opengraph-admin', 'metasync_og', [
365 'ajax_url' => admin_url('admin-ajax.php'),
366 'nonce' => wp_create_nonce('metasync_og_preview_nonce'),
367 'current_permalink' => $current_permalink,
368 'strings' => [
369 'select_image' => esc_html__('Select Image', 'metasync'),
370 'use_image' => esc_html__('Use This Image', 'metasync'),
371 'remove_image' => esc_html__('Remove Image', 'metasync'),
372 ]
373 ]);
374 }
375
376 /**
377 * Output Open Graph and Twitter Card tags in wp_head
378 */
379 public function output_opengraph_tags() {
380 if (!is_singular($this->supported_post_types)) {
381 return;
382 }
383
384 global $post;
385
386 # Ensure post is a valid object
387 if (!$post instanceof WP_Post) {
388 return;
389 }
390
391 # Check if Open Graph is enabled for this post
392 $og_enabled = get_post_meta($post->ID, '_metasync_og_enabled', true);
393 if (empty($og_enabled) || $og_enabled !== '1') {
394 return;
395 }
396
397 # Check for conflicts with other SEO plugins (allow override via filter)
398 if (apply_filters('metasync_opengraph_check_conflicts', true) && $this->has_seo_plugin_conflicts()) {
399 return;
400 }
401
402 # Get Open Graph data
403 $og_title = get_post_meta($post->ID, '_metasync_og_title', true) ?: $post->post_title;
404 $og_description = get_post_meta($post->ID, '_metasync_og_description', true) ?: $this->get_post_excerpt($post);
405 $og_image = get_post_meta($post->ID, '_metasync_og_image', true) ?: $this->get_featured_image_url($post->ID);
406 $og_url = get_post_meta($post->ID, '_metasync_og_url', true) ?: $this->get_canonical_url($post);
407 $og_type = get_post_meta($post->ID, '_metasync_og_type', true) ?: 'article';
408
409 # Get Twitter Card data
410 $twitter_card = get_post_meta($post->ID, '_metasync_twitter_card', true) ?: 'summary_large_image';
411 $twitter_site = get_post_meta($post->ID, '_metasync_twitter_site', true);
412 $twitter_title = get_post_meta($post->ID, '_metasync_twitter_title', true) ?: $og_title;
413 $twitter_description = get_post_meta($post->ID, '_metasync_twitter_description', true) ?: $og_description;
414 $twitter_image = get_post_meta($post->ID, '_metasync_twitter_image', true) ?: $og_image;
415 $twitter_image_alt = get_post_meta($post->ID, '_metasync_twitter_image_alt', true);
416
417 # Get Twitter App Card data
418 $twitter_app_id_iphone = get_post_meta($post->ID, '_metasync_twitter_app_id_iphone', true);
419 $twitter_app_id_ipad = get_post_meta($post->ID, '_metasync_twitter_app_id_ipad', true);
420 $twitter_app_id_googleplay = get_post_meta($post->ID, '_metasync_twitter_app_id_googleplay', true);
421 $twitter_app_url_iphone = get_post_meta($post->ID, '_metasync_twitter_app_url_iphone', true);
422 $twitter_app_url_ipad = get_post_meta($post->ID, '_metasync_twitter_app_url_ipad', true);
423 $twitter_app_url_googleplay = get_post_meta($post->ID, '_metasync_twitter_app_url_googleplay', true);
424 $twitter_app_country = get_post_meta($post->ID, '_metasync_twitter_app_country', true);
425
426 # Get Twitter Player Card data
427 $twitter_player = get_post_meta($post->ID, '_metasync_twitter_player', true);
428 $twitter_player_width = get_post_meta($post->ID, '_metasync_twitter_player_width', true);
429 $twitter_player_height = get_post_meta($post->ID, '_metasync_twitter_player_height', true);
430
431 # Output Open Graph tags
432 echo "\n<!-- MetaSync Open Graph Tags -->\n";
433 if ($og_title) {
434 echo '<meta property="og:title" content="' . esc_attr($og_title) . '">' . "\n";
435 }
436 if ($og_description) {
437 echo '<meta property="og:description" content="' . esc_attr($og_description) . '">' . "\n";
438 }
439 if ($og_image) {
440 echo '<meta property="og:image" content="' . esc_url($og_image) . '">' . "\n";
441 }
442 if ($og_url) {
443 echo '<meta property="og:url" content="' . esc_url($og_url) . '">' . "\n";
444 }
445 if ($og_type) {
446 echo '<meta property="og:type" content="' . esc_attr($og_type) . '">' . "\n";
447 }
448
449 # Output Twitter Card tags
450 echo "<!-- MetaSync Twitter Card Tags -->\n";
451 if ($twitter_card) {
452 echo '<meta name="twitter:card" content="' . esc_attr($twitter_card) . '">' . "\n";
453 }
454 if ($twitter_site) {
455 echo '<meta name="twitter:site" content="' . esc_attr($twitter_site) . '">' . "\n";
456 }
457 if ($twitter_title) {
458 echo '<meta name="twitter:title" content="' . esc_attr($twitter_title) . '">' . "\n";
459 }
460 if ($twitter_description) {
461 echo '<meta name="twitter:description" content="' . esc_attr($twitter_description) . '">' . "\n";
462 }
463 if ($twitter_image) {
464 echo '<meta name="twitter:image" content="' . esc_url($twitter_image) . '">' . "\n";
465 }
466 if ($twitter_image_alt) {
467 echo '<meta name="twitter:image:alt" content="' . esc_attr($twitter_image_alt) . '">' . "\n";
468 }
469
470 # Output Twitter App Card tags (only if card type is 'app')
471 if ($twitter_card === 'app') {
472 if ($twitter_app_id_iphone) {
473 echo '<meta name="twitter:app:id:iphone" content="' . esc_attr($twitter_app_id_iphone) . '">' . "\n";
474 }
475 if ($twitter_app_id_ipad) {
476 echo '<meta name="twitter:app:id:ipad" content="' . esc_attr($twitter_app_id_ipad) . '">' . "\n";
477 }
478 if ($twitter_app_id_googleplay) {
479 echo '<meta name="twitter:app:id:googleplay" content="' . esc_attr($twitter_app_id_googleplay) . '">' . "\n";
480 }
481 if ($twitter_app_url_iphone) {
482 echo '<meta name="twitter:app:url:iphone" content="' . esc_url($twitter_app_url_iphone) . '">' . "\n";
483 }
484 if ($twitter_app_url_ipad) {
485 echo '<meta name="twitter:app:url:ipad" content="' . esc_url($twitter_app_url_ipad) . '">' . "\n";
486 }
487 if ($twitter_app_url_googleplay) {
488 echo '<meta name="twitter:app:url:googleplay" content="' . esc_url($twitter_app_url_googleplay) . '">' . "\n";
489 }
490 if ($twitter_app_country) {
491 echo '<meta name="twitter:app:country" content="' . esc_attr($twitter_app_country) . '">' . "\n";
492 }
493 }
494
495 # Output Twitter Player Card tags (only if card type is 'player')
496 if ($twitter_card === 'player') {
497 if ($twitter_player) {
498 echo '<meta name="twitter:player" content="' . esc_url($twitter_player) . '">' . "\n";
499 }
500 if ($twitter_player_width) {
501 echo '<meta name="twitter:player:width" content="' . esc_attr($twitter_player_width) . '">' . "\n";
502 }
503 if ($twitter_player_height) {
504 echo '<meta name="twitter:player:height" content="' . esc_attr($twitter_player_height) . '">' . "\n";
505 }
506 }
507
508 echo "<!-- End MetaSync Social Media Tags -->\n\n";
509 }
510
511 /**
512 * AJAX handler for generating social media preview
513 */
514 public function ajax_generate_preview() {
515
516 try {
517 # Check nonce
518 if (!check_ajax_referer('metasync_og_preview_nonce', 'nonce', false)) {
519 wp_send_json_error(['message' => 'Security check failed']);
520 return;
521 }
522
523 # Get and sanitize data
524 $title = sanitize_text_field($_POST['title'] ?? '');
525 $description = sanitize_textarea_field($_POST['description'] ?? '');
526 $image = esc_url_raw($_POST['image'] ?? '');
527 $url = esc_url_raw($_POST['url'] ?? '');
528
529 # Get Twitter Card data
530 $twitter_title = sanitize_text_field($_POST['twitter_title'] ?? '');
531 $twitter_description = sanitize_textarea_field($_POST['twitter_description'] ?? '');
532 $twitter_image = esc_url_raw($_POST['twitter_image'] ?? '');
533
534 # Generate preview HTML
535 $preview_html = $this->generate_preview_html($title, $description, $image, $url, $twitter_title, $twitter_description, $twitter_image);
536
537 if (empty($preview_html)) {
538 wp_send_json_error(['message' => 'Failed to generate preview HTML']);
539 return;
540 }
541
542 wp_send_json_success(['preview' => $preview_html]);
543
544 } catch (Exception $e) {
545 wp_send_json_error(['message' => 'Server error: ' . $e->getMessage()]);
546 }
547 }
548
549 /**
550 * Generate HTML for social media preview
551 */
552 private function generate_preview_html($title, $description, $image, $url, $twitter_title = '', $twitter_description = '', $twitter_image = '') {
553 # Parse domain from URL
554 $domain = '';
555 if (!empty($url)) {
556 $parsed = parse_url($url);
557 $domain = $parsed['host'] ?? '';
558 }
559
560 # Fallback to site URL if no domain found
561 if (empty($domain)) {
562 $site_url = get_site_url();
563 $parsed = parse_url($site_url);
564 $domain = $parsed['host'] ?? 'your-site.com';
565 }
566
567 # Provide fallbacks for empty values
568 if (empty($title)) {
569 $title = 'Your Post Title';
570 }
571 if (empty($description)) {
572 $description = 'Your post description will appear here when shared on social media platforms.';
573 }
574
575 # Use Twitter Card data for Twitter preview, fallback to Open Graph
576 $twitter_display_title = !empty($twitter_title) ? $twitter_title : $title;
577 $twitter_display_description = !empty($twitter_description) ? $twitter_description : $description;
578 $twitter_display_image = !empty($twitter_image) ? $twitter_image : $image;
579
580 # Get site name for avatars
581 $site_name = get_bloginfo('name') ?: 'Your Site';
582 $site_initial = strtoupper(substr($site_name, 0, 1));
583
584 ob_start();
585 ?>
586 <div class="metasync-preview-tabs">
587 <button class="metasync-preview-tab facebook active" data-platform="facebook">
588 Facebook
589 </button>
590 <button class="metasync-preview-tab twitter" data-platform="twitter">
591 Twitter/X
592 </button>
593 <button class="metasync-preview-tab linkedin" data-platform="linkedin">
594 LinkedIn
595 </button>
596 </div>
597
598 <div class="metasync-preview-content">
599 <!-- Facebook Preview -->
600 <div class="metasync-preview-panel facebook active" data-platform="facebook">
601 <div class="facebook-preview">
602 <div class="facebook-post-header">
603 <div class="facebook-avatar"><?php echo esc_html($site_initial); ?></div>
604 <div class="facebook-post-info">
605 <h4><?php echo esc_html($site_name); ?></h4>
606 <p>2 hours ago 🌍</p>
607 </div>
608 </div>
609 <div class="facebook-link-preview">
610 <?php if (!empty($image)): ?>
611 <div class="facebook-preview-image">
612 <img src="<?php echo esc_url($image); ?>" alt="<?php echo esc_attr($title); ?>" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
613 <div class="preview-placeholder" style="display: none;">
614 <span>📷</span>
615 <p>Image failed to load</p>
616 </div>
617 </div>
618 <?php else: ?>
619 <div class="facebook-preview-image preview-no-image">
620 <div class="preview-placeholder">
621 <span>📷</span>
622 <p>No image selected</p>
623 </div>
624 </div>
625 <?php endif; ?>
626 <div class="facebook-preview-content">
627 <div class="facebook-preview-domain"><?php echo esc_html(strtoupper($domain)); ?></div>
628 <div class="facebook-preview-title"><?php echo esc_html($title); ?></div>
629 <div class="facebook-preview-description"><?php echo esc_html($description); ?></div>
630 </div>
631 </div>
632 </div>
633 </div>
634
635 <!-- Twitter Preview -->
636 <div class="metasync-preview-panel twitter" data-platform="twitter">
637 <div class="twitter-preview">
638 <div class="twitter-post-header">
639 <div class="twitter-avatar"><?php echo esc_html($site_initial); ?></div>
640 <div class="twitter-user-info">
641 <h4><?php echo esc_html($site_name); ?></h4>
642 <p>@<?php echo esc_html(strtolower(str_replace(' ', '', $site_name ?? ''))); ?> 2h</p>
643 </div>
644 </div>
645 <div class="twitter-post-text">
646 Check out this amazing content! 🚀
647 </div>
648 <div class="twitter-card">
649 <?php if (!empty($twitter_display_image)): ?>
650 <div class="twitter-card-image">
651 <img src="<?php echo esc_url($twitter_display_image); ?>" alt="<?php echo esc_attr($twitter_display_title); ?>" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
652 <div class="preview-placeholder" style="display: none;">
653 <span>📷</span>
654 <p>Image failed to load</p>
655 </div>
656 </div>
657 <?php else: ?>
658 <div class="twitter-card-image preview-no-image">
659 <div class="preview-placeholder">
660 <span>📷</span>
661 <p>No image selected</p>
662 </div>
663 </div>
664 <?php endif; ?>
665 <div class="twitter-card-content">
666 <div class="twitter-card-domain"><?php echo esc_html($domain); ?></div>
667 <div class="twitter-card-title"><?php echo esc_html($twitter_display_title); ?></div>
668 <div class="twitter-card-description"><?php echo esc_html($twitter_display_description); ?></div>
669 </div>
670 </div>
671 </div>
672 </div>
673
674 <!-- LinkedIn Preview -->
675 <div class="metasync-preview-panel linkedin" data-platform="linkedin">
676 <div class="linkedin-preview">
677 <div class="linkedin-post-header">
678 <div class="linkedin-avatar"><?php echo esc_html($site_initial); ?></div>
679 <div class="linkedin-user-info">
680 <h4><?php echo esc_html($site_name); ?></h4>
681 <p>2 hours ago</p>
682 </div>
683 </div>
684 <div class="linkedin-link-preview">
685 <?php if (!empty($image)): ?>
686 <div class="linkedin-preview-image">
687 <img src="<?php echo esc_url($image); ?>" alt="<?php echo esc_attr($title); ?>" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
688 <div class="preview-placeholder" style="display: none;">
689 <span>📷</span>
690 <p>Image failed to load</p>
691 </div>
692 </div>
693 <?php else: ?>
694 <div class="linkedin-preview-image preview-no-image">
695 <div class="preview-placeholder">
696 <span>📷</span>
697 <p>No image selected</p>
698 </div>
699 </div>
700 <?php endif; ?>
701 <div class="linkedin-preview-content">
702 <div class="linkedin-preview-title"><?php echo esc_html($title); ?></div>
703 <div class="linkedin-preview-description"><?php echo esc_html($description); ?></div>
704 <div class="linkedin-preview-domain"><?php echo esc_html($domain); ?></div>
705 </div>
706 </div>
707 </div>
708 </div>
709 </div>
710 <?php
711 return ob_get_clean();
712 }
713
714 /**
715 * Get post excerpt for Open Graph description
716 */
717 private function get_post_excerpt($post) {
718 if (!empty($post->post_excerpt)) {
719 return $post->post_excerpt;
720 }
721
722 # Generate excerpt from content
723 $content = $post->post_content;
724
725 # Process shortcodes to get the actual rendered content (what users see on frontend)
726 # This converts page builder shortcodes into their actual HTML output
727 $content = do_shortcode($content);
728
729 # Apply WordPress content filters (same filters used on the frontend)
730 # This ensures we get the exact same content as displayed on the page
731 $content = apply_filters('the_content', $content);
732
733 # Remove HTML tags to get clean text
734 $content = wp_strip_all_tags($content);
735
736 # Remove extra whitespace, line breaks, and special characters
737 $content = preg_replace('/\s+/', ' ', $content);
738 $content = trim($content);
739
740 # If content is still empty or too short, fallback to post title
741 if (empty($content) || strlen($content) < 20) {
742 $content = $post->post_title;
743 }
744
745 # Generate excerpt
746 $excerpt = wp_trim_words($content, 30, '...');
747
748 return $excerpt;
749 }
750
751 /**
752 * Get featured image URL
753 */
754 private function get_featured_image_url($post_id) {
755 $thumbnail_id = get_post_thumbnail_id($post_id);
756 if ($thumbnail_id) {
757 $image_url = wp_get_attachment_image_url($thumbnail_id, 'large');
758 return $image_url;
759 }
760 return '';
761 }
762
763 /**
764 * Get supported post types
765 */
766 public function get_supported_post_types() {
767 return apply_filters('metasync_opengraph_post_types', $this->supported_post_types);
768 }
769
770 /**
771 * Add debug menu for testing
772 */
773 private function has_seo_plugin_conflicts() {
774 # List of SEO plugins that might output Open Graph tags
775 $seo_plugins = [
776 'wordpress-seo/wp-seo.php', # Yoast SEO
777 'seo-by-rank-math/rank-math.php', # RankMath
778 'all-in-one-seo-pack/all_in_one_seo_pack.php', # AIOSEO
779 'seopress/seopress.php', # SEOPress
780 'the-seo-framework/autodescription.php', # The SEO Framework
781 ];
782
783 foreach ($seo_plugins as $plugin) {
784 if (is_plugin_active($plugin)) {
785 return true;
786 }
787 }
788
789 return false;
790 }
791
792 /**
793 * Check if a specific SEO plugin is handling Open Graph for current post
794 */
795 private function seo_plugin_has_og_data($post_id) {
796 # Check if Yoast SEO has Open Graph data
797 if (is_plugin_active('wordpress-seo/wp-seo.php')) {
798 $yoast_title = get_post_meta($post_id, '_yoast_wpseo_title', true);
799 $yoast_desc = get_post_meta($post_id, '_yoast_wpseo_metadesc', true);
800 if (!empty($yoast_title) || !empty($yoast_desc)) {
801 return true;
802 }
803 }
804
805 # Check if RankMath has Open Graph data
806 if (is_plugin_active('seo-by-rank-math/rank-math.php') || is_plugin_active('seo-by-rankmath/rank-math.php')) {
807 $rm_title = get_post_meta($post_id, 'rank_math_title', true);
808 $rm_desc = get_post_meta($post_id, 'rank_math_description', true);
809 if (!empty($rm_title) || !empty($rm_desc)) {
810 return true;
811 }
812 }
813
814 return false;
815 }
816
817 /**
818 * Get the canonical URL for a post
819 */
820 public function get_canonical_url($post) {
821 # Try to get the permalink using WordPress function
822 $permalink = get_permalink($post->ID);
823
824 # If permalink is not available or is the default query URL, try alternative methods
825 if (!$permalink || strpos($permalink, '?p=') !== false || strpos($permalink, '?page_id=') !== false) {
826 # Force WordPress to generate the proper permalink by temporarily setting post status
827 $original_status = $post->post_status;
828 if ($post->post_status === 'auto-draft') {
829 $post->post_status = 'publish';
830 }
831
832 # Try get_permalink again with the updated status
833 $permalink = get_permalink($post->ID);
834
835 # Restore original status
836 $post->post_status = $original_status;
837 }
838
839 # If still not working, use WordPress core functions to build proper permalink
840 if (!$permalink || strpos($permalink, '?p=') !== false || strpos($permalink, '?page_id=') !== false) {
841 # Use WordPress core function that respects permalink structure
842 # This properly handles custom structures, hierarchies, and post types
843 # Load admin function if not already available
844 if (!function_exists('get_sample_permalink')) {
845 require_once ABSPATH . 'wp-admin/includes/post.php';
846 }
847 $permalink = get_sample_permalink($post->ID);
848
849 if (is_array($permalink)) {
850 # get_sample_permalink returns array with template and slug
851 # Replace %postname% or %pagename% with actual slug
852 $permalink = str_replace(
853 array('%pagename%', '%postname%'),
854 $post->post_name,
855 $permalink[0]
856 );
857 }
858
859 # Final fallback: if still problematic, construct URL respecting post type structure
860 if (!$permalink || strpos($permalink, '?p=') !== false || strpos($permalink, '?page_id=') !== false) {
861 if (!empty($post->post_name)) {
862 # For pages, check if there's a parent hierarchy
863 if ($post->post_type === 'page' && $post->post_parent) {
864 # Get parent page path for proper hierarchy
865 $parent = get_post($post->post_parent);
866 $parent_path = '';
867
868 # Build full path including all parent pages
869 while ($parent) {
870 $parent_path = $parent->post_name . '/' . $parent_path;
871 $parent = $parent->post_parent ? get_post($parent->post_parent) : null;
872 }
873
874 $permalink = home_url('/' . $parent_path . $post->post_name . '/');
875 } else {
876 # For posts and pages without parents, use post type archive base
877 $post_type_obj = get_post_type_object($post->post_type);
878 $slug = $post_type_obj->rewrite['slug'] ?? '';
879
880 if ($slug && $post->post_type !== 'page') {
881 $permalink = home_url('/' . $slug . '/' . $post->post_name . '/');
882 } else {
883 $permalink = home_url('/' . $post->post_name . '/');
884 }
885 }
886 } else {
887 # Fallback to post ID format if no slug available
888 $permalink = home_url('/?p=' . $post->ID);
889 }
890 }
891 }
892
893 return $permalink;
894 }
895
896 /**
897 * Update OpenGraph URL when post is saved
898 */
899 public function update_opengraph_url($post_id) {
900 # Only update for supported post types
901 if (!in_array(get_post_type($post_id), $this->get_supported_post_types())) {
902 return;
903 }
904
905 # Skip autosaves and revisions
906 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
907 return;
908 }
909
910 if (wp_is_post_revision($post_id)) {
911 return;
912 }
913
914 # Get the post object
915 $post = get_post($post_id);
916 if (!$post) {
917 return;
918 }
919
920 # Check if OpenGraph is enabled
921 $og_enabled = get_post_meta($post_id, '_metasync_og_enabled', true);
922 if (empty($og_enabled) || $og_enabled !== '1') {
923 return;
924 }
925
926 # Get the current OpenGraph URL
927 $current_og_url = get_post_meta($post_id, '_metasync_og_url', true);
928
929 # Generate the proper canonical URL
930 $canonical_url = $this->get_canonical_url($post);
931
932 # Update the OpenGraph URL for new posts or if it's empty/incorrect
933 # This ensures the URL is populated after first save (even as draft)
934 if (empty($current_og_url) ||
935 strpos($current_og_url, '?p=') !== false) {
936
937 update_post_meta($post_id, '_metasync_og_url', $canonical_url);
938 }
939 }
940
941 /**
942 * Check if post permalink changed and update og:url if needed
943 */
944 public function check_permalink_change($post_id, $post_after, $post_before) {
945 # Only check for supported post types
946 if (!in_array(get_post_type($post_id), $this->get_supported_post_types())) {
947 return;
948 }
949
950 # Skip autosaves and revisions
951 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
952 return;
953 }
954
955 if (wp_is_post_revision($post_id)) {
956 return;
957 }
958
959 # Check if OpenGraph is enabled
960 $og_enabled = get_post_meta($post_id, '_metasync_og_enabled', true);
961 if (empty($og_enabled) || $og_enabled !== '1') {
962 return;
963 }
964
965 # Get current og:url
966 $current_og_url = get_post_meta($post_id, '_metasync_og_url', true);
967 if (empty($current_og_url)) {
968 return;
969 }
970
971 # Check if the permalink actually changed by comparing post_name (slug)
972 if ($post_before->post_name === $post_after->post_name) {
973 return;
974 }
975
976 # Generate the old and new permalinks
977 $old_permalink = $this->get_canonical_url($post_before);
978 $new_permalink = $this->get_canonical_url($post_after);
979
980 # If permalinks are the same, no need to update
981 if ($old_permalink === $new_permalink) {
982 return;
983 }
984
985 # Check if the current og:url matches the old permalink
986 # This means the og:url was set to the post permalink (not a custom URL)
987 if ($current_og_url === $old_permalink) {
988 # Update og:url to the new permalink
989 update_post_meta($post_id, '_metasync_og_url', $new_permalink);
990 }
991 }
992
993 /**
994 * Check if post status changed and update og:url if needed
995 */
996 public function check_status_change($new_status, $old_status, $post) {
997 # Only check for supported post types
998 if (!in_array(get_post_type($post->ID), $this->get_supported_post_types())) {
999 return;
1000 }
1001
1002 # Skip autosaves and revisions
1003 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
1004 return;
1005 }
1006
1007 if (wp_is_post_revision($post->ID)) {
1008 return;
1009 }
1010
1011 # Only check when transitioning to published status
1012 if ($new_status !== 'publish' || $old_status === 'publish') {
1013 return;
1014 }
1015
1016 # Check if OpenGraph is enabled
1017 $og_enabled = get_post_meta($post->ID, '_metasync_og_enabled', true);
1018 if (empty($og_enabled) || $og_enabled !== '1') {
1019 return;
1020 }
1021
1022 # Get current og:url
1023 $current_og_url = get_post_meta($post->ID, '_metasync_og_url', true);
1024
1025 # Generate the current permalink
1026 $current_permalink = $this->get_canonical_url($post);
1027
1028 # If og:url is empty or matches the old format, update it
1029 if (empty($current_og_url) || strpos($current_og_url, '?p=') !== false) {
1030 update_post_meta($post->ID, '_metasync_og_url', $current_permalink);
1031 }
1032 }
1033
1034 /**
1035 * Check if post slug changed via edit slug functionality
1036 */
1037 public function check_slug_change() {
1038 # Get the post ID from the request
1039 $post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
1040 if (!$post_id) {
1041 return;
1042 }
1043
1044 # Only check for supported post types
1045 if (!in_array(get_post_type($post_id), $this->get_supported_post_types())) {
1046 return;
1047 }
1048
1049 # Check if OpenGraph is enabled
1050 $og_enabled = get_post_meta($post_id, '_metasync_og_enabled', true);
1051 if (empty($og_enabled) || $og_enabled !== '1') {
1052 return;
1053 }
1054
1055 # Get current og:url
1056 $current_og_url = get_post_meta($post_id, '_metasync_og_url', true);
1057 if (empty($current_og_url)) {
1058 return;
1059 }
1060
1061 # Get the post object
1062 $post = get_post($post_id);
1063 if (!$post) {
1064 return;
1065 }
1066
1067 # Generate the current permalink
1068 $current_permalink = $this->get_canonical_url($post);
1069
1070 # Check if the current og:url matches the old permalink format
1071 # This means the og:url was set to the post permalink (not a custom URL)
1072 if ($current_og_url !== $current_permalink && strpos($current_og_url, '?p=') === false) {
1073 # Check if the og:url was the old permalink by comparing with a generated old permalink
1074 $old_post = clone $post;
1075 $old_slug = isset($_POST['new_slug']) ? sanitize_title($_POST['new_slug']) : $post->post_name;
1076
1077 # If the og:url doesn't match the current permalink, it might be the old one
1078 # We'll update it to the new permalink
1079 if ($current_og_url !== $current_permalink) {
1080 update_post_meta($post_id, '_metasync_og_url', $current_permalink);
1081 }
1082 }
1083 }
1084 }
1085