Merge c9beef0bc2
into 279ae66120
commit
55a4798532
@ -0,0 +1,479 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| CONTENTS: |
|
||||
| Roundcube OAuth2 utilities |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
/**
|
||||
* Roundcube OAuth2 utilities
|
||||
*
|
||||
* @package Webmail
|
||||
* @subpackage Utils
|
||||
*/
|
||||
class rcmail_oauth
|
||||
{
|
||||
/** @var rcmail */
|
||||
protected $rcmail;
|
||||
|
||||
/** @var array */
|
||||
protected $options = array();
|
||||
|
||||
/** @var string */
|
||||
protected $last_error = null;
|
||||
|
||||
/** @var boolean */
|
||||
protected $no_redirect = false;
|
||||
|
||||
/** @var rcmail_oauth */
|
||||
static protected $instance;
|
||||
|
||||
/**
|
||||
* Singleton factory
|
||||
*
|
||||
* @return rcmail_oauth The one and only instance
|
||||
*/
|
||||
static function get_instance($options = array())
|
||||
{
|
||||
if (!self::$instance) {
|
||||
self::$instance = new rcmail_oauth($options);
|
||||
self::$instance->init();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
*
|
||||
* @param array $options Config options:
|
||||
*/
|
||||
public function __construct($options = array())
|
||||
{
|
||||
$this->rcmail = rcmail::get_instance();
|
||||
$this->options = (array) $options + array(
|
||||
'provider' => $this->rcmail->config->get('oauth_provider'),
|
||||
'auth_uri' => $this->rcmail->config->get('oauth_auth_uri'),
|
||||
'token_uri' => $this->rcmail->config->get('oauth_token_uri'),
|
||||
'client_id' => $this->rcmail->config->get('oauth_client_id'),
|
||||
'client_secret' => $this->rcmail->config->get('oauth_client_secret'),
|
||||
'identity_uri' => $this->rcmail->config->get('oauth_identity_uri'),
|
||||
'identity_fields' => $this->rcmail->config->get('oauth_identity_fields', ['email']),
|
||||
'scope' => $this->rcmail->config->get('oauth_scope'),
|
||||
'verify_peer' => $this->rcmail->config->get('oauth_verify_peer', true),
|
||||
'auth_parameters' => $this->rcmail->config->get('oauth_auth_parameters', array()),
|
||||
'login_redirect' => $this->rcmail->config->get('oauth_login_redirect', false),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this instance
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function init()
|
||||
{
|
||||
// subscrbe to storage and smtp init events
|
||||
if ($this->is_enabled()) {
|
||||
$this->rcmail->plugins->register_hook('storage_init', [$this, 'storage_init']);
|
||||
$this->rcmail->plugins->register_hook('smtp_connect', [$this, 'smtp_connect']);
|
||||
$this->rcmail->plugins->register_hook('logout_after', [$this, 'logout_after']);
|
||||
$this->rcmail->plugins->register_hook('unauthenticated', [$this, 'unauthenticated']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OAuth is generally enabled in config
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function is_enabled()
|
||||
{
|
||||
return !empty($this->options['provider']) &&
|
||||
!empty($this->options['token_uri']) &&
|
||||
!empty($this->options['client_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose a fully qualified redirect URI for auth requests
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_redirect_uri()
|
||||
{
|
||||
// rewrite redirect URL to not contain query parameters because some providers do not support this
|
||||
return preg_replace('/\?_task=[a-z]+/', 'index.php/login/oauth', $this->rcmail->url([], true, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the last error occured
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_last_error()
|
||||
{
|
||||
return $this->last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to decode a JWT
|
||||
*
|
||||
* @param string $jwt
|
||||
* @return array Hash array with decoded body
|
||||
*/
|
||||
public function jwt_decode($jwt)
|
||||
{
|
||||
list($headb64, $bodyb64, $cryptob64) = explode('.', $jwt);
|
||||
$header = json_decode(base64_decode($headb64), true);
|
||||
$body = json_decode(base64_decode($bodyb64), true);
|
||||
|
||||
if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {
|
||||
throw new RuntimeException('Failed to validate JWT: invalid azp value');
|
||||
} else if (isset($body['aud']) && $body['aud'] !== $this->options['client_id']) {
|
||||
throw new RuntimeException('Failed to validate JWT: invalid aud value');
|
||||
} else if (!isset($body['azp']) && !isset($body['aud'])) {
|
||||
throw new RuntimeException('Failed to validate JWT: missing aud/azp value');
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login action: redirect to `oauth_auth_uri`
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function login_redirect()
|
||||
{
|
||||
if (!empty($this->options['auth_uri']) && !empty($this->options['client_id'])) {
|
||||
// create a secret string
|
||||
$_SESSION['oauth_state'] = rcube_utils::random_bytes(12);
|
||||
|
||||
// compose full oauth login uri
|
||||
$delimiter = strpos($this->options['auth_uri'], '?') > 0 ? '&' : '?';
|
||||
$query = http_build_query([
|
||||
'response_type' => 'code',
|
||||
'client_id' => $this->options['client_id'],
|
||||
'scope' => $this->options['scope'],
|
||||
'redirect_uri' => $this->get_redirect_uri(),
|
||||
'state' => $_SESSION['oauth_state'],
|
||||
] + (array)$this->options['auth_parameters']);
|
||||
$this->rcmail->output->redirect($this->options['auth_uri'] . $delimiter . $query); // exit
|
||||
} else {
|
||||
// log error about missing config options
|
||||
rcube::raise_error(array(
|
||||
'message' => "Missing required OAuth config options 'oauth_auth_uri', 'oauth_client_id'",
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request access token with auth code returned from oauth login
|
||||
*
|
||||
* @param string $auth_code
|
||||
* @param string $state
|
||||
* @return array Authorization data as hash array with entries
|
||||
* `username` as the authentication user name
|
||||
* `authorization` as the oauth authorization string "<type> <access-token>"
|
||||
* `token` as the complete oauth response to be stored in session
|
||||
*/
|
||||
public function request_access_token($auth_code, $state = null)
|
||||
{
|
||||
$oauth_token_uri = $this->options['token_uri'];
|
||||
$oauth_client_id = $this->options['client_id'];
|
||||
$oauth_client_secret = $this->options['client_secret'];
|
||||
$oauth_identity_uri = $this->options['identity_uri'];
|
||||
|
||||
if (!empty($oauth_token_uri) && !empty($oauth_client_secret)) {
|
||||
// validate state parameter against $_SESSION['oauth_state']
|
||||
if (!empty($_SESSION['oauth_state']) && $_SESSION['oauth_state'] !== $state) {
|
||||
throw new RuntimeException('Invalid state parameter');
|
||||
}
|
||||
|
||||
// send token request to get a real access token for the given auth code
|
||||
try {
|
||||
$client = new Client([
|
||||
'timeout' => 10.0,
|
||||
'verify' => $this->options['verify_peer'],
|
||||
]);
|
||||
$response = $client->post($oauth_token_uri, array(
|
||||
'body' => array(
|
||||
'code' => $auth_code,
|
||||
'client_id' => $oauth_client_id,
|
||||
'client_secret' => $oauth_client_secret,
|
||||
'redirect_uri' => $this->get_redirect_uri(),
|
||||
'grant_type' => 'authorization_code',
|
||||
),
|
||||
));
|
||||
$data = $response->json();
|
||||
|
||||
// auth success
|
||||
if (!empty($data['access_token'])) {
|
||||
$username = null;
|
||||
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
|
||||
|
||||
// decode JWT id_token if provided
|
||||
if (!empty($data['id_token'])) {
|
||||
try {
|
||||
$identity = $this->jwt_decode($data['id_token']);
|
||||
foreach ($this->options['identity_fields'] as $field) {
|
||||
if (isset($identity[$field])) {
|
||||
$username = $identity[$field];
|
||||
unset($data['id_token']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// log error
|
||||
rcube::raise_error(array(
|
||||
'message' => $e->getMessage(),
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// request user identity (email)
|
||||
if (empty($username) && !empty($oauth_identity_uri)) {
|
||||
$identity = $client->get($oauth_identity_uri, array(
|
||||
'headers' => array(
|
||||
'Authorization' => $authorization,
|
||||
'Accept' => 'application/json',
|
||||
),
|
||||
))->json();
|
||||
foreach ($this->options['identity_fields'] as $field) {
|
||||
if (isset($identity[$field])) {
|
||||
$username = $identity[$field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data['identity'] = $username;
|
||||
$this->mask_auth_data($data);
|
||||
|
||||
$this->rcmail->session->remove('oauth_state');
|
||||
|
||||
// return auth data
|
||||
return array(
|
||||
'username' => $username,
|
||||
'authorization' => $authorization,
|
||||
'token' => $data,
|
||||
);
|
||||
} else {
|
||||
throw new Exception('Unexpected response from OAuth service');
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
$this->last_error = "OAuth token request failed: " . $e->getMessage();
|
||||
rcube::raise_error(array(
|
||||
'message' => $this->last_error . '; ' . $e->getResponse(),
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
$this->last_error = "OAuth token request failed: " . $e->getMessage();
|
||||
rcube::raise_error(array(
|
||||
'message' => $this->last_error,
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$this->last_error = "Missing required OAuth config options 'oauth_token_uri', 'oauth_client_id', 'oauth_client_secret'";
|
||||
rcube::raise_error(array(
|
||||
'message' => $this->last_error,
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a new access token using the refresh_token grant type
|
||||
*
|
||||
* If successful, this will update the `oauth_token` entry in
|
||||
* session data.
|
||||
*
|
||||
* @param array $token
|
||||
* @return array Updated authorization data
|
||||
*/
|
||||
public function refresh_access_token(array $token)
|
||||
{
|
||||
$oauth_token_uri = $this->options['token_uri'];
|
||||
$oauth_client_id = $this->options['client_id'];
|
||||
$oauth_client_secret = $this->options['client_secret'];
|
||||
|
||||
// send token request to get a real access token for the given auth code
|
||||
try {
|
||||
$client = new Client([
|
||||
'timeout' => 10.0,
|
||||
'verify' => $this->options['verify_peer'],
|
||||
]);
|
||||
$response = $client->post($oauth_token_uri, array(
|
||||
'body' => array(
|
||||
'client_id' => $oauth_client_id,
|
||||
'client_secret' => $oauth_client_secret,
|
||||
'refresh_token' => $this->rcmail->decrypt($token['refresh_token']),
|
||||
'grant_type' => 'refresh_token',
|
||||
),
|
||||
));
|
||||
$data = $response->json();
|
||||
|
||||
// auth success
|
||||
if (!empty($data['access_token'])) {
|
||||
// update access token stored as password
|
||||
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
|
||||
$_SESSION['password'] = $this->rcmail->encrypt($authorization);
|
||||
|
||||
$this->mask_auth_data($data);
|
||||
|
||||
// update session data
|
||||
$_SESSION['oauth_token'] = array_merge($token, $data);
|
||||
|
||||
return [
|
||||
'token' => $data,
|
||||
'authorization' => $authorization,
|
||||
];
|
||||
}
|
||||
} catch (RequestException $e) {
|
||||
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
|
||||
rcube::raise_error(array(
|
||||
'message' => $this->last_error . '; ' . $e->getResponse(),
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
|
||||
rcube::raise_error(array(
|
||||
'message' => $this->last_error,
|
||||
'file' => __FILE__,
|
||||
'line' => __LINE__,
|
||||
), true, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify some properties of the received auth response
|
||||
*
|
||||
* @param array $token
|
||||
* @return void
|
||||
*/
|
||||
protected function mask_auth_data(&$data)
|
||||
{
|
||||
// compute absolute token expiration date
|
||||
$data['expires'] = time() + $data['expires_in'] - 600;
|
||||
|
||||
// encrypt refresh token if provided
|
||||
if (isset($data['refresh_token'])) {
|
||||
$data['refresh_token'] = $this->rcmail->encrypt($data['refresh_token']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the given access token data if still valid
|
||||
*
|
||||
* ... and attempt to refresh if possible.
|
||||
*
|
||||
* @param array $token
|
||||
* @return void
|
||||
*/
|
||||
protected function check_token_validity($token)
|
||||
{
|
||||
if ($token['expires'] < time() && isset($token['refresh_token'])) {
|
||||
$this->refresh_access_token($token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for 'storage_init' hook
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function storage_init($options)
|
||||
{
|
||||
if (isset($_SESSION['oauth_token']) && $options['driver'] === 'imap') {
|
||||
// check token validity
|
||||
$this->check_token_validity($_SESSION['oauth_token']);
|
||||
|
||||
// enforce XOAUTH2 authorization type
|
||||
$options['auth_type'] = 'XOAUTH2';
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for 'smtp_connect' hook
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function smtp_connect($options)
|
||||
{
|
||||
if (isset($_SESSION['oauth_token'])) {
|
||||
// check token validity
|
||||
$this->check_token_validity($_SESSION['oauth_token']);
|
||||
|
||||
// enforce XOAUTH2 authorization type
|
||||
$options['smtp_user'] = '%u';
|
||||
$options['smtp_pass'] = '%p';
|
||||
$options['smtp_auth_type'] = 'XOAUTH2';
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for 'logout_after' hook
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function logout_after($options)
|
||||
{
|
||||
$this->no_redirect = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for 'unauthenticated' hook
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function unauthenticated($options)
|
||||
{
|
||||
if ($this->options['login_redirect'] &&
|
||||
!$this->rcmail->output->ajax_call &&
|
||||
!$this->no_redirect &&
|
||||
empty($options['error']) &&
|
||||
$options['http_code'] === 200
|
||||
) {
|
||||
$this->login_redirect();
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
+-----------------------------------------------------------------------+
|
||||
| This file is part of the Roundcube Webmail client |
|
||||
| |
|
||||
| Copyright (C) The Roundcube Dev Team |
|
||||
| |
|
||||
| Licensed under the GNU General Public License version 3 or |
|
||||
| any later version with exceptions for skins & plugins. |
|
||||
| See the README file for a full license statement. |
|
||||
| |
|
||||
| PURPOSE: |
|
||||
| Perform OAuth2 user login |
|
||||
+-----------------------------------------------------------------------+
|
||||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||||
+-----------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
$rcmail = rcmail::get_instance();
|
||||
|
||||
$auth_code = rcube_utils::get_input_value('code', rcube_utils::INPUT_GET);
|
||||
$auth_error = rcube_utils::get_input_value('error', rcube_utils::INPUT_GET);
|
||||
|
||||
// auth code return from oauth login
|
||||
if (!empty($auth_code)) {
|
||||
$auth = $rcmail->oauth->request_access_token($auth_code, rcube_utils::get_input_value('state', rcube_utils::INPUT_GET));
|
||||
|
||||
// oauth success
|
||||
if ($auth && isset($auth['username'], $auth['authorization'], $auth['token'])) {
|
||||
// enforce XOAUTH2 auth type
|
||||
$rcmail->config->set('imap_auth_type', 'XOAUTH2');
|
||||
|
||||
// use access_token and user info for IMAP login
|
||||
$storage_host = $rcmail->autoselect_host();
|
||||
if ($rcmail->login($auth['username'], $auth['authorization'], $storage_host, true)) {
|
||||
// replicate post-login tasks from index.php
|
||||
$rcmail->session->remove('temp');
|
||||
$rcmail->session->regenerate_id(false);
|
||||
|
||||
// send auth cookie if necessary
|
||||
$rcmail->session->set_auth_cookie();
|
||||
|
||||
// save OAuth token in session
|
||||
$_SESSION['oauth_token'] = $auth['token'];
|
||||
|
||||
// log successful login
|
||||
$rcmail->log_login();
|
||||
|
||||
// allow plugins to control the redirect url after login success
|
||||
$redir = $rcmail->plugins->exec_hook('login_after', array('_task' => 'mail'));
|
||||
unset($redir['abort'], $redir['_err']);
|
||||
|
||||
// send redirect
|
||||
header('Location: ' . $rcmail->url($redir, true, false, $secure));
|
||||
exit;
|
||||
} else {
|
||||
$OUTPUT->show_message('loginfailed', 'warning');
|
||||
|
||||
// log failed login
|
||||
$error_code = $rcmail->login_error();
|
||||
$rcmail->log_login($auth['username'], true, $error_code);
|
||||
|
||||
$rcmail->plugins->exec_hook('login_failed', array(
|
||||
'code' => $error_code,
|
||||
'host' => $storage_host,
|
||||
'user' => $auth['username'],
|
||||
));
|
||||
|
||||
$rcmail->kill_session();
|
||||
// fall through -> login page
|
||||
}
|
||||
} else {
|
||||
$OUTPUT->show_message('oauthloginfailed', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// error return from oauth login
|
||||
else if (!empty($auth_error)) {
|
||||
$error_message = rcube_utils::get_input_value('error_description', rcube_utils::INPUT_GET) ?: $auth_error;
|
||||
$OUTPUT->show_message($error_message, 'warning');
|
||||
}
|
||||
|
||||
// login action: redirect to `oauth_auth_uri`
|
||||
else if ($rcmail->task === 'login') {
|
||||
// this will always exit() the process
|
||||
$rcmail->oauth->login_redirect();
|
||||
}
|
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#4285F4" d="M45.12 24.5c0-1.56-.14-3.06-.4-4.5H24v8.51h11.84c-.51 2.75-2.06 5.08-4.39 6.64v5.52h7.11c4.16-3.83 6.56-9.47 6.56-16.17z"></path><path fill="#34A853" d="M24 46c5.94 0 10.92-1.97 14.56-5.33l-7.11-5.52c-1.97 1.32-4.49 2.1-7.45 2.1-5.73 0-10.58-3.87-12.31-9.07H4.34v5.7C7.96 41.07 15.4 46 24 46z"></path><path fill="#FBBC05" d="M11.69 28.18C11.25 26.86 11 25.45 11 24s.25-2.86.69-4.18v-5.7H4.34C2.85 17.09 2 20.45 2 24c0 3.55.85 6.91 2.34 9.88l7.35-5.7z"></path><path fill="#EA4335" d="M24 10.75c3.23 0 6.13 1.11 8.41 3.29l6.31-6.31C34.91 4.18 29.93 2 24 2 15.4 2 7.96 6.93 4.34 14.12l7.35 5.7c1.73-5.2 6.58-9.07 12.31-9.07z"></path><path fill="none" d="M2 2h44v44H2z"></path></svg>
|
After Width: | Height: | Size: 764 B |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 116 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,145.515L77.643,145.515L77.639,197.07L26.088,197.07L26.088,145.515Z" style="fill:rgb(246,83,20);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.907,145.515L134.462,145.515C134.462,162.7 134.465,179.885 134.459,197.07C117.277,197.067 100.092,197.07 82.91,197.07C82.904,179.885 82.907,162.7 82.907,145.515Z" style="fill:rgb(127,187,65);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,202.331C43.273,202.337 60.458,202.329 77.643,202.337C77.646,219.522 77.643,236.704 77.643,253.889L26.088,253.889L26.088,202.331Z" style="fill:rgb(0,161,241);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.91,202.337C100.092,202.331 117.277,202.334 134.462,202.334L134.462,253.889L82.907,253.889C82.91,236.704 82.904,219.519 82.91,202.337Z" style="fill:rgb(255,187,0);"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path fill="#4285F4" d="M45.12 24.5c0-1.56-.14-3.06-.4-4.5H24v8.51h11.84c-.51 2.75-2.06 5.08-4.39 6.64v5.52h7.11c4.16-3.83 6.56-9.47 6.56-16.17z"></path><path fill="#34A853" d="M24 46c5.94 0 10.92-1.97 14.56-5.33l-7.11-5.52c-1.97 1.32-4.49 2.1-7.45 2.1-5.73 0-10.58-3.87-12.31-9.07H4.34v5.7C7.96 41.07 15.4 46 24 46z"></path><path fill="#FBBC05" d="M11.69 28.18C11.25 26.86 11 25.45 11 24s.25-2.86.69-4.18v-5.7H4.34C2.85 17.09 2 20.45 2 24c0 3.55.85 6.91 2.34 9.88l7.35-5.7z"></path><path fill="#EA4335" d="M24 10.75c3.23 0 6.13 1.11 8.41 3.29l6.31-6.31C34.91 4.18 29.93 2 24 2 15.4 2 7.96 6.93 4.34 14.12l7.35 5.7c1.73-5.2 6.58-9.07 12.31-9.07z"></path><path fill="none" d="M2 2h44v44H2z"></path></svg>
|
After Width: | Height: | Size: 764 B |
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 116 116" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,145.515L77.643,145.515L77.639,197.07L26.088,197.07L26.088,145.515Z" style="fill:rgb(246,83,20);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.907,145.515L134.462,145.515C134.462,162.7 134.465,179.885 134.459,197.07C117.277,197.067 100.092,197.07 82.91,197.07C82.904,179.885 82.907,162.7 82.907,145.515Z" style="fill:rgb(127,187,65);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M26.088,202.331C43.273,202.337 60.458,202.329 77.643,202.337C77.646,219.522 77.643,236.704 77.643,253.889L26.088,253.889L26.088,202.331Z" style="fill:rgb(0,161,241);"/></g>
|
||||
<g transform="matrix(1,0,0,1,-22.0613,-141.704)"><path d="M82.91,202.337C100.092,202.331 117.277,202.334 134.462,202.334L134.462,253.889L82.907,253.889C82.91,236.704 82.904,219.519 82.91,202.337Z" style="fill:rgb(255,187,0);"/></g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
Loading…
Reference in New Issue