PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 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 All 507 releases
← All changes | jetpack_vendor/automattic/jetpack-seo/src/class-author-schema-node.php +280 -0 16.2-beta → 16.3-beta View file →
@@ -1,0 +1,280 @@
1 +<?php
2 +/**
3 + * Author Person / ProfilePage Schema.org node builder.
4 + *
5 + * @package automattic/jetpack-seo-package
6 + */
7 +
8 +namespace Automattic\Jetpack\SEO;
9 +
10 +use WP_User;
11 +
12 +/**
13 + * Builds author Person / ProfilePage nodes and owns the extra author profile meta.
14 + */
15 +class Author_Schema_Node {
16 +
17 + /**
18 + * User meta key for the author's job title.
19 + */
20 + const META_JOB_TITLE = 'jetpack_seo_job_title';
21 +
22 + /**
23 + * User meta key for profile links emitted as Person `sameAs`.
24 + */
25 + const META_SAME_AS = 'jetpack_seo_same_as';
26 +
27 + /**
28 + * Wire the WP user-profile fields.
29 + *
30 + * @return void
31 + */
32 + public static function init() {
33 + register_meta(
34 + 'user',
35 + self::META_JOB_TITLE,
36 + array(
37 + 'type' => 'string',
38 + 'single' => true,
39 + 'default' => '',
40 + 'sanitize_callback' => 'sanitize_text_field',
41 + 'show_in_rest' => true,
42 + )
43 + );
44 + register_meta(
45 + 'user',
46 + self::META_SAME_AS,
47 + array(
48 + 'type' => 'array',
49 + 'single' => true,
50 + 'default' => array(),
51 + 'sanitize_callback' => array( __CLASS__, 'sanitize_url_list' ),
52 + 'show_in_rest' => array(
53 + 'schema' => array(
54 + 'type' => 'array',
55 + 'items' => array(
56 + 'type' => 'string',
57 + 'format' => 'uri',
58 + ),
59 + ),
60 + ),
61 + )
62 + );
63 +
64 + add_action( 'show_user_profile', array( __CLASS__, 'render_profile_fields' ) );
65 + add_action( 'edit_user_profile', array( __CLASS__, 'render_profile_fields' ) );
66 + add_action( 'personal_options_update', array( __CLASS__, 'save_profile_fields' ) );
67 + add_action( 'edit_user_profile_update', array( __CLASS__, 'save_profile_fields' ) );
68 + }
69 +
70 + /**
71 + * Build a Person node for a WP user.
72 + *
73 + * @param WP_User|int|null $user User object or ID.
74 + * @return array|null
75 + */
76 + public static function build_person( $user ) {
77 + $user = self::get_user( $user );
78 + if ( ! $user ) {
79 + return null;
80 + }
81 +
82 + $name = self::text( $user->display_name );
83 + if ( '' === $name ) {
84 + return null;
85 + }
86 +
87 + $node = array(
88 + '@type' => 'Person',
89 + '@id' => Schema_Node_Ids::person( $user->ID, $user->user_nicename ),
90 + 'name' => $name,
91 + );
92 +
93 + $image = get_avatar_url( $user->ID, array( 'size' => 512 ) );
94 + if ( is_string( $image ) && '' !== $image ) {
95 + $node['image'] = $image;
96 + }
97 +
98 + $description = self::text( get_the_author_meta( 'description', $user->ID ) );
99 + if ( '' !== $description ) {
100 + $node['description'] = $description;
101 + }
102 +
103 + // Prefer the user's Website field; fall back to the author archive so the
104 + // Person always carries a URL identifying the author.
105 + $url = self::url( $user->user_url );
106 + $node['url'] = '' !== $url ? $url : get_author_posts_url( $user->ID, $user->user_nicename );
107 +
108 + $given_name = self::text( get_the_author_meta( 'first_name', $user->ID ) );
109 + if ( '' !== $given_name ) {
110 + $node['givenName'] = $given_name;
111 + }
112 +
113 + $family_name = self::text( get_the_author_meta( 'last_name', $user->ID ) );
114 + if ( '' !== $family_name ) {
115 + $node['familyName'] = $family_name;
116 + }
117 +
118 + $job_title = self::text( get_user_meta( $user->ID, self::META_JOB_TITLE, true ) );
119 + if ( '' !== $job_title ) {
120 + $node['jobTitle'] = $job_title;
121 + }
122 +
123 + $same_as = self::sanitize_url_list( get_user_meta( $user->ID, self::META_SAME_AS, true ) );
124 + if ( ! empty( $same_as ) ) {
125 + $node['sameAs'] = $same_as;
126 + }
127 +
128 + return $node;
129 + }
130 +
131 + /**
132 + * Build a ProfilePage node for an author archive.
133 + *
134 + * @param WP_User|int|null $user User object or ID.
135 + * @return array|null
136 + */
137 + public static function build_profile_page( $user ) {
138 + $user = self::get_user( $user );
139 + if ( ! $user || '' === self::text( $user->display_name ) ) {
140 + return null;
141 + }
142 +
143 + return array(
144 + '@type' => 'ProfilePage',
145 + '@id' => Schema_Node_Ids::profile_page( $user->ID, $user->user_nicename ),
146 + 'url' => get_author_posts_url( $user->ID, $user->user_nicename ),
147 + 'mainEntity' => array( '@id' => Schema_Node_Ids::person( $user->ID, $user->user_nicename ) ),
148 + );
149 + }
150 +
151 + /**
152 + * Render the Jetpack SEO author profile fields.
153 + *
154 + * @param WP_User $user User being edited.
155 + * @return void
156 + */
157 + public static function render_profile_fields( WP_User $user ) {
158 + $job_field = self::META_JOB_TITLE;
159 + $links_field = self::META_SAME_AS;
160 + $job_title = get_user_meta( $user->ID, $job_field, true );
161 + $same_as = self::sanitize_url_list( get_user_meta( $user->ID, $links_field, true ) );
162 +
163 + printf(
164 + '<h2>%1$s</h2>
165 + <table class="form-table" role="presentation">
166 + <tr>
167 + <th><label for="%2$s">%3$s</label></th>
168 + <td><input type="text" class="regular-text" name="%2$s" id="%2$s" value="%4$s" /></td>
169 + </tr>
170 + <tr>
171 + <th><label for="%5$s">%6$s</label></th>
172 + <td>
173 + <textarea class="large-text code" rows="5" name="%5$s" id="%5$s">%7$s</textarea>
174 + <p class="description">%8$s</p>
175 + </td>
176 + </tr>
177 + </table>',
178 + esc_html__( 'Jetpack SEO author schema', 'jetpack-seo' ),
179 + esc_attr( $job_field ),
180 + esc_html__( 'Job title', 'jetpack-seo' ),
181 + esc_attr( $job_title ),
182 + esc_attr( $links_field ),
183 + esc_html__( 'Profile links', 'jetpack-seo' ),
184 + esc_textarea( implode( "\n", $same_as ) ),
185 + esc_html__( 'One public profile URL per line.', 'jetpack-seo' )
186 + );
187 + }
188 +
189 + /**
190 + * Save the Jetpack SEO author profile fields.
191 + *
192 + * @param int $user_id User ID.
193 + * @return void
194 + */
195 + public static function save_profile_fields( $user_id ) {
196 + if ( ! current_user_can( 'edit_user', $user_id ) ) {
197 + return;
198 + }
199 +
200 + $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
201 + if ( ! wp_verify_nonce( $nonce, 'update-user_' . $user_id ) ) {
202 + return;
203 + }
204 +
205 + $job_title = isset( $_POST[ self::META_JOB_TITLE ] )
206 + ? self::text( sanitize_text_field( wp_unslash( $_POST[ self::META_JOB_TITLE ] ) ) )
207 + : '';
208 + if ( '' === $job_title ) {
209 + delete_user_meta( $user_id, self::META_JOB_TITLE );
210 + } else {
211 + update_user_meta( $user_id, self::META_JOB_TITLE, $job_title );
212 + }
213 +
214 + $same_as = isset( $_POST[ self::META_SAME_AS ] )
215 + ? preg_split( '/\r\n|\r|\n/', sanitize_textarea_field( wp_unslash( $_POST[ self::META_SAME_AS ] ) ) )
216 + : array();
217 + $same_as = self::sanitize_url_list( $same_as );
218 + if ( empty( $same_as ) ) {
219 + delete_user_meta( $user_id, self::META_SAME_AS );
220 + } else {
221 + update_user_meta( $user_id, self::META_SAME_AS, $same_as );
222 + }
223 + }
224 +
225 + /**
226 + * Normalize a list of URLs: keep unique absolute http(s) URLs. Delegates to
227 + * the shared {@see Schema_Settings::sanitize_url_list()} sanitizer.
228 + *
229 + * @param mixed $value Raw value.
230 + * @return array<int, string>
231 + */
232 + public static function sanitize_url_list( $value ) {
233 + return Schema_Settings::sanitize_url_list( $value );
234 + }
235 +
236 + /**
237 + * Resolve a WP user.
238 + *
239 + * @param WP_User|int|null $user User object or ID.
240 + * @return WP_User|null
241 + */
242 + private static function get_user( $user ) {
243 + if ( $user instanceof WP_User ) {
244 + return $user;
245 + }
246 +
247 + // Plugins such as PublishPress Authors replace the author archive's queried
248 + // object with their own class; casting that to int silently yields user 1.
249 + if ( ! is_numeric( $user ) ) {
250 + return null;
251 + }
252 +
253 + $user = get_userdata( (int) $user );
254 + return $user instanceof WP_User ? $user : null;
255 + }
256 +
257 + /**
258 + * Normalize a scalar value to trimmed plain text.
259 + *
260 + * @param mixed $value Raw value.
261 + * @return string
262 + */
263 + private static function text( $value ) {
264 + if ( ! is_scalar( $value ) ) {
265 + return '';
266 + }
267 + return trim( wp_strip_all_tags( (string) $value ) );
268 + }
269 +
270 + /**
271 + * Normalize an absolute http(s) URL.
272 + *
273 + * @param mixed $value Raw value.
274 + * @return string
275 + */
276 + private static function url( $value ) {
277 + $list = Schema_Settings::sanitize_url_list( array( $value ) );
278 + return $list[0] ?? '';
279 + }
280 +}