← 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 | +} | |