PluginProbe
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings / 7.1.1
MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings v7.1.1
7.2.1 7.2 7.1.2 7.1.1 7.1 7.0.4 7.0.6 7.0.7 6.3.8 6.3.7 6.3.6 6.3.5 6.3.4 6.3.3 6.3.1 trunk 5.7.3 5.7.5 5.8.1 5.8.2 5.8.3 5.8.4 5.8.6 6.0.4 6.0.5 All 36 releases
mlsimport / includes / standalone / class-mlsimport-term-meta.php

class-mlsimport-term-meta.php in MLSImport: IDX Plugin & MLS Plugin for Real Estate Listings 7.1.1, at includes/standalone/class-mlsimport-term-meta.php

218 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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