Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
78.15% |
93 / 119 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
| Author_Schema_Node | |
78.15% |
93 / 119 |
|
33.33% |
3 / 9 |
41.02 | |
0.00% |
0 / 1 |
| init | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
1 | |||
| build_person | |
93.75% |
30 / 32 |
|
0.00% |
0 / 1 |
11.03 | |||
| build_profile_page | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
| render_profile_fields | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
| save_profile_fields | |
77.78% |
14 / 18 |
|
0.00% |
0 / 1 |
8.70 | |||
| sanitize_url_list | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| get_user | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
| text | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| url | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| 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 | $user = get_userdata( (int) $user ); |
| 248 | return $user instanceof WP_User ? $user : null; |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Normalize a scalar value to trimmed plain text. |
| 253 | * |
| 254 | * @param mixed $value Raw value. |
| 255 | * @return string |
| 256 | */ |
| 257 | private static function text( $value ) { |
| 258 | if ( ! is_scalar( $value ) ) { |
| 259 | return ''; |
| 260 | } |
| 261 | return trim( wp_strip_all_tags( (string) $value ) ); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Normalize an absolute http(s) URL. |
| 266 | * |
| 267 | * @param mixed $value Raw value. |
| 268 | * @return string |
| 269 | */ |
| 270 | private static function url( $value ) { |
| 271 | $list = Schema_Settings::sanitize_url_list( array( $value ) ); |
| 272 | return $list[0] ?? ''; |
| 273 | } |
| 274 | } |