diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index a180ba8e1a3f..1f7f5b605bb1 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.0.13 + +* Fix `GoogleUserCircleAvatar` to handle new style profile image URLs. + ## 4.0.12 * Move google_sign_in plugin to google_sign_in/google_sign_in to prepare for federated implementations. diff --git a/packages/google_sign_in/google_sign_in/lib/src/fife.dart b/packages/google_sign_in/google_sign_in/lib/src/fife.dart new file mode 100644 index 000000000000..14ecf5fd6083 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/lib/src/fife.dart @@ -0,0 +1,70 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A regular expression that matches against the "size directive" path +/// segment of Google profile image URLs. +/// +/// The format is is "`/sNN-c/`", where `NN` is the max width/height of the +/// image, and "`c`" indicates we want the image cropped. +final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$'); + +/// Adds [size] (and crop) directive to [photoUrl]. +/// +/// There are two formats for photoUrls coming from the Sign In backend. +/// +/// The two formats can be told apart by the number of path segments in the +/// URL (path segments: parts of the URL separated by slashes "/"): +/// +/// * If the URL has 2 or less path segments, it is a *new* style URL. +/// * If the URL has more than 2 path segments, it is an old style URL. +/// +/// Old style URLs encode the image transformation directives as the last +/// path segment. Look at the [sizeDirective] Regular Expression for more +/// information about these URLs. +/// +/// New style URLs carry the same directives at the end of the URL, +/// after an = sign, like: "`=s120-c-fSoften=1,50,0`". +/// +/// Directives may contain the "=" sign (`fSoften=1,50,0`), but it seems the +/// base URL of the images don't. "Everything after the first = sign" is a +/// good heuristic to split new style URLs. +/// +/// Each directive is separated from others by dashes. Directives are the same +/// as described in the [sizeDirective] RegExp. +/// +/// Modified image URLs are recomposed by performing the parsing steps in reverse. +String addSizeDirectiveToUrl(String photoUrl, double size) { + final Uri profileUri = Uri.parse(photoUrl); + final List pathSegments = List.from(profileUri.pathSegments); + if (pathSegments.length <= 2) { + final String imagePath = pathSegments.last; + // Does this have any existing transformation directives? + final int directiveSeparator = imagePath.indexOf('='); + if (directiveSeparator >= 0) { + // Split the baseUrl from the sizing directive by the first "=" + final String baseUrl = imagePath.substring(0, directiveSeparator); + final String directive = imagePath.substring(directiveSeparator + 1); + // Split the directive by "-" + final Set directives = Set.from(directive.split('-')) + // Remove the size directive, if present, and any empty values + ..removeWhere((String s) => s.isEmpty || sizeDirective.hasMatch(s)) + // Add the size and crop directives + ..addAll(['c', 's${size.round()}']); + // Recompose the URL by performing the reverse of the parsing + pathSegments.last = '$baseUrl=${directives.join("-")}'; + } else { + pathSegments.last = '${pathSegments.last}=c-s${size.round()}'; + } + } else { + // Old style URLs + pathSegments + ..removeWhere(sizeDirective.hasMatch) + ..insert(pathSegments.length - 1, 's${size.round()}-c'); + } + return Uri( + scheme: profileUri.scheme, + host: profileUri.host, + pathSegments: pathSegments, + ).toString(); +} diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 01ab6c64c00c..3375628f47b5 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -8,6 +8,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'src/common.dart'; +import 'src/fife.dart' as fife; /// Builds a CircleAvatar profile image of the appropriate resolution class GoogleUserCircleAvatar extends StatelessWidget { @@ -33,7 +34,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// /// The format is is "`/sNN-c/`", where `NN` is the max width/height of the /// image, and "`c`" indicates we want the image cropped. - static final RegExp sizeDirective = RegExp(r'^s[0-9]{1,5}(-c)?$'); + static final RegExp sizeDirective = fife.sizeDirective; /// The Google user's identity; guaranteed to be non-null. final GoogleIdentity identity; @@ -67,8 +68,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { ); } - /// Adds sizing information to [photoUrl], inserted as the last path segment - /// before the image filename. The format is described in [sizeDirective]. + /// Adds correct sizing information to [photoUrl]. /// /// Falls back to the default profile photo if [photoUrl] is [null]. static String _sizedProfileImageUrl(String photoUrl, double size) { @@ -77,17 +77,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { // the default profile photo as a last resort. return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c'; } - final Uri profileUri = Uri.parse(photoUrl); - final List pathSegments = - List.from(profileUri.pathSegments); - pathSegments - ..removeWhere(sizeDirective.hasMatch) - ..insert(pathSegments.length - 1, 's${size.round()}-c'); - return Uri( - scheme: profileUri.scheme, - host: profileUri.host, - pathSegments: pathSegments, - ).toString(); + return fife.addSizeDirectiveToUrl(photoUrl, size); } Widget _buildClippedImage(BuildContext context, BoxConstraints constraints) { diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index d3331d24f55d..833ef6733d2d 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. author: Flutter Team homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in -version: 4.0.12 +version: 4.0.13 flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in/test/fife_test.dart b/packages/google_sign_in/google_sign_in/test/fife_test.dart new file mode 100644 index 000000000000..bfc4937a7c64 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/test/fife_test.dart @@ -0,0 +1,66 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in/src/fife.dart'; + +void main() { + group('addSizeDirectiveToUrl', () { + const double size = 20; + + group('Old style URLs', () { + const String base = + 'https://lh3.googleusercontent.com/-ukEAtRyRhw8/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rfhID9XACtdb9q_xK43VSXQvBV11Q.CMID'; + const String expected = '$base/s20-c/photo.jpg'; + + test('with directives, sets size', () { + final String url = '$base/s64-c/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, sets size and crop', () { + final String url = '$base/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no crop, sets size and crop', () { + final String url = '$base/s64/photo.jpg'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + }); + + group('New style URLs', () { + const String base = + 'https://lh3.googleusercontent.com/a-/AAuE7mC0Lh4F4uDtEaY7hpe-GIsbDpqfMZ3_2UhBQ8Qk'; + const String expected = '$base=c-s20'; + + test('with directives, sets size', () { + final String url = '$base=s120-c'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, sets size and crop', () { + final String url = base; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no directives, but with an equals sign, sets size and crop', () { + final String url = '$base='; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('no crop, adds crop', () { + final String url = '$base=s120'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + + test('many directives, sets size and crop, preserves other directives', + () { + final String url = '$base=s120-c-fSoften=1,50,0'; + final String expected = '$base=c-fSoften=1,50,0-s20'; + expect(addSizeDirectiveToUrl(url, size), expected); + }); + }); + }); +}