v1.2.0
This commit is contained in:
parent
ddaaff71ad
commit
3bb9ed33e1
12 changed files with 1762 additions and 409 deletions
359
includes/class-robotstxt-og-term-meta.php
Normal file
359
includes/class-robotstxt-og-term-meta.php
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
<?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 : '';
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue