Sync your WordPress posts to standard.site records on your PDS
at main 976 lines 28 kB view raw
1<?php 2declare(strict_types=1); 3/** 4 * Admin functionality for Wireservice. 5 * 6 * @package Wireservice 7 */ 8 9namespace Wireservice; 10 11if (! defined('ABSPATH')) { 12 exit; 13} 14 15class Admin 16{ 17 /** 18 * Constructor. 19 */ 20 public function __construct( 21 private ConnectionsManager $connections_manager, 22 private API $api, 23 private Publication $publication, 24 private Document $document, 25 ) {} 26 27 /** 28 * Initialize admin hooks. 29 * 30 * @return void 31 */ 32 public function init(): void 33 { 34 add_action("admin_menu", [$this, "add_admin_menu"]); 35 add_action("admin_init", [$this, "register_settings"]); 36 add_action("admin_init", [$this, "maybe_adopt_existing_publication"]); 37 add_action("admin_enqueue_scripts", [$this, "enqueue_settings_assets"]); 38 add_action("admin_post_wireservice_sync_publication", [ 39 $this, 40 "handle_sync_publication", 41 ]); 42 add_action("admin_post_wireservice_reset_data", [ 43 $this, 44 "handle_reset_data", 45 ]); 46 add_action("admin_post_wireservice_save_doc_settings", [ 47 $this, 48 "handle_save_doc_settings", 49 ]); 50 add_action("wp_ajax_wireservice_backfill_count", [ 51 $this, 52 "handle_backfill_count", 53 ]); 54 add_action("wp_ajax_wireservice_backfill_batch", [ 55 $this, 56 "handle_backfill_batch", 57 ]); 58 add_action("wp_ajax_wireservice_get_publication_record", [ 59 $this, 60 "handle_get_publication_record", 61 ]); 62 add_action("wp_ajax_wireservice_list_document_records", [ 63 $this, 64 "handle_list_document_records", 65 ]); 66 add_filter("plugin_action_links_wireservice/wireservice.php", [ 67 $this, 68 "add_settings_link", 69 ]); 70 } 71 72 /** 73 * Enqueue scripts for the settings page. 74 * 75 * @param string $hook_suffix The current admin page hook suffix. 76 * @return void 77 */ 78 public function enqueue_settings_assets(string $hook_suffix): void 79 { 80 if ($hook_suffix !== "settings_page_wireservice") { 81 return; 82 } 83 84 wp_enqueue_media(); 85 wp_enqueue_style("wp-color-picker"); 86 87 wp_enqueue_style( 88 "wireservice-settings", 89 WIRESERVICE_PLUGIN_URL . "assets/css/settings.css", 90 [], 91 WIRESERVICE_VERSION, 92 ); 93 94 wp_enqueue_script( 95 "wireservice-settings", 96 WIRESERVICE_PLUGIN_URL . "assets/js/settings.js", 97 ["wp-color-picker"], 98 WIRESERVICE_VERSION, 99 true, 100 ); 101 102 wp_localize_script("wireservice-settings", "wireserviceBackfill", [ 103 "ajaxUrl" => admin_url("admin-ajax.php"), 104 "nonce" => wp_create_nonce("wireservice_backfill"), 105 "resetConfirm" => __("Are you sure you want to reset all Wireservice data? This cannot be undone.", "wireservice"), 106 ]); 107 108 $active_tab = sanitize_key(filter_input(INPUT_GET, "tab") ?? "settings"); 109 110 if ($active_tab === "records") { 111 wp_enqueue_script( 112 "wireservice-records", 113 WIRESERVICE_PLUGIN_URL . "assets/js/records.js", 114 [], 115 WIRESERVICE_VERSION, 116 true, 117 ); 118 119 wp_localize_script("wireservice-records", "wireserviceRecords", [ 120 "ajaxUrl" => admin_url("admin-ajax.php"), 121 "nonce" => wp_create_nonce("wireservice_records"), 122 ]); 123 } 124 } 125 126 /** 127 * Add admin menu page. 128 * 129 * @return void 130 */ 131 public function add_admin_menu(): void 132 { 133 add_options_page( 134 __("Wireservice", "wireservice"), 135 __("Wireservice", "wireservice"), 136 "manage_options", 137 "wireservice", 138 [$this, "render_settings_page"], 139 ); 140 } 141 142 /** 143 * Register plugin settings. 144 * 145 * @return void 146 */ 147 public function register_settings(): void 148 { 149 register_setting("wireservice", "wireservice_connection", [ 150 "type" => "object", 151 "sanitize_callback" => [$this, "sanitize_connection"], 152 "show_in_rest" => false, 153 "default" => [], 154 ]); 155 156 register_setting("wireservice", "wireservice_client_id", [ 157 "type" => "string", 158 "sanitize_callback" => "sanitize_text_field", 159 "show_in_rest" => false, 160 "default" => "", 161 ]); 162 163 register_setting("wireservice", "wireservice_client_secret", [ 164 "type" => "string", 165 "sanitize_callback" => "sanitize_text_field", 166 "show_in_rest" => false, // Don't expose secret via REST. 167 "default" => "", 168 ]); 169 170 register_setting("wireservice", "wireservice_oauth_url", [ 171 "type" => "string", 172 "sanitize_callback" => "esc_url_raw", 173 "show_in_rest" => false, 174 "default" => "https://aip.wireservice.net", 175 ]); 176 177 register_setting("wireservice", "wireservice_pub_settings", [ 178 "type" => "array", 179 "sanitize_callback" => [$this, "sanitize_pub_settings"], 180 "show_in_rest" => false, 181 "default" => SourceOptions::PUB_DEFAULTS, 182 ]); 183 184 register_setting("wireservice", "wireservice_doc_settings", [ 185 "type" => "array", 186 "sanitize_callback" => [$this, "sanitize_doc_settings"], 187 "show_in_rest" => false, 188 "default" => SourceOptions::DOC_DEFAULTS, 189 ]); 190 } 191 192 /** 193 * Sanitize connection data. 194 * 195 * @param mixed $value The value to sanitize. 196 * @return array 197 */ 198 public function sanitize_connection($value): array 199 { 200 if (!is_array($value)) { 201 return []; 202 } 203 204 return [ 205 "access_token" => isset($value["access_token"]) 206 ? sanitize_text_field($value["access_token"]) 207 : "", 208 "refresh_token" => isset($value["refresh_token"]) 209 ? sanitize_text_field($value["refresh_token"]) 210 : "", 211 "expires_at" => isset($value["expires_at"]) 212 ? absint($value["expires_at"]) 213 : 0, 214 "handle" => isset($value["handle"]) 215 ? sanitize_text_field($value["handle"]) 216 : "", 217 "did" => isset($value["did"]) ? sanitize_text_field($value["did"]) : "", 218 ]; 219 } 220 221 /** 222 * Sanitize publication settings. 223 * 224 * @param mixed $value The value to sanitize. 225 * @return array 226 */ 227 public function sanitize_pub_settings($value): array 228 { 229 if (!is_array($value)) { 230 return SourceOptions::PUB_DEFAULTS; 231 } 232 233 return [ 234 "name_source" => isset($value["name_source"]) 235 ? SourceOptions::validate_pub_name_key( 236 sanitize_text_field($value["name_source"]), 237 ) 238 : "wordpress_title", 239 "description_source" => isset($value["description_source"]) 240 ? SourceOptions::validate_pub_desc_key( 241 sanitize_text_field($value["description_source"]), 242 ) 243 : "wordpress_tagline", 244 "custom_name" => isset($value["custom_name"]) 245 ? sanitize_text_field($value["custom_name"]) 246 : "", 247 "custom_description" => isset($value["custom_description"]) 248 ? sanitize_textarea_field($value["custom_description"]) 249 : "", 250 "icon_source" => isset($value["icon_source"]) 251 ? SourceOptions::validate_pub_icon_key( 252 sanitize_text_field($value["icon_source"]), 253 ) 254 : "none", 255 "custom_icon_id" => isset($value["custom_icon_id"]) 256 ? absint($value["custom_icon_id"]) 257 : 0, 258 "theme_background" => isset($value["theme_background"]) 259 ? sanitize_hex_color($value["theme_background"]) ?: "" 260 : "", 261 "theme_foreground" => isset($value["theme_foreground"]) 262 ? sanitize_hex_color($value["theme_foreground"]) ?: "" 263 : "", 264 "theme_accent" => isset($value["theme_accent"]) 265 ? sanitize_hex_color($value["theme_accent"]) ?: "" 266 : "", 267 "theme_accent_foreground" => isset($value["theme_accent_foreground"]) 268 ? sanitize_hex_color($value["theme_accent_foreground"]) ?: "" 269 : "", 270 "show_in_discover" => isset($value["show_in_discover"]) 271 ? sanitize_text_field($value["show_in_discover"]) 272 : "", 273 ]; 274 } 275 276 /** 277 * Sanitize document settings. 278 * 279 * @param mixed $value The value to sanitize. 280 * @return array 281 */ 282 public function sanitize_doc_settings($value): array 283 { 284 if (!is_array($value)) { 285 return SourceOptions::DOC_DEFAULTS; 286 } 287 288 return [ 289 "enabled" => isset($value["enabled"]) 290 ? sanitize_text_field($value["enabled"]) 291 : "0", 292 "title_source" => isset($value["title_source"]) 293 ? SourceOptions::validate_doc_title_key( 294 sanitize_text_field($value["title_source"]), 295 ) 296 : "wordpress_title", 297 "description_source" => isset($value["description_source"]) 298 ? SourceOptions::validate_doc_desc_key( 299 sanitize_text_field($value["description_source"]), 300 ) 301 : "wordpress_excerpt", 302 "image_source" => isset($value["image_source"]) 303 ? SourceOptions::validate_doc_image_key( 304 sanitize_text_field($value["image_source"]), 305 ) 306 : "wordpress_featured", 307 "include_content" => isset($value["include_content"]) 308 ? sanitize_text_field($value["include_content"]) 309 : "0", 310 ]; 311 } 312 313 /** 314 * Render the settings page. 315 * 316 * @return void 317 */ 318 public function render_settings_page(): void 319 { 320 if (!current_user_can("manage_options")) { 321 return; 322 } 323 324 $connection = get_option("wireservice_connection", []); 325 $is_connected = !empty($connection["access_token"]); 326 327 // Connection data. 328 $session = null; 329 $authorize_url = ""; 330 $oauth_url = ""; 331 $client_error = ""; 332 333 if ($is_connected) { 334 $session = $this->api->get_session(); 335 } else { 336 $authorize_url = $this->connections_manager->get_authorize_url(); 337 $oauth_url = get_option("wireservice_oauth_url"); 338 $client_error = get_transient("wireservice_client_error"); 339 } 340 341 // Publication and document data (only needed when connected). 342 $pub_uri = ""; 343 $yoast_active = false; 344 $name_source = "wordpress_title"; 345 $desc_source = "wordpress_tagline"; 346 $custom_name = ""; 347 $custom_description = ""; 348 $name_sources = []; 349 $desc_sources = []; 350 $doc_title_source = "wordpress_title"; 351 $doc_desc_source = "wordpress_excerpt"; 352 $doc_image_source = "wordpress_featured"; 353 $doc_include_content = "0"; 354 $doc_enabled = "0"; 355 $doc_title_sources = []; 356 $doc_desc_sources = []; 357 $doc_image_sources = []; 358 $icon_source = "none"; 359 $custom_icon_id = 0; 360 $icon_preview_url = ""; 361 $icon_sources = []; 362 $theme_background = ""; 363 $theme_foreground = ""; 364 $theme_accent = ""; 365 $theme_accent_foreground = ""; 366 $show_in_discover = ""; 367 368 if ($is_connected) { 369 $pub_uri = $this->publication->get_at_uri(); 370 $yoast_active = Yoast::is_active(); 371 372 $pub = SourceOptions::get_pub_settings(); 373 $name_source = $pub["name_source"]; 374 $desc_source = $pub["description_source"]; 375 $custom_name = $pub["custom_name"]; 376 $custom_description = $pub["custom_description"]; 377 $icon_source = $pub["icon_source"]; 378 $custom_icon_id = $pub["custom_icon_id"]; 379 $theme_background = $pub["theme_background"]; 380 $theme_foreground = $pub["theme_foreground"]; 381 $theme_accent = $pub["theme_accent"]; 382 $theme_accent_foreground = $pub["theme_accent_foreground"]; 383 $show_in_discover = $pub["show_in_discover"]; 384 385 if ($icon_source === "custom" && $custom_icon_id) { 386 $icon_preview_url = wp_get_attachment_image_url( 387 $custom_icon_id, 388 [64, 64], 389 ); 390 } elseif ($icon_source === "wordpress_site_icon") { 391 $icon_preview_url = get_site_icon_url(64); 392 } 393 394 $name_sources = SourceOptions::pub_name_sources($custom_name); 395 $desc_sources = SourceOptions::pub_description_sources( 396 $custom_description, 397 ); 398 $icon_sources = SourceOptions::pub_icon_sources(); 399 400 $doc = SourceOptions::get_doc_settings(); 401 $doc_title_source = $doc["title_source"]; 402 $doc_desc_source = $doc["description_source"]; 403 $doc_image_source = $doc["image_source"]; 404 $doc_include_content = $doc["include_content"]; 405 $doc_enabled = $doc["enabled"]; 406 407 $doc_title_sources = SourceOptions::doc_title_sources(); 408 $doc_desc_sources = SourceOptions::doc_description_sources(); 409 $doc_image_sources = SourceOptions::doc_image_sources(); 410 } 411 412 $active_tab = sanitize_key(filter_input(INPUT_GET, "tab") ?? "settings"); 413 414 include WIRESERVICE_PLUGIN_DIR . "templates/settings-page.php"; 415 } 416 417 /** 418 * Add settings link to plugin actions. 419 * 420 * @param array $links Existing plugin action links. 421 * @return array 422 */ 423 public function add_settings_link(array $links): array 424 { 425 $settings_link = sprintf( 426 '<a href="%s">%s</a>', 427 esc_url(admin_url("options-general.php?page=wireservice")), 428 esc_html__("Settings", "wireservice"), 429 ); 430 array_unshift($links, $settings_link); 431 return $links; 432 } 433 434 /** 435 * Handle syncing the publication to ATProto. 436 * 437 * @return void 438 */ 439 public function handle_sync_publication(): void 440 { 441 if (!current_user_can("manage_options")) { 442 wp_die( 443 esc_html__("You do not have permission to do this.", "wireservice"), 444 ); 445 } 446 447 check_admin_referer( 448 "wireservice_sync_publication", 449 "wireservice_pub_nonce", 450 ); 451 452 $pub = SourceOptions::get_pub_settings(); 453 454 $pub["name_source"] = isset($_POST["wireservice_pub_name_source"]) 455 ? SourceOptions::validate_pub_name_key( 456 sanitize_text_field(wp_unslash($_POST["wireservice_pub_name_source"])), 457 ) 458 : $pub["name_source"]; 459 $pub["description_source"] = isset( 460 $_POST["wireservice_pub_description_source"], 461 ) 462 ? SourceOptions::validate_pub_desc_key( 463 sanitize_text_field( 464 wp_unslash($_POST["wireservice_pub_description_source"]), 465 ), 466 ) 467 : $pub["description_source"]; 468 469 if (isset($_POST["wireservice_pub_custom_name"])) { 470 $pub["custom_name"] = sanitize_text_field( 471 wp_unslash($_POST["wireservice_pub_custom_name"]), 472 ); 473 } 474 if (isset($_POST["wireservice_pub_custom_description"])) { 475 $pub["custom_description"] = sanitize_textarea_field( 476 wp_unslash($_POST["wireservice_pub_custom_description"]), 477 ); 478 } 479 480 $pub["icon_source"] = isset($_POST["wireservice_pub_icon_source"]) 481 ? SourceOptions::validate_pub_icon_key( 482 sanitize_text_field( 483 wp_unslash($_POST["wireservice_pub_icon_source"]), 484 ), 485 ) 486 : $pub["icon_source"]; 487 $pub["custom_icon_id"] = isset($_POST["wireservice_pub_custom_icon_id"]) 488 ? absint($_POST["wireservice_pub_custom_icon_id"]) 489 : $pub["custom_icon_id"]; 490 491 $pub["theme_background"] = isset( 492 $_POST["wireservice_pub_theme_background"], 493 ) 494 ? sanitize_hex_color( 495 wp_unslash($_POST["wireservice_pub_theme_background"]), 496 ) ?: "" 497 : $pub["theme_background"]; 498 $pub["theme_foreground"] = isset( 499 $_POST["wireservice_pub_theme_foreground"], 500 ) 501 ? sanitize_hex_color( 502 wp_unslash($_POST["wireservice_pub_theme_foreground"]), 503 ) ?: "" 504 : $pub["theme_foreground"]; 505 $pub["theme_accent"] = isset($_POST["wireservice_pub_theme_accent"]) 506 ? sanitize_hex_color( 507 wp_unslash($_POST["wireservice_pub_theme_accent"]), 508 ) ?: "" 509 : $pub["theme_accent"]; 510 $pub["theme_accent_foreground"] = isset( 511 $_POST["wireservice_pub_theme_accent_foreground"], 512 ) 513 ? sanitize_hex_color( 514 wp_unslash($_POST["wireservice_pub_theme_accent_foreground"]), 515 ) ?: "" 516 : $pub["theme_accent_foreground"]; 517 518 $pub["show_in_discover"] = isset( 519 $_POST["wireservice_pub_show_in_discover"], 520 ) 521 ? "1" 522 : "0"; 523 524 update_option("wireservice_pub_settings", $pub); 525 526 $name_source = $pub["name_source"]; 527 $desc_source = $pub["description_source"]; 528 529 $name = $this->resolve_publication_name($name_source); 530 $description = $this->resolve_publication_description($desc_source); 531 532 $icon_attachment_id = $this->resolve_publication_icon( 533 $pub["icon_source"], 534 $pub["custom_icon_id"], 535 ); 536 537 $pub_data = [ 538 "url" => isset($_POST["wireservice_pub_url"]) 539 ? sanitize_url(wp_unslash($_POST["wireservice_pub_url"])) 540 : home_url(), 541 "name" => $name, 542 "description" => $description, 543 "icon_attachment_id" => $icon_attachment_id, 544 "theme_background" => $pub["theme_background"], 545 "theme_foreground" => $pub["theme_foreground"], 546 "theme_accent" => $pub["theme_accent"], 547 "theme_accent_foreground" => $pub["theme_accent_foreground"], 548 "show_in_discover" => $pub["show_in_discover"], 549 ]; 550 $this->publication->save_publication_data($pub_data); 551 552 $result = $this->publication->sync_to_atproto($pub_data); 553 554 if (is_wp_error($result)) { 555 $error_data = $result->get_error_data(); 556 $debug_info = $result->get_error_message(); 557 if ($error_data) { 558 $debug_info .= " (Data: " . wp_json_encode($error_data) . ")"; 559 } 560 add_settings_error( 561 "wireservice", 562 "publication_sync_failed", 563 sprintf( 564 /* translators: %s: error message */ 565 __("Failed to sync publication: %s", "wireservice"), 566 $debug_info, 567 ), 568 "error", 569 ); 570 } else { 571 add_settings_error( 572 "wireservice", 573 "publication_synced", 574 __("Publication synced to AT Protocol.", "wireservice"), 575 "success", 576 ); 577 } 578 579 set_transient("settings_errors", get_settings_errors(), 30); 580 581 wp_safe_redirect( 582 admin_url("options-general.php?page=wireservice&settings-updated=true"), 583 ); 584 exit(); 585 } 586 587 /** 588 * Handle resetting all plugin data. 589 * 590 * @return void 591 */ 592 public function handle_reset_data(): void 593 { 594 if (!current_user_can("manage_options")) { 595 wp_die( 596 esc_html__("You do not have permission to do this.", "wireservice"), 597 ); 598 } 599 600 check_admin_referer("wireservice_reset_data", "wireservice_reset_nonce"); 601 602 // Remove all plugin options. 603 delete_option("wireservice_connection"); 604 delete_option("wireservice_client_id"); 605 delete_option("wireservice_client_secret"); 606 delete_option("wireservice_oauth_url"); 607 delete_option("wireservice_publication"); 608 delete_option("wireservice_publication_uri"); 609 delete_option("wireservice_pub_settings"); 610 delete_option("wireservice_doc_settings"); 611 612 // Remove transients. 613 delete_transient("wireservice_code_verifier"); 614 delete_transient("wireservice_oauth_state"); 615 616 // Remove post meta for documents. 617 delete_post_meta_by_key("_wireservice_document_uri"); 618 delete_post_meta_by_key("_wireservice_title_source"); 619 delete_post_meta_by_key("_wireservice_description_source"); 620 delete_post_meta_by_key("_wireservice_image_source"); 621 delete_post_meta_by_key("_wireservice_custom_title"); 622 delete_post_meta_by_key("_wireservice_custom_description"); 623 delete_post_meta_by_key("_wireservice_custom_image_id"); 624 delete_post_meta_by_key("_wireservice_include_content"); 625 626 add_settings_error( 627 "wireservice", 628 "data_reset", 629 __("All Wireservice data has been reset.", "wireservice"), 630 "success", 631 ); 632 633 set_transient("settings_errors", get_settings_errors(), 30); 634 635 wp_safe_redirect( 636 admin_url("options-general.php?page=wireservice&settings-updated=true"), 637 ); 638 exit(); 639 } 640 641 /** 642 * Resolve the publication name based on the selected source. 643 * 644 * @param string $source The source key. 645 * @return string 646 */ 647 private function resolve_publication_name(string $source): string 648 { 649 $value = match ($source) { 650 "yoast_organization" => Yoast::get_organization_name(), 651 "yoast_website" => Yoast::get_website_name(), 652 "custom" => SourceOptions::get_pub_settings()["custom_name"], 653 default => get_bloginfo("name"), 654 }; 655 656 return $value ?: get_bloginfo("name"); 657 } 658 659 /** 660 * Resolve the publication description based on the selected source. 661 * 662 * @param string $source The source key. 663 * @return string 664 */ 665 private function resolve_publication_description(string $source): string 666 { 667 $value = match ($source) { 668 "yoast_homepage" => Yoast::get_homepage_description(), 669 "custom" => SourceOptions::get_pub_settings()["custom_description"], 670 default => get_bloginfo("description"), 671 }; 672 673 return $value ?: get_bloginfo("description"); 674 } 675 676 /** 677 * Resolve the publication icon attachment ID based on the selected source. 678 * 679 * @param string $source The source key. 680 * @param int $custom_icon_id The custom icon attachment ID. 681 * @return int The attachment ID, or 0 if none. 682 */ 683 private function resolve_publication_icon( 684 string $source, 685 int $custom_icon_id, 686 ): int { 687 return match ($source) { 688 "wordpress_site_icon" => (int) get_option("site_icon", 0), 689 "custom" => $custom_icon_id, 690 default => 0, 691 }; 692 } 693 694 /** 695 * Handle AJAX request to count unsynced posts for backfill. 696 * 697 * @return void 698 */ 699 public function handle_backfill_count(): void 700 { 701 if (!current_user_can("manage_options")) { 702 wp_send_json_error("Unauthorized.", 403); 703 return; 704 } 705 706 check_ajax_referer("wireservice_backfill", "nonce"); 707 708 $post_types = apply_filters("wireservice_syncable_post_types", [ 709 "post", 710 "page", 711 ]); 712 713 $all_post_ids = get_posts([ 714 "post_type" => $post_types, 715 "post_status" => "publish", 716 "posts_per_page" => -1, 717 "fields" => "ids", 718 ]); 719 720 update_meta_cache("post", $all_post_ids); 721 722 $unsynced_ids = []; 723 foreach ($all_post_ids as $post_id) { 724 if (!get_post_meta($post_id, Document::META_KEY_URI, true)) { 725 $unsynced_ids[] = $post_id; 726 } 727 } 728 729 wp_send_json_success([ 730 "total" => count($unsynced_ids), 731 "post_ids" => $unsynced_ids, 732 ]); 733 } 734 735 /** 736 * Handle AJAX request to sync a batch of posts for backfill. 737 * 738 * @return void 739 */ 740 public function handle_backfill_batch(): void 741 { 742 if (!current_user_can("manage_options")) { 743 wp_send_json_error("Unauthorized.", 403); 744 return; 745 } 746 747 check_ajax_referer("wireservice_backfill", "nonce"); 748 749 $post_ids = isset($_POST["post_ids"]) 750 ? array_map("absint", (array) $_POST["post_ids"]) 751 : []; 752 753 if (empty($post_ids)) { 754 wp_send_json_error("No post IDs provided."); 755 } 756 757 $results = []; 758 foreach ($post_ids as $post_id) { 759 $post = get_post($post_id); 760 761 if (!$post || !$this->document->should_sync($post)) { 762 $results[] = [ 763 "id" => $post_id, 764 "success" => false, 765 "error" => __("Post not eligible for sync.", "wireservice"), 766 ]; 767 continue; 768 } 769 770 $response = $this->document->sync_to_atproto($post); 771 772 if (is_wp_error($response)) { 773 $results[] = [ 774 "id" => $post_id, 775 "title" => get_the_title($post), 776 "success" => false, 777 "error" => $response->get_error_message(), 778 ]; 779 } else { 780 $results[] = [ 781 "id" => $post_id, 782 "title" => get_the_title($post), 783 "success" => true, 784 ]; 785 } 786 } 787 788 wp_send_json_success(["results" => $results]); 789 } 790 791 /** 792 * Handle saving document settings. 793 * 794 * @return void 795 */ 796 public function handle_save_doc_settings(): void 797 { 798 if (!current_user_can("manage_options")) { 799 wp_die( 800 esc_html__("You do not have permission to do this.", "wireservice"), 801 ); 802 } 803 804 check_admin_referer( 805 "wireservice_save_doc_settings", 806 "wireservice_doc_nonce", 807 ); 808 809 $doc = SourceOptions::get_doc_settings(); 810 811 $doc["enabled"] = isset($_POST["wireservice_doc_enabled"]) ? "1" : "0"; 812 813 if (isset($_POST["wireservice_doc_title_source"])) { 814 $doc["title_source"] = SourceOptions::validate_doc_title_key( 815 sanitize_text_field( 816 wp_unslash($_POST["wireservice_doc_title_source"]), 817 ), 818 ); 819 } 820 821 if (isset($_POST["wireservice_doc_description_source"])) { 822 $doc["description_source"] = SourceOptions::validate_doc_desc_key( 823 sanitize_text_field( 824 wp_unslash($_POST["wireservice_doc_description_source"]), 825 ), 826 ); 827 } 828 829 if (isset($_POST["wireservice_doc_image_source"])) { 830 $doc["image_source"] = SourceOptions::validate_doc_image_key( 831 sanitize_text_field( 832 wp_unslash($_POST["wireservice_doc_image_source"]), 833 ), 834 ); 835 } 836 837 $doc["include_content"] = isset($_POST["wireservice_doc_include_content"]) 838 ? "1" 839 : "0"; 840 841 update_option("wireservice_doc_settings", $doc); 842 843 add_settings_error( 844 "wireservice", 845 "doc_settings_saved", 846 __("Document settings saved.", "wireservice"), 847 "success", 848 ); 849 850 set_transient("settings_errors", get_settings_errors(), 30); 851 852 wp_safe_redirect( 853 admin_url("options-general.php?page=wireservice&settings-updated=true"), 854 ); 855 exit(); 856 } 857 858 /** 859 * Handle AJAX request to fetch the publication record from the PDS. 860 * 861 * @return void 862 */ 863 public function handle_get_publication_record(): void 864 { 865 if (!current_user_can("manage_options")) { 866 wp_send_json_error("Unauthorized.", 403); 867 return; 868 } 869 870 check_ajax_referer("wireservice_records", "nonce"); 871 872 $pub_uri = $this->publication->get_at_uri(); 873 874 if (empty($pub_uri)) { 875 wp_send_json_error("No publication record exists."); 876 } 877 878 $rkey = AtUri::get_rkey($pub_uri); 879 $result = $this->api->get_record("site.standard.publication", $rkey); 880 881 if (is_wp_error($result)) { 882 wp_send_json_error($result->get_error_message()); 883 } 884 885 wp_send_json_success($result); 886 } 887 888 /** 889 * Handle AJAX request to list document records from the PDS. 890 * 891 * @return void 892 */ 893 public function handle_list_document_records(): void 894 { 895 if (!current_user_can("manage_options")) { 896 wp_send_json_error("Unauthorized.", 403); 897 return; 898 } 899 900 check_ajax_referer("wireservice_records", "nonce"); 901 902 $pub_uri = $this->publication->get_at_uri(); 903 $pub_data = $this->publication->get_publication_data(); 904 $pub_url = rtrim($pub_data["url"] ?? "", "/"); 905 906 $cursor = isset($_POST["cursor"]) 907 ? sanitize_text_field(wp_unslash($_POST["cursor"])) 908 : null; 909 910 $result = $this->api->list_records( 911 "site.standard.document", 912 50, 913 $cursor ?: null, 914 ); 915 916 if (is_wp_error($result)) { 917 wp_send_json_error($result->get_error_message()); 918 } 919 920 $result["records"] = array_values(array_filter( 921 $result["records"] ?? [], 922 function (array $record) use ($pub_uri, $pub_url): bool { 923 $site = $record["value"]["site"] ?? ""; 924 return $site === $pub_uri || rtrim($site, "/") === $pub_url; 925 }, 926 )); 927 928 wp_send_json_success($result); 929 } 930 931 /** 932 * Adopt an existing publication record from the PDS after a fresh OAuth connection. 933 * 934 * Runs on admin_init when redirected back from OAuth (connected=1 query param). 935 * If the PDS already has a publication record matching this site's URL, adopts it 936 * so the user doesn't create a duplicate. 937 * 938 * @return void 939 */ 940 public function maybe_adopt_existing_publication(): void 941 { 942 $page = sanitize_key(filter_input(INPUT_GET, "page") ?? ""); 943 $connected = sanitize_key(filter_input(INPUT_GET, "connected") ?? ""); 944 945 if ($page !== "wireservice" || $connected !== "1") { 946 return; 947 } 948 949 if (!current_user_can("manage_options")) { 950 return; 951 } 952 953 if (!$this->connections_manager->is_connected()) { 954 return; 955 } 956 957 if ($this->publication->get_at_uri()) { 958 return; 959 } 960 961 $record = $this->publication->find_matching_record(); 962 963 if (!$record) { 964 return; 965 } 966 967 $this->publication->adopt_record($record); 968 969 add_settings_error( 970 "wireservice", 971 "publication_adopted", 972 __("An existing publication record was found on your PDS and has been linked.", "wireservice"), 973 "info", 974 ); 975 } 976}