359 lines
9.5 KiB
PHP
359 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* Term Meta Class
|
|
*
|
|
* Provides per-term Open Graph fields (title, image, description) on the
|
|
* category and tag edit pages (term.php), and exposes the stored image to the
|
|
* taxonomy archive image resolution via the `robotstxt_og_taxonomy_image`
|
|
* filter.
|
|
*
|
|
* @package ROBOTSTXT_OG
|
|
* @since 1.2.0
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit; // Exit if accessed directly.
|
|
}
|
|
|
|
/**
|
|
* Class Robotstxt_OG_Term_Meta
|
|
*
|
|
* @since 1.2.0
|
|
*/
|
|
class Robotstxt_OG_Term_Meta {
|
|
|
|
/**
|
|
* Term meta key for the custom OG title.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const META_TITLE = '_og_title';
|
|
|
|
/**
|
|
* Term meta key for the custom OG image URL.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const META_IMAGE = '_og_image';
|
|
|
|
/**
|
|
* Term meta key for the custom OG description.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const META_DESCRIPTION = '_og_description';
|
|
|
|
/**
|
|
* Term meta key for the custom OG image alt text.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const META_IMAGE_ALT = '_og_image_alt';
|
|
|
|
/**
|
|
* Nonce action for the term edit form.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const NONCE_ACTION = 'robotstxt_og_term_meta';
|
|
|
|
/**
|
|
* Nonce field name for the term edit form.
|
|
*
|
|
* @since 1.2.0
|
|
* @var string
|
|
*/
|
|
public const NONCE_NAME = 'robotstxt_og_term_meta_nonce';
|
|
|
|
/**
|
|
* Initialize hooks.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @return void
|
|
*/
|
|
public function init(): void {
|
|
add_action( 'init', array( $this, 'register_term_meta' ) );
|
|
add_filter( 'robotstxt_og_taxonomy_image', array( $this, 'filter_taxonomy_image' ), 10, 2 );
|
|
|
|
if ( ! is_admin() ) {
|
|
return;
|
|
}
|
|
|
|
foreach ( $this->get_supported_taxonomies() as $taxonomy ) {
|
|
add_action( "{$taxonomy}_edit_form_fields", array( $this, 'render_term_fields' ) );
|
|
add_action(
|
|
"edited_{$taxonomy}",
|
|
function ( $term_id ) use ( $taxonomy ): void {
|
|
$this->save_term_meta( (int) $term_id, $taxonomy );
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the list of taxonomies the OG fields are exposed on.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @return string[] Taxonomy slugs.
|
|
*/
|
|
protected function get_supported_taxonomies(): array {
|
|
$taxonomies = array( 'category', 'post_tag' );
|
|
|
|
/**
|
|
* Filter the taxonomies that expose per-term Open Graph fields.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param string[] $taxonomies Taxonomy slugs.
|
|
*/
|
|
return apply_filters( 'robotstxt_og_term_meta_taxonomies', $taxonomies );
|
|
}
|
|
|
|
/**
|
|
* Register term meta for REST API / block editor access.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @return void
|
|
*/
|
|
public function register_term_meta(): void {
|
|
$fields = array(
|
|
self::META_TITLE => 'sanitize_text_field',
|
|
self::META_IMAGE => 'sanitize_url',
|
|
self::META_IMAGE_ALT => 'sanitize_text_field',
|
|
self::META_DESCRIPTION => 'sanitize_textarea_field',
|
|
);
|
|
|
|
foreach ( $this->get_supported_taxonomies() as $taxonomy ) {
|
|
$cap = $this->get_edit_cap( $taxonomy );
|
|
|
|
foreach ( $fields as $key => $sanitize ) {
|
|
register_term_meta(
|
|
$taxonomy,
|
|
$key,
|
|
array(
|
|
'type' => 'string',
|
|
'single' => true,
|
|
'sanitize_callback' => $sanitize,
|
|
'auth_callback' => static function () use ( $cap ) {
|
|
return current_user_can( $cap );
|
|
},
|
|
'show_in_rest' => true,
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the edit capability for a taxonomy.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param string $taxonomy Taxonomy slug.
|
|
* @return string Capability name.
|
|
*/
|
|
protected function get_edit_cap( string $taxonomy ): string {
|
|
$tax_obj = get_taxonomy( $taxonomy );
|
|
|
|
if ( $tax_obj && isset( $tax_obj->cap->edit_terms ) && '' !== $tax_obj->cap->edit_terms ) {
|
|
return $tax_obj->cap->edit_terms;
|
|
}
|
|
|
|
return 'manage_categories';
|
|
}
|
|
|
|
/**
|
|
* Supply the stored term image URL during taxonomy archive resolution.
|
|
*
|
|
* Hooked to `robotstxt_og_taxonomy_image`. The resolver still validates the
|
|
* URL and resolves a compatible format, so a raw stored value is fine.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param string $url Default URL (empty string).
|
|
* @param int $term_id Term ID.
|
|
* @return string
|
|
*/
|
|
public function filter_taxonomy_image( string $url, $term_id ): string {
|
|
$term_id = (int) $term_id;
|
|
|
|
if ( $term_id <= 0 ) {
|
|
return $url;
|
|
}
|
|
|
|
$stored = get_term_meta( $term_id, self::META_IMAGE, true );
|
|
$stored = is_string( $stored ) ? $stored : '';
|
|
|
|
return '' !== $stored ? $stored : $url;
|
|
}
|
|
|
|
/**
|
|
* Render the OG fields on the term edit page.
|
|
*
|
|
* Fires inside the existing term-edit form-table, so rows are emitted
|
|
* directly (no wrapping table).
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param WP_Term $term The term being edited.
|
|
* @return void
|
|
*/
|
|
public function render_term_fields( WP_Term $term ): void {
|
|
wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
|
|
|
|
$title = $this->get_meta_string( (int) $term->term_id, self::META_TITLE );
|
|
$image = $this->get_meta_string( (int) $term->term_id, self::META_IMAGE );
|
|
$alt = $this->get_meta_string( (int) $term->term_id, self::META_IMAGE_ALT );
|
|
$desc = $this->get_meta_string( (int) $term->term_id, self::META_DESCRIPTION );
|
|
?>
|
|
<tr class="form-field robotstxt-og-term-row">
|
|
<th scope="row">
|
|
<label for="robotstxt_og_term_title"><?php esc_html_e( 'Custom OG Title', 'robotstxt-og' ); ?></label>
|
|
</th>
|
|
<td>
|
|
<input
|
|
type="text"
|
|
id="robotstxt_og_term_title"
|
|
name="robotstxt_og_term_title"
|
|
value="<?php echo esc_attr( $title ); ?>"
|
|
class="large-text"
|
|
/>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Overrides og:title for this term archive. Leave blank to use the term name.', 'robotstxt-og' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr class="form-field robotstxt-og-term-row">
|
|
<th scope="row">
|
|
<label for="robotstxt_og_term_image"><?php esc_html_e( 'OG Image URL', 'robotstxt-og' ); ?></label>
|
|
</th>
|
|
<td>
|
|
<input
|
|
type="url"
|
|
id="robotstxt_og_term_image"
|
|
name="robotstxt_og_term_image"
|
|
value="<?php echo esc_attr( $image ); ?>"
|
|
class="large-text"
|
|
/>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Absolute HTTPS URL for og:image on this term archive. JPEG or PNG recommended.', 'robotstxt-og' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr class="form-field robotstxt-og-term-row">
|
|
<th scope="row">
|
|
<label for="robotstxt_og_term_image_alt"><?php esc_html_e( 'OG Image Alt', 'robotstxt-og' ); ?></label>
|
|
</th>
|
|
<td>
|
|
<input
|
|
type="text"
|
|
id="robotstxt_og_term_image_alt"
|
|
name="robotstxt_og_term_image_alt"
|
|
value="<?php echo esc_attr( $alt ); ?>"
|
|
class="large-text"
|
|
/>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Alt text for the term archive og:image. Leave blank to use the media library alt text.', 'robotstxt-og' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr class="form-field robotstxt-og-term-row">
|
|
<th scope="row">
|
|
<label for="robotstxt_og_term_description"><?php esc_html_e( 'Custom OG Description', 'robotstxt-og' ); ?></label>
|
|
</th>
|
|
<td>
|
|
<textarea
|
|
id="robotstxt_og_term_description"
|
|
name="robotstxt_og_term_description"
|
|
class="large-text"
|
|
rows="3"
|
|
><?php echo esc_textarea( $desc ); ?></textarea>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Overrides og:description for this term archive. Leave blank to use the term description.', 'robotstxt-og' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Save the term OG fields.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param int $term_id Term ID.
|
|
* @param string $taxonomy Taxonomy slug.
|
|
* @return void
|
|
*/
|
|
public function save_term_meta( int $term_id, string $taxonomy ): void {
|
|
$nonce_raw = filter_input( INPUT_POST, self::NONCE_NAME, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
|
$nonce = $nonce_raw ? sanitize_text_field( wp_unslash( $nonce_raw ) ) : '';
|
|
|
|
if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! current_user_can( $this->get_edit_cap( $taxonomy ) ) ) {
|
|
return;
|
|
}
|
|
|
|
$title_raw = filter_input( INPUT_POST, 'robotstxt_og_term_title', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
|
$title = $title_raw ? sanitize_text_field( wp_unslash( $title_raw ) ) : '';
|
|
|
|
$image_raw = filter_input( INPUT_POST, 'robotstxt_og_term_image', FILTER_UNSAFE_RAW );
|
|
$image = $image_raw ? sanitize_url( wp_unslash( $image_raw ) ) : '';
|
|
|
|
$alt_raw = filter_input( INPUT_POST, 'robotstxt_og_term_image_alt', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
|
|
$alt = $alt_raw ? sanitize_text_field( wp_unslash( $alt_raw ) ) : '';
|
|
|
|
$desc_raw = filter_input( INPUT_POST, 'robotstxt_og_term_description', FILTER_UNSAFE_RAW );
|
|
$desc = $desc_raw ? sanitize_textarea_field( wp_unslash( $desc_raw ) ) : '';
|
|
|
|
$this->store_meta( $term_id, self::META_TITLE, $title );
|
|
$this->store_meta( $term_id, self::META_IMAGE, $image );
|
|
$this->store_meta( $term_id, self::META_IMAGE_ALT, $alt );
|
|
$this->store_meta( $term_id, self::META_DESCRIPTION, $desc );
|
|
}
|
|
|
|
/**
|
|
* Store (or delete) a single term meta value.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param int $term_id Term ID.
|
|
* @param string $key Meta key.
|
|
* @param string $value Sanitized value.
|
|
* @return void
|
|
*/
|
|
protected function store_meta( int $term_id, string $key, string $value ): void {
|
|
if ( '' === trim( $value ) ) {
|
|
delete_term_meta( $term_id, $key );
|
|
return;
|
|
}
|
|
|
|
update_term_meta( $term_id, $key, $value );
|
|
}
|
|
|
|
/**
|
|
* Read a term meta value as a string.
|
|
*
|
|
* @since 1.2.0
|
|
*
|
|
* @param int $term_id Term ID.
|
|
* @param string $key Meta key.
|
|
* @return string
|
|
*/
|
|
private function get_meta_string( int $term_id, string $key ): string {
|
|
$raw = get_term_meta( $term_id, $key, true );
|
|
|
|
return is_string( $raw ) ? $raw : '';
|
|
}
|
|
}
|