Sync your WordPress posts to standard.site records on your PDS
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

address feedback

+428 -275
+4 -4
README.txt
··· 1 1 === Wireservice === 2 - Contributors: tylerfisher 3 - Tags: atproto, bluesky, fediverse, syndication 2 + Contributors: tylrfishr 3 + Tags: atproto, bluesky, syndication 4 4 Requires at least: 6.7 5 - Tested up to: 6.9.1 5 + Tested up to: 6.9 6 6 Requires PHP: 8.4 7 7 Stable tag: 1.1.0 8 8 License: AGPLv3 or later 9 9 License URI: https://www.gnu.org/licenses/agpl-3.0.html 10 10 11 - A WordPress plugin that publishes your posts and pages to the [AT Protocol](https://atproto.com) using the [standard.site](https://standard.site) lexicons (`site.standard.publication` and `site.standard.document`). 11 + Publish your posts and pages to the AT Protocol using the standard.site lexicons. 12 12 13 13 ## Requirements 14 14
+140
assets/css/settings.css
··· 1 + .wireservice-settings { 2 + max-width: 800px; 3 + } 4 + .wireservice-connection-status { 5 + padding: 15px; 6 + border-radius: 4px; 7 + margin: 15px 0; 8 + } 9 + .wireservice-connection-status.connected { 10 + background: #d4edda; 11 + border: 1px solid #c3e6cb; 12 + } 13 + .wireservice-connection-status.disconnected { 14 + background: #f8f9fa; 15 + border: 1px solid #dee2e6; 16 + } 17 + .wireservice-error { 18 + color: #dc3545; 19 + font-weight: 500; 20 + } 21 + .wireservice-profile { 22 + display: flex; 23 + gap: 15px; 24 + align-items: flex-start; 25 + margin-bottom: 15px; 26 + } 27 + .wireservice-avatar { 28 + width: 64px; 29 + height: 64px; 30 + border-radius: 50%; 31 + object-fit: cover; 32 + } 33 + .wireservice-profile-info { 34 + flex: 1; 35 + } 36 + .wireservice-display-name { 37 + display: block; 38 + font-size: 16px; 39 + margin-bottom: 2px; 40 + } 41 + .wireservice-handle { 42 + color: #666; 43 + font-size: 14px; 44 + } 45 + .wireservice-bio { 46 + margin: 8px 0; 47 + font-size: 14px; 48 + color: #333; 49 + } 50 + .wireservice-stats { 51 + display: flex; 52 + gap: 15px; 53 + font-size: 13px; 54 + color: #666; 55 + margin: 8px 0 0; 56 + } 57 + .wireservice-advanced-settings summary { 58 + cursor: pointer; 59 + font-size: 14px; 60 + font-weight: 600; 61 + color: #50575e; 62 + padding: 4px 0; 63 + } 64 + .wireservice-advanced-settings summary:hover { 65 + color: #1d2327; 66 + } 67 + .wireservice-progress-bar { 68 + width: 100%; 69 + height: 20px; 70 + background: #f0f0f1; 71 + border-radius: 3px; 72 + overflow: hidden; 73 + margin: 10px 0; 74 + } 75 + .wireservice-progress-bar-fill { 76 + height: 100%; 77 + background: #2271b1; 78 + transition: width 0.3s ease; 79 + } 80 + .wireservice-backfill-errors { 81 + margin-top: 10px; 82 + } 83 + .wireservice-backfill-errors summary { 84 + cursor: pointer; 85 + color: #d63638; 86 + font-weight: 500; 87 + } 88 + .wireservice-records { 89 + max-width: 800px; 90 + } 91 + .wireservice-record-table { 92 + border-collapse: collapse; 93 + } 94 + .wireservice-record-table th { 95 + width: 160px; 96 + text-align: left; 97 + padding: 8px 12px; 98 + vertical-align: top; 99 + font-weight: 600; 100 + white-space: nowrap; 101 + } 102 + .wireservice-record-table td { 103 + padding: 8px 12px; 104 + word-break: break-all; 105 + } 106 + .wireservice-record-table code { 107 + font-size: 12px; 108 + background: #f0f0f1; 109 + padding: 2px 6px; 110 + border-radius: 3px; 111 + } 112 + .wireservice-document-card { 113 + background: #fff; 114 + border: 1px solid #c3c4c7; 115 + border-radius: 4px; 116 + margin-bottom: 16px; 117 + } 118 + .wireservice-document-card-header { 119 + padding: 12px 16px; 120 + border-bottom: 1px solid #f0f0f1; 121 + } 122 + .wireservice-document-card-header h3 { 123 + margin: 0; 124 + font-size: 14px; 125 + } 126 + .wireservice-document-card-body { 127 + padding: 0; 128 + } 129 + .wireservice-document-card-body .wireservice-record-table { 130 + border: none; 131 + } 132 + .wireservice-color-swatch { 133 + display: inline-block; 134 + width: 16px; 135 + height: 16px; 136 + border-radius: 3px; 137 + border: 1px solid #ddd; 138 + vertical-align: middle; 139 + margin-right: 4px; 140 + }
+9
assets/js/settings.js
··· 283 283 jQuery(".wireservice-color-picker").wpColorPicker(); 284 284 285 285 initBackfill(); 286 + 287 + var resetForm = document.getElementById("wireservice-reset-form"); 288 + if (resetForm) { 289 + resetForm.addEventListener("submit", function (e) { 290 + if (!confirm(wireserviceBackfill.resetConfirm)) { 291 + e.preventDefault(); 292 + } 293 + }); 294 + } 286 295 }); 287 296 })();
+2 -2
composer.lock
··· 4 4 "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 5 "This file is @generated automatically" 6 6 ], 7 - "content-hash": "4fa9ecb03ab563b18aca2ad0ef114f65", 7 + "content-hash": "a13d44b6d64f15e27515432346109171", 8 8 "packages": [ 9 9 { 10 10 "name": "brick/math", ··· 527 527 "prefer-stable": false, 528 528 "prefer-lowest": false, 529 529 "platform": { 530 - "php": ">=7.4" 530 + "php": "^8.4" 531 531 }, 532 532 "platform-dev": [], 533 533 "plugin-api-version": "2.3.0"
+78 -28
includes/Admin.php
··· 33 33 { 34 34 add_action("admin_menu", [$this, "add_admin_menu"]); 35 35 add_action("admin_init", [$this, "register_settings"]); 36 + add_action("admin_init", [$this, "maybe_adopt_existing_publication"]); 36 37 add_action("admin_enqueue_scripts", [$this, "enqueue_settings_assets"]); 37 38 add_action("admin_post_wireservice_sync_publication", [ 38 39 $this, ··· 83 84 wp_enqueue_media(); 84 85 wp_enqueue_style("wp-color-picker"); 85 86 87 + wp_enqueue_style( 88 + "wireservice-settings", 89 + WIRESERVICE_PLUGIN_URL . "assets/css/settings.css", 90 + [], 91 + WIRESERVICE_VERSION, 92 + ); 93 + 86 94 wp_enqueue_script( 87 95 "wireservice-settings", 88 96 WIRESERVICE_PLUGIN_URL . "assets/js/settings.js", ··· 94 102 wp_localize_script("wireservice-settings", "wireserviceBackfill", [ 95 103 "ajaxUrl" => admin_url("admin-ajax.php"), 96 104 "nonce" => wp_create_nonce("wireservice_backfill"), 105 + "resetConfirm" => __("Are you sure you want to reset all Wireservice data? This cannot be undone.", "wireservice"), 97 106 ]); 98 107 99 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab navigation. 100 - $active_tab = isset($_GET["tab"]) ? sanitize_key($_GET["tab"]) : "settings"; 108 + $active_tab = sanitize_key(filter_input(INPUT_GET, "tab") ?? "settings"); 101 109 102 110 if ($active_tab === "records") { 103 111 wp_enqueue_script( ··· 401 409 $doc_image_sources = SourceOptions::doc_image_sources(); 402 410 } 403 411 412 + $active_tab = sanitize_key(filter_input(INPUT_GET, "tab") ?? "settings"); 413 + 404 414 include WIRESERVICE_PLUGIN_DIR . "templates/settings-page.php"; 405 415 } 406 416 ··· 604 614 delete_transient("wireservice_oauth_state"); 605 615 606 616 // Remove post meta for documents. 607 - global $wpdb; 608 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_document_uri"]); 609 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_title_source"]); 610 - $wpdb->delete($wpdb->postmeta, [ 611 - "meta_key" => "_wireservice_description_source", 612 - ]); 613 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_image_source"]); 614 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_custom_title"]); 615 - $wpdb->delete($wpdb->postmeta, [ 616 - "meta_key" => "_wireservice_custom_description", 617 - ]); 618 - $wpdb->delete($wpdb->postmeta, [ 619 - "meta_key" => "_wireservice_custom_image_id", 620 - ]); 621 - $wpdb->delete($wpdb->postmeta, [ 622 - "meta_key" => "_wireservice_include_content", 623 - ]); 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"); 624 625 625 626 add_settings_error( 626 627 "wireservice", ··· 709 710 "page", 710 711 ]); 711 712 712 - $query = new \WP_Query([ 713 + $all_post_ids = get_posts([ 713 714 "post_type" => $post_types, 714 715 "post_status" => "publish", 715 716 "posts_per_page" => -1, 716 717 "fields" => "ids", 717 - "meta_query" => [ 718 - [ 719 - "key" => Document::META_KEY_URI, 720 - "compare" => "NOT EXISTS", 721 - ], 722 - ], 723 718 ]); 724 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 + 725 729 wp_send_json_success([ 726 - "total" => $query->found_posts, 727 - "post_ids" => $query->posts, 730 + "total" => count($unsynced_ids), 731 + "post_ids" => $unsynced_ids, 728 732 ]); 729 733 } 730 734 ··· 922 926 )); 923 927 924 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 + ); 925 975 } 926 976 }
+32 -8
includes/ConnectionsManager.php
··· 71 71 $state = wp_generate_password(32, false); 72 72 set_transient("wireservice_oauth_state", $state, HOUR_IN_SECONDS); 73 73 74 + // Store a WordPress nonce for callback verification. 75 + set_transient( 76 + "wireservice_oauth_nonce", 77 + wp_create_nonce("wireservice_oauth_callback"), 78 + HOUR_IN_SECONDS, 79 + ); 80 + 74 81 $params = [ 75 82 "client_id" => $client_id, 76 83 "redirect_uri" => $this->get_redirect_uri(), ··· 185 192 */ 186 193 public function handle_oauth_callback(): void 187 194 { 188 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended 189 - if (!isset($_GET["page"]) || "wireservice" !== $_GET["page"]) { 195 + $page = filter_input(INPUT_GET, "page"); 196 + if ($page !== "wireservice") { 197 + return; 198 + } 199 + 200 + $code = filter_input(INPUT_GET, "code"); 201 + $state = filter_input(INPUT_GET, "state"); 202 + if ($code === null || $state === null) { 203 + return; 204 + } 205 + 206 + // Verify user permissions and the stored WordPress nonce. 207 + if (!current_user_can("manage_options")) { 190 208 return; 191 209 } 192 210 193 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended 194 - if (!isset($_GET["code"]) || !isset($_GET["state"])) { 211 + $stored_nonce = get_transient("wireservice_oauth_nonce"); 212 + if (!$stored_nonce || !wp_verify_nonce($stored_nonce, "wireservice_oauth_callback")) { 213 + add_settings_error( 214 + "wireservice", 215 + "invalid_nonce", 216 + __("Security check failed. Please try again.", "wireservice"), 217 + "error", 218 + ); 195 219 return; 196 220 } 197 221 198 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended 199 - $code = sanitize_text_field(wp_unslash($_GET["code"])); 200 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended 201 - $state = sanitize_text_field(wp_unslash($_GET["state"])); 222 + delete_transient("wireservice_oauth_nonce"); 223 + 224 + $code = sanitize_text_field($code); 225 + $state = sanitize_text_field($state); 202 226 203 227 // Verify state. 204 228 $stored_state = get_transient("wireservice_oauth_state");
+1 -1
includes/DPoP.php
··· 84 84 85 85 return $proof->proof; 86 86 } catch (\Throwable $e) { 87 - error_log("DPoP proof generation failed: " . $e->getMessage()); 87 + wp_trigger_error(__METHOD__, "DPoP proof generation failed: " . $e->getMessage()); 88 88 return false; 89 89 } 90 90 }
+85
includes/Publication.php
··· 234 234 } 235 235 236 236 /** 237 + * Find an existing publication record on the PDS that matches this site's URL. 238 + * 239 + * @return array|null The matching record (uri, cid, value keys) or null. 240 + */ 241 + public function find_matching_record(): ?array 242 + { 243 + $site_url = rtrim(home_url(), "/"); 244 + $cursor = null; 245 + 246 + do { 247 + $result = $this->api->list_records(self::LEXICON, 100, $cursor); 248 + 249 + if (is_wp_error($result)) { 250 + return null; 251 + } 252 + 253 + foreach ($result["records"] ?? [] as $record) { 254 + $record_url = rtrim($record["value"]["url"] ?? "", "/"); 255 + if ($record_url === $site_url) { 256 + return $record; 257 + } 258 + } 259 + 260 + $cursor = $result["cursor"] ?? null; 261 + } while ($cursor); 262 + 263 + return null; 264 + } 265 + 266 + /** 267 + * Adopt an existing publication record from the PDS. 268 + * 269 + * @param array $record The record array with uri, cid, and value keys. 270 + * @return void 271 + */ 272 + public function adopt_record(array $record): void 273 + { 274 + if (empty($record["uri"]) || !is_string($record["uri"]) || !isset($record["value"])) { 275 + return; 276 + } 277 + 278 + $this->save_at_uri(sanitize_text_field($record["uri"])); 279 + 280 + $value = $record["value"]; 281 + $theme = $value["basicTheme"] ?? null; 282 + 283 + // save_publication_data() sanitizes all values (esc_url_raw, sanitize_text_field, 284 + // sanitize_textarea_field, sanitize_hex_color, absint). 285 + $data = [ 286 + "url" => $value["url"] ?? home_url(), 287 + "name" => $value["name"] ?? "", 288 + "description" => $value["description"] ?? "", 289 + "icon_attachment_id" => 0, 290 + "theme_background" => $theme ? self::rgb_to_hex($theme["background"] ?? null) : "", 291 + "theme_foreground" => $theme ? self::rgb_to_hex($theme["foreground"] ?? null) : "", 292 + "theme_accent" => $theme ? self::rgb_to_hex($theme["accent"] ?? null) : "", 293 + "theme_accent_foreground" => $theme ? self::rgb_to_hex($theme["accentForeground"] ?? null) : "", 294 + "show_in_discover" => isset($value["preferences"]["showInDiscover"]) 295 + ? ($value["preferences"]["showInDiscover"] ? "1" : "0") 296 + : "", 297 + ]; 298 + 299 + $this->save_publication_data($data); 300 + } 301 + 302 + /** 303 + * Convert an RGB color array to a hex color string. 304 + * 305 + * @param array|null $color RGB array with r, g, b keys. 306 + * @return string Hex color (e.g. "#ff0000") or empty string. 307 + */ 308 + private static function rgb_to_hex(?array $color): string 309 + { 310 + if ($color === null || !isset($color["r"], $color["g"], $color["b"])) { 311 + return ""; 312 + } 313 + 314 + $r = max(0, min(255, (int) $color["r"])); 315 + $g = max(0, min(255, (int) $color["g"])); 316 + $b = max(0, min(255, (int) $color["b"])); 317 + 318 + return sprintf("#%02x%02x%02x", $r, $g, $b); 319 + } 320 + 321 + /** 237 322 * Delete the publication record from ATProto. 238 323 * 239 324 * @return array|\WP_Error|null The response, error, or null if no record exists.
+27 -35
includes/Setup.php
··· 181 181 182 182 if (empty($at_uri)) { 183 183 status_header(404); 184 - echo "Publication not found"; 184 + echo esc_html("Publication not found"); 185 185 exit(); 186 186 } 187 187 ··· 441 441 return; 442 442 } 443 443 444 - $fields = [ 445 - ["wireservice_title_source", "_wireservice_title_source", "sanitize_text_field"], 446 - ["wireservice_description_source", "_wireservice_description_source", "sanitize_text_field"], 447 - ["wireservice_image_source", "_wireservice_image_source", "sanitize_text_field"], 448 - ["wireservice_custom_title", "_wireservice_custom_title", "sanitize_text_field"], 449 - ["wireservice_custom_description", "_wireservice_custom_description", "sanitize_textarea_field"], 450 - ["wireservice_custom_image_id", "_wireservice_custom_image_id", "absint"], 451 - ]; 452 - 453 - foreach ($fields as [$post_key, $meta_key, $sanitizer]) { 454 - $this->save_meta_field($post_id, $post_key, $meta_key, $sanitizer); 444 + if (isset($_POST["wireservice_title_source"])) { 445 + $this->save_meta_field($post_id, "_wireservice_title_source", sanitize_text_field(wp_unslash($_POST["wireservice_title_source"]))); 455 446 } 456 - 457 - $this->save_meta_field( 458 - $post_id, 459 - "wireservice_include_content", 460 - "_wireservice_include_content", 461 - "sanitize_text_field", 462 - allow_falsy: true, 463 - ); 447 + if (isset($_POST["wireservice_description_source"])) { 448 + $this->save_meta_field($post_id, "_wireservice_description_source", sanitize_text_field(wp_unslash($_POST["wireservice_description_source"]))); 449 + } 450 + if (isset($_POST["wireservice_image_source"])) { 451 + $this->save_meta_field($post_id, "_wireservice_image_source", sanitize_text_field(wp_unslash($_POST["wireservice_image_source"]))); 452 + } 453 + if (isset($_POST["wireservice_custom_title"])) { 454 + $this->save_meta_field($post_id, "_wireservice_custom_title", sanitize_text_field(wp_unslash($_POST["wireservice_custom_title"]))); 455 + } 456 + if (isset($_POST["wireservice_custom_description"])) { 457 + $this->save_meta_field($post_id, "_wireservice_custom_description", sanitize_textarea_field(wp_unslash($_POST["wireservice_custom_description"]))); 458 + } 459 + if (isset($_POST["wireservice_custom_image_id"])) { 460 + $this->save_meta_field($post_id, "_wireservice_custom_image_id", absint(wp_unslash($_POST["wireservice_custom_image_id"]))); 461 + } 462 + if (isset($_POST["wireservice_include_content"])) { 463 + $this->save_meta_field($post_id, "_wireservice_include_content", sanitize_text_field(wp_unslash($_POST["wireservice_include_content"])), allow_falsy: true); 464 + } 464 465 } 465 466 466 467 /** 467 - * Save a single meta box field from POST data. 468 + * Save or delete a single post meta field. 468 469 * 469 - * @param int $post_id The post ID. 470 - * @param string $post_key The $_POST key. 471 - * @param string $meta_key The post meta key. 472 - * @param callable $sanitizer Sanitization function. 473 - * @param bool $allow_falsy Whether to store falsy values like "0". 470 + * @param int $post_id The post ID. 471 + * @param string $meta_key The post meta key. 472 + * @param string|int $value The sanitized value. 473 + * @param bool $allow_falsy Whether to store falsy values like "0". 474 474 * @return void 475 475 */ 476 476 private function save_meta_field( 477 477 int $post_id, 478 - string $post_key, 479 478 string $meta_key, 480 - callable $sanitizer, 479 + string|int $value, 481 480 bool $allow_falsy = false, 482 481 ): void { 483 - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified in save_document_meta_box(). 484 - if (!isset($_POST[$post_key])) { 485 - return; 486 - } 487 - 488 - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified in caller; value sanitized by $sanitizer. 489 - $value = $sanitizer(wp_unslash($_POST[$post_key])); 490 482 $is_empty = $allow_falsy ? $value === "" : empty($value); 491 483 492 484 if ($is_empty) {
+12 -12
templates/document-meta-box.php
··· 25 25 <strong><?php esc_html_e("Title Source", "wireservice"); ?></strong> 26 26 </label><br> 27 27 <select name="wireservice_title_source" id="wireservice_title_source" style="width: 100%;"> 28 - <?php foreach ($title_sources as $key => $label): ?> 29 - <option value="<?php echo esc_attr($key); ?>" <?php selected($title_source, $key); ?>> 30 - <?php echo esc_html($label); ?> 28 + <?php foreach ($title_sources as $wireservice_key => $wireservice_label): ?> 29 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($title_source, $wireservice_key); ?>> 30 + <?php echo esc_html($wireservice_label); ?> 31 31 </option> 32 32 <?php endforeach; ?> 33 33 </select> ··· 48 48 <strong><?php esc_html_e("Description Source", "wireservice"); ?></strong> 49 49 </label><br> 50 50 <select name="wireservice_description_source" id="wireservice_description_source" style="width: 100%;"> 51 - <?php foreach ($desc_sources as $key => $label): ?> 52 - <option value="<?php echo esc_attr($key); ?>" <?php selected($desc_source, $key); ?>> 53 - <?php echo esc_html($label); ?> 51 + <?php foreach ($desc_sources as $wireservice_key => $wireservice_label): ?> 52 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($desc_source, $wireservice_key); ?>> 53 + <?php echo esc_html($wireservice_label); ?> 54 54 </option> 55 55 <?php endforeach; ?> 56 56 </select> ··· 70 70 <strong><?php esc_html_e("Cover Image Source", "wireservice"); ?></strong> 71 71 </label><br> 72 72 <select name="wireservice_image_source" id="wireservice_image_source" style="width: 100%;"> 73 - <?php foreach ($image_sources as $key => $label): ?> 74 - <option value="<?php echo esc_attr($key); ?>" <?php selected($image_source, $key); ?>> 75 - <?php echo esc_html($label); ?> 73 + <?php foreach ($image_sources as $wireservice_key => $wireservice_label): ?> 74 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($image_source, $wireservice_key); ?>> 75 + <?php echo esc_html($wireservice_label); ?> 76 76 </option> 77 77 <?php endforeach; ?> 78 78 </select> 79 79 <div id="wireservice-custom-image-field" style="display:none; margin-top: 8px;"> 80 80 <div id="wireservice-custom-image-preview"> 81 81 <?php if (!empty($custom_image_id)): 82 - $thumb = wp_get_attachment_image_url($custom_image_id, "thumbnail"); 82 + $wireservice_thumb = wp_get_attachment_image_url($custom_image_id, "thumbnail"); 83 83 ?> 84 - <img src="<?php echo esc_url($thumb); ?>" 84 + <img src="<?php echo esc_url($wireservice_thumb); ?>" 85 85 style="max-width:150px;height:auto;display:block;margin-bottom:8px;" /> 86 86 <?php endif; ?> 87 87 </div> ··· 97 97 <button type="button" 98 98 class="button" 99 99 id="wireservice-remove-image" 100 - style="<?php echo empty($custom_image_id) ? 'display:none;' : ''; ?>"> 100 + style="<?php echo esc_attr(empty($custom_image_id) ? 'display:none;' : ''); ?>"> 101 101 <?php esc_html_e("Remove Image", "wireservice"); ?> 102 102 </button> 103 103 <p class="description"><?php esc_html_e("Images must be less than 1MB.", "wireservice"); ?></p>
+29 -172
templates/settings-page.php
··· 42 42 <div class="wrap"> 43 43 <h1><?php echo esc_html(get_admin_page_title()); ?></h1> 44 44 45 - <?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab navigation. 46 - $active_tab = isset($_GET["tab"]) ? sanitize_key($_GET["tab"]) : "settings"; ?> 45 + <?php // $active_tab is set by render_settings_page() before including this template. ?> 47 46 <nav class="nav-tab-wrapper"> 48 47 <a href="<?php echo esc_url(admin_url("options-general.php?page=wireservice&tab=settings")); ?>" 49 - class="nav-tab <?php echo $active_tab === "settings" ? "nav-tab-active" : ""; ?>"> 48 + class="nav-tab <?php echo esc_attr($active_tab === "settings" ? "nav-tab-active" : ""); ?>"> 50 49 <?php esc_html_e("Settings", "wireservice"); ?> 51 50 </a> 52 51 <?php if ($is_connected && $pub_uri): ?> 53 52 <a href="<?php echo esc_url(admin_url("options-general.php?page=wireservice&tab=records")); ?>" 54 - class="nav-tab <?php echo $active_tab === "records" ? "nav-tab-active" : ""; ?>"> 53 + class="nav-tab <?php echo esc_attr($active_tab === "records" ? "nav-tab-active" : ""); ?>"> 55 54 <?php esc_html_e("Records", "wireservice"); ?> 56 55 </a> 57 56 <?php endif; ?> ··· 143 142 </th> 144 143 <td> 145 144 <select name="wireservice_pub_name_source" id="wireservice_pub_name_source" class="regular-text"> 146 - <?php foreach ($name_sources as $key => $source): ?> 147 - <option value="<?php echo esc_attr($key); ?>" data-value="<?php echo esc_attr($source["value"]); ?>" <?php selected($name_source, $key); ?>> 148 - <?php echo esc_html($source["label"]); ?> 145 + <?php foreach ($name_sources as $wireservice_key => $wireservice_source): ?> 146 + <option value="<?php echo esc_attr($wireservice_key); ?>" data-value="<?php echo esc_attr($wireservice_source["value"]); ?>" <?php selected($name_source, $wireservice_key); ?>> 147 + <?php echo esc_html($wireservice_source["label"]); ?> 149 148 </option> 150 149 <?php endforeach; ?> 151 150 </select> ··· 171 170 </th> 172 171 <td> 173 172 <select name="wireservice_pub_description_source" id="wireservice_pub_description_source" class="regular-text"> 174 - <?php foreach ($desc_sources as $key => $source): ?> 175 - <option value="<?php echo esc_attr($key); ?>" data-value="<?php echo esc_attr($source["value"]); ?>" <?php selected($desc_source, $key); ?>> 176 - <?php echo esc_html($source["label"]); ?> 173 + <?php foreach ($desc_sources as $wireservice_key => $wireservice_source): ?> 174 + <option value="<?php echo esc_attr($wireservice_key); ?>" data-value="<?php echo esc_attr($wireservice_source["value"]); ?>" <?php selected($desc_source, $wireservice_key); ?>> 175 + <?php echo esc_html($wireservice_source["label"]); ?> 177 176 </option> 178 177 <?php endforeach; ?> 179 178 </select> 180 179 <p class="description" id="wireservice-pub-desc-current-value"> 181 180 <?php esc_html_e("Current value:", "wireservice"); ?> 182 181 <em class="wireservice-preview-text"><?php 183 - $desc_value = $desc_sources[$desc_source]["value"] ?? $desc_sources["wordpress_tagline"]["value"]; 184 - echo esc_html(mb_substr($desc_value, 0, 100) . (mb_strlen($desc_value) > 100 ? "..." : "")); 182 + $wireservice_desc_value = $desc_sources[$desc_source]["value"] ?? $desc_sources["wordpress_tagline"]["value"]; 183 + echo esc_html(mb_substr($wireservice_desc_value, 0, 100) . (mb_strlen($wireservice_desc_value) > 100 ? "..." : "")); 185 184 ?></em> 186 185 </p> 187 186 <div id="wireservice-pub-custom-desc-field" style="display:none; margin-top: 8px;"> ··· 199 198 </th> 200 199 <td> 201 200 <select name="wireservice_pub_icon_source" id="wireservice_pub_icon_source" class="regular-text"> 202 - <?php foreach ($icon_sources as $key => $source): ?> 203 - <option value="<?php echo esc_attr($key); ?>" <?php selected($icon_source, $key); ?>> 204 - <?php echo esc_html($source["label"]); ?> 201 + <?php foreach ($icon_sources as $wireservice_key => $wireservice_source): ?> 202 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($icon_source, $wireservice_key); ?>> 203 + <?php echo esc_html($wireservice_source["label"]); ?> 205 204 </option> 206 205 <?php endforeach; ?> 207 206 </select> 208 207 <p class="description"><?php esc_html_e("Square image to identify your publication. Should be at least 256×256 and less than 1MB.", "wireservice"); ?></p> 209 - <div id="wireservice-pub-icon-preview" style="margin-top: 8px;<?php echo empty($icon_preview_url) ? ' display:none;' : ''; ?>"> 208 + <div id="wireservice-pub-icon-preview" style="<?php echo esc_attr('margin-top: 8px;' . (empty($icon_preview_url) ? ' display:none;' : '')); ?>"> 210 209 <?php if ($icon_preview_url): ?> 211 210 <img src="<?php echo esc_url($icon_preview_url); ?>" alt="" style="width: 64px; height: 64px; object-fit: cover; border-radius: 4px;"> 212 211 <?php endif; ?> ··· 214 213 <div id="wireservice-pub-custom-icon-field" style="display:none; margin-top: 8px;"> 215 214 <input type="hidden" name="wireservice_pub_custom_icon_id" id="wireservice_pub_custom_icon_id" value="<?php echo esc_attr($custom_icon_id); ?>"> 216 215 <button type="button" class="button" id="wireservice-pub-icon-upload"><?php esc_html_e("Select Image", "wireservice"); ?></button> 217 - <button type="button" class="button" id="wireservice-pub-icon-remove" style="<?php echo empty($custom_icon_id) ? 'display:none;' : ''; ?>"><?php esc_html_e("Remove", "wireservice"); ?></button> 216 + <button type="button" class="button" id="wireservice-pub-icon-remove" style="<?php echo esc_attr(empty($custom_icon_id) ? 'display:none;' : ''); ?>"><?php esc_html_e("Remove", "wireservice"); ?></button> 218 217 <div id="wireservice-pub-custom-icon-preview" style="margin-top: 8px;"> 219 218 <?php if ($custom_icon_id): ?> 220 - <?php $custom_preview = wp_get_attachment_image_url($custom_icon_id, [64, 64]); ?> 221 - <?php if ($custom_preview): ?> 222 - <img src="<?php echo esc_url($custom_preview); ?>" alt="" style="width: 64px; height: 64px; object-fit: cover; border-radius: 4px;"> 219 + <?php $wireservice_custom_preview = wp_get_attachment_image_url($custom_icon_id, [64, 64]); ?> 220 + <?php if ($wireservice_custom_preview): ?> 221 + <img src="<?php echo esc_url($wireservice_custom_preview); ?>" alt="" style="width: 64px; height: 64px; object-fit: cover; border-radius: 4px;"> 223 222 <?php endif; ?> 224 223 <?php endif; ?> 225 224 </div> ··· 323 322 </th> 324 323 <td> 325 324 <select name="wireservice_doc_title_source" id="wireservice_doc_title_source"> 326 - <?php foreach ($doc_title_sources as $key => $label): ?> 327 - <option value="<?php echo esc_attr($key); ?>" <?php selected($doc_title_source, $key); ?>> 328 - <?php echo esc_html($label); ?> 325 + <?php foreach ($doc_title_sources as $wireservice_key => $wireservice_label): ?> 326 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($doc_title_source, $wireservice_key); ?>> 327 + <?php echo esc_html($wireservice_label); ?> 329 328 </option> 330 329 <?php endforeach; ?> 331 330 </select> ··· 338 337 </th> 339 338 <td> 340 339 <select name="wireservice_doc_description_source" id="wireservice_doc_description_source"> 341 - <?php foreach ($doc_desc_sources as $key => $label): ?> 342 - <option value="<?php echo esc_attr($key); ?>" <?php selected($doc_desc_source, $key); ?>> 343 - <?php echo esc_html($label); ?> 340 + <?php foreach ($doc_desc_sources as $wireservice_key => $wireservice_label): ?> 341 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($doc_desc_source, $wireservice_key); ?>> 342 + <?php echo esc_html($wireservice_label); ?> 344 343 </option> 345 344 <?php endforeach; ?> 346 345 </select> ··· 353 352 </th> 354 353 <td> 355 354 <select name="wireservice_doc_image_source" id="wireservice_doc_image_source"> 356 - <?php foreach ($doc_image_sources as $key => $label): ?> 357 - <option value="<?php echo esc_attr($key); ?>" <?php selected($doc_image_source, $key); ?>> 358 - <?php echo esc_html($label); ?> 355 + <?php foreach ($doc_image_sources as $wireservice_key => $wireservice_label): ?> 356 + <option value="<?php echo esc_attr($wireservice_key); ?>" <?php selected($doc_image_source, $wireservice_key); ?>> 357 + <?php echo esc_html($wireservice_label); ?> 359 358 </option> 360 359 <?php endforeach; ?> 361 360 </select> ··· 434 433 435 434 <h2><?php esc_html_e("Reset Plugin Data", "wireservice"); ?></h2> 436 435 <p class="description"><?php esc_html_e("This will remove all plugin settings, stored connections, and document sync data from the database. This action cannot be undone.", "wireservice"); ?></p> 437 - <form method="post" action="<?php echo esc_url(admin_url("admin-post.php")); ?>" onsubmit="return confirm('<?php echo esc_js(__("Are you sure you want to reset all Wireservice data? This cannot be undone.", "wireservice")); ?>');"> 436 + <form method="post" action="<?php echo esc_url(admin_url("admin-post.php")); ?>" id="wireservice-reset-form"> 438 437 <?php wp_nonce_field("wireservice_reset_data", "wireservice_reset_nonce"); ?> 439 438 <input type="hidden" name="action" value="wireservice_reset_data"> 440 439 <button type="submit" class="button button-secondary" style="color: #dc3545;"> ··· 446 445 <?php include WIRESERVICE_PLUGIN_DIR . "templates/records-page.php"; ?> 447 446 <?php endif; ?> 448 447 </div> 449 - <style> 450 - .wireservice-settings { 451 - max-width: 800px; 452 - } 453 - .wireservice-connection-status { 454 - padding: 15px; 455 - border-radius: 4px; 456 - margin: 15px 0; 457 - } 458 - .wireservice-connection-status.connected { 459 - background: #d4edda; 460 - border: 1px solid #c3e6cb; 461 - } 462 - .wireservice-connection-status.disconnected { 463 - background: #f8f9fa; 464 - border: 1px solid #dee2e6; 465 - } 466 - .wireservice-error { 467 - color: #dc3545; 468 - font-weight: 500; 469 - } 470 - .wireservice-profile { 471 - display: flex; 472 - gap: 15px; 473 - align-items: flex-start; 474 - margin-bottom: 15px; 475 - } 476 - .wireservice-avatar { 477 - width: 64px; 478 - height: 64px; 479 - border-radius: 50%; 480 - object-fit: cover; 481 - } 482 - .wireservice-profile-info { 483 - flex: 1; 484 - } 485 - .wireservice-display-name { 486 - display: block; 487 - font-size: 16px; 488 - margin-bottom: 2px; 489 - } 490 - .wireservice-handle { 491 - color: #666; 492 - font-size: 14px; 493 - } 494 - .wireservice-bio { 495 - margin: 8px 0; 496 - font-size: 14px; 497 - color: #333; 498 - } 499 - .wireservice-stats { 500 - display: flex; 501 - gap: 15px; 502 - font-size: 13px; 503 - color: #666; 504 - margin: 8px 0 0; 505 - } 506 - .wireservice-advanced-settings summary { 507 - cursor: pointer; 508 - font-size: 14px; 509 - font-weight: 600; 510 - color: #50575e; 511 - padding: 4px 0; 512 - } 513 - .wireservice-advanced-settings summary:hover { 514 - color: #1d2327; 515 - } 516 - .wireservice-progress-bar { 517 - width: 100%; 518 - height: 20px; 519 - background: #f0f0f1; 520 - border-radius: 3px; 521 - overflow: hidden; 522 - margin: 10px 0; 523 - } 524 - .wireservice-progress-bar-fill { 525 - height: 100%; 526 - background: #2271b1; 527 - transition: width 0.3s ease; 528 - } 529 - .wireservice-backfill-errors { 530 - margin-top: 10px; 531 - } 532 - .wireservice-backfill-errors summary { 533 - cursor: pointer; 534 - color: #d63638; 535 - font-weight: 500; 536 - } 537 - .wireservice-records { 538 - max-width: 800px; 539 - } 540 - .wireservice-record-table { 541 - border-collapse: collapse; 542 - } 543 - .wireservice-record-table th { 544 - width: 160px; 545 - text-align: left; 546 - padding: 8px 12px; 547 - vertical-align: top; 548 - font-weight: 600; 549 - white-space: nowrap; 550 - } 551 - .wireservice-record-table td { 552 - padding: 8px 12px; 553 - word-break: break-all; 554 - } 555 - .wireservice-record-table code { 556 - font-size: 12px; 557 - background: #f0f0f1; 558 - padding: 2px 6px; 559 - border-radius: 3px; 560 - } 561 - .wireservice-document-card { 562 - background: #fff; 563 - border: 1px solid #c3c4c7; 564 - border-radius: 4px; 565 - margin-bottom: 16px; 566 - } 567 - .wireservice-document-card-header { 568 - padding: 12px 16px; 569 - border-bottom: 1px solid #f0f0f1; 570 - } 571 - .wireservice-document-card-header h3 { 572 - margin: 0; 573 - font-size: 14px; 574 - } 575 - .wireservice-document-card-body { 576 - padding: 0; 577 - } 578 - .wireservice-document-card-body .wireservice-record-table { 579 - border: none; 580 - } 581 - .wireservice-color-swatch { 582 - display: inline-block; 583 - width: 16px; 584 - height: 16px; 585 - border-radius: 3px; 586 - border: 1px solid #ddd; 587 - vertical-align: middle; 588 - margin-right: 4px; 589 - } 590 - </style>
+8 -12
uninstall.php
··· 28 28 delete_transient("wireservice_oauth_state"); 29 29 30 30 // Remove post meta for documents. 31 - global $wpdb; 32 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_document_uri"]); 33 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_title_source"]); 34 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_description_source"]); 35 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_image_source"]); 36 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_custom_title"]); 37 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_custom_description"]); 38 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_custom_image_id"]); 39 - $wpdb->delete($wpdb->postmeta, ["meta_key" => "_wireservice_include_content"]); 40 - 41 - // Remove any custom tables. 42 - $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}wireservice_logs"); 31 + delete_post_meta_by_key("_wireservice_document_uri"); 32 + delete_post_meta_by_key("_wireservice_title_source"); 33 + delete_post_meta_by_key("_wireservice_description_source"); 34 + delete_post_meta_by_key("_wireservice_image_source"); 35 + delete_post_meta_by_key("_wireservice_custom_title"); 36 + delete_post_meta_by_key("_wireservice_custom_description"); 37 + delete_post_meta_by_key("_wireservice_custom_image_id"); 38 + delete_post_meta_by_key("_wireservice_include_content");
+1 -1
wireservice.php
··· 2 2 declare(strict_types=1); 3 3 /** 4 4 * Plugin Name: Wireservice 5 - * Plugin URI: https://wireservice.net 5 + * Plugin URI: https://wordpress.wireservice.net 6 6 * Description: A WordPress plugin for publishing posts to the AT Protocol based on the standard.site lexicon. 7 7 * Version: 1.1.0 8 8 * Author: Tyler Fisher