| 1 |
<?php |
| 2 |
/** |
| 3 |
* Standalone (theme_id 990) per-term settings: a featured image and an HTML |
| 4 |
* description for every plugin taxonomy term. |
| 5 |
* |
| 6 |
* The id coercion is pure PHP (no WordPress, no DB) so it is unit-testable in |
| 7 |
* isolation; the render / save / enqueue methods are thin WordPress wrappers. |
| 8 |
* |
| 9 |
* @package Mlsimport |
| 10 |
*/ |
| 11 |
|
| 12 |
if ( ! defined( 'ABSPATH' ) ) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
/** |
| 17 |
* Adds an image + HTML-description settings area to every plugin taxonomy term. |
| 18 |
*/ |
| 19 |
class Mlsimport_Term_Meta { |
| 20 |
|
| 21 |
/** Term meta key: featured-image attachment id. */ |
| 22 |
const IMAGE_KEY = 'mlsimport_term_image_id'; |
| 23 |
|
| 24 |
/** Term meta key: HTML description. */ |
| 25 |
const HTML_KEY = 'mlsimport_term_html'; |
| 26 |
|
| 27 |
/** Nonce action / field name for the term settings save. */ |
| 28 |
const NONCE_ACTION = 'mlsimport_save_term_meta'; |
| 29 |
const NONCE_FIELD = 'mlsimport_term_meta_nonce'; |
| 30 |
|
| 31 |
/** |
| 32 |
* Coerce a posted featured-image value to a non-negative attachment id. |
| 33 |
* `0` means "no image". Non-integer, negative or non-numeric input all |
| 34 |
* resolve to 0 so a bad value can never persist as an image reference. |
| 35 |
* |
| 36 |
* @param mixed $raw Raw posted value. |
| 37 |
* @return int Attachment id, or 0 for none. |
| 38 |
*/ |
| 39 |
public static function image_id( $raw ): int { |
| 40 |
// Trim string input so " 42 " is treated the same as "42". |
| 41 |
$raw = is_string( $raw ) ? trim( $raw ) : $raw; |
| 42 |
// A real int: keep it only when positive, otherwise 0 (= no image). |
| 43 |
if ( is_int( $raw ) ) { |
| 44 |
return $raw > 0 ? $raw : 0; |
| 45 |
} |
| 46 |
// A string of digits (no sign, no decimals) casts to the id, else 0. |
| 47 |
return ( is_string( $raw ) && ctype_digit( $raw ) ) ? (int) $raw : 0; |
| 48 |
} |
| 49 |
|
| 50 |
/* --------------------------------------------------------------------- * |
| 51 |
* WordPress wiring (thin wrappers over the pure core above). |
| 52 |
* --------------------------------------------------------------------- */ |
| 53 |
|
| 54 |
/** |
| 55 |
* Register the admin hooks for every plugin taxonomy. Admin only. |
| 56 |
* |
| 57 |
* @return void |
| 58 |
*/ |
| 59 |
public static function register(): void { |
| 60 |
// Admin-only screens; nothing to wire on the front end. |
| 61 |
if ( ! is_admin() ) { |
| 62 |
return; |
| 63 |
} |
| 64 |
// The CPT class supplies the taxonomy list; bail if it isn't loaded. |
| 65 |
if ( ! class_exists( 'Mlsimport_Standalone_Cpt' ) ) { |
| 66 |
return; |
| 67 |
} |
| 68 |
// For every plugin taxonomy: render the fields on add/edit, save on create/edit. |
| 69 |
foreach ( Mlsimport_Standalone_Cpt::taxonomy_slugs() as $taxonomy ) { |
| 70 |
add_action( "{$taxonomy}_add_form_fields", array( __CLASS__, 'add_fields' ) ); |
| 71 |
add_action( "{$taxonomy}_edit_form_fields", array( __CLASS__, 'edit_fields' ), 10, 2 ); |
| 72 |
add_action( "created_{$taxonomy}", array( __CLASS__, 'save' ) ); |
| 73 |
add_action( "edited_{$taxonomy}", array( __CLASS__, 'save' ) ); |
| 74 |
} |
| 75 |
// Load the media library + picker JS on the term screens. |
| 76 |
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); |
| 77 |
} |
| 78 |
|
| 79 |
/** |
| 80 |
* Enqueue the media library + image-picker JS on the term screens of our |
| 81 |
* taxonomies only. |
| 82 |
* |
| 83 |
* @param string $hook Current admin page hook. |
| 84 |
* @return void |
| 85 |
*/ |
| 86 |
public static function assets( $hook ): void { |
| 87 |
// Only the term list (edit-tags.php) and single-term (term.php) screens. |
| 88 |
if ( 'edit-tags.php' !== $hook && 'term.php' !== $hook ) { |
| 89 |
return; |
| 90 |
} |
| 91 |
// Which taxonomy's screen this is (read from the query for routing only). |
| 92 |
$taxonomy = isset( $_GET['taxonomy'] ) ? sanitize_key( wp_unslash( $_GET['taxonomy'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing. |
| 93 |
// Skip taxonomies that aren't ours. |
| 94 |
if ( ! in_array( $taxonomy, Mlsimport_Standalone_Cpt::taxonomy_slugs(), true ) ) { |
| 95 |
return; |
| 96 |
} |
| 97 |
// Load the WP media library, then the picker script that drives the control. |
| 98 |
wp_enqueue_media(); |
| 99 |
$base = defined( 'MLSIMPORT_PLUGIN_URL' ) ? MLSIMPORT_PLUGIN_URL : plugin_dir_url( dirname( __DIR__ ) . '/mlsimport.php' ); |
| 100 |
$ver = defined( 'MLSIMPORT_VERSION' ) ? MLSIMPORT_VERSION : false; |
| 101 |
wp_enqueue_script( 'mlsimport-term-meta', $base . 'admin/js/mlsimport-term-meta.js', array( 'jquery' ), $ver, true ); |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Fields on the "Add new term" screen: a plain HTML textarea (TinyMCE does |
| 106 |
* not initialise reliably in the inline add-tag AJAX form) + image picker. |
| 107 |
* |
| 108 |
* @param string $taxonomy Current taxonomy slug. |
| 109 |
* @return void |
| 110 |
*/ |
| 111 |
public static function add_fields( $taxonomy ): void { |
| 112 |
wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); |
| 113 |
?> |
| 114 |
<div class="form-field mlsimport-term-image"> |
| 115 |
<label><?php esc_html_e( 'Featured image', 'mlsimport' ); ?></label> |
| 116 |
<?php self::image_control( 0 ); ?> |
| 117 |
</div> |
| 118 |
<div class="form-field mlsimport-term-html"> |
| 119 |
<label for="mlsimport_term_html"><?php esc_html_e( 'HTML description', 'mlsimport' ); ?></label> |
| 120 |
<textarea name="<?php echo esc_attr( self::HTML_KEY ); ?>" id="mlsimport_term_html" rows="5" cols="50"></textarea> |
| 121 |
<p class="description"><?php esc_html_e( 'Save the term, then edit it to use the full visual editor.', 'mlsimport' ); ?></p> |
| 122 |
</div> |
| 123 |
<?php |
| 124 |
} |
| 125 |
|
| 126 |
/** |
| 127 |
* Fields on the "Edit term" screen: full wp_editor + image picker, pre-filled |
| 128 |
* from stored meta. Rendered as table rows to match core's edit-term layout. |
| 129 |
* |
| 130 |
* @param WP_Term $term Current term. |
| 131 |
* @param string $taxonomy Current taxonomy slug. |
| 132 |
* @return void |
| 133 |
*/ |
| 134 |
public static function edit_fields( $term, $taxonomy ): void { |
| 135 |
wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); |
| 136 |
$image_id = (int) get_term_meta( $term->term_id, self::IMAGE_KEY, true ); |
| 137 |
$html = (string) get_term_meta( $term->term_id, self::HTML_KEY, true ); |
| 138 |
?> |
| 139 |
<tr class="form-field mlsimport-term-image"> |
| 140 |
<th scope="row"><label><?php esc_html_e( 'Featured image', 'mlsimport' ); ?></label></th> |
| 141 |
<td><?php self::image_control( $image_id ); ?></td> |
| 142 |
</tr> |
| 143 |
<tr class="form-field mlsimport-term-html"> |
| 144 |
<th scope="row"><label for="mlsimport_term_html"><?php esc_html_e( 'HTML description', 'mlsimport' ); ?></label></th> |
| 145 |
<td> |
| 146 |
<?php |
| 147 |
wp_editor( |
| 148 |
$html, |
| 149 |
'mlsimport_term_html', |
| 150 |
array( |
| 151 |
'textarea_name' => self::HTML_KEY, |
| 152 |
'textarea_rows' => 8, |
| 153 |
'media_buttons' => true, |
| 154 |
) |
| 155 |
); |
| 156 |
?> |
| 157 |
</td> |
| 158 |
</tr> |
| 159 |
<?php |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* The shared image picker control: a hidden id input, a preview, and the |
| 164 |
* set/remove buttons the picker JS drives. |
| 165 |
* |
| 166 |
* @param int $image_id Currently stored attachment id (0 = none). |
| 167 |
* @return void |
| 168 |
*/ |
| 169 |
private static function image_control( int $image_id ): void { |
| 170 |
$src = $image_id ? wp_get_attachment_image_url( $image_id, 'medium' ) : ''; |
| 171 |
?> |
| 172 |
<div class="mlsimport-term-image__wrap"> |
| 173 |
<input type="hidden" class="mlsimport-term-image__id" name="<?php echo esc_attr( self::IMAGE_KEY ); ?>" value="<?php echo esc_attr( (string) $image_id ); ?>"> |
| 174 |
<img class="mlsimport-term-image__preview" src="<?php echo esc_url( $src ); ?>" alt="" style="max-width:150px;height:auto;display:<?php echo $src ? 'block' : 'none'; ?>;margin-bottom:8px;"> |
| 175 |
<button type="button" class="button mlsimport-term-image__set"><?php esc_html_e( 'Set image', 'mlsimport' ); ?></button> |
| 176 |
<button type="button" class="button mlsimport-term-image__remove" style="display:<?php echo $src ? 'inline-block' : 'none'; ?>;"><?php esc_html_e( 'Remove', 'mlsimport' ); ?></button> |
| 177 |
</div> |
| 178 |
<?php |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Persist the term settings: nonce + capability guard, then whitelist-write |
| 183 |
* the image id and the kses-filtered HTML. An empty/zero image removes the |
| 184 |
* meta rather than storing 0. |
| 185 |
* |
| 186 |
* @param int $term_id Term id. |
| 187 |
* @return void |
| 188 |
*/ |
| 189 |
public static function save( $term_id ): void { |
| 190 |
// Verify our nonce before trusting any posted value. |
| 191 |
if ( ! isset( $_POST[ self::NONCE_FIELD ] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ), self::NONCE_ACTION ) ) { |
| 192 |
return; |
| 193 |
} |
| 194 |
// Require the term-management capability. |
| 195 |
if ( ! current_user_can( 'manage_categories' ) ) { |
| 196 |
return; |
| 197 |
} |
| 198 |
|
| 199 |
// Coerce the posted image to an attachment id; store it, or clear on 0/none. |
| 200 |
$image_id = isset( $_POST[ self::IMAGE_KEY ] ) ? self::image_id( wp_unslash( $_POST[ self::IMAGE_KEY ] ) ) : 0; // phpcs:ignore WordPress.Security.ValidatedSanitized.InputNotSanitized -- coerced to int by image_id(). |
| 201 |
if ( $image_id > 0 ) { |
| 202 |
update_term_meta( $term_id, self::IMAGE_KEY, $image_id ); |
| 203 |
} else { |
| 204 |
delete_term_meta( $term_id, self::IMAGE_KEY ); |
| 205 |
} |
| 206 |
|
| 207 |
// HTML description: kses-filter it, store when non-empty, else remove the meta. |
| 208 |
if ( isset( $_POST[ self::HTML_KEY ] ) ) { |
| 209 |
$html = wp_kses_post( wp_unslash( $_POST[ self::HTML_KEY ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitized.InputNotSanitized -- wp_kses_post sanitizes. |
| 210 |
if ( '' !== $html ) { |
| 211 |
update_term_meta( $term_id, self::HTML_KEY, $html ); |
| 212 |
} else { |
| 213 |
delete_term_meta( $term_id, self::HTML_KEY ); |
| 214 |
} |
| 215 |
} |
| 216 |
} |
| 217 |
} |
| 218 |
|