diff --git a/README.md b/README.md index 7f8fca07..cbf46867 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ via [wp-cli](https://developer.wordpress.org/cli/commands/config/). | `rtcamp.google_login_state` | Filters the state to pass to the Google API. | | `rtcamp.default_algorithm` | Filters default algorithm for openssl signature verification | | `rtcamp.google_redirect_url` | Filters the URL to which the user will be redirected post successful authentication | +| `rtcamp.google_use_saved_profile_picture_for_avatar` | Filter to bypass the use of saved profile picture for avatar | +| `rtcamp.google_should_save_user_profile_picture` | Filter to bypass the profile picture saving process. | +| `rtcamp.google_download_profile_picture` | Filter to control downloading the profile picture if it is not already downloaded. By default, the profile picture is downloaded only if it has not already been downloaded | #### Actions diff --git a/src/Container.php b/src/Container.php index 8ce01d65..56e17827 100644 --- a/src/Container.php +++ b/src/Container.php @@ -23,6 +23,7 @@ use RtCamp\GoogleLogin\Modules\Login; use RtCamp\GoogleLogin\Modules\OneTapLogin; use RtCamp\GoogleLogin\Modules\Settings; +use RtCamp\GoogleLogin\Modules\UserProfile; use RtCamp\GoogleLogin\Utils\Authenticator; use RtCamp\GoogleLogin\Utils\GoogleClient; use RtCamp\GoogleLogin\Modules\Shortcode; @@ -190,6 +191,18 @@ public function define_services(): void { }; + /** + * Define User Profile service to manage user profile. + * + * @param PimpleContainer $c Pimple container object. + * + * @return UserProfile + */ + $this->container['user_profile'] = function ( PimpleContainer $c ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found + return new UserProfile(); + }; + + /** * Define any additional services. * diff --git a/src/Modules/UserProfile.php b/src/Modules/UserProfile.php new file mode 100644 index 00000000..195bf216 --- /dev/null +++ b/src/Modules/UserProfile.php @@ -0,0 +1,209 @@ + + */ + +declare(strict_types=1); + +namespace RtCamp\GoogleLogin\Modules; + +use RtCamp\GoogleLogin\Interfaces\Module as ModuleInterface; +use RtCamp\GoogleLogin\Utils\UserProfileHelper; +use function RtCamp\GoogleLogin\plugin; + +/** + * Class UserProfile. + * + * @package RtCamp\GoogleLogin\Modules + */ +class UserProfile implements ModuleInterface { + + /** + * Module name. + * + * @since n.e.x.t + * + * @return string + */ + public function name(): string { + return 'user_profile'; + } + + /** + * Initialize the UserProfile module. + * + * @since n.e.x.t + * @return void + */ + public function init(): void { + add_action( 'get_avatar_url', [ $this, 'return_avatar_url' ], 10, 3 ); + + // Render the profile edit options. + add_action( 'show_user_profile', [ $this, 'render_user_profile_edit_options' ] ); + add_action( 'edit_user_profile', [ $this, 'render_user_profile_edit_options' ] ); + + // Save the profile edit options. + add_action( 'personal_options_update', [ $this, 'save_user_profile_edit_options' ] ); + add_action( 'edit_user_profile_update', [ $this, 'save_user_profile_edit_options' ] ); + + add_action( 'admin_notices', [ $this, 'display_google_profile_picture_notice' ] ); + } + + /** + * Return the stored profile picture during the account creation. + * + * @since n.e.x.t + * + * @param string $url The URL of the avatar. + * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID, Gravatar SHA-256 or MD5 hash, user email, WP_User object, WP_Post object, or WP_Comment object. + * @param array $args Arguments passed to get_avatar_data() , after processing. + * + * @return string The URL of the avatar. + */ + public function return_avatar_url( $url, $id_or_email, $args ): string { + /** + * Filter to bypass the use of saved profile picture for avatar. + * + * @since n.e.x.t + * + * @param boolean $use_saved_profile_picture_for_avatar Whether to bypass the use of the saved profile picture for avatar or not. + * @param string $url The URL of the avatar. + * @param mixed $id_or_email The avatar to retrieve. Accepts a user ID or, user email, WP_User object, WP_User object, WP_Post object, or WP_Comment object. + * @param array $args Arguments passed to get_avatar_data() , after processing. + */ + $use_avatar_url = apply_filters( 'rtcamp.google_use_saved_profile_picture_for_avatar', true, $url, $id_or_email, $args ); + + if ( ! $use_avatar_url ) { + return $url; + } + + // Do not interfere on profile edit page. + if ( defined( 'IS_PROFILE_PAGE' ) && IS_PROFILE_PAGE ) { + return $url; + } + + // Do not interfere on user edit screen in admin. + $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; + if ( $current_screen && 'user-edit' === $current_screen->id ) { + return $url; + } + + $wp_user = null; + if ( is_int( $id_or_email ) ) { + $wp_user = get_user_by( 'id', $id_or_email ); + } elseif ( is_string( $id_or_email ) && is_email( $id_or_email ) ) { + $wp_user = get_user_by( 'email', $id_or_email ); + } elseif ( $id_or_email instanceof \WP_User ) { + $wp_user = $id_or_email; + } elseif ( $id_or_email instanceof \WP_Post ) { + $wp_user = get_user_by( 'id', (int) $id_or_email->post_author ); + } elseif ( $id_or_email instanceof \WP_Comment ) { + $wp_user = get_user_by( 'id', (int) $id_or_email->user_id ); + } + + // Bail early if the user is not found. + if ( ! $wp_user ) { + return $url; + } + + // Bail if user has chosen gravatar as avatar source. + $profile_picture_source = UserProfileHelper::get_profile_picture_source( $wp_user->ID ); + if ( $profile_picture_source && 'gravatar' === $profile_picture_source ) { + return $url; + } + + // Return the saved google avatar URL. + $width = isset( $args['width'] ) ? absint( $args['width'] ) : 96; + $height = isset( $args['height'] ) ? absint( $args['height'] ) : 96; + + $profile_picture_id = UserProfileHelper::get_saved_google_profile_picture_id( $wp_user->ID ); + + if ( ! empty( $profile_picture_id ) ) { + $profile_picture_url = wp_get_attachment_image_url( $profile_picture_id, [ $width, $height ] ); + if ( $profile_picture_url ) { + $url = $profile_picture_url; + } + } + + return $url; + } + + /** + * Render user profile edit template + * + * @since n.e.x.t + * @param \WP_User $wp_user WP_User object. + * @return void + */ + public function render_user_profile_edit_options( \WP_User $wp_user ) { + require_once plugin()->template_dir . 'user-profile-edit.php'; //phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingCustomFunction + } + + /** + * Save user profile edit options. + * + * @since n.e.x.t + * + * @param int $user_id User ID. + * @return void + */ + public function save_user_profile_edit_options( int $user_id ) { + if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-user_' . $user_id ) ) { + return; + } + + if ( ! current_user_can( 'edit_user', $user_id ) ) { + return; + } + + if ( isset( $_POST['rtlwg_profile_picture_source'] ) ) { + $avatar_source = sanitize_text_field( wp_unslash( $_POST['rtlwg_profile_picture_source'] ) ); + if ( ! in_array( $avatar_source, [ 'google', 'gravatar' ], true ) ) { + return; + } + UserProfileHelper::save_profile_picture_source( $user_id, $avatar_source ); + } + } + + /** + * Display notice to the user that profile picture has been downloaded and they can use that. + * + * @since n.e.x.t + * + * @return void + */ + public function display_google_profile_picture_notice(): void { + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; + $profile_picture_source = UserProfileHelper::get_profile_picture_source( get_current_user_id() ); + $has_google_profile_picture = UserProfileHelper::has_google_profile_picture( get_current_user_id() ); + + // Bail early if the current screen is not profile screen. + if ( null === $screen || 'profile' !== $screen->id ) { + return; + } + + // Bail early if user has set google profile picture source. + if ( 'google' === $profile_picture_source ) { + return; + } + + if ( ! $has_google_profile_picture ) { + return; + } + + ?> +
+

+ + + + +

+
+ email ) ) { $user_wp = get_user_by( 'email', $user->email ); + $this->save_user_profile_picture( $user_wp->ID, $user ); + /** * Fires once the user has been authenticated. * @@ -112,6 +115,11 @@ public function register( stdClass $user ): ?WP_User { ] ); + $this->save_user_profile_picture( $uid, $user ); + + // Save the profile picture source to google for the first login where the user registration happens. + UserProfileHelper::save_profile_picture_source( $uid, 'google' ); + /** * Fires once the user has been registered successfully. */ @@ -179,4 +187,150 @@ private function can_register_with_email( string $email ): bool { return in_array( $email_parts[1], $whitelisted_domains, true ); } + + /** + * Save user profile picture. + * + * @since n.e.x.t + * + * @param int $user_id WP User ID. + * @param \stdClass $user User object returned by Google. + * @return void + */ + private function save_user_profile_picture( $user_id, $user ): void { + global $wp_filesystem; + + /** + * Filter to bypass the profile picture saving process. + * + * @since n.e.x.t + * + * @param boolean $save Whether to save profile picture or not. + * @param int $user_id WP User ID. + * @param \stdClass $user User object returned by Google. + */ + $save_profile_picture = apply_filters( 'rtcamp.google_should_save_user_profile_picture', true, $user_id, $user ); + + if ( ! $save_profile_picture ) { + return; + } + + if ( is_null( $wp_filesystem ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + if ( ! function_exists( 'media_handle_sideload' ) ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + } + + if ( ! isset( $user->picture ) || empty( $user->picture ) ) { + return; + } + + $user_has_google_profile_picture = UserProfileHelper::has_google_profile_picture( $user_id ); + + /** + * Filter to control downloading the profile picture if it is not already downloaded. + * By default, the profile picture is downloaded only if it has not already been downloaded. + * + * @since n.e.x.t + * + * @param boolean $download_profile_picture Whether to download profile picture. + * @param int $user_id WP User ID. + * @param \stdClass $user User object returned by Google. + */ + $download_profile_picture = apply_filters( + 'rtcamp.google_download_profile_picture', + ! $user_has_google_profile_picture, + $user_id, + $user + ); + + // Bail early if we are not to download the profile picture. + if ( false === $download_profile_picture ) { + return; + } + + $profile_picture_filename = $this->download_profile_picture( $user->picture ); + + if ( null === $profile_picture_filename ) { + return; + } + + $file_array = array( + 'name' => basename( $profile_picture_filename ), + 'tmp_name' => $profile_picture_filename, + ); + + // Intentionally passing 0 as $post_id to create an orphaned attachment for user profile picture. + // The attachment is tracked via user meta for management and cleanup. + $attachment_id = media_handle_sideload( $file_array, 0 ); + + if ( is_wp_error( $attachment_id ) ) { + // Cleanup temporary file. + $wp_filesystem->delete( $profile_picture_filename ); + return; + } + + // Set the google profile picture attachment to the user. + UserProfileHelper::set_google_profile_picture_to_user( $user_id, $attachment_id ); + + // Save the original google profile picture URL. + UserProfileHelper::save_original_google_profile_picture_url( $user_id, $user->picture ); + } + + /** + * Download profile picture from given URL and return the saved profile picture file path. + * + * @since n.e.x.t + * + * @param string $profile_picture_url Profile picture URL. + * @return string|null Profile picture file path or null on failure. + */ + private function download_profile_picture( string $profile_picture_url ) { + global $wp_filesystem; + + if ( is_null( $wp_filesystem ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + + // Using larger image size. By default, profile picture has 96 width size with cropped. + $stripped_picture_url = str_replace( '=s96-c', '', $profile_picture_url ); + + $profile_picture_filename = download_url( $stripped_picture_url ); + + if ( is_wp_error( $profile_picture_filename ) ) { + return null; + } + + if ( str_ends_with( $profile_picture_filename, '.tmp' ) && $wp_filesystem ) { + $profile_picture_mime_type = wp_get_image_mime( $profile_picture_filename ); + + $profile_picture_extension = 'jpg'; // Default extension. + $mime_types = wp_get_mime_types(); + foreach ( $mime_types as $ext => $mime_type ) { + if ( $profile_picture_mime_type === $mime_type ) { + $profile_picture_extension = current( explode( '|', $ext ) ); + break; + } + } + + $new_profile_picture_filename = str_replace( '.tmp', ".{$profile_picture_extension}", $profile_picture_filename ); + $is_file_moved = $wp_filesystem->move( $profile_picture_filename, $new_profile_picture_filename, true ); + + if ( ! $is_file_moved ) { + // Cleanup temporary file. + $wp_filesystem->delete( $profile_picture_filename ); + return null; + } + + $profile_picture_filename = $new_profile_picture_filename; + } + + return $profile_picture_filename; + } } diff --git a/src/Utils/UserProfileHelper.php b/src/Utils/UserProfileHelper.php new file mode 100644 index 00000000..c7ea8a61 --- /dev/null +++ b/src/Utils/UserProfileHelper.php @@ -0,0 +1,124 @@ +ID ); +$rtlwg_profile_picture_source = UserProfileHelper::get_profile_picture_source( $wp_user->ID ); +?> + +
+

+

+ + + + + + + + + + + + +