PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / modules / seo-tools / class-jetpack-seo-admin-columns.php

class-jetpack-seo-admin-columns.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at modules/seo-tools/class-jetpack-seo-admin-columns.php

259 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Adds factual SEO columns to wp-admin post list tables.
4 *
5 * Surfaces the per-post SEO *state* at a glance — schema type, whether a meta
6 * description is set, and search visibility — without grading it. Whether a
7 * given setting should be configured depends on the post's purpose, so we
8 * report facts and let the author decide.
9 *
10 * @package automattic/jetpack
11 */
12
13 /**
14 * Registers read-only SEO columns on every public post-list table.
15 */
16 class Jetpack_SEO_Admin_Columns {
17
18 /**
19 * The column IDs this class registers on the post-list tables.
20 *
21 * @var string[]
22 */
23 const COLUMNS = array( 'jetpack_seo_schema', 'jetpack_seo_description', 'jetpack_seo_search' );
24
25 /**
26 * User meta listing the post-list screen IDs whose saved hidden-column set
27 * has already been backfilled with the SEO columns.
28 */
29 const BACKFILL_USER_META_KEY = 'jetpack_seo_columns_backfilled_screens';
30
31 /**
32 * Wire all hooks.
33 *
34 * @return void
35 */
36 public static function init() {
37 add_action( 'admin_init', array( __CLASS__, 'register_columns_for_post_types' ) );
38 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
39 add_filter( 'default_hidden_columns', array( __CLASS__, 'default_hidden_columns' ), 10, 2 );
40 add_action( 'current_screen', array( __CLASS__, 'backfill_hidden_columns' ) );
41 }
42
43 /**
44 * Hide the SEO columns by default in Screen Options.
45 *
46 * Three extra always-on columns squeeze the title column unreadably narrow,
47 * so we default them to hidden using core's `default_hidden_columns` filter —
48 * the standard mechanism for choosing which columns start hidden in Screen
49 * Options. It only reaches users who have never customized Screen Options for
50 * the screen; everyone else is handled by self::backfill_hidden_columns().
51 *
52 * @param string[] $hidden Column IDs hidden by default.
53 * @param WP_Screen $screen Current screen.
54 * @return string[]
55 */
56 public static function default_hidden_columns( $hidden, $screen ) {
57 if ( isset( $screen->base ) && 'edit' === $screen->base ) {
58 $hidden = array_merge( $hidden, self::COLUMNS );
59 }
60 return $hidden;
61 }
62
63 /**
64 * Hide the SEO columns for users whose Screen Options predate them.
65 *
66 * `default_hidden_columns` is a first-run default: core only consults it when
67 * the user has no saved hidden-column set for the screen (`$use_defaults =
68 * ! is_array( $hidden )` in wp-admin/includes/screen.php). Toggling any single
69 * column checkbox writes that user meta, so a user who customized Screen
70 * Options at any point — years before these columns existed — never gets the
71 * default and sees all three columns crowding the title column.
72 *
73 * Backfill those users once per screen by merging the SEO columns into the set
74 * they already have, the way core seeds its own nav-menu column defaults in
75 * wp-admin/includes/nav-menu.php. Screens are recorded as they're visited, so a
76 * post type registered later still gets handled the first time it's opened, and
77 * a user who turns the columns back on afterwards keeps them.
78 *
79 * @param WP_Screen $screen Current screen.
80 * @return void
81 */
82 public static function backfill_hidden_columns( $screen ) {
83 if ( ! ( $screen instanceof WP_Screen ) || 'edit' !== $screen->base ) {
84 return;
85 }
86
87 $user_id = get_current_user_id();
88 if ( ! $user_id ) {
89 return;
90 }
91
92 $backfilled = get_user_meta( $user_id, self::BACKFILL_USER_META_KEY, true );
93 $backfilled = is_array( $backfilled ) ? $backfilled : array();
94 if ( in_array( $screen->id, $backfilled, true ) ) {
95 return;
96 }
97
98 $option_name = 'manage' . $screen->id . 'columnshidden';
99 $hidden = get_user_option( $option_name, $user_id );
100
101 /*
102 * A non-array means the user has never customized this screen, so
103 * `default_hidden_columns` already hides the columns for them and there is
104 * nothing to backfill — just record the screen so we stop looking.
105 */
106 if ( is_array( $hidden ) ) {
107 $missing = array_values( array_diff( self::COLUMNS, $hidden ) );
108 if ( ! empty( $missing ) ) {
109 /*
110 * $is_global = true writes the unprefixed key — the same one core's
111 * `hidden-columns` AJAX handler writes and that the get_user_option()
112 * read above falls back to. Passing false would write a blog-prefixed
113 * key that core itself never updates, so the two would diverge.
114 */
115 update_user_option( $user_id, $option_name, array_values( array_merge( $hidden, $missing ) ), true );
116 }
117 }
118
119 $backfilled[] = $screen->id;
120 update_user_meta( $user_id, self::BACKFILL_USER_META_KEY, $backfilled );
121 }
122
123 /**
124 * Register columns + renderers for each supported post type.
125 *
126 * @return void
127 */
128 public static function register_columns_for_post_types() {
129 foreach ( self::get_supported_post_types() as $post_type ) {
130 add_filter( "manage_{$post_type}_posts_columns", array( __CLASS__, 'add_columns' ) );
131 add_action( "manage_{$post_type}_posts_custom_column", array( __CLASS__, 'render_column' ), 10, 2 );
132 }
133 }
134
135 /**
136 * Post types that get the SEO columns.
137 *
138 * Defers to the SEO package so the columns cover exactly the post types the
139 * Jetpack > SEO Content tab lists, rather than a second copy of the same query
140 * that can drift from it. The guard covers older bundled snapshots of the
141 * package that predate Post_Types.
142 *
143 * @return string[]
144 */
145 private static function get_supported_post_types() {
146 if ( method_exists( '\Automattic\Jetpack\SEO\Post_Types', 'get_supported_content_types' ) ) {
147 return \Automattic\Jetpack\SEO\Post_Types::get_supported_content_types();
148 }
149
150 $post_types = get_post_types(
151 array(
152 'public' => true,
153 'show_ui' => true,
154 'show_in_rest' => true,
155 ),
156 'names'
157 );
158 unset( $post_types['attachment'] );
159
160 return array_values( $post_types );
161 }
162
163 /**
164 * Insert the SEO columns just after the title column.
165 *
166 * @param array $columns Existing columns keyed by column name.
167 * @return array
168 */
169 public static function add_columns( $columns ) {
170 $new = array();
171 foreach ( $columns as $key => $label ) {
172 $new[ $key ] = $label;
173 if ( 'title' === $key ) {
174 $new['jetpack_seo_schema'] = __( 'Schema', 'jetpack' );
175 $new['jetpack_seo_description'] = __( 'Meta description', 'jetpack' );
176 $new['jetpack_seo_search'] = __( 'Search', 'jetpack' );
177 }
178 }
179 return $new;
180 }
181
182 /**
183 * Render a single cell — factual state only.
184 *
185 * @param string $column Column identifier.
186 * @param int $post_id Current row post ID.
187 * @return void
188 */
189 public static function render_column( $column, $post_id ) {
190 if ( ! in_array( $column, self::COLUMNS, true ) ) {
191 return;
192 }
193
194 $coverage = Jetpack_SEO_Posts::get_post_seo_coverage( $post_id );
195
196 switch ( $column ) {
197 case 'jetpack_seo_schema':
198 $schema = Jetpack_SEO_Posts::get_post_schema_type( $post_id );
199 echo esc_html( '' !== $schema ? self::schema_type_label( $schema ) : '—' );
200 break;
201
202 case 'jetpack_seo_description':
203 // wp_kses_post() sanitizes the markup and signals the escaping to PHPCS;
204 // the muted branch wraps its (already-escaped) label in a <span>.
205 echo wp_kses_post(
206 $coverage['has_description']
207 ? esc_html__( 'Set', 'jetpack' )
208 : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Not set', 'jetpack' ) . '</span>'
209 );
210 break;
211
212 case 'jetpack_seo_search':
213 echo wp_kses_post(
214 $coverage['noindex']
215 ? esc_html__( 'Hidden', 'jetpack' )
216 : '<span class="jetpack-seo-col-muted">' . esc_html__( 'Visible', 'jetpack' ) . '</span>'
217 );
218 break;
219 }
220 }
221
222 /**
223 * Display label for an allowed schema type.
224 *
225 * @param string $schema Schema type slug.
226 * @return string
227 */
228 private static function schema_type_label( $schema ) {
229 switch ( $schema ) {
230 case 'article':
231 return __( 'Article', 'jetpack' );
232 case 'faq':
233 return __( 'FAQ', 'jetpack' );
234 default:
235 return ucfirst( $schema );
236 }
237 }
238
239 /**
240 * Minimal column-width styling on edit.php only (no color-coding —
241 * these columns report state, not a grade).
242 *
243 * @param string $hook_suffix Current admin hook suffix.
244 * @return void
245 */
246 public static function enqueue_assets( $hook_suffix ) {
247 if ( 'edit.php' !== $hook_suffix ) {
248 return;
249 }
250 wp_register_style( 'jetpack-seo-admin-columns', false, array(), JETPACK__VERSION );
251 wp_add_inline_style(
252 'jetpack-seo-admin-columns',
253 '.column-jetpack_seo_schema,.column-jetpack_seo_description,.column-jetpack_seo_search{width:9em}' .
254 '.jetpack-seo-col-muted{color:#787c82}'
255 );
256 wp_enqueue_style( 'jetpack-seo-admin-columns' );
257 }
258 }
259