Index: lib/MT/App.pm =================================================================== --- lib/MT/App.pm (revision 14) +++ lib/MT/App.pm (working copy) @@ -1322,16 +1322,45 @@ my $new_login = 0; + my $external_author; + my $mode = $app->param('__authmode'); + if ($mode && $mode =~ /^(?:login_external|handle_sign_in)$/) { + MT::Auth->invalidate_credentials({ app => $app }); + my $extkey = $app->param('key'); + return $app->error('Invalid key.') unless $extkey eq 'MTOpenID'; + my $auth_class = "MT::Auth::MTOpenID"; + eval "require $auth_class;"; + if ( my $e = $@ ) { + return $app->error( $e ); + } + my $res = $auth_class->$mode( $app, $extkey ); + # $res is MT::Author object + if ($mode eq 'handle_sign_in') { + die "error on login" unless $res; + $external_author = $res; + } else { + return; + } + } + require MT::Auth; - my $ctx = MT::Auth->fetch_credentials({ app => $app }); - unless ($ctx) { - if ( $app->param('submit') ) { - return $app->error($app->translate('Invalid login.')); + my $ctx; + my $res; + unless ($external_author) { + $ctx = MT::Auth->fetch_credentials({ app => $app }); + unless ($ctx) { + if ( $app->param('submit') ) { + return $app->error($app->translate('Invalid login.')); + } + return; } - return; + $res = MT::Auth->validate_credentials($ctx) || MT::Auth::UNKNOWN(); + } else { + $app->{username} = $external_author->name; + $ctx = { app => $app, username => $external_author->name, permanent => 1, auth_type => 'MT' }; + $res = MT::Auth::NEW_LOGIN(); } - my $res = MT::Auth->validate_credentials($ctx) || MT::Auth::UNKNOWN(); my $user = $ctx->{username}; if ($res == MT::Auth::UNKNOWN()) { Index: lib/MT/Auth/MT.pm =================================================================== --- lib/MT/Auth/MT.pm (revision 14) +++ lib/MT/Auth/MT.pm (working copy) @@ -116,7 +116,10 @@ $result = MT::Auth::SUCCESS(); } else { my $error; - if ($author->is_valid_password($password, 0, \$error)) { + if ($author->external_id) { + $app->error('is not a valid user'); + $result = MT::Auth::INVALID_PASSWORD(); + } elsif ($author->is_valid_password($password, 0, \$error)) { $app->user($author); $result = MT::Auth::NEW_LOGIN(); } else { Index: lib/MT/Auth/MTOpenID.pm =================================================================== --- lib/MT/Auth/MTOpenID.pm (revision 0) +++ lib/MT/Auth/MTOpenID.pm (revision 0) @@ -0,0 +1,437 @@ +# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd. +# This program is distributed under the terms of the +# GNU General Public License, version 2. +# +# $Id$ + +package MT::Auth::MTOpenID; + +use strict; +# use base 'MT::Auth::MT'; +use base 'MT::ErrorHandler'; + +use MT::Author qw(AUTHOR); +use MT::Util qw( decode_url is_valid_email escape_unicode ts2epoch ); +use MT::I18N qw( encode_text ); + +sub can_recover_password { 0 } +sub is_profile_needed { 1 } +sub password_exists { 0 } +sub delegate_auth { 1 } +sub can_logout { 1 } + +sub _validate_blog { + my $class = shift; + my ($app) = @_; + + my $blog_id = $app->{query}->param('blog_id'); + my $blog_id_re = $app->config->AllowOpenIDUserBlog; + unless ($blog_id_re) { + $app->error('AllowOpenIDUserBlog (regexp) not set'); + die; + } + unless ($blog_id =~ /^$blog_id_re$/) { + $app->error("blog not allowed"); + die; + } +} + +sub login_external { + my $class = shift; + my ($app) = @_; + my $q = $app->{query}; + return $app->errtrans("Invalid request.") + unless $q->param('blog_id'); + $class->_validate_blog($app); + my $blog = MT::Blog->load(scalar $q->param('blog_id')); + my %param = $app->param_hash; + my $csr = _get_csr(\%param, $blog) or return; + my $identity = $q->param('openid_url'); + if (!$identity && + (my $u = $q->param('openid_userid')) && $class->can('url_for_userid')) { + $identity = $class->url_for_userid($u); + } + my $claimed_identity = $csr->claimed_identity($identity); + if (!$claimed_identity) { + my ($err_code, $err_msg) = ($csr->errcode, $csr->errtext); + if ($err_code eq 'no_head_tag' || $err_code eq 'no_identity_server' || $err_code eq 'url_gone') { + $err_msg = $app->translate('The address entered does not appear to be an OpenID'); + } + elsif ($err_code eq 'empty_url' || $err_code eq 'bogus_url') { + $err_msg = $app->translate('The text entered does not appear to be a web address'); + } + elsif ($err_code eq 'url_fetch_error') { + $err_msg =~ s{ \A Error \s fetching \s URL: \s }{}xms; + $err_msg = $app->translate('Unable to connect to [_1]: [_2]', $identity, $err_msg); + } + return $app->error($app->translate("Could not verify the OpenID provided: [_1]", $err_msg)); + } + + my $root = $class->_get_root($blog); + my $return_to = $app->base . $app->uri . '?__authmode=handle_sign_in' + . '&blog_id=' . $q->param('blog_id') + . '&static=' . $q->param('static') + . '&key=' . $q->param('key'); + $return_to .= '&entry_id=' . $q->param('entry_id') if $q->param('entry_id'); + + my $check_url = $claimed_identity->check_url( + return_to => $return_to, + trust_root => $root, + ); + + return $app->redirect($check_url); +} + +sub handle_sign_in { + my $class = shift; + my ($app, $auth_type) = @_; + my $q = $app->{query}; + my $INTERVAL = 60 * 60 * 24 * 7; + + $auth_type = 'MT'; + + my $blog_id = $q->param('blog_id'); + $class->_validate_blog($app); + + my $blog = MT::Blog->load($blog_id); + + my $cmntr; + my $session; + + my %param = $app->param_hash; + my $csr = _get_csr(\%param, $blog) or return 0; + + if(my $setup_url = $csr->user_setup_url( post_grant => 'return' )) { + return $app->redirect($setup_url); + } elsif(my $vident = $csr->verified_identity) { + my $name = $vident->url; + $cmntr = $app->model('author')->load( + { + external_id => _url_hash($vident->url), + type => MT::Author::AUTHOR(), + auth_type => $auth_type, + } + ); + + my $nick; + if ( $cmntr ) { + if ( ( $cmntr->modified_on + && ( ts2epoch($blog, $cmntr->modified_on) > time - $INTERVAL ) ) + || ( $cmntr->created_on + && ( ts2epoch($blog, $cmntr->created_on) > time - $INTERVAL ) ) ) + { + $nick = $cmntr->nickname; + } + else { + $nick = $class->get_nickname($vident); + $cmntr->nickname($nick); + $cmntr->save or return 0; + } + } + else { + $nick = $class->get_nickname($vident); + $cmntr = $app->model('author')->new; + $cmntr->password('(null)'); + $cmntr->name($name); + $cmntr->type(MT::Author::AUTHOR()); + $cmntr->status(MT::Author::ACTIVE()); + $cmntr->auth_type($auth_type); + $cmntr->external_id(_url_hash($vident->url)); + $cmntr->nickname($nick); + $cmntr->email($app->config->OpenIDUserEmail || 'nobody@localhost'); + $cmntr->hint('MTOS'); + $cmntr->save or die $cmntr->errstr; + } + return 0 unless $cmntr; + + $nick = $name unless $nick; + + my $rs_role = $app->model('role')->load( + { + name => $app->config->OpenIDUserRole || 'Author', + } + ); + + my $rs_blog = $app->model('permission')->load( + { + author_id => $cmntr->id, + blog_id => $blog->id, + } + ); + unless ($rs_blog) { + $rs_blog = $app->model('permission')->new; + $rs_blog->author_id($cmntr->id); + $rs_blog->blog_id($blog->id); + $rs_blog->permissions($rs_role->permissions); + $rs_blog->save; + } + + my $rs_asso = $app->model('association')->load( + { + author_id => $cmntr->id, + blog_id => $blog->id, + } + ); + unless ($rs_asso) { + $rs_asso = $app->model('association')->new; + $rs_asso->author_id($cmntr->id); + $rs_asso->blog_id($blog->id); + $rs_asso->created_by(1); + $rs_asso->role_id($rs_role->id); + $rs_asso->type(1); + $rs_asso->save; + } + + } else { + $app->error("xxx"); + return 0; + } + unless ($cmntr) { + $app->error("no cmntr"); + return 0; + } + + $app->user($cmntr); + + return $cmntr; +} + +sub _get_ua { + return MT->new_ua; +} + +sub _get_csr { + my ($params, $blog) = @_; + my $secret = MT->config->SecretToken; + my $ua = _get_ua() or return; + require Net::OpenID::Consumer; + Net::OpenID::Consumer->new( + ua => $ua, + args => $params, + consumer_secret => $secret, + ); +} + +sub _get_declared_foaf { + my ($vident) = @_; + my $req = MT::Request->instance(); + my $foaf = $req->stash( 'foaf:' . _url_hash($vident->url) ); + return $foaf if $foaf; + + my $ua = _get_ua() or return ''; + + if ( my $foaf_url = $vident->declared_foaf ) { + my $resp = $ua->get($foaf_url); + if ( $resp->is_success ) { + $foaf = $resp->content; + $req->stash( 'foaf:' . _url_hash($vident->url), $foaf ); + return $foaf; + } + } + + q(); +} + +sub get_nickname { + my $class = shift; + my ($vident) = @_; + _get_nickname(@_); +} + +sub _get_nickname { + my ($vident) = @_; + + ## FOAF + if ( my $foaf = _get_declared_foaf($vident) ) { + my $name; + + require XML::XPath; + my $xml = XML::XPath->new( xml => $foaf ); + $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); + $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/'); + my ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:name'); + ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:nick') + unless $name_el; + if ($name_el) + { + $name = $name_el->string_value; + } + $xml->cleanup; + + return MT::I18N::utf8_off($name) if $name; + } + + ## Atom + if(my $atom_url = $vident->declared_atom) { + if (my $ua = _get_ua()) { + my $resp = $ua->get($atom_url); + if($resp->is_success) { + my $name; + + require XML::XPath; + my $xml = XML::XPath->new( xml => $resp->content ); + if(my ($name_el) = $xml->findnodes('/feed/author/name')) { + $name = $name_el->string_value; + } + $xml->cleanup; + + return MT::I18N::utf8_off($name) if $name; + } + } + } + + return $vident->display ? $vident->display : $vident->url; +} + +sub get_userpicasset { + my $class = shift; + my ($vident) = @_; + my $foaf = _get_declared_foaf($vident); + return undef unless $foaf; + + require XML::XPath; + my $xml = XML::XPath->new( xml => $foaf ); + $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); + $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/'); + my $resource = $xml->getNodeText('/RDF:RDF/FOAF:Person/FOAF:img/@RDF:resource'); + my $url; + if ($resource) { + $url = $resource->value(); + } + $xml->cleanup; + return undef unless $url; + + return _asset_from_url($url); +} + +sub _asset_from_url { + my ($image_url) = @_; + my $ua = _get_ua() or return; + my $resp = $ua->get($image_url); + return undef unless $resp->is_success; + my $image = $resp->content; + return undef unless $image; + my $mimetype = $resp->header('Content-Type'); + my $def_ext = { + 'image/jpeg' => '.jpg', + 'image/png' => '.png', + 'image/gif' => '.gif'}->{$mimetype}; + + require Image::Size; + my ( $w, $h, $id ) = Image::Size::imgsize(\$image); + + require MT::FileMgr; + my $fmgr = MT::FileMgr->new('Local'); + + my $save_path = '%s/support/uploads/'; + my $local_path = + File::Spec->catdir( MT->instance->static_file_path, 'support', 'uploads' ); + $local_path =~ s|/$|| + unless $local_path eq '/'; ## OS X doesn't like / at the end in mkdir(). + unless ( $fmgr->exists($local_path) ) { + $fmgr->mkpath($local_path); + } + my $filename = substr($image_url, rindex($image_url, '/')); + if ( $filename =~ m!\.\.|\0|\|! ) { + return undef; + } + my ($base, $uploaded_path, $ext) = File::Basename::fileparse($filename, '\.[^\.]*'); + $ext = $def_ext if $def_ext; # trust content type higher than extension + + # Find unique name for the file. + my $i = 1; + my $base_copy = $base; + while ($fmgr->exists(File::Spec->catfile($local_path, $base . $ext))) { + $base = $base_copy . '_' . $i++; + } + + my $local_relative = File::Spec->catfile($save_path, $base . $ext); + my $local = File::Spec->catfile($local_path, $base . $ext); + $fmgr->put_data( $image, $local, 'upload' ); + + require MT::Asset; + my $asset_pkg = MT::Asset->handler_for_file($local); + return undef if $asset_pkg ne 'MT::Asset::Image'; + + my $asset; + $asset = $asset_pkg->new(); + $asset->file_path($local_relative); + $asset->file_name($base.$ext); + my $ext_copy = $ext; + $ext_copy =~ s/\.//; + $asset->file_ext($ext_copy); + $asset->blog_id(0); + + my $original = $asset->clone; + my $url = $local_relative; + $url =~ s!\\!/!g; + $asset->url($url); + $asset->image_width($w); + $asset->image_height($h); + $asset->mime_type($mimetype); + + $asset->save + or return undef; + + MT->run_callbacks( + 'api_upload_file.' . $asset->class, + File => $local, file => $local, + Url => $url, url => $url, + Size => length($image), size => length($image), + Asset => $asset, asset => $asset, + Type => $asset->class, type => $asset->class, + ); + MT->run_callbacks( + 'api_upload_image', + File => $local, file => $local, + Url => $url, url => $url, + Size => length($image), size => length($image), + Asset => $asset, asset => $asset, + Height => $h, height => $h, + Width => $w, width => $w, + Type => 'image', type => 'image', + ImageType => $id, image_type => $id, + ); + + $asset; +} + +sub _url_hash { + my ($url) = @_; + + if (eval { require Digest::MD5; 1; }) { + return Digest::MD5::md5_hex($url); + } + return substr $url, 0, 255; +} + +sub _get_root { + my $class = shift; + my ($blog) = @_; + my $path = MT->config->CGIPath; + if ($path =~ m!^/!) { + # relative path, prepend blog domain + my ($blog_domain) = $blog->archive_url =~ m|(.+://[^/]+)|; + $path = $blog_domain . $path; + } + $path .= '/' unless $path =~ m!/$!; + $path; +} + +1; + +__END__ + +=head1 NAME + +MT::Auth::MT + +=head1 METHODS + +TODO + +=head1 AUTHOR & COPYRIGHT + +Please see L. + +=cut