2010-11-01 23:50:45 +00:00
< ? php
/**
* StatusNet , the distributed open - source microblogging tool
*
2010-11-05 06:34:06 +00:00
* Login or register a local user based on a Facebook user
2010-11-01 23:50:45 +00:00
*
* PHP version 5
*
* LICENCE : This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation , either version 3 of the License , or
* ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
*
* @ category Plugin
* @ package StatusNet
* @ author Zach Copley < zach @ status . net >
* @ copyright 2010 StatusNet , Inc .
* @ license http :// www . fsf . org / licensing / licenses / agpl - 3.0 . html GNU Affero General Public License version 3.0
* @ link http :// status . net /
*/
if ( ! defined ( 'STATUSNET' )) {
exit ( 1 );
}
2010-11-05 06:34:06 +00:00
class FacebookfinishloginAction extends Action
2010-11-01 23:50:45 +00:00
{
private $facebook = null ; // Facebook client
private $fbuid = null ; // Facebook user ID
private $fbuser = null ; // Facebook user object (JSON)
function prepare ( $args ) {
parent :: prepare ( $args );
$this -> facebook = new Facebook (
array (
'appId' => common_config ( 'facebook' , 'appid' ),
'secret' => common_config ( 'facebook' , 'secret' ),
'cookie' => true ,
)
);
// Check for a Facebook user session
$session = $this -> facebook -> getSession ();
$me = null ;
if ( $session ) {
try {
$this -> fbuid = $this -> facebook -> getUser ();
$this -> fbuser = $this -> facebook -> api ( '/me' );
} catch ( FacebookApiException $e ) {
common_log ( LOG_ERROR , $e , __FILE__ );
}
}
if ( ! empty ( $this -> fbuser )) {
// OKAY, all is well... proceed to register
common_debug ( " Found a valid Facebook user. " , __FILE__ );
} else {
// This shouldn't happen in the regular course of things
list ( $proxy , $ip ) = common_client_ip ();
common_log (
LOG_WARNING ,
sprintf (
'Failed Facebook authentication attempt, proxy = %s, ip = %s.' ,
$proxy ,
$ip
),
__FILE__
);
$this -> clientError (
2011-04-08 09:59:10 +01:00
// TRANS: Client error displayed when trying to connect to Facebook while not logged in.
2010-11-01 23:50:45 +00:00
_m ( 'You must be logged into Facebook to register a local account using Facebook.' )
);
}
return true ;
}
function handle ( $args )
{
parent :: handle ( $args );
if ( common_is_real_login ()) {
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
// User is already logged in, are her accounts already linked?
$flink = Foreign_link :: getByForeignID ( $this -> fbuid , FACEBOOK_SERVICE );
if ( ! empty ( $flink )) {
// User already has a linked Facebook account and shouldn't be here!
common_debug (
sprintf (
'There\'s already a local user %d linked with Facebook user %s.' ,
$flink -> user_id ,
$this -> fbuid
)
);
$this -> clientError (
2011-04-08 09:59:10 +01:00
// TRANS: Client error displayed when trying to connect to a Facebook account that is already linked
// TRANS: in the same StatusNet site.
2010-11-01 23:50:45 +00:00
_m ( 'There is already a local account linked with that Facebook account.' )
);
} else {
// Possibly reconnect an existing account
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
$this -> connectUser ();
}
} else if ( $_SERVER [ 'REQUEST_METHOD' ] == 'POST' ) {
2010-11-16 02:30:08 +00:00
$this -> handlePost ();
} else {
$this -> tryLogin ();
}
}
2010-11-01 23:50:45 +00:00
2010-11-16 02:30:08 +00:00
function handlePost ()
{
$token = $this -> trimmed ( 'token' );
2010-11-01 23:50:45 +00:00
2010-11-16 02:30:08 +00:00
if ( ! $token || $token != common_session_token ()) {
$this -> showForm (
2011-04-08 09:59:10 +01:00
// TRANS: Client error displayed when the session token does not match or is not given.
2010-11-16 02:30:08 +00:00
_m ( 'There was a problem with your session token. Try again, please.' )
);
return ;
}
if ( $this -> arg ( 'create' )) {
if ( ! $this -> boolean ( 'license' )) {
2010-11-01 23:50:45 +00:00
$this -> showForm (
2011-04-08 09:59:10 +01:00
// TRANS: Form validation error displayed when user has not agreed to the license.
2011-04-01 21:32:56 +01:00
_m ( 'You cannot register if you do not agree to the license.' ),
2010-11-16 02:30:08 +00:00
$this -> trimmed ( 'newname' )
);
2010-11-01 23:50:45 +00:00
return ;
}
2010-11-16 02:30:08 +00:00
// We has a valid Facebook session and the Facebook user has
// agreed to the SN license, so create a new user
$this -> createNewUser ();
2010-11-01 23:50:45 +00:00
2010-11-16 02:30:08 +00:00
} else if ( $this -> arg ( 'connect' )) {
2010-11-01 23:50:45 +00:00
2010-11-16 02:30:08 +00:00
$this -> connectNewUser ();
2010-11-01 23:50:45 +00:00
} else {
2010-11-16 02:30:08 +00:00
$this -> showForm (
2011-04-08 09:59:10 +01:00
// TRANS: Form validation error displayed when an unhandled error occurs.
2010-11-16 02:30:08 +00:00
_m ( 'An unknown error has occured.' ),
$this -> trimmed ( 'newname' )
);
2010-11-01 23:50:45 +00:00
}
}
function showPageNotice ()
{
if ( $this -> error ) {
$this -> element ( 'div' , array ( 'class' => 'error' ), $this -> error );
} else {
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
$this -> element (
'div' , 'instructions' ,
sprintf (
2011-04-08 09:59:10 +01:00
// TRANS: Form instructions for connecting to Facebook.
// TRANS: %s is the site name.
2011-04-01 21:32:56 +01:00
_m ( 'This is the first time you have logged into %s so we must connect your Facebook to a local account. You can either create a new local account, or connect with an existing local account.' ),
2010-11-01 23:50:45 +00:00
common_config ( 'site' , 'name' )
)
);
}
}
function title ()
{
// TRANS: Page title.
return _m ( 'Facebook Setup' );
}
function showForm ( $error = null , $username = null )
{
$this -> error = $error ;
$this -> username = $username ;
$this -> showPage ();
}
function showPage ()
{
parent :: showPage ();
}
/**
2011-04-08 09:59:10 +01:00
* @ todo FIXME : Much of this duplicates core code , which is very fragile .
2010-11-01 23:50:45 +00:00
* Should probably be replaced with an extensible mini version of
* the core registration form .
*/
function showContent ()
{
if ( ! empty ( $this -> message_text )) {
$this -> element ( 'p' , null , $this -> message );
return ;
}
$this -> elementStart ( 'form' , array ( 'method' => 'post' ,
'id' => 'form_settings_facebook_connect' ,
'class' => 'form_settings' ,
2010-11-05 06:34:06 +00:00
'action' => common_local_url ( 'facebookfinishlogin' )));
2010-11-01 23:50:45 +00:00
$this -> elementStart ( 'fieldset' , array ( 'id' => 'settings_facebook_connect_options' ));
2011-04-08 09:59:10 +01:00
// TRANS: Fieldset legend.
2010-11-01 23:50:45 +00:00
$this -> element ( 'legend' , null , _m ( 'Connection options' ));
$this -> elementStart ( 'ul' , 'form_data' );
$this -> elementStart ( 'li' );
$this -> element ( 'input' , array ( 'type' => 'checkbox' ,
'id' => 'license' ,
'class' => 'checkbox' ,
'name' => 'license' ,
'value' => 'true' ));
$this -> elementStart ( 'label' , array ( 'class' => 'checkbox' , 'for' => 'license' ));
// TRANS: %s is the name of the license used by the user for their status updates.
$message = _m ( 'My text and files are available under %s ' .
'except this private data: password, ' .
'email address, IM address, and phone number.' );
$link = '<a href="' .
htmlspecialchars ( common_config ( 'license' , 'url' )) .
'">' .
htmlspecialchars ( common_config ( 'license' , 'title' )) .
'</a>' ;
$this -> raw ( sprintf ( htmlspecialchars ( $message ), $link ));
$this -> elementEnd ( 'label' );
$this -> elementEnd ( 'li' );
$this -> elementEnd ( 'ul' );
$this -> elementStart ( 'fieldset' );
$this -> hidden ( 'token' , common_session_token ());
$this -> element ( 'legend' , null ,
2011-04-08 09:59:10 +01:00
// TRANS: Fieldset legend.
2010-11-01 23:50:45 +00:00
_m ( 'Create new account' ));
$this -> element ( 'p' , null ,
2011-04-08 09:59:10 +01:00
// TRANS: Form instructions.
2010-11-01 23:50:45 +00:00
_m ( 'Create a new user with this nickname.' ));
$this -> elementStart ( 'ul' , 'form_data' );
2011-01-20 23:55:36 +00:00
// Hook point for captcha etc
Event :: handle ( 'StartRegistrationFormData' , array ( $this ));
2010-11-01 23:50:45 +00:00
$this -> elementStart ( 'li' );
// TRANS: Field label.
$this -> input ( 'newname' , _m ( 'New nickname' ),
( $this -> username ) ? $this -> username : '' ,
2011-04-08 09:59:10 +01:00
// TRANS: Field title.
2011-04-01 21:32:56 +01:00
_m ( '1-64 lowercase letters or numbers, no punctuation or spaces.' ));
2010-11-01 23:50:45 +00:00
$this -> elementEnd ( 'li' );
2011-01-20 23:55:36 +00:00
// Hook point for captcha etc
Event :: handle ( 'EndRegistrationFormData' , array ( $this ));
2010-11-01 23:50:45 +00:00
$this -> elementEnd ( 'ul' );
2011-04-08 09:59:10 +01:00
// TRANS: Submit button to create a new account.
2010-11-01 23:50:45 +00:00
$this -> submit ( 'create' , _m ( 'BUTTON' , 'Create' ));
$this -> elementEnd ( 'fieldset' );
$this -> elementStart ( 'fieldset' );
$this -> element ( 'legend' , null ,
2011-04-08 09:59:10 +01:00
// TRANS: Fieldset legend.
2010-11-01 23:50:45 +00:00
_m ( 'Connect existing account' ));
$this -> element ( 'p' , null ,
2011-04-08 09:59:10 +01:00
// TRANS: Form instructions.
2010-11-01 23:50:45 +00:00
_m ( 'If you already have an account, login with your username and password to connect it to your Facebook.' ));
$this -> elementStart ( 'ul' , 'form_data' );
$this -> elementStart ( 'li' );
// TRANS: Field label.
$this -> input ( 'nickname' , _m ( 'Existing nickname' ));
$this -> elementEnd ( 'li' );
$this -> elementStart ( 'li' );
2011-04-08 09:59:10 +01:00
// TRANS: Field label.
2010-11-01 23:50:45 +00:00
$this -> password ( 'password' , _m ( 'Password' ));
$this -> elementEnd ( 'li' );
$this -> elementEnd ( 'ul' );
2011-04-08 09:59:10 +01:00
// TRANS: Submit button to connect a Facebook account to an existing StatusNet account.
2010-11-01 23:50:45 +00:00
$this -> submit ( 'connect' , _m ( 'BUTTON' , 'Connect' ));
$this -> elementEnd ( 'fieldset' );
$this -> elementEnd ( 'fieldset' );
$this -> elementEnd ( 'form' );
}
function message ( $msg )
{
$this -> message_text = $msg ;
$this -> showPage ();
}
function createNewUser ()
{
2011-01-06 20:15:59 +00:00
if ( ! Event :: handle ( 'StartRegistrationTry' , array ( $this ))) {
return ;
}
2010-11-01 23:50:45 +00:00
if ( common_config ( 'site' , 'closed' )) {
// TRANS: Client error trying to register with registrations not allowed.
$this -> clientError ( _m ( 'Registration not allowed.' ));
return ;
}
$invite = null ;
if ( common_config ( 'site' , 'inviteonly' )) {
$code = $_SESSION [ 'invitecode' ];
if ( empty ( $code )) {
// TRANS: Client error trying to register with registrations 'invite only'.
$this -> clientError ( _m ( 'Registration not allowed.' ));
return ;
}
$invite = Invitation :: staticGet ( $code );
if ( empty ( $invite )) {
// TRANS: Client error trying to register with an invalid invitation code.
$this -> clientError ( _m ( 'Not a valid invitation code.' ));
return ;
}
}
2010-11-29 23:11:07 +00:00
try {
$nickname = Nickname :: normalize ( $this -> trimmed ( 'newname' ));
} catch ( NicknameException $e ) {
$this -> showForm ( $e -> getMessage ());
2010-11-01 23:50:45 +00:00
return ;
}
if ( ! User :: allowed_nickname ( $nickname )) {
2011-04-08 09:59:10 +01:00
// TRANS: Form validation error displayed when picking a nickname that is not allowed.
2010-11-01 23:50:45 +00:00
$this -> showForm ( _m ( 'Nickname not allowed.' ));
return ;
}
if ( User :: staticGet ( 'nickname' , $nickname )) {
2011-04-08 09:59:10 +01:00
// TRANS: Form validation error displayed when picking a nickname that is already in use.
2010-11-01 23:50:45 +00:00
$this -> showForm ( _m ( 'Nickname already in use. Try another one.' ));
return ;
}
$args = array (
2010-11-09 23:14:50 +00:00
'nickname' => $nickname ,
'fullname' => $this -> fbuser [ 'first_name' ]
. ' ' . $this -> fbuser [ 'last_name' ],
'homepage' => $this -> fbuser [ 'website' ],
'bio' => $this -> fbuser [ 'about' ],
'location' => $this -> fbuser [ 'location' ][ 'name' ]
2010-11-01 23:50:45 +00:00
);
2010-11-16 02:30:08 +00:00
// It's possible that the email address is already in our
// DB. It's a unique key, so we need to check
if ( $this -> isNewEmail ( $this -> fbuser [ 'email' ])) {
$args [ 'email' ] = $this -> fbuser [ 'email' ];
$args [ 'email_confirmed' ] = true ;
}
2010-11-01 23:50:45 +00:00
if ( ! empty ( $invite )) {
$args [ 'code' ] = $invite -> code ;
}
2010-11-16 02:30:08 +00:00
$user = User :: register ( $args );
2010-11-01 23:50:45 +00:00
$result = $this -> flinkUser ( $user -> id , $this -> fbuid );
if ( ! $result ) {
2011-04-08 09:59:10 +01:00
// TRANS: Server error displayed when connecting to Facebook fails.
2010-11-01 23:50:45 +00:00
$this -> serverError ( _m ( 'Error connecting user to Facebook.' ));
return ;
}
2010-11-16 02:30:08 +00:00
// Add a Foreign_user record
Facebookclient :: addFacebookUser ( $this -> fbuser );
2010-11-09 23:14:50 +00:00
$this -> setAvatar ( $user );
2010-11-01 23:50:45 +00:00
common_set_user ( $user );
common_real_login ( true );
common_log (
LOG_INFO ,
sprintf (
2010-11-16 02:30:08 +00:00
'Registered new user %s (%d) from Facebook user %s, (fbuid %d)' ,
$user -> nickname ,
2010-11-01 23:50:45 +00:00
$user -> id ,
2010-11-16 02:30:08 +00:00
$this -> fbuser [ 'name' ],
2010-11-01 23:50:45 +00:00
$this -> fbuid
),
__FILE__
);
2011-01-06 20:15:59 +00:00
Event :: handle ( 'EndRegistrationTry' , array ( $this ));
2010-11-16 02:30:08 +00:00
$this -> goHome ( $user -> nickname );
2010-11-01 23:50:45 +00:00
}
2010-11-09 23:14:50 +00:00
/*
* Attempt to download the user ' s Facebook picture and create a
* StatusNet avatar for the new user .
*/
function setAvatar ( $user )
{
$picUrl = sprintf (
'http://graph.facebook.com/%s/picture?type=large' ,
$this -> fbuid
);
// fetch the picture from Facebook
$client = new HTTPClient ();
// fetch the actual picture
$response = $client -> get ( $picUrl );
if ( $response -> isOk ()) {
$finalUrl = $client -> getUrl ();
2010-11-16 02:30:08 +00:00
// Make sure the filename is unique becuase it's possible for a user
// to deauthorize our app, and then come back in as a new user but
// have the same Facebook picture (avatar URLs have a unique index
// and their URLs are based on the filenames).
$filename = 'facebook-' . common_good_rand ( 4 ) . '-'
. substr ( strrchr ( $finalUrl , '/' ), 1 );
2010-11-09 23:14:50 +00:00
$ok = file_put_contents (
Avatar :: path ( $filename ),
$response -> getBody ()
);
if ( ! $ok ) {
common_log (
LOG_WARNING ,
sprintf (
'Couldn\'t save Facebook avatar %s' ,
$tmp
),
__FILE__
);
} else {
2010-11-16 02:30:08 +00:00
// save it as an avatar
2010-11-09 23:14:50 +00:00
$profile = $user -> getProfile ();
if ( $profile -> setOriginal ( $filename )) {
common_log (
LOG_INFO ,
sprintf (
2010-11-16 02:30:08 +00:00
'Saved avatar for %s (%d) from Facebook picture for '
. '%s (fbuid %d), filename = %s' ,
2010-11-09 23:14:50 +00:00
$user -> nickname ,
$user -> id ,
2010-11-16 02:30:08 +00:00
$this -> fbuser [ 'name' ],
2010-11-09 23:14:50 +00:00
$this -> fbuid ,
2010-11-16 02:30:08 +00:00
$filename
2010-11-09 23:14:50 +00:00
),
__FILE__
);
}
}
}
}
2010-11-01 23:50:45 +00:00
function connectNewUser ()
{
$nickname = $this -> trimmed ( 'nickname' );
$password = $this -> trimmed ( 'password' );
if ( ! common_check_user ( $nickname , $password )) {
2011-04-08 09:59:10 +01:00
// TRANS: Form validation error displayed when username/password combination is incorrect.
2010-11-01 23:50:45 +00:00
$this -> showForm ( _m ( 'Invalid username or password.' ));
return ;
}
$user = User :: staticGet ( 'nickname' , $nickname );
if ( ! empty ( $user )) {
2010-11-16 02:30:08 +00:00
common_debug (
sprintf (
'Found a legit user to connect to Facebook: %s (%d)' ,
$user -> nickname ,
$user -> id
),
__FILE__
);
2010-11-01 23:50:45 +00:00
}
2010-11-16 02:30:08 +00:00
$this -> tryLinkUser ( $user );
2010-11-01 23:50:45 +00:00
common_set_user ( $user );
common_real_login ( true );
$this -> goHome ( $user -> nickname );
}
function connectUser ()
{
$user = common_current_user ();
2010-11-16 02:30:08 +00:00
$this -> tryLinkUser ( $user );
common_redirect ( common_local_url ( 'facebookfinishlogin' ), 303 );
}
2010-11-01 23:50:45 +00:00
2010-11-16 02:30:08 +00:00
function tryLinkUser ( $user )
{
2010-11-01 23:50:45 +00:00
$result = $this -> flinkUser ( $user -> id , $this -> fbuid );
if ( empty ( $result )) {
2011-04-08 09:59:10 +01:00
// TRANS: Server error displayed when connecting to Facebook fails.
2010-11-01 23:50:45 +00:00
$this -> serverError ( _m ( 'Error connecting user to Facebook.' ));
return ;
}
2010-11-05 06:34:06 +00:00
common_debug (
sprintf (
2010-11-16 02:30:08 +00:00
'Connected Facebook user %s (fbuid %d) to local user %s (%d)' ,
$this -> fbuser [ 'name' ],
2010-11-05 06:34:06 +00:00
$this -> fbuid ,
2010-11-16 02:30:08 +00:00
$user -> nickname ,
2010-11-05 06:34:06 +00:00
$user -> id
),
__FILE__
);
2010-11-01 23:50:45 +00:00
}
function tryLogin ()
{
2010-11-05 06:34:06 +00:00
common_debug (
sprintf (
'Trying login for Facebook user %s' ,
$this -> fbuid
),
__FILE__
);
2010-11-01 23:50:45 +00:00
2010-11-05 06:34:06 +00:00
$flink = Foreign_link :: getByForeignID ( $this -> fbuid , FACEBOOK_SERVICE );
2010-11-01 23:50:45 +00:00
if ( ! empty ( $flink )) {
$user = $flink -> getUser ();
if ( ! empty ( $user )) {
2010-11-05 06:34:06 +00:00
common_log (
LOG_INFO ,
sprintf (
'Logged in Facebook user %s as user %d (%s)' ,
$this -> fbuid ,
$user -> nickname ,
$user -> id
),
__FILE__
);
2010-11-01 23:50:45 +00:00
common_set_user ( $user );
common_real_login ( true );
$this -> goHome ( $user -> nickname );
}
} else {
2010-11-05 06:34:06 +00:00
common_debug (
sprintf (
'No flink found for fbuid: %s - new user' ,
$this -> fbuid
),
__FILE__
);
2010-11-01 23:50:45 +00:00
$this -> showForm ( null , $this -> bestNewNickname ());
}
}
function goHome ( $nickname )
{
$url = common_get_returnto ();
if ( $url ) {
// We don't have to return to it again
common_set_returnto ( null );
} else {
$url = common_local_url ( 'all' ,
array ( 'nickname' =>
$nickname ));
}
common_redirect ( $url , 303 );
}
function flinkUser ( $user_id , $fbuid )
{
$flink = new Foreign_link ();
$flink -> user_id = $user_id ;
$flink -> foreign_id = $fbuid ;
$flink -> service = FACEBOOK_SERVICE ;
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
// Pull the access token from the Facebook cookies
$flink -> credentials = $this -> facebook -> getAccessToken ();
$flink -> created = common_sql_now ();
$flink_id = $flink -> insert ();
return $flink_id ;
}
function bestNewNickname ()
{
if ( ! empty ( $this -> fbuser [ 'name' ])) {
$nickname = $this -> nicknamize ( $this -> fbuser [ 'name' ]);
if ( $this -> isNewNickname ( $nickname )) {
return $nickname ;
}
}
// Try the full name
2010-11-16 02:30:08 +00:00
$fullname = trim ( $this -> fbuser [ 'first_name' ] .
' ' . $this -> fbuser [ 'last_name' ]);
2010-11-01 23:50:45 +00:00
if ( ! empty ( $fullname )) {
$fullname = $this -> nicknamize ( $fullname );
if ( $this -> isNewNickname ( $fullname )) {
return $fullname ;
}
}
return null ;
}
/**
* Given a string , try to make it work as a nickname
*/
function nicknamize ( $str )
{
$str = preg_replace ( '/\W/' , '' , $str );
return strtolower ( $str );
}
2010-11-16 02:30:08 +00:00
/*
* Is the desired nickname already taken ?
*
* @ return boolean result
*/
function isNewNickname ( $str )
{
2010-11-29 23:11:07 +00:00
if ( ! Nickname :: isValid ( $str )) {
2010-11-01 23:50:45 +00:00
return false ;
}
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
if ( ! User :: allowed_nickname ( $str )) {
return false ;
}
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
if ( User :: staticGet ( 'nickname' , $str )) {
return false ;
}
2010-11-16 02:30:08 +00:00
2010-11-01 23:50:45 +00:00
return true ;
}
2010-11-16 02:30:08 +00:00
/*
* Do we already have a user record with this email ?
* ( emails have to be unique but they can change )
*
* @ param string $email the email address to check
*
* @ return boolean result
*/
function isNewEmail ( $email )
{
// we shouldn't have to validate the format
$result = User :: staticGet ( 'email' , $email );
if ( empty ( $result )) {
common_debug ( " XXXXXXXXXXXXXXXXXX We've never seen this email before!!! " );
return true ;
}
common_debug ( " XXXXXXXXXXXXXXXXXX dupe email address!!!! " );
return false ;
}
2010-11-01 23:50:45 +00:00
}