'GET', 'callback' => array( __CLASS__, 'list_authors' ), 'permission_callback' => $auth ) ); register_rest_route( Gralus_Rest::NS, '/author', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'save' ), 'permission_callback' => $auth ) ); } /** The chosen author when that user can still write posts, else the first administrator. */ public static function author_id() { $chosen = (int) get_option( self::OPTION, 0 ); if ( $chosen && user_can( $chosen, 'edit_posts' ) ) { return $chosen; } $admins = get_users( array( 'role' => 'administrator', 'number' => 1, 'orderby' => 'ID', 'fields' => 'ID' ) ); return $admins ? (int) $admins[0] : 0; } /** One user as Gralus shows it. No email addresses or logins leave the site. */ private static function describe( WP_User $u ) { $photo = (int) get_user_meta( $u->ID, self::PHOTO_META, true ); return array( 'id' => (int) $u->ID, 'name' => $u->display_name, 'roles' => array_values( $u->roles ), 'bio' => (string) get_user_meta( $u->ID, 'description', true ), 'url' => (string) $u->user_url, 'jobTitle' => (string) get_user_meta( $u->ID, self::JOB_META, true ), 'linkedin' => (string) get_user_meta( $u->ID, self::LINKEDIN_META, true ), 'avatar' => (string) get_avatar_url( $u->ID, array( 'size' => 192 ) ), 'ownPhoto' => $photo > 0 && wp_attachment_is_image( $photo ), 'posts' => (int) count_user_posts( $u->ID, 'post', true ), 'profile' => get_author_posts_url( $u->ID ), ); } /** GET /authors: the users who can write posts, and which one Gralus publishes under. */ public static function list_authors() { $users = get_users( array( 'capability' => 'edit_posts', 'number' => 50, 'orderby' => 'post_count', 'order' => 'DESC', ) ); return array( 'chosen' => (int) get_option( self::OPTION, 0 ), 'current' => self::author_id(), 'users' => array_map( array( __CLASS__, 'describe' ), $users ), ); } /** * POST /author: { id, bio?, url?, jobTitle?, linkedin?, photoId? }. Makes that user the author of new articles and * updates only the fields sent. photoId is a Media Library image (uploaded through /media); 0 removes the photo. */ public static function save( WP_REST_Request $request ) { $p = (array) $request->get_json_params(); $id = isset( $p['id'] ) ? (int) $p['id'] : 0; $user = $id ? get_userdata( $id ) : false; if ( ! $user || ! user_can( $user, 'edit_posts' ) ) { return new WP_Error( 'gralus_author', 'That user can\'t write posts on this site.', array( 'status' => 400 ) ); } $update = array( 'ID' => $id ); if ( array_key_exists( 'bio', $p ) ) { $update['description'] = wp_kses_post( wp_unslash( (string) $p['bio'] ) ); } if ( array_key_exists( 'url', $p ) ) { $update['user_url'] = esc_url_raw( (string) $p['url'] ); } if ( count( $update ) > 1 ) { $res = wp_update_user( $update ); if ( is_wp_error( $res ) ) { return $res; } } if ( array_key_exists( 'jobTitle', $p ) ) { update_user_meta( $id, self::JOB_META, sanitize_text_field( (string) $p['jobTitle'] ) ); } if ( array_key_exists( 'linkedin', $p ) ) { $link = esc_url_raw( (string) $p['linkedin'] ); if ( '' !== $link && ! preg_match( '#^https://([a-z]{2,3}\.)?linkedin\.com/#i', $link ) ) { return new WP_Error( 'gralus_author', 'That isn\'t a LinkedIn address.', array( 'status' => 400 ) ); } update_user_meta( $id, self::LINKEDIN_META, $link ); } if ( array_key_exists( 'photoId', $p ) ) { $photo = (int) $p['photoId']; if ( $photo && ! wp_attachment_is_image( $photo ) ) { return new WP_Error( 'gralus_author', 'The photo isn\'t an image in the Media Library.', array( 'status' => 400 ) ); } if ( $photo ) { update_user_meta( $id, self::PHOTO_META, $photo ); } else { delete_user_meta( $id, self::PHOTO_META ); } } update_option( self::OPTION, $id, false ); return array( 'ok' => true, 'author' => self::describe( get_userdata( $id ) ) ); } /** Show the photo chosen in Gralus wherever WordPress shows the user's avatar. */ public static function avatar( $args, $id_or_email ) { $user_id = 0; if ( $id_or_email instanceof WP_User ) { $user_id = $id_or_email->ID; } elseif ( $id_or_email instanceof WP_Post ) { $user_id = (int) $id_or_email->post_author; } elseif ( $id_or_email instanceof WP_Comment ) { $user_id = (int) $id_or_email->user_id; } elseif ( is_numeric( $id_or_email ) ) { $user_id = (int) $id_or_email; } $photo = $user_id ? (int) get_user_meta( $user_id, self::PHOTO_META, true ) : 0; if ( $photo ) { $size = isset( $args['size'] ) ? (int) $args['size'] : 96; $src = wp_get_attachment_image_src( $photo, array( $size, $size ) ); if ( $src ) { $args['url'] = $src[0]; $args['found_avatar'] = true; } } return $args; } /** * On posts by the chosen author, when no SEO plugin describes the author already: Article markup naming the author * as a Person (name, job title, bio, photo, website and LinkedIn). */ public static function print_schema() { if ( ! is_singular( 'post' ) || defined( 'WPSEO_VERSION' ) || class_exists( 'RankMath' ) ) { return; } $post = get_queried_object(); if ( ! $post instanceof WP_Post || (int) $post->post_author !== (int) get_option( self::OPTION, 0 ) ) { return; } $u = get_userdata( (int) $post->post_author ); if ( ! $u ) { return; } $person = array( '@type' => 'Person', 'name' => $u->display_name, 'url' => get_author_posts_url( $u->ID ), ); $job = (string) get_user_meta( $u->ID, self::JOB_META, true ); $bio = wp_strip_all_tags( (string) get_user_meta( $u->ID, 'description', true ) ); $same = array_values( array_filter( array( (string) $u->user_url, (string) get_user_meta( $u->ID, self::LINKEDIN_META, true ) ) ) ); if ( '' !== $job ) { $person['jobTitle'] = $job; } if ( '' !== $bio ) { $person['description'] = $bio; } if ( $same ) { $person['sameAs'] = $same; } $avatar = get_avatar_url( $u->ID, array( 'size' => 256 ) ); if ( $avatar ) { $person['image'] = $avatar; } $data = array( '@context' => 'https://schema.org', '@type' => 'Article', 'headline' => wp_strip_all_tags( get_the_title( $post ) ), 'datePublished' => get_post_time( 'c', true, $post ), 'dateModified' => get_post_modified_time( 'c', true, $post ), 'mainEntityOfPage' => get_permalink( $post ), 'author' => $person, ); wp_print_inline_script_tag( wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG ), array( 'type' => 'application/ld+json' ) ); } }