From ddaaff71ad9a29566d10bb877f1dcfb4acea1383 Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Sat, 28 Mar 2026 07:59:52 +0000 Subject: [PATCH] v1.1.0 --- CHANGELOG.md | 38 -- README.md | 132 ------ admin/class-robotstxt-og-admin-settings.php | 47 +- admin/views/settings-page.php | 20 +- changelog.txt | 29 +- docs/FILTERS-HOOKS.md | 240 ---------- docs/OG-TWITTER-REFERENCE.md | 183 ------- docs/WP-CLI.md | 103 ---- includes/class-robotstxt-og-cli.php | 12 +- .../class-robotstxt-og-image-fallback.php | 179 ++++++- .../class-robotstxt-og-image-resolver.php | 80 +++- includes/class-robotstxt-og-meta-box.php | 10 +- includes/class-robotstxt-og-rest-api.php | 25 +- includes/class-robotstxt-og-tags.php | 22 +- languages/robotstxt-og-ca.mo | Bin 0 -> 11046 bytes languages/robotstxt-og-ca.po | 447 ++++++++++++++++++ languages/robotstxt-og-es_ES.mo | Bin 10610 -> 10998 bytes languages/robotstxt-og-es_ES.po | 24 +- readme.txt | 25 +- robotstxt-og.php | 9 +- robotstxt-updater.php | 36 +- uninstall.php | 4 +- update.json | 12 +- 23 files changed, 879 insertions(+), 798 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 README.md delete mode 100644 docs/FILTERS-HOOKS.md delete mode 100644 docs/OG-TWITTER-REFERENCE.md delete mode 100644 docs/WP-CLI.md create mode 100644 languages/robotstxt-og-ca.mo create mode 100644 languages/robotstxt-og-ca.po diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index bd297af..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,38 +0,0 @@ -# Changelog - -All notable changes to OpenGraph (by ROBOTSTXT) will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [1.0.0] - 2026-02-18 - -### Added -- Automatic format detection for AVIF, WebP, GIF, BMP, SVG, and TIFF featured images -- HTTP HEAD-based resolution of compatible JPEG/PNG alternatives -- Postmeta caching (`_og_image_fallback_url`) with automatic invalidation on featured image change -- Negative caching via transients (1-hour TTL) to avoid repeated failed HEAD requests -- Open Graph meta tags: `og:image`, `og:image:secure_url`, `og:image:width`, `og:image:height`, `og:image:type` -- Direct `wp_head` tag injection (when no SEO plugin is active) -- Yoast SEO integration via `wpseo_opengraph_image` filter -- RankMath integration via `rank_math/opengraph/facebook/og_image` filter -- Global fallback image setting (configurable in Settings > OpenGraph) -- Taxonomy archive image support via `robotstxt_og_taxonomy_image` filter -- Admin settings page (Settings > OpenGraph) with three tabs: Settings, Tools, Diagnostics -- WP-CLI commands: `wp og-fallback resolve` and `wp og-fallback clear-cache` -- REST API: `POST /wp-json/robotstxt-og/v1/resolve/{post_id}` and `GET /wp-json/robotstxt-og/v1/status/{post_id}` -- Developer filters: `robotstxt_og_external_image_enabled`, `robotstxt_og_external_image_timeout`, `robotstxt_og_taxonomy_image`, `robotstxt_og_enable_logging` -- Auto-updater via Gitea (`robotstxt-updater.php`) -- Multisite compatible with per-site configuration -- Full internationalization support (text domain: `robotstxt-og`) -- `uninstall.php` with optional data deletion on plugin removal - -### Security -- Nonce verification on all forms and state-changing actions -- `manage_options` capability check on all admin pages, REST endpoints, and WP-CLI commands -- Input sanitization via WordPress APIs throughout -- Output escaping on all dynamic HTML -- WordPress database APIs exclusively (no direct SQL writes; `$wpdb->prepare()` for reads) -- CSRF protection on all state-changing actions - -[1.0.0]: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/tag/1.0.0 diff --git a/README.md b/README.md deleted file mode 100644 index 1838829..0000000 --- a/README.md +++ /dev/null @@ -1,132 +0,0 @@ -# OpenGraph (by ROBOTSTXT) - -> Intelligent Open Graph image fallback for social media crawlers. - -Automatically detects when a post's featured image uses a modern format (AVIF, WebP) that social media crawlers cannot render, and resolves a compatible JPEG/PNG alternative to use in `og:image` meta tags. - -## Requirements - -- **WordPress:** 6.7+ -- **PHP:** 8.2–8.5 -- **WP-CLI:** 2.x (optional, for CLI tools) - -## Installation - -```bash -# From the plugin directory -composer install -``` - -Activate the plugin through the WordPress admin or WP-CLI: - -```bash -wp plugin activate robotstxt-og --allow-root -``` - -## Architecture - -``` -robotstxt-og/ -├── robotstxt-og.php # Main plugin file, constants, bootstrap -├── includes/ -│ ├── class-robotstxt-og-image-fallback.php # Singleton, orchestration -│ ├── class-robotstxt-og-image-resolver.php # Image detection, HEAD requests, caching -│ ├── class-robotstxt-og-tags.php # OG tag injection, SEO plugin filters -│ ├── class-robotstxt-og-cli.php # WP-CLI commands -│ └── class-robotstxt-og-rest-api.php # REST API endpoints -├── admin/ -│ ├── class-robotstxt-og-admin-settings.php # Admin page, settings, cache actions -│ └── views/settings-page.php # Settings page template (tabbed) -├── assets/ -│ ├── admin.js # Media uploader integration -│ └── admin.css # Admin styles -├── languages/ -│ └── robotstxt-og.pot # Translation template -├── docs/ -│ ├── FILTERS-HOOKS.md # Developer filter/hook reference -│ ├── WP-CLI.md # WP-CLI command reference -│ └── IDEA.md # Original concept document -├── bin/ -│ └── deploy.sh # Build and ZIP packaging script -├── robotstxt-updater.php # Auto-updater (Gitea-based) -├── uninstall.php # Data cleanup on uninstall -├── readme.txt # WordPress.org plugin readme -└── changelog.txt # Full changelog (WordPress.org format) -``` - -## Development - -### Code Standards - -```bash -# Lint -vendor/bin/phpcs . - -# Auto-fix -vendor/bin/phpcbf . - -# PHP compatibility check (8.2–8.5) -vendor/bin/phpcs -p . --standard=PHPCompatibility --runtime-set testVersion 8.2-8.5 -``` - -### Regenerate translation template - -```bash -wp i18n make-pot . languages/robotstxt-og.pot --domain=robotstxt-og --exclude=vendor,node_modules,tests --allow-root -``` - -### Build release ZIP - -```bash -bash bin/deploy.sh -``` - -The ZIP is created at `../robotstxt-og-{version}.zip` (i.e. in `wp-content/plugins/`). - -## WP-CLI Commands - -```bash -# Resolve fallback for a single post -wp og-fallback resolve 123 - -# Re-resolve all posts with featured images -wp og-fallback resolve --all - -# Dry-run (no changes saved) -wp og-fallback resolve --all --dry-run - -# Clear cache for a single post -wp og-fallback clear-cache 123 - -# Clear all caches -wp og-fallback clear-cache --all -``` - -## REST API - -All endpoints require the `manage_options` capability. - -| Method | Endpoint | Description | -|--------|----------|-------------| -| `POST` | `/wp-json/robotstxt-og/v1/resolve/{post_id}` | Force re-resolve fallback image | -| `GET` | `/wp-json/robotstxt-og/v1/status/{post_id}` | Get current cache status | - -## Filters - -| Filter | Default | Description | -|--------|---------|-------------| -| `robotstxt_og_external_image_enabled` | `true` | Enable/disable external image resolution | -| `robotstxt_og_external_image_timeout` | `5` | HEAD request timeout (seconds) | -| `robotstxt_og_taxonomy_image` | `''` | Image URL for taxonomy archive pages | -| `robotstxt_og_enable_logging` | `false` | Enable debug logging to `debug.log` | - -Full documentation: [`docs/FILTERS-HOOKS.md`](docs/FILTERS-HOOKS.md) - -## License - -GPL-3.0-or-later — see [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) - -## Author - -**ROBOTSTXT** — [robotstxt.es](https://www.robotstxt.es/) -Contributors: javiercasares, robotstxt diff --git a/admin/class-robotstxt-og-admin-settings.php b/admin/class-robotstxt-og-admin-settings.php index bcec130..540a298 100644 --- a/admin/class-robotstxt-og-admin-settings.php +++ b/admin/class-robotstxt-og-admin-settings.php @@ -54,6 +54,24 @@ class Robotstxt_OG_Admin_Settings { add_action( 'admin_init', array( $this, 'handle_resolve_all' ) ); add_action( 'admin_init', array( $this, 'handle_clear_single' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) ); + + // Allow editors (edit_others_posts) to save settings via the Settings API. + // options.php defaults to manage_options; this filter lowers it for our group. + add_filter( 'option_page_capability_robotstxt_og_settings', array( $this, 'get_settings_capability' ) ); + } + + /** + * Return the capability required to save plugin settings. + * + * Allows users with edit_others_posts (editors and above) to save plugin + * settings through the WordPress Settings API (options.php). + * + * @since 1.0.3 + * + * @return string Capability slug. + */ + public function get_settings_capability(): string { + return 'edit_others_posts'; } /** @@ -77,7 +95,7 @@ class Robotstxt_OG_Admin_Settings { } // Verify nonce. - $nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_SPECIAL_CHARS ); + $nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_clear_single_' . $post_id ) ) { @@ -85,7 +103,7 @@ class Robotstxt_OG_Admin_Settings { } // Check permissions. - if ( ! current_user_can( 'manage_options' ) ) { + if ( ! current_user_can( 'edit_others_posts' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-og' ) ); } @@ -120,7 +138,7 @@ class Robotstxt_OG_Admin_Settings { add_options_page( __( 'OpenGraph Settings', 'robotstxt-og' ), __( 'OpenGraph', 'robotstxt-og' ), - 'manage_options', + 'edit_others_posts', 'robotstxt-og-settings', array( $this, 'render_settings_page' ) ); @@ -344,7 +362,8 @@ class Robotstxt_OG_Admin_Settings { * @return void */ public function render_fallback_image_field(): void { - $image_url = (string) get_option( 'robotstxt_og_fallback_image', '' ); + $option_value = get_option( 'robotstxt_og_fallback_image', '' ); + $image_url = is_string( $option_value ) ? $option_value : ''; ?> ' . esc_html__( 'Value', 'robotstxt-og' ) . ''; foreach ( $headers as $header_key => $header_value ) { echo '' . esc_html( $header_key ) . ''; - echo '' . esc_html( is_array( $header_value ) ? implode( ', ', $header_value ) : $header_value ) . ''; + if ( is_array( $header_value ) ) { + $display_value = implode( ', ', array_map( static fn( $v ) => is_scalar( $v ) ? (string) $v : '', $header_value ) ); + } elseif ( is_string( $header_value ) ) { + $display_value = $header_value; + } else { + $display_value = ''; + } + echo '' . esc_html( $display_value ) . ''; } echo ''; } @@ -324,7 +334,7 @@ $page_url = admin_url( 'options-general.php?page=robotstxt-og-settings' ); post_id ); ?> - + post_title ); ?> diff --git a/changelog.txt b/changelog.txt index f0e123e..989399e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,18 +1,39 @@ == Changelog == -= 1.0.1 = += 1.1.0 = -_Release date: 2026-03-05_ +_Release date: 2026-03-28_ -**Compatibility** +**Fixed** -* Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0. +* Fatal `TypeError` in `handle_thumbnail_change()`: the `deleted_post_meta` action passes an array of meta IDs as its first argument (unlike `updated_post_meta`, which passes a single `int`). The method signature now accepts `int|array` to handle both actions correctly. + +**Security** + +* Added SSRF protection in `Robotstxt_OG_Image_Resolver::is_safe_url()`: all outbound HTTP HEAD requests now validate that the target host resolves to a public IP address. Private (RFC 1918), loopback, link-local, and reserved ranges are blocked. +* Replaced deprecated `FILTER_SANITIZE_SPECIAL_CHARS` with `FILTER_SANITIZE_FULL_SPECIAL_CHARS` in nonce and input handling throughout admin and meta box classes. + +**Added** + +* GDPR Privacy API: registered `wp_privacy_personal_data_exporters` and `wp_privacy_personal_data_erasers` filters. Custom `_og_title` and `_og_description` post meta are now included in WordPress personal data export and erase requests. +* PHPUnit 10 test suite using Brain\Monkey for WordPress function mocking. Covers SSRF IP validation, image format detection (`ensure_compatible_format()`), external URL detection, and cache clearing. No WordPress installation required to run tests (`vendor/bin/phpunit`). + +**Changed** + +* Settings page and REST API permission check changed from `manage_options` to `edit_others_posts`. Editors and above can now access OG settings and trigger fallback image resolution. The WordPress Settings API form is unlocked via the `option_page_capability_robotstxt_og_settings` filter. +* REST API `check_permission()` now also verifies `edit_post` for the specific post ID requested. +* Minimum supported WordPress version raised from 6.7 to 6.8 (two previous major versions back from WordPress 7.0). +* Added `Network: true` plugin header, formally documenting Multisite compatibility. **Tests** +* WordPress: 6.8, 6.9, 7.0 +* PHP: 8.2, 8.5 * PHP_CodeSniffer: 3.13.5 * WordPress Coding Standards: 3.3.0 * PHPCompatibility: 9.3.5 +* PHPStan: level 9 +* PHPUnit: 10.5 = 1.0.0 = diff --git a/docs/FILTERS-HOOKS.md b/docs/FILTERS-HOOKS.md deleted file mode 100644 index 51a8ac3..0000000 --- a/docs/FILTERS-HOOKS.md +++ /dev/null @@ -1,240 +0,0 @@ -# Filters & Hooks Reference - -Developer reference for all filters and actions provided by the **OpenGraph (by ROBOTSTXT)** plugin. - -## Table of Contents - -- [Filters](#filters) - - [robotstxt_og_external_image_enabled](#robotstxt_og_external_image_enabled) - - [robotstxt_og_external_image_timeout](#robotstxt_og_external_image_timeout) - - [robotstxt_og_taxonomy_image](#robotstxt_og_taxonomy_image) - - [robotstxt_og_enable_logging](#robotstxt_og_enable_logging) -- [Actions](#actions) -- [SEO Plugin Integrations](#seo-plugin-integrations) - ---- - -## Filters - -### `robotstxt_og_external_image_enabled` - -Controls whether the plugin attempts to resolve fallback images for external URLs (images hosted on a different domain than the WordPress site). - -**Default:** `true` - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$enabled` | `bool` | Whether external image resolution is enabled. | -| `$image_url` | `string` | The external image URL being evaluated. | - -**Returns:** `bool` - -**Example — disable external image resolution entirely:** - -```php -add_filter( 'robotstxt_og_external_image_enabled', '__return_false' ); -``` - -**Example — disable only for a specific CDN domain:** - -```php -add_filter( 'robotstxt_og_external_image_enabled', function ( bool $enabled, string $image_url ): bool { - if ( str_contains( $image_url, 'cdn.example.com' ) ) { - return false; - } - return $enabled; -}, 10, 2 ); -``` - ---- - -### `robotstxt_og_external_image_timeout` - -Sets the HTTP request timeout (in seconds) used when verifying whether a fallback image URL exists via a HEAD request. - -**Default:** `5` (seconds) - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$timeout` | `int` | Timeout in seconds for the HEAD request. | -| `$url` | `string` | The image URL being tested. | - -**Returns:** `int` - -**Example — increase timeout for slow external servers:** - -```php -add_filter( 'robotstxt_og_external_image_timeout', function ( int $timeout, string $url ): int { - if ( str_contains( $url, 'slow-cdn.example.com' ) ) { - return 15; - } - return $timeout; -}, 10, 2 ); -``` - -**Example — set a global lower timeout for performance:** - -```php -add_filter( 'robotstxt_og_external_image_timeout', function (): int { - return 3; -} ); -``` - ---- - -### `robotstxt_og_taxonomy_image` - -Provides a fallback OG image URL for taxonomy archive pages (categories, tags, custom taxonomies). By default, taxonomy archives do not have a featured image, so this filter is the primary way to supply one. - -**Default:** `''` (empty string — no image) - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$image_url` | `string` | Image URL to use. Empty string by default. | -| `$term_id` | `int` | The term ID of the current taxonomy archive. | - -**Returns:** `string` A valid image URL, or empty string to skip. - -**Example — use a custom field set on the term:** - -```php -add_filter( 'robotstxt_og_taxonomy_image', function ( string $image_url, int $term_id ): string { - $custom_image_id = get_term_meta( $term_id, 'og_image_id', true ); - - if ( $custom_image_id ) { - $url = wp_get_attachment_url( (int) $custom_image_id ); - return $url ? $url : $image_url; - } - - return $image_url; -}, 10, 2 ); -``` - -**Example — use a WooCommerce category thumbnail:** - -```php -add_filter( 'robotstxt_og_taxonomy_image', function ( string $image_url, int $term_id ): string { - $thumbnail_id = get_term_meta( $term_id, 'thumbnail_id', true ); - - if ( $thumbnail_id ) { - $url = wp_get_attachment_url( (int) $thumbnail_id ); - return $url ? $url : $image_url; - } - - return $image_url; -}, 10, 2 ); -``` - ---- - -### `robotstxt_og_enable_logging` - -Enables or disables debug logging to `wp-content/debug.log`. When enabled, resolution events (cache hits, cache misses, format detection, HEAD request results) are written to the error log. - -**Default:** `false` - -**Parameters:** - -| Parameter | Type | Description | -|-----------|------|-------------| -| `$enabled` | `bool` | Whether debug logging is active. | - -**Returns:** `bool` - -**Note:** Requires `WP_DEBUG` and `WP_DEBUG_LOG` to be enabled in `wp-config.php` for output to appear in `debug.log`. - -**Example — enable logging (e.g. during development, in `wp-config.php`):** - -```php -// wp-config.php -define( 'WP_DEBUG', true ); -define( 'WP_DEBUG_LOG', true ); -``` - -```php -// functions.php or a mu-plugin -add_filter( 'robotstxt_og_enable_logging', '__return_true' ); -``` - -**Example — enable logging only for specific users:** - -```php -add_filter( 'robotstxt_og_enable_logging', function ( bool $enabled ): bool { - return current_user_can( 'manage_options' ) ? true : $enabled; -} ); -``` - ---- - -## Actions - -The plugin does not currently expose custom action hooks. WordPress core hooks used internally include: - -| Hook | Context | Purpose | -|------|---------|---------| -| `plugins_loaded` | Global | Loads text domain for translations. | -| `wp_head` | Frontend | Injects `og:image` meta tags (only when no SEO plugin is active). | -| `admin_menu` | Admin | Registers the Settings > OpenGraph settings page. | -| `admin_init` | Admin | Registers settings, handles cache clear/resolve actions. | -| `admin_enqueue_scripts` | Admin | Enqueues media uploader and admin CSS/JS. | -| `rest_api_init` | REST API | Registers the `robotstxt-og/v1` REST endpoints. | -| `updated_post_meta` | Global | Auto-clears fallback cache when `_thumbnail_id` changes. | -| `deleted_post_meta` | Global | Auto-clears fallback cache when `_thumbnail_id` is removed. | - ---- - -## SEO Plugin Integrations - -When a supported SEO plugin is detected, the plugin switches from direct `og:image` tag injection to filtering the SEO plugin's output. This prevents duplicate meta tags. - -### Yoast SEO - -**Filter:** `wpseo_opengraph_image` - -When Yoast SEO is active (`WPSEO_VERSION` is defined), the plugin hooks into this filter to provide the resolved fallback image. The plugin only overrides the value if a valid fallback URL is resolved; otherwise it returns the original Yoast value unchanged. - -### RankMath - -**Filter:** `rank_math/opengraph/facebook/og_image` - -When RankMath is active (`RankMath` class exists), the plugin hooks into this filter with the same logic as the Yoast integration. - -### Adding Support for Other SEO Plugins - -To integrate with another SEO plugin, hook into the plugin's OG image filter and call the resolver manually: - -```php -add_filter( 'your_seo_plugin_og_image_filter', function ( string $image ) : string { - if ( ! is_singular() ) { - return $image; - } - - $post_id = get_queried_object_id(); - $resolver = Robotstxt_OG_Image_Fallback::get_instance()->get_resolver(); - $fallback = $resolver->get_fallback_image( $post_id ); - - return ! empty( $fallback ) ? $fallback : $image; -} ); -``` - ---- - -## Postmeta Keys (Internal Cache) - -These postmeta keys are used internally for caching and should not be modified directly: - -| Meta Key | Type | Description | -|----------|------|-------------| -| `_og_image_fallback_url` | `string` | Cached resolved fallback image URL for a post. | - -These transient keys are used for negative caching (failed HEAD request results): - -| Transient Key Pattern | TTL | Description | -|-----------------------|-----|-------------| -| `robotstxt_og_miss_{md5_of_url}` | 1 hour | Marks a URL as unreachable to prevent repeated requests. | diff --git a/docs/OG-TWITTER-REFERENCE.md b/docs/OG-TWITTER-REFERENCE.md deleted file mode 100644 index c6a9e74..0000000 --- a/docs/OG-TWITTER-REFERENCE.md +++ /dev/null @@ -1,183 +0,0 @@ -# Open Graph & Twitter Cards — Reference - -This document covers the Open Graph (OG) and Twitter Cards meta tag specifications relevant to this plugin, including which tags are mandatory, recommended, optional, and which this plugin outputs automatically. - ---- - -## Open Graph Protocol - -Defined by Facebook/Meta. All OG tags use the `property` attribute. - -### Core Tags (og: namespace) - -| Property | Type | Required | Notes | -|---|---|---|---| -| `og:title` | string | **Required** | Title of the content. Used by all social crawlers. | -| `og:type` | string | **Required** | Content type: `website`, `article`, `video.movie`, etc. | -| `og:url` | URL | **Required** | Canonical URL of the page. | -| `og:description` | string | Recommended | Short description (2–4 sentences). Max ~300 chars. | -| `og:site_name` | string | Recommended | Name of the overall site (e.g. "My Blog"). | -| `og:locale` | string | Recommended | Locale in `language_TERRITORY` format (e.g. `es_ES`). | -| `og:image` | URL | **Required** (for cards) | Must be JPEG or PNG for social crawler compatibility. Min 200×200 px. Recommended 1200×630 px. | -| `og:image:secure_url` | URL | Optional | HTTPS version of `og:image`. Same value when the site is HTTPS-only. | -| `og:image:type` | MIME type | Recommended | MIME type of the image (`image/jpeg`, `image/png`). | -| `og:image:width` | integer | Recommended | Width in pixels. Avoids reflow in crawler previews. | -| `og:image:height` | integer | Recommended | Height in pixels. | -| `og:image:alt` | string | Recommended | Alt text for the image. Required for accessibility audits. | - -### Article Tags (article: namespace) - -Used when `og:type = article`. All are optional but recommended for news/blog content. - -| Property | Type | Notes | -|---|---|---| -| `article:published_time` | ISO 8601 datetime | Publication date (`c` format in PHP: `get_the_date('c')`). | -| `article:modified_time` | ISO 8601 datetime | Last modification date. | -| `article:author` | URL | Profile page of the author (Facebook profile URL). Often omitted. | -| `article:section` | string | Primary category or section (e.g. "Technology"). | -| `article:tag` | string | Topic tags. Can be repeated once per tag. | -| `article:expiration_time` | ISO 8601 datetime | When the article expires (rarely used). | - -### Video Tags (video: namespace) - -For `og:type = video.movie`, `video.episode`, etc. Out of scope for this plugin. - ---- - -## Twitter Cards - -Defined by X (formerly Twitter). Tags use the `name` attribute (not `property`). - -Twitter falls back to `og:*` tags if the corresponding `twitter:*` tag is absent — **except** `twitter:card`, which is always required. - -### Card Types - -| Value | Description | -|---|---| -| `summary` | Small square image (minimum 144×144 px). | -| `summary_large_image` | Large rectangular image (minimum 300×157 px, recommended 1200×628 px). Most common for blog/news content. | -| `app` | Promotes a mobile app. | -| `player` | Embeds a video/audio player. | - -### Twitter Tags - -| Name | Required | Falls back to | Notes | -|---|---|---|---| -| `twitter:card` | **Required** | — | Must always be present. Without it, no Twitter Card is shown. | -| `twitter:site` | Recommended | — | `@username` of the site's Twitter/X account. | -| `twitter:creator` | Optional | — | `@username` of the content author. | -| `twitter:title` | Recommended | `og:title` | Title of the content. | -| `twitter:description` | Recommended | `og:description` | Description. Max 200 chars. | -| `twitter:image` | Recommended | `og:image` | Must be JPEG, PNG, WebP, or GIF. Max 5 MB. | -| `twitter:image:alt` | Recommended | `og:image:alt` | Alt text for the image. Max 420 chars. | - ---- - -## What This Plugin Outputs - -### When no SEO plugin is active (direct injection) - -The plugin outputs a **complete** set of OG and Twitter Card tags via `wp_head` (priority 5). - -#### Open Graph tags - -```html - - - - - - - - - - - - - - - - - - - - - -``` - -#### Twitter Card tags - -```html - - - -``` - -> Twitter falls back to `og:title`, `og:description`, and `og:image` automatically, so those tags are not duplicated. - -### When Yoast SEO or RankMath is active - -The plugin acts as a **corrector only**: it filters the image URL via the SEO plugin's filter hook, converting incompatible formats (AVIF/WebP) to JPEG/PNG. All other OG/Twitter tags are managed by the SEO plugin. - ---- - -## og:type Values Reference - -| Value | When to use | -|---|---| -| `website` | Default for homepages and most pages. | -| `article` | Blog posts, news articles. This plugin uses this for `is_singular('post')`. | -| `profile` | User profile pages. | -| `video.movie` | Movie pages. | -| `video.episode` | TV episode pages. | -| `music.song` | Song pages. | -| `music.album` | Album pages. | - ---- - -## Per-Post Overrides (Editor Meta Box) - -The plugin adds an **"Open Graph / Social Media"** meta box to all post editors, allowing per-post overrides of: - -| Field | OG Tag | Fallback | -|---|---|---| -| Custom Title | `og:title` | Post title (`get_the_title()`) | -| Custom Description | `og:description` | Post excerpt, or empty | - -These overrides are stored as post meta: -- `_og_title` — custom OG title -- `_og_description` — custom OG description - ---- - -## Context → og:type Mapping (this plugin) - -| WordPress context | `og:type` | -|---|---| -| `is_singular('post')` | `article` | -| `is_singular('page')` | `website` | -| `is_singular(other)` | `website` | -| `is_front_page()` / `is_home()` | `website` | -| `is_tax()` / `is_category()` / `is_tag()` | `website` | - ---- - -## Image Compatibility - -Social crawlers (Facebook, X, LinkedIn, WhatsApp, Telegram) generally require: -- Format: **JPEG or PNG** (WebP partial support; AVIF not supported) -- Minimum size: 200×200 px (Facebook requires 200×200 for `summary`) -- Recommended: 1200×630 px for `summary_large_image` -- Max file size: 8 MB (Facebook), 5 MB (Twitter) - -This plugin's core function is to detect when a featured image is in an incompatible format (AVIF, WebP) and automatically serve a JPEG/PNG alternative via fallback resolution. - ---- - -## Validation Tools - -- **Facebook**: [Sharing Debugger](https://developers.facebook.com/tools/debug/) -- **Twitter/X**: [Card Validator](https://cards-dev.twitter.com/validator) -- **LinkedIn**: [Post Inspector](https://www.linkedin.com/post-inspector/) -- **OpenGraph.xyz**: [OpenGraph preview](https://www.opengraph.xyz/) -- **Metatags.io**: [Meta tag preview](https://metatags.io/) diff --git a/docs/WP-CLI.md b/docs/WP-CLI.md deleted file mode 100644 index 800cff9..0000000 --- a/docs/WP-CLI.md +++ /dev/null @@ -1,103 +0,0 @@ -# WP-CLI Command Reference - -The plugin registers WP-CLI commands under the `og-fallback` namespace. - -## Commands - -### `wp og-fallback resolve` - -Resolves (or re-resolves) the OG fallback image for one or all posts with featured images. - -**Usage:** - -```bash -wp og-fallback resolve [] [--all] [--dry-run] [--post-type=] -``` - -**Arguments:** - -| Argument | Description | -|----------|-------------| -| `` | (optional) Single post ID to resolve. | -| `--all` | Process all posts that have a featured image. | -| `--dry-run` | Preview without making any changes. | -| `--post-type=` | Limit `--all` to a specific post type (default: `any`). | - -**Examples:** - -```bash -# Resolve fallback for a single post -wp og-fallback resolve 123 - -# Preview for a single post without saving -wp og-fallback resolve 123 --dry-run - -# Re-resolve all posts (clears cache first) -wp og-fallback resolve --all - -# Re-resolve only 'product' post type posts -wp og-fallback resolve --all --post-type=product - -# Dry-run all (shows count, no changes) -wp og-fallback resolve --all --dry-run -``` - -**Output examples:** - -``` -Success: Post 123 resolved to https://example.com/uploads/image.jpg -Warning: Post 456: no compatible image found. -Success: Resolved 47 posts. Failed: 2. -Found 49 posts with featured images. (dry-run, no changes made) -``` - ---- - -### `wp og-fallback clear-cache` - -Deletes cached fallback URLs from postmeta. - -**Usage:** - -```bash -wp og-fallback clear-cache [] [--all] [--dry-run] -``` - -**Arguments:** - -| Argument | Description | -|----------|-------------| -| `` | (optional) Single post ID to clear cache for. | -| `--all` | Clear all cached fallback URLs. | -| `--dry-run` | Preview without making any changes. | - -**Examples:** - -```bash -# Clear cache for a single post -wp og-fallback clear-cache 123 - -# Clear all cached fallback URLs -wp og-fallback clear-cache --all - -# Preview how many entries would be cleared -wp og-fallback clear-cache --all --dry-run -``` - -**Output examples:** - -``` -Success: Cleared cached fallback URL for post 123. -Warning: Post 456 has no cached fallback URL. -Success: Cleared 47 cached fallback URLs. -Found 47 cached fallback URLs. (dry-run, no changes made) -``` - ---- - -## Notes - -- Both commands require WP-CLI 2.x. -- No capability check is enforced at the CLI level (WP-CLI access implies server-level trust). -- The `--all` flag with `resolve` is equivalent to clicking **Re-resolve All Images** in the admin Tools tab. -- The `--all` flag with `clear-cache` is equivalent to clicking **Clear All Cached URLs** in the admin Tools tab. diff --git a/includes/class-robotstxt-og-cli.php b/includes/class-robotstxt-og-cli.php index f0be40c..17d2173 100644 --- a/includes/class-robotstxt-og-cli.php +++ b/includes/class-robotstxt-og-cli.php @@ -87,14 +87,14 @@ class Robotstxt_OG_CLI extends WP_CLI_Command { * * @since 1.0.0 * - * @param array $args Positional arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. * @return void */ public function resolve( array $args, array $assoc_args ): void { $all = isset( $assoc_args['all'] ); $dry_run = isset( $assoc_args['dry-run'] ); - $post_type = isset( $assoc_args['post-type'] ) ? $assoc_args['post-type'] : 'any'; + $post_type = ( isset( $assoc_args['post-type'] ) && is_string( $assoc_args['post-type'] ) ) ? $assoc_args['post-type'] : 'any'; if ( $all ) { $this->resolve_all( $dry_run, $post_type ); @@ -132,8 +132,8 @@ class Robotstxt_OG_CLI extends WP_CLI_Command { * * @since 1.0.0 * - * @param array $args Positional arguments. - * @param array $assoc_args Associative arguments. + * @param array $args Positional arguments. + * @param array $assoc_args Associative arguments. * @return void */ public function clear_cache( array $args, array $assoc_args ): void { @@ -320,7 +320,7 @@ class Robotstxt_OG_CLI extends WP_CLI_Command { if ( $dry_run ) { /* translators: 1: post ID, 2: cached URL or 'none' */ - WP_CLI::line( sprintf( __( 'Post %1$d cached URL: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ! empty( $cached ) ? $cached : 'none' ) ); + WP_CLI::line( sprintf( __( 'Post %1$d cached URL: %2$s (dry-run, no changes made)', 'robotstxt-og' ), $post_id, ( is_string( $cached ) && ! empty( $cached ) ) ? $cached : 'none' ) ); return; } diff --git a/includes/class-robotstxt-og-image-fallback.php b/includes/class-robotstxt-og-image-fallback.php index de0425c..f9825ff 100644 --- a/includes/class-robotstxt-og-image-fallback.php +++ b/includes/class-robotstxt-og-image-fallback.php @@ -136,6 +136,10 @@ class Robotstxt_OG_Image_Fallback { add_action( 'updated_post_meta', array( $this, 'handle_thumbnail_change' ), 10, 4 ); add_action( 'deleted_post_meta', array( $this, 'handle_thumbnail_change' ), 10, 4 ); + // Register GDPR privacy data handlers. + add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_privacy_exporter' ) ); + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_privacy_eraser' ) ); + // Register activation and deactivation hooks. register_activation_hook( ROBOTSTXT_OG_PATH . 'robotstxt-og.php', array( $this, 'activate' ) ); register_deactivation_hook( ROBOTSTXT_OG_PATH . 'robotstxt-og.php', array( $this, 'deactivate' ) ); @@ -209,13 +213,13 @@ class Robotstxt_OG_Image_Fallback { * * @since 1.0.0 * - * @param int $meta_id ID of the meta data entry. - * @param int $post_id Post ID. - * @param string $meta_key Meta key being updated. - * @param mixed $meta_value New meta value (unused). + * @param int|int[] $meta_id ID or array of IDs of the meta data entry. + * @param int $post_id Post ID. + * @param string $meta_key Meta key being updated. + * @param mixed $meta_value New meta value (unused). * @return void */ - public function handle_thumbnail_change( int $meta_id, int $post_id, string $meta_key, $meta_value = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed + public function handle_thumbnail_change( int|array $meta_id, int $post_id, string $meta_key, $meta_value = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed if ( '_thumbnail_id' !== $meta_key ) { return; } @@ -223,6 +227,171 @@ class Robotstxt_OG_Image_Fallback { $this->resolver->clear_cache( $post_id ); } + /** + * Register the personal data exporter. + * + * @since 1.0.3 + * + * @param array> $exporters List of exporters. + * @return array> Updated exporters list. + */ + public function register_privacy_exporter( array $exporters ): array { + $exporters[] = array( + 'exporter_friendly_name' => __( 'OpenGraph Custom Post Data', 'robotstxt-og' ), + 'callback' => array( $this, 'export_privacy_data' ), + ); + return $exporters; + } + + /** + * Register the personal data eraser. + * + * @since 1.0.3 + * + * @param array> $erasers List of erasers. + * @return array> Updated erasers list. + */ + public function register_privacy_eraser( array $erasers ): array { + $erasers[] = array( + 'eraser_friendly_name' => __( 'OpenGraph Custom Post Data', 'robotstxt-og' ), + 'callback' => array( $this, 'erase_privacy_data' ), + ); + return $erasers; + } + + /** + * Export personal data stored by this plugin for a given email address. + * + * Exports the custom og:title and og:description stored in post meta for + * all posts authored by the user with the given email address. + * + * @since 1.0.3 + * + * @param string $email_address User email address. + * @param int $page Pagination page (1-based). + * @return array{data: array>, done: bool} Export result. + */ + public function export_privacy_data( string $email_address, int $page = 1 ): array { + $user = get_user_by( 'email', $email_address ); + + if ( ! $user ) { + return array( + 'data' => array(), + 'done' => true, + ); + } + + $posts = get_posts( + array( + 'author' => $user->ID, + 'post_type' => 'any', + 'post_status' => 'any', + 'posts_per_page' => 100, + 'paged' => $page, + 'fields' => 'ids', + ) + ); + + $data = array(); + + foreach ( $posts as $post_id ) { + $og_title = get_post_meta( $post_id, '_og_title', true ); + $og_desc = get_post_meta( $post_id, '_og_description', true ); + + if ( empty( $og_title ) && empty( $og_desc ) ) { + continue; + } + + $item_data = array(); + + if ( ! empty( $og_title ) && is_string( $og_title ) ) { + $item_data[] = array( + 'name' => __( 'Custom OG Title', 'robotstxt-og' ), + 'value' => $og_title, + ); + } + + if ( ! empty( $og_desc ) && is_string( $og_desc ) ) { + $item_data[] = array( + 'name' => __( 'Custom OG Description', 'robotstxt-og' ), + 'value' => $og_desc, + ); + } + + if ( ! empty( $item_data ) ) { + $data[] = array( + 'group_id' => 'robotstxt-og-post-meta', + 'group_label' => __( 'OpenGraph Post Meta', 'robotstxt-og' ), + 'item_id' => 'post-' . $post_id, + 'data' => $item_data, + ); + } + } + + $done = count( $posts ) < 100; + + return array( + 'data' => $data, + 'done' => $done, + ); + } + + /** + * Erase personal data stored by this plugin for a given email address. + * + * Removes custom og:title and og:description from all posts authored by + * the user with the given email address. + * + * @since 1.0.3 + * + * @param string $email_address User email address. + * @param int $page Pagination page (1-based). + * @return array{items_removed: int, items_retained: int, messages: string[], done: bool} Erase result. + */ + public function erase_privacy_data( string $email_address, int $page = 1 ): array { + $user = get_user_by( 'email', $email_address ); + + if ( ! $user ) { + return array( + 'items_removed' => 0, + 'items_retained' => 0, + 'messages' => array(), + 'done' => true, + ); + } + + $posts = get_posts( + array( + 'author' => $user->ID, + 'post_type' => 'any', + 'post_status' => 'any', + 'posts_per_page' => 100, + 'paged' => $page, + 'fields' => 'ids', + ) + ); + + $items_removed = 0; + + foreach ( $posts as $post_id ) { + $deleted_title = delete_post_meta( $post_id, '_og_title' ); + $deleted_desc = delete_post_meta( $post_id, '_og_description' ); + + if ( $deleted_title || $deleted_desc ) { + ++$items_removed; + } + } + + $done = count( $posts ) < 100; + + return array( + 'items_removed' => $items_removed, + 'items_retained' => 0, + 'messages' => array(), + 'done' => $done, + ); + } + /** * Get image resolver instance. * diff --git a/includes/class-robotstxt-og-image-resolver.php b/includes/class-robotstxt-og-image-resolver.php index 1320482..c124e01 100644 --- a/includes/class-robotstxt-og-image-resolver.php +++ b/includes/class-robotstxt-og-image-resolver.php @@ -51,7 +51,7 @@ class Robotstxt_OG_Image_Resolver { // Check postmeta cache first. $cached_url = get_post_meta( $post_id, '_og_image_fallback_url', true ); - if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) { + if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) { $this->log( 'cache_hit', array( @@ -103,7 +103,8 @@ class Robotstxt_OG_Image_Resolver { } // Detect file extension. - $path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) ); + $url_path = wp_parse_url( $image_url, PHP_URL_PATH ); + $path_info = pathinfo( is_string( $url_path ) ? $url_path : '' ); $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : ''; // If already a compatible format, save and return. @@ -191,9 +192,14 @@ class Robotstxt_OG_Image_Resolver { // Strip query string for URL manipulation. $clean_url = strtok( $image_url, '?' ); + if ( ! is_string( $clean_url ) ) { + return ''; + } + // Parse URL and get base path without extension. - $path_info = pathinfo( wp_parse_url( $clean_url, PHP_URL_PATH ) ); - $filename = $path_info['filename'] ?? ''; + $url_path = wp_parse_url( $clean_url, PHP_URL_PATH ); + $path_info = pathinfo( is_string( $url_path ) ? $url_path : '' ); + $filename = $path_info['filename']; $dir = $path_info['dirname'] ?? ''; if ( empty( $filename ) ) { @@ -224,10 +230,52 @@ class Robotstxt_OG_Image_Resolver { return ''; } + /** + * Check whether a URL is safe to make server-side HTTP requests to. + * + * Rejects non-http(s) schemes and URLs that resolve to private or + * reserved IP ranges to prevent Server-Side Request Forgery (SSRF). + * + * @since 1.0.3 + * + * @param string $url URL to validate. + * @return bool True if the URL is safe to request. + */ + private function is_safe_url( string $url ): bool { + $scheme = wp_parse_url( $url, PHP_URL_SCHEME ); + if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { + return false; + } + + $host = wp_parse_url( $url, PHP_URL_HOST ); + if ( ! is_string( $host ) || '' === $host ) { + return false; + } + + // Strip IPv6 brackets for validation. + $host_bare = trim( $host, '[]' ); + + // If the host is already an IP address, validate it directly. + if ( filter_var( $host_bare, FILTER_VALIDATE_IP ) !== false ) { + return (bool) filter_var( $host_bare, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); + } + + // Resolve hostname to IP and validate it is not private/reserved. + $resolved = gethostbyname( $host_bare ); + + // gethostbyname() returns the original string on failure. + if ( $resolved === $host_bare ) { + return false; + } + + return (bool) filter_var( $resolved, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); + } + /** * Check if a URL exists using HTTP HEAD request. * * Uses negative caching to avoid repeated requests for non-existent URLs. + * URLs resolving to private or reserved IP ranges are rejected (SSRF protection). * * @since 1.0.0 * @@ -235,6 +283,12 @@ class Robotstxt_OG_Image_Resolver { * @return bool True if URL returns 200-299 status code. */ private function url_exists( string $url ): bool { + // Reject URLs that resolve to private/reserved IP ranges (SSRF protection). + if ( ! $this->is_safe_url( $url ) ) { + $this->log( 'ssrf_blocked', array( 'url' => $url ) ); + return false; + } + // Check negative cache to avoid repeated failed requests. $cache_key = self::NEGATIVE_CACHE_PREFIX . md5( $url ); @@ -333,7 +387,8 @@ class Robotstxt_OG_Image_Resolver { * @return string Compatible image URL or empty string. */ private function get_global_fallback(): string { - $fallback_url = (string) get_option( 'robotstxt_og_fallback_image', '' ); + $option_value = get_option( 'robotstxt_og_fallback_image', '' ); + $fallback_url = is_string( $option_value ) ? $option_value : ''; if ( empty( $fallback_url ) || ! $this->is_valid_url( $fallback_url ) ) { return ''; @@ -354,7 +409,8 @@ class Robotstxt_OG_Image_Resolver { * @return string Compatible image URL or empty string. */ public function get_homepage_image(): string { - $image_url = (string) get_option( 'robotstxt_og_homepage_image', '' ); + $option_value = get_option( 'robotstxt_og_homepage_image', '' ); + $image_url = is_string( $option_value ) ? $option_value : ''; if ( ! empty( $image_url ) && $this->is_valid_url( $image_url ) ) { return esc_url_raw( $image_url ); @@ -378,7 +434,8 @@ class Robotstxt_OG_Image_Resolver { * @return string Compatible image URL or empty string. */ public function ensure_compatible_format( string $image_url ): string { - $path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) ); + $url_path = wp_parse_url( $image_url, PHP_URL_PATH ); + $path_info = pathinfo( is_string( $url_path ) ? $url_path : '' ); $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : ''; // Already a compatible format. @@ -459,7 +516,7 @@ class Robotstxt_OG_Image_Resolver { // Check term meta cache first. $cached_url = get_term_meta( $term_id, '_og_image_fallback_url', true ); - if ( ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) { + if ( is_string( $cached_url ) && ! empty( $cached_url ) && $this->is_valid_url( $cached_url ) ) { $this->log( 'taxonomy_cache_hit', array( @@ -504,7 +561,8 @@ class Robotstxt_OG_Image_Resolver { } // Detect file extension and resolve if needed. - $path_info = pathinfo( wp_parse_url( $image_url, PHP_URL_PATH ) ); + $url_path = wp_parse_url( $image_url, PHP_URL_PATH ); + $path_info = pathinfo( is_string( $url_path ) ? $url_path : '' ); $extension = isset( $path_info['extension'] ) ? strtolower( $path_info['extension'] ) : ''; if ( in_array( $extension, array( 'jpg', 'jpeg', 'png' ), true ) ) { @@ -555,8 +613,8 @@ class Robotstxt_OG_Image_Resolver { * * @since 1.0.0 * - * @param string $event Event name (e.g., 'cache_hit', 'head_request_error'). - * @param array $context Additional context data. + * @param string $event Event name (e.g., 'cache_hit', 'head_request_error'). + * @param array $context Additional context data. * @return void */ private function log( string $event, array $context = array() ): void { diff --git a/includes/class-robotstxt-og-meta-box.php b/includes/class-robotstxt-og-meta-box.php index aeafa04..7f24ce1 100644 --- a/includes/class-robotstxt-og-meta-box.php +++ b/includes/class-robotstxt-og-meta-box.php @@ -118,8 +118,10 @@ class Robotstxt_OG_Meta_Box { public function render_meta_box( WP_Post $post ): void { wp_nonce_field( 'robotstxt_og_meta_box', 'robotstxt_og_meta_box_nonce' ); - $og_title = (string) get_post_meta( $post->ID, '_og_title', true ); - $og_description = (string) get_post_meta( $post->ID, '_og_description', true ); + $raw_title = get_post_meta( $post->ID, '_og_title', true ); + $og_title = is_string( $raw_title ) ? $raw_title : ''; + $raw_desc = get_post_meta( $post->ID, '_og_description', true ); + $og_description = is_string( $raw_desc ) ? $raw_desc : ''; ?> @@ -182,7 +184,7 @@ class Robotstxt_OG_Meta_Box { } // Verify nonce. - $nonce_raw = filter_input( INPUT_POST, 'robotstxt_og_meta_box_nonce', FILTER_SANITIZE_SPECIAL_CHARS ); + $nonce_raw = filter_input( INPUT_POST, 'robotstxt_og_meta_box_nonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'robotstxt_og_meta_box' ) ) { @@ -197,7 +199,7 @@ class Robotstxt_OG_Meta_Box { } // Save og:title. - $og_title_raw = filter_input( INPUT_POST, 'robotstxt_og_title', FILTER_SANITIZE_SPECIAL_CHARS ); + $og_title_raw = filter_input( INPUT_POST, 'robotstxt_og_title', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $og_title = $og_title_raw ? sanitize_text_field( wp_unslash( $og_title_raw ) ) : ''; if ( empty( $og_title ) ) { diff --git a/includes/class-robotstxt-og-rest-api.php b/includes/class-robotstxt-og-rest-api.php index a1f63f8..234e5e9 100644 --- a/includes/class-robotstxt-og-rest-api.php +++ b/includes/class-robotstxt-og-rest-api.php @@ -121,7 +121,8 @@ class Robotstxt_OG_REST_API { * @return WP_REST_Response|WP_Error Response object. */ public function handle_resolve( WP_REST_Request $request ) { - $post_id = (int) $request->get_param( 'post_id' ); + $param = $request->get_param( 'post_id' ); + $post_id = is_numeric( $param ) ? (int) $param : 0; // Verify post exists. $post = get_post( $post_id ); @@ -159,7 +160,8 @@ class Robotstxt_OG_REST_API { * @return WP_REST_Response|WP_Error Response object. */ public function handle_status( WP_REST_Request $request ) { - $post_id = (int) $request->get_param( 'post_id' ); + $param = $request->get_param( 'post_id' ); + $post_id = is_numeric( $param ) ? (int) $param : 0; // Verify post exists. $post = get_post( $post_id ); @@ -189,14 +191,15 @@ class Robotstxt_OG_REST_API { /** * Check REST API permission. * - * Requires manage_options capability. + * Requires edit_others_posts capability (editors and above). * * @since 1.0.0 * + * @param WP_REST_Request $request REST request object. * @return bool|WP_Error True if authorized, WP_Error otherwise. */ - public function check_permission() { - if ( ! current_user_can( 'manage_options' ) ) { + public function check_permission( WP_REST_Request $request ) { + if ( ! current_user_can( 'edit_others_posts' ) ) { return new WP_Error( 'rest_forbidden', __( 'You do not have permission to access this endpoint.', 'robotstxt-og' ), @@ -204,6 +207,18 @@ class Robotstxt_OG_REST_API { ); } + // For post-specific endpoints, also verify the user can edit that post. + $param = $request->get_param( 'post_id' ); + $post_id = is_numeric( $param ) ? (int) $param : 0; + + if ( $post_id > 0 && ! current_user_can( 'edit_post', $post_id ) ) { + return new WP_Error( + 'rest_forbidden', + __( 'You do not have permission to edit this post.', 'robotstxt-og' ), + array( 'status' => 403 ) + ); + } + return true; } } diff --git a/includes/class-robotstxt-og-tags.php b/includes/class-robotstxt-og-tags.php index a021663..792d0d9 100644 --- a/includes/class-robotstxt-og-tags.php +++ b/includes/class-robotstxt-og-tags.php @@ -225,7 +225,8 @@ class Robotstxt_OG_Tags { private function get_og_title(): string { if ( is_singular() ) { $post_id = get_queried_object_id(); - $custom_title = (string) get_post_meta( $post_id, '_og_title', true ); + $raw_title = get_post_meta( $post_id, '_og_title', true ); + $custom_title = is_string( $raw_title ) ? $raw_title : ''; if ( ! empty( $custom_title ) ) { return $custom_title; @@ -262,13 +263,15 @@ class Robotstxt_OG_Tags { private function get_og_description(): string { if ( is_singular() ) { $post_id = get_queried_object_id(); - $custom_desc = (string) get_post_meta( $post_id, '_og_description', true ); + $raw_desc = get_post_meta( $post_id, '_og_description', true ); + $custom_desc = is_string( $raw_desc ) ? $raw_desc : ''; if ( ! empty( $custom_desc ) ) { return $custom_desc; } - $excerpt = (string) get_post_field( 'post_excerpt', $post_id ); + $raw_excerpt = get_post_field( 'post_excerpt', $post_id ); + $excerpt = is_string( $raw_excerpt ) ? $raw_excerpt : ''; if ( ! empty( $excerpt ) ) { return wp_strip_all_tags( $excerpt ); @@ -455,7 +458,8 @@ class Robotstxt_OG_Tags { } // Alt text from the media library (set when uploading/editing the image). - $alt_text = (string) get_post_meta( $image_id, '_wp_attachment_image_alt', true ); + $raw_alt = get_post_meta( $image_id, '_wp_attachment_image_alt', true ); + $alt_text = is_string( $raw_alt ) ? $raw_alt : ''; if ( ! empty( $alt_text ) ) { printf( @@ -489,7 +493,7 @@ class Robotstxt_OG_Tags { $published = get_the_date( 'c', $post ); - if ( ! empty( $published ) ) { + if ( is_string( $published ) && ! empty( $published ) ) { printf( '' . "\n", esc_attr( $published ) @@ -498,7 +502,7 @@ class Robotstxt_OG_Tags { $modified = get_the_modified_date( 'c', $post ); - if ( ! empty( $modified ) ) { + if ( is_string( $modified ) && ! empty( $modified ) ) { printf( '' . "\n", esc_attr( $modified ) @@ -541,7 +545,8 @@ class Robotstxt_OG_Tags { * @return void */ private function output_twitter_card_tags( string $image_url ): void { - $card_type = (string) get_option( 'robotstxt_og_twitter_card_type', 'summary_large_image' ); + $card_option = get_option( 'robotstxt_og_twitter_card_type', 'summary_large_image' ); + $card_type = is_string( $card_option ) ? $card_option : 'summary_large_image'; if ( ! in_array( $card_type, array( 'summary', 'summary_large_image' ), true ) ) { $card_type = 'summary_large_image'; @@ -554,7 +559,8 @@ class Robotstxt_OG_Tags { ); // Site handle (e.g. @example). - $twitter_site = sanitize_text_field( (string) get_option( 'robotstxt_og_twitter_site', '' ) ); + $twitter_option = get_option( 'robotstxt_og_twitter_site', '' ); + $twitter_site = sanitize_text_field( is_string( $twitter_option ) ? $twitter_option : '' ); if ( ! empty( $twitter_site ) ) { // Ensure the handle includes the @ prefix. diff --git a/languages/robotstxt-og-ca.mo b/languages/robotstxt-og-ca.mo new file mode 100644 index 0000000000000000000000000000000000000000..e0edebd10fc2d5b15c095f85b4d91305aed050a2 GIT binary patch literal 11046 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 G!T$j%6XkON literal 0 HcmV?d00001 diff --git a/languages/robotstxt-og-ca.po b/languages/robotstxt-og-ca.po new file mode 100644 index 0000000..65e786c --- /dev/null +++ b/languages/robotstxt-og-ca.po @@ -0,0 +1,447 @@ +# Translation of OpenGraph (by ROBOTSTXT) in Catalan +# This file is distributed under the same license as the OpenGraph (by ROBOTSTXT) package. +# Copyright (C) 2026 ROBOTSTXT +# +msgid "" +msgstr "" +"Project-Id-Version: OpenGraph (by ROBOTSTXT) 1.1.0\n" +"Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/issues\n" +"POT-Creation-Date: 2026-03-28\n" +"PO-Revision-Date: 2026-03-28 00:00+0000\n" +"Last-Translator: ROBOTSTXT \n" +"Language-Team: Catalan\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Manual\n" +"X-Domain: robotstxt-og\n" + +#. Plugin Name of the plugin +msgid "OpenGraph (by ROBOTSTXT)" +msgstr "OpenGraph (by ROBOTSTXT)" + +#. Plugin URI of the plugin +msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og" +msgstr "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og" + +#. Description of the plugin +msgid "Intelligent Open Graph image fallback for social media crawlers. Automatically detects and serves compatible image formats (JPEG/PNG) when modern formats (AVIF/WebP) are used as featured images." +msgstr "Imatge de reserva intel·ligent per a Open Graph en rastrejadors de xarxes socials. Detecta i serveix automàticament formats d'imatge compatibles (JPEG/PNG) quan s'utilitzen formats moderns (AVIF/WebP) com a imatges destacades." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "OpenGraph Settings" +msgstr "Configuració d'OpenGraph" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "OpenGraph" +msgstr "OpenGraph" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "You do not have sufficient permissions to access this page." +msgstr "No tens permisos suficients per accedir a aquesta pàgina." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Security check failed" +msgstr "Ha fallat la verificació de seguretat" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "General Settings" +msgstr "Configuració general" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Configure OpenGraph image fallback behavior." +msgstr "Configura el comportament de la imatge de reserva per a OpenGraph." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Global Fallback Image" +msgstr "Imatge de reserva global" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Homepage Image" +msgstr "Imatge de la pàgina principal" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Data Management" +msgstr "Gestió de dades" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Social Media Tags" +msgstr "Etiquetes per a xarxes socials" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Choose which social media meta tag groups to output and configure their behavior." +msgstr "Tria quins grups de metaetiquetes per a xarxes socials vols generar i configura'n el comportament." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Facebook / OG Tags" +msgstr "Etiquetes Facebook / OG" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Twitter Card Tags" +msgstr "Etiquetes Twitter Card" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Twitter Card Type" +msgstr "Tipus de Twitter Card" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Select Image" +msgstr "Selecciona la imatge" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Remove Image" +msgstr "Elimina la imatge" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Used as Open Graph image when a post has no featured image or when no compatible format (JPEG/PNG) is found." +msgstr "S'utilitza com a imatge d'Open Graph quan una entrada no té imatge destacada o no es troba cap format compatible (JPEG/PNG)." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Dedicated Open Graph image for the site homepage. Falls back to the Global Fallback Image if not set." +msgstr "Imatge d'Open Graph dedicada a la pàgina principal del lloc. Si no es configura, s'utilitza la imatge de reserva global." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Delete all plugin data when uninstalling" +msgstr "Elimina totes les dades del connector en desinstal·lar" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Warning: This will permanently delete all cached fallback URLs and plugin settings. This action cannot be undone." +msgstr "Avís: Això eliminarà permanentment totes les URL de reserva en memòria cau i la configuració del connector. Aquesta acció no es pot desfer." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Enable Facebook / Open Graph image tags" +msgstr "Activa les etiquetes d'imatge per a Facebook / Open Graph" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Outputs og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, and og:image:alt meta tags." +msgstr "Genera les metaetiquetes og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type i og:image:alt." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Enable Twitter Card image tags" +msgstr "Activa les etiquetes d'imatge per a Twitter Card" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Outputs twitter:card, twitter:site, and twitter:image meta tags." +msgstr "Genera les metaetiquetes twitter:card, twitter:site i twitter:image." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Summary with large image" +msgstr "Resum amb imatge gran" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Summary (small image)" +msgstr "Resum (imatge petita)" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Controls the twitter:card meta tag value. \"Summary with large image\" is recommended for most sites." +msgstr "Controla el valor de la metaetiqueta twitter:card. Es recomana «Resum amb imatge gran» per a la majoria de llocs." + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Twitter/X Site Handle" +msgstr "Compte Twitter/X del lloc" + +#: admin/class-robotstxt-og-admin-settings.php +msgid "Your site's Twitter/X account handle (e.g. @example). Used for the twitter:site meta tag." +msgstr "El compte Twitter/X del teu lloc (p. ex. @exemple). S'utilitza per a la metaetiqueta twitter:site." + +#: admin/class-robotstxt-og-admin-settings.php +#. translators: %d: number of cleared cache entries +msgid "Successfully cleared %d cached fallback URLs." +msgstr "S'han eliminat correctament %d URL de reserva en memòria cau." + +#: admin/class-robotstxt-og-admin-settings.php +#. translators: 1: number of successful resolutions 2: number of failed resolutions +msgid "Batch resolution complete. Success: %1$d, Failed: %2$d" +msgstr "Resolució per lots completada. Correctes: %1$d, Fallides: %2$d" + +#: admin/class-robotstxt-og-admin-settings.php +#. translators: %d: post ID +msgid "Successfully cleared cached fallback URL for post #%d." +msgstr "S'ha eliminat correctament la URL de reserva en memòria cau per a l'entrada #%d." + +#: admin/views/settings-page.php +msgid "Settings tabs" +msgstr "Pestanyes de configuració" + +#: admin/views/settings-page.php +msgid "Settings" +msgstr "Configuració" + +#: admin/views/settings-page.php +msgid "Tools" +msgstr "Eines" + +#: admin/views/settings-page.php +msgid "Diagnostics" +msgstr "Diagnòstics" + +#: admin/views/settings-page.php +msgid "Save Settings" +msgstr "Desa la configuració" + +#: admin/views/settings-page.php +msgid "Cache Management" +msgstr "Gestió de la memòria cau" + +#: admin/views/settings-page.php +msgid "Use these tools to manage cached fallback image URLs." +msgstr "Utilitza aquestes eines per gestionar les URL d'imatge de reserva emmagatzemades en memòria cau." + +#: admin/views/settings-page.php +msgid "Clear All Caches" +msgstr "Buida tota la memòria cau" + +#: admin/views/settings-page.php +msgid "Clear All Cached URLs" +msgstr "Elimina totes les URL en memòria cau" + +#: admin/views/settings-page.php +msgid "Delete all cached fallback URLs. Images will be re-resolved on next page view." +msgstr "Elimina totes les URL de reserva en memòria cau. Les imatges es resoldran de nou en la propera visita a la pàgina." + +#: admin/views/settings-page.php +msgid "Re-resolve All Images" +msgstr "Torna a resoldre totes les imatges" + +#: admin/views/settings-page.php +msgid "Re-resolve All Images Now" +msgstr "Torna a resoldre totes les imatges ara" + +#: admin/views/settings-page.php +msgid "Clear cache and immediately re-resolve all posts with featured images. May take time on large sites." +msgstr "Buida la memòria cau i torna a resoldre immediatament totes les entrades amb imatge destacada. Pot trigar en llocs grans." + +#: admin/views/settings-page.php +msgid "Plugin Information" +msgstr "Informació del connector" + +#: admin/views/settings-page.php +msgid "Version" +msgstr "Versió" + +#: admin/views/settings-page.php +msgid "Documentation" +msgstr "Documentació" + +#: admin/views/settings-page.php +msgid "View Documentation" +msgstr "Visualitza la documentació" + +#: admin/views/settings-page.php +msgid "Support" +msgstr "Suport" + +#: admin/views/settings-page.php +msgid "Report an Issue" +msgstr "Informa d'un problema" + +#: admin/views/settings-page.php +msgid "Statistics" +msgstr "Estadístiques" + +#: admin/views/settings-page.php +msgid "Posts with featured images" +msgstr "Entrades amb imatge destacada" + +#: admin/views/settings-page.php +msgid "Posts with cached fallback URLs" +msgstr "Entrades amb URL de reserva en memòria cau" + +#: admin/views/settings-page.php +msgid "Coverage" +msgstr "Cobertura" + +#: admin/views/settings-page.php +msgid "N/A" +msgstr "N/D" + +#: admin/views/settings-page.php +msgid "Test URL" +msgstr "Prova la URL" + +#: admin/views/settings-page.php +msgid "Test whether a URL is reachable via HTTP HEAD request." +msgstr "Comprova si una URL és accessible mitjançant una petició HTTP HEAD." + +#: admin/views/settings-page.php +msgid "URL to test" +msgstr "URL a provar" + +#: admin/views/settings-page.php +msgid "Test Result" +msgstr "Resultat de la prova" + +#: admin/views/settings-page.php +msgid "Error:" +msgstr "Error:" + +#: admin/views/settings-page.php +msgid "HTTP Status:" +msgstr "Estat HTTP:" + +#: admin/views/settings-page.php +msgid "Header" +msgstr "Capçalera" + +#: admin/views/settings-page.php +msgid "Value" +msgstr "Valor" + +#: admin/views/settings-page.php +#. translators: %d: total entries +msgid "Cached Fallback URLs (%d)" +msgstr "URL de reserva en memòria cau (%d)" + +#: admin/views/settings-page.php +msgid "No cached fallback URLs found." +msgstr "No s'han trobat URL de reserva en memòria cau." + +#: admin/views/settings-page.php +msgid "Post ID" +msgstr "ID d'entrada" + +#: admin/views/settings-page.php +msgid "Title" +msgstr "Títol" + +#: admin/views/settings-page.php +msgid "Type" +msgstr "Tipus" + +#: admin/views/settings-page.php +msgid "Cached Fallback URL" +msgstr "URL de reserva en memòria cau" + +#: admin/views/settings-page.php +msgid "Actions" +msgstr "Accions" + +#: admin/views/settings-page.php +msgid "Clear" +msgstr "Neteja" + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: number of posts found +msgid "Found %d posts with featured images. (dry-run, no changes made)" +msgstr "S'han trobat %d entrades amb imatge destacada. (simulació, no s'han fet canvis)" + +#: includes/class-robotstxt-og-cli.php +msgid "No posts with featured images found." +msgstr "No s'han trobat entrades amb imatge destacada." + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: number of posts to process +msgid "Resolving %d posts" +msgstr "S'estan resolent %d entrades" + +#: includes/class-robotstxt-og-cli.php +#. translators: 1: successful count, 2: failed count +msgid "Resolved %1$d posts. Failed: %2$d." +msgstr "S'han resolt %1$d entrades. Fallides: %2$d." + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: post ID +msgid "Post %d not found." +msgstr "L'entrada %d no s'ha trobat." + +#: includes/class-robotstxt-og-cli.php +#. translators: 1: post ID, 2: image URL +msgid "Post %1$d has featured image: %2$s (dry-run, no changes made)" +msgstr "L'entrada %1$d té imatge destacada: %2$s (simulació, no s'han fet canvis)" + +#: includes/class-robotstxt-og-cli.php +#. translators: 1: post ID, 2: resolved URL +msgid "Post %1$d resolved to %2$s" +msgstr "L'entrada %1$d s'ha resolt a %2$s" + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: post ID +msgid "Post %d: no compatible image found." +msgstr "Entrada %d: no s'ha trobat cap imatge compatible." + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: number of cache entries found +msgid "Found %d cached fallback URLs. (dry-run, no changes made)" +msgstr "S'han trobat %d URL de reserva en memòria cau. (simulació, no s'han fet canvis)" + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: number of cleared entries +msgid "Cleared %d cached fallback URLs." +msgstr "S'han eliminat %d URL de reserva en memòria cau." + +#: includes/class-robotstxt-og-cli.php +#. translators: 1: post ID, 2: cached URL or 'none' +msgid "Post %1$d cached URL: %2$s (dry-run, no changes made)" +msgstr "URL en memòria cau de l'entrada %1$d: %2$s (simulació, no s'han fet canvis)" + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: post ID +msgid "Post %d has no cached fallback URL." +msgstr "L'entrada %d no té URL de reserva en memòria cau." + +#: includes/class-robotstxt-og-cli.php +#. translators: %d: post ID +msgid "Cleared cached fallback URL for post %d." +msgstr "S'ha eliminat la URL de reserva en memòria cau per a l'entrada %d." + +#: includes/class-robotstxt-og-cli.php +msgid "Please specify a post ID or use --all flag." +msgstr "Especifica un ID d'entrada o utilitza l'opció --all." + +#: includes/class-robotstxt-og-rest-api.php +msgid "Post not found." +msgstr "L'entrada no s'ha trobat." + +#: includes/class-robotstxt-og-rest-api.php +msgid "You do not have permission to access this endpoint." +msgstr "No tens permís per accedir a aquest punt d'accés." + +#: includes/class-robotstxt-og-rest-api.php +msgid "You do not have permission to edit this post." +msgstr "No tens permís per editar aquesta entrada." + +#: includes/class-robotstxt-og-rest-api.php +msgid "The post ID to resolve the fallback image for." +msgstr "L'ID de l'entrada per a la qual resoldre la imatge de reserva." + +#: includes/class-robotstxt-og-rest-api.php +msgid "The post ID to get the fallback image status for." +msgstr "L'ID de l'entrada de la qual obtenir l'estat de la imatge de reserva." + +#: includes/class-robotstxt-og-meta-box.php +msgid "Open Graph / Social Media" +msgstr "Open Graph / Xarxes Socials" + +#: includes/class-robotstxt-og-meta-box.php +msgid "Custom Title" +msgstr "Títol personalitzat" + +#: includes/class-robotstxt-og-meta-box.php +msgid "Overrides the default og:title for this post. Leave blank to use the post title automatically." +msgstr "Substitueix l'og:title predeterminat per a aquesta entrada. Deixa-ho en blanc per utilitzar el títol de l'entrada automàticament." + +#: includes/class-robotstxt-og-meta-box.php +msgid "Custom Description" +msgstr "Descripció personalitzada" + +#: includes/class-robotstxt-og-meta-box.php +msgid "Overrides the default og:description for this post. Leave blank to use the excerpt automatically." +msgstr "Substitueix l'og:description predeterminat per a aquesta entrada. Deixa-ho en blanc per utilitzar l'extracte automàticament." + +#: includes/class-robotstxt-og-image-fallback.php +msgid "OpenGraph Custom Post Data" +msgstr "Dades personalitzades d'Open Graph" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "OpenGraph Post Meta" +msgstr "Metadades Open Graph de l'entrada" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "Custom OG Title" +msgstr "Títol OG personalitzat" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "Custom OG Description" +msgstr "Descripció OG personalitzada" diff --git a/languages/robotstxt-og-es_ES.mo b/languages/robotstxt-og-es_ES.mo index b46230c5375b4625b8d3cb85664e8f238ed58484..ef652136a6d7e1a10a2a050620302b002834b533 100644 GIT binary patch delta 2529 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 U{jrSf0e|d9ZfWXgOXr_`2C5LqbN~PV diff --git a/languages/robotstxt-og-es_ES.po b/languages/robotstxt-og-es_ES.po index a55decf..e32945e 100644 --- a/languages/robotstxt-og-es_ES.po +++ b/languages/robotstxt-og-es_ES.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: OpenGraph (by ROBOTSTXT) 1.1.0\n" "Report-Msgid-Bugs-To: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/issues\n" -"POT-Creation-Date: 2026-02-18\n" -"PO-Revision-Date: 2026-02-18 00:00+0000\n" +"POT-Creation-Date: 2026-03-28\n" +"PO-Revision-Date: 2026-03-28 00:00+0000\n" "Last-Translator: ROBOTSTXT \n" "Language-Team: Spanish (Spain)\n" "Language: es_ES\n" @@ -398,6 +398,26 @@ msgstr "Entrada no encontrada." msgid "You do not have permission to access this endpoint." msgstr "No tienes permiso para acceder a este endpoint." +#: includes/class-robotstxt-og-rest-api.php +msgid "You do not have permission to edit this post." +msgstr "No tienes permiso para editar esta entrada." + +#: includes/class-robotstxt-og-image-fallback.php +msgid "OpenGraph Custom Post Data" +msgstr "Datos personalizados de Open Graph" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "OpenGraph Post Meta" +msgstr "Metadatos Open Graph de la entrada" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "Custom OG Title" +msgstr "Título OG personalizado" + +#: includes/class-robotstxt-og-image-fallback.php +msgid "Custom OG Description" +msgstr "Descripción OG personalizada" + #: includes/class-robotstxt-og-rest-api.php msgid "The post ID to resolve the fallback image for." msgstr "El ID de entrada para el que resolver la imagen de respaldo." diff --git a/readme.txt b/readme.txt index 155de7b..438f77b 100644 --- a/readme.txt +++ b/readme.txt @@ -1,11 +1,11 @@ === OpenGraph (by ROBOTSTXT) === Contributors: javiercasares, robotstxt Tags: opengraph, open graph, twitter card, social media, seo -Requires at least: 6.7 +Requires at least: 6.8 Tested up to: 7.0 -Stable tag: 1.0.1 +Stable tag: 1.1.0 Requires PHP: 8.2 -Version: 1.0.1 +Version: 1.1.0 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt @@ -42,7 +42,7 @@ Social media crawlers — Facebook, Twitter/X, LinkedIn, WhatsApp — do not sup * WP-CLI commands for batch operations * REST API endpoints for programmatic access * Multisite compatible (per-site configuration) -* Full internationalization support (es_ES included) +* Full internationalization support (es_ES and ca included) == Using the plugin == @@ -78,7 +78,7 @@ Clear all cached fallback URLs: = REST API = -The plugin exposes two REST API endpoints under the `robotstxt-og/v1` namespace. Both require the `manage_options` capability (administrator authentication). +The plugin exposes two REST API endpoints under the `robotstxt-og/v1` namespace. Both require the `edit_others_posts` capability (editor and above). **Force re-resolve a post's fallback image:** @@ -163,7 +163,7 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/ == Compatibility == -* WordPress: 6.7 - 7.0 +* WordPress: 6.8 - 7.0 * PHP: 8.2 - 8.5 * WP-CLI: 2.x * MariaDB: 10.6+ @@ -175,11 +175,18 @@ Yes. The plugin hooks into `wp_head` for direct tag injection and filters Yoast/ == Changelog == -= 1.0.1 = += 1.1.0 = -_Release date: 2026-03-05_ +_Release date: 2026-03-28_ -* Confirmed compatibility with WordPress 7.0. +* Fixed: Fatal `TypeError` in `handle_thumbnail_change()` when `deleted_post_meta` passes an array of meta IDs as the first argument. +* Security: Added SSRF protection — outbound HEAD requests now block private and reserved IP ranges. +* Security: Replaced deprecated `FILTER_SANITIZE_SPECIAL_CHARS` with `FILTER_SANITIZE_FULL_SPECIAL_CHARS`. +* Added: GDPR Privacy API — custom OG title and description are included in WordPress personal data export and erase. +* Changed: Settings page and REST API now require `edit_others_posts`, allowing editors to manage OG settings. +* Changed: Minimum supported WordPress version raised to 6.8. +* Changed: Added `Network: true` header confirming Multisite compatibility. +* Added: PHPUnit test suite (Brain\Monkey) covering SSRF protection, format detection, and cache clearing. = 1.0.0 = diff --git a/robotstxt-og.php b/robotstxt-og.php index ef65245..5116aac 100644 --- a/robotstxt-og.php +++ b/robotstxt-og.php @@ -3,8 +3,8 @@ * Plugin Name: OpenGraph (by ROBOTSTXT) * Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-og * Description: Intelligent Open Graph image fallback for social media crawlers. Automatically detects and serves compatible image formats (JPEG/PNG) when modern formats (AVIF/WebP) are used as featured images. - * Version: 1.0.1 - * Requires at least: 6.7 + * Version: 1.1.0 + * Requires at least: 6.8 * Requires PHP: 8.2 * Author: ROBOTSTXT * Author URI: https://www.robotstxt.es/ @@ -12,11 +12,12 @@ * License URI: https://www.gnu.org/licenses/gpl-3.0.html * Text Domain: robotstxt-og * Domain Path: /languages + * Network: true * Gitea Plugin URI: ROBOTSTXT/robotstxt-og * Contributors: javiercasares, robotstxt * * @package ROBOTSTXT_OG - * @version 1.0.1 + * @version 1.1.0 */ if ( ! defined( 'ABSPATH' ) ) { @@ -24,7 +25,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Define plugin constants. -define( 'ROBOTSTXT_OG_VERSION', '1.0.1' ); +define( 'ROBOTSTXT_OG_VERSION', '1.1.0' ); define( 'ROBOTSTXT_OG_PATH', plugin_dir_path( __FILE__ ) ); define( 'ROBOTSTXT_OG_URL', plugin_dir_url( __FILE__ ) ); define( 'ROBOTSTXT_OG_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/robotstxt-updater.php b/robotstxt-updater.php index c53a6b4..612c64c 100644 --- a/robotstxt-updater.php +++ b/robotstxt-updater.php @@ -60,7 +60,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Plugin headers. * - * @var array + * @var array */ private array $plugin_data; @@ -105,7 +105,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Get plugin headers. * - * @return array Plugin data. + * @return array Plugin data. */ private function get_plugin_data(): array { if ( ! function_exists( 'get_plugin_data' ) ) { @@ -125,7 +125,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { */ private function build_json_url(): string { // Try Gitea Plugin URI (format: "OWNER/REPO" or full URL). - if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) ) { + if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) && is_string( $this->plugin_data['Gitea Plugin URI'] ) ) { $gitea_uri = $this->plugin_data['Gitea Plugin URI']; // If it's already a full URL, use it. @@ -141,7 +141,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { } // Fallback: try to extract from Plugin URI. - if ( ! empty( $this->plugin_data['PluginURI'] ) ) { + if ( ! empty( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ) { $plugin_uri = $this->plugin_data['PluginURI']; if ( str_contains( $plugin_uri, 'git.robotstxt.es' ) ) { return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json'; @@ -160,7 +160,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { * @return object The modified transient. */ public function inject_update_info( $transient ) { - if ( ! is_object( $transient ) ) { + if ( ! ( $transient instanceof stdClass ) ) { $transient = new stdClass(); } @@ -183,7 +183,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { return $transient; } - if ( version_compare( $remote['version'], $current_version, '>' ) ) { + if ( is_string( $current_version ) && is_string( $remote['version'] ) && version_compare( $remote['version'], $current_version, '>' ) ) { $update = (object) array( 'slug' => $remote['slug'] ?? $this->plugin_slug, 'plugin' => $this->plugin_basename, @@ -204,11 +204,11 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Provide "View details" modal content. * - * @param false|object|array $result The result object or array. - * @param string $action The type of information being requested. - * @param object $args Plugin API arguments. + * @param false|object|array $result The result object or array. + * @param string $action The type of information being requested. + * @param object $args Plugin API arguments. * - * @return false|object The plugin information object or false. + * @return false|object|array The plugin information object or false. */ public function provide_plugin_details( $result, string $action, object $args ) { if ( 'plugin_information' !== $action ) { @@ -245,7 +245,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Get remote data with caching and HMAC signature verification. * - * @return array Remote data. + * @return array Remote data. */ private function get_remote_data(): array { $cached = get_site_transient( $this->cache_key ); @@ -255,7 +255,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) { $expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT ); - if ( hash_equals( $expected_sig, $cached['signature'] ) ) { + if ( is_string( $cached['signature'] ) && hash_equals( $expected_sig, $cached['signature'] ) ) { // Signature valid, return data. return is_array( $cached['data'] ) ? $cached['data'] : array(); } @@ -283,7 +283,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS ); } - return is_array( $remote ) ? $remote : array(); + return $remote; } // Legacy cache format without signature (backward compatibility). @@ -293,7 +293,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Fetch JSON from remote URL. * - * @return array Decoded JSON data. + * @return array Decoded JSON data. */ private function fetch_json(): array { $response = wp_remote_get( @@ -324,18 +324,18 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { /** * Check compatibility. * - * @param array $remote Remote data. + * @param array $remote Remote data. * * @return bool True if compatible. */ private function is_compatible( array $remote ): bool { - if ( ! empty( $remote['requires_php'] ) ) { + if ( ! empty( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ) { if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) { return false; } } - if ( ! empty( $remote['requires'] ) ) { + if ( ! empty( $remote['requires'] ) && is_string( $remote['requires'] ) ) { if ( version_compare( get_bloginfo( 'version' ), $remote['requires'], '<' ) ) { return false; } @@ -355,7 +355,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) { } // This is a cache clear request - now verify nonce. - $nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW ); + $nonce_raw = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'robotstxt_clear_update_cache' ) ) { diff --git a/uninstall.php b/uninstall.php index c088696..52bd9fd 100644 --- a/uninstall.php +++ b/uninstall.php @@ -15,7 +15,7 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { /** * Delete plugin data if user opted in to data deletion. */ -function robotstxt_og_uninstall_cleanup() { +function robotstxt_og_uninstall_cleanup(): void { // Check if user opted in to delete data on uninstall. $delete_data = get_option( 'robotstxt_og_delete_data_on_uninstall', false ); @@ -72,7 +72,7 @@ function robotstxt_og_uninstall_cleanup() { ); foreach ( $sites as $site ) { - switch_to_blog( $site->blog_id ); + switch_to_blog( (int) $site->blog_id ); // Delete site-specific options. delete_option( 'robotstxt_og_fallback_image' ); diff --git a/update.json b/update.json index 955dc75..e220c75 100644 --- a/update.json +++ b/update.json @@ -1,20 +1,20 @@ { "name": "OpenGraph (by ROBOTSTXT)", "slug": "robotstxt-og", - "version": "1.0.1", - "download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.0.1/robotstxt-og-1.0.1.zip", - "requires": "6.7", + "version": "1.1.0", + "download_url": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og/releases/download/1.1.0/robotstxt-og-1.1.0.zip", + "requires": "6.8", "requires_php": "8.2", "tested": "7.0", - "last_updated": "2026-03-05", + "last_updated": "2026-03-28", "author": "ROBOTSTXT", "author_profile": "https://www.robotstxt.es/", "homepage": "https://git.robotstxt.es/ROBOTSTXT/robotstxt-og", "description": "Intelligent Open Graph meta tags with automatic image fallback from AVIF/WebP to JPEG/PNG for social media crawlers. Outputs a complete set of og:* and twitter:* meta tags, detects incompatible image formats, and resolves JPEG/PNG alternatives via HTTP HEAD requests.", - "changelog": "

1.0.1 — 2026-03-05

  • Compatibility: Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0.

1.0.0 — 2026-02-18

  • Initial Release: First stable release of OpenGraph (by ROBOTSTXT).
  • Added: Full Open Graph meta tags: og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, og:image:alt.
  • Added: Article tags for singular posts: article:published_time, article:modified_time, article:section, article:tag.
  • Added: Twitter Card tags: twitter:card, twitter:site, twitter:image.
  • Added: Automatic detection and HTTP HEAD-based resolution of JPEG/PNG alternatives for AVIF, WebP, GIF, BMP, SVG, and TIFF images.
  • Added: Postmeta caching with automatic invalidation on featured image change.
  • Added: Per-post OG title and description overrides via post editor meta box.
  • Added: Global fallback image URL and homepage-specific image URL settings.
  • Added: Yoast SEO and RankMath integration (no duplicate tags).
  • Added: Admin settings page with Settings, Tools, and Diagnostics tabs.
  • Added: WP-CLI commands: wp og-fallback resolve and wp og-fallback clear-cache.
  • Added: REST API: POST /wp-json/robotstxt-og/v1/resolve/{post_id} and GET /wp-json/robotstxt-og/v1/status/{post_id}.
  • Added: Developer filters: robotstxt_og_external_image_enabled, robotstxt_og_external_image_timeout, robotstxt_og_taxonomy_image, robotstxt_og_enable_logging.
  • Added: Multisite compatible, full i18n support, Spanish (es_ES) translation included.
  • Security: Nonce verification, manage_options capability checks, input sanitization, output escaping, prepared SQL statements throughout.
", + "changelog": "

1.1.0 — 2026-03-28

  • Fixed: Fatal TypeError in handle_thumbnail_change(): the deleted_post_meta action passes an array of meta IDs as its first argument. The method signature now accepts int|array to handle both updated_post_meta and deleted_post_meta correctly.
  • Security: Added SSRF protection to all outbound HTTP HEAD requests via is_safe_url() — private and reserved IP ranges are now blocked.
  • Security: Replaced deprecated FILTER_SANITIZE_SPECIAL_CHARS with FILTER_SANITIZE_FULL_SPECIAL_CHARS throughout.
  • Added: GDPR Privacy API support — custom OG title and description post meta are now included in WordPress personal data export and erase requests.
  • Changed: Settings page and REST API now require edit_others_posts capability, allowing editors to manage OG settings and refresh fallback images.
  • Changed: Minimum supported WordPress version raised to 6.8. Network: true header added.
  • Added: PHPUnit test suite with Brain\\Monkey; covers SSRF logic, image format detection, and cache clearing.

1.0.0 — 2026-02-18

  • Initial Release: First stable release of OpenGraph (by ROBOTSTXT).
", "sections": { "description": "

Social media crawlers — Facebook, Twitter/X, LinkedIn, WhatsApp — do not support modern image formats such as AVIF and WebP. When a post’s featured image uses one of these formats, the platform shows a broken or missing image preview.

OpenGraph (by ROBOTSTXT) outputs a complete set of Open Graph and Twitter Card meta tags and solves this automatically. It detects whether each post’s featured image is in a supported format and, when needed, resolves and caches a compatible JPEG or PNG alternative.

  • Full og:* and twitter:* meta tag output
  • Automatic AVIF/WebP/GIF/BMP/SVG/TIFF detection with JPEG/PNG fallback
  • Per-post OG title and description overrides via post editor meta box
  • Global fallback image URL and homepage-specific image URL
  • Yoast SEO and RankMath integration (no duplicate tags)
  • Postmeta caching with automatic invalidation
  • Admin panel with Settings, Tools, and Diagnostics tabs
  • WP-CLI commands and REST API endpoints
  • Multisite compatible
", - "changelog": "

1.0.1 — 2026-03-05

  • Compatibility: Confirmed compatibility with WordPress 7.0. Supported range is now WordPress 6.7 – 7.0.

1.0.0 — 2026-02-18

  • Initial Release: First stable release of OpenGraph (by ROBOTSTXT).
  • Added: Full Open Graph meta tags: og:title, og:type, og:url, og:description, og:site_name, og:locale, og:image, og:image:width, og:image:height, og:image:type, og:image:alt.
  • Added: Article tags for singular posts: article:published_time, article:modified_time, article:section, article:tag.
  • Added: Twitter Card tags: twitter:card, twitter:site, twitter:image.
  • Added: Automatic detection and HTTP HEAD-based resolution of JPEG/PNG alternatives for AVIF, WebP, GIF, BMP, SVG, and TIFF images.
  • Added: Postmeta caching with automatic invalidation on featured image change.
  • Added: Per-post OG title and description overrides via post editor meta box.
  • Added: Global fallback image URL and homepage-specific image URL settings.
  • Added: Yoast SEO and RankMath integration (no duplicate tags).
  • Added: Admin settings page with Settings, Tools, and Diagnostics tabs.
  • Added: WP-CLI commands: wp og-fallback resolve and wp og-fallback clear-cache.
  • Added: REST API: POST /wp-json/robotstxt-og/v1/resolve/{post_id} and GET /wp-json/robotstxt-og/v1/status/{post_id}.
  • Added: Developer filters: robotstxt_og_external_image_enabled, robotstxt_og_external_image_timeout, robotstxt_og_taxonomy_image, robotstxt_og_enable_logging.
  • Added: Multisite compatible, full i18n support, Spanish (es_ES) translation included.
  • Security: Nonce verification, manage_options capability checks, input sanitization, output escaping, prepared SQL statements throughout.
" + "changelog": "

1.1.0 — 2026-03-28

  • Fixed: Fatal TypeError in handle_thumbnail_change(): the deleted_post_meta action passes an array of meta IDs as its first argument. The method signature now accepts int|array to handle both updated_post_meta and deleted_post_meta correctly.
  • Security: Added SSRF protection to all outbound HTTP HEAD requests via is_safe_url() — private and reserved IP ranges are now blocked.
  • Security: Replaced deprecated FILTER_SANITIZE_SPECIAL_CHARS with FILTER_SANITIZE_FULL_SPECIAL_CHARS throughout.
  • Added: GDPR Privacy API support — custom OG title and description post meta are now included in WordPress personal data export and erase requests.
  • Changed: Settings page and REST API now require edit_others_posts capability, allowing editors to manage OG settings and refresh fallback images.
  • Changed: Minimum supported WordPress version raised to 6.8. Network: true header added.
  • Added: PHPUnit test suite with Brain\\Monkey; covers SSRF logic, image format detection, and cache clearing.

1.0.0 — 2026-02-18

  • Initial Release: First stable release of OpenGraph (by ROBOTSTXT).
" }, "banners": { "low": "",