Sync your WordPress posts to standard.site records on your PDS
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}