root/branches/release-39/lib/MT/Auth/OpenID.pm @ 2517

Revision 2517, 14.7 kB (checked in by fumiakiy, 18 months ago)

Trust_root now include CommentScript. BugId:80052

Trust_root, return_to and other parameters for check_url can be modified by creating a new authentication module and overriding the check_url_params method.

Removed _get_root and consolidated the genearation of trust_root and return_to to one method because it does not make sense to create trust_root and return_to individually per spec.

  • Property svn:keywords set to Author Date Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::Auth::OpenID;
8use strict;
9
10use MT::Util qw( decode_url is_valid_email escape_unicode ts2epoch );
11use MT::I18N qw( encode_text );
12
13sub login {
14    my $class = shift;
15    my ($app) = @_;
16    my $q = $app->{query};
17    return $app->errtrans("Invalid request.")
18        unless $q->param('blog_id');
19    my $blog = MT::Blog->load(scalar $q->param('blog_id'));
20    my %param = $app->param_hash;
21    my $csr = _get_csr(\%param, $blog) or return;
22    my $identity = $q->param('openid_url');
23    if (!$identity &&
24        (my $u = $q->param('openid_userid')) && $class->can('url_for_userid')) {
25        $identity = $class->url_for_userid($u);
26    }
27    my $claimed_identity = $csr->claimed_identity($identity);
28    if (!$claimed_identity) {
29        my ($err_code, $err_msg) = ($csr->errcode, $csr->errtext);
30        if ($err_code eq 'no_head_tag' || $err_code eq 'no_identity_server' || $err_code eq 'url_gone') {
31            $err_msg = $app->translate('The address entered does not appear to be an OpenID');
32        }
33        elsif ($err_code eq 'empty_url' || $err_code eq 'bogus_url') {
34            $err_msg = $app->translate('The text entered does not appear to be a web address');
35        }
36        elsif ($err_code eq 'url_fetch_error') {
37            $err_msg =~ s{ \A Error \s fetching \s URL: \s }{}xms;
38            $err_msg = $app->translate('Unable to connect to [_1]: [_2]', $identity, $err_msg);
39        }
40        return $app->error($app->translate("Could not verify the OpenID provided: [_1]", $err_msg));
41    }
42
43    my %params = $class->check_url_params( $app, $blog );
44
45    my $check_url = $claimed_identity->check_url(
46        %params
47    );
48
49    return $app->redirect($check_url);
50}
51
52sub handle_sign_in {
53    my $class = shift;
54    my ($app, $auth_type) = @_;
55    my $q = $app->{query};
56    my $INTERVAL = 60 * 60 * 24 * 7;
57
58    $auth_type ||= 'OpenID';
59
60    my $blog = MT::Blog->load($q->param('blog_id'));
61
62    my $cmntr;
63    my $session;
64
65    my %param = $app->param_hash;
66    my $csr = _get_csr(\%param, $blog) or return 0;
67
68    if(my $setup_url = $csr->user_setup_url( post_grant => 'return' )) {
69        return $app->redirect($setup_url);
70    } elsif(my $vident = $csr->verified_identity) {
71        my $name = $vident->url;
72        $cmntr = $app->model('author')->load(
73            {
74                name => $name,
75                type => MT::Author::COMMENTER(),
76                auth_type => $auth_type,
77            }
78        );
79        my $nick;
80        if ( $cmntr ) {
81            if ( ( $cmntr->modified_on
82                && ( ts2epoch($blog, $cmntr->modified_on) > time - $INTERVAL ) )
83              || ( $cmntr->created_on
84                && ( ts2epoch($blog, $cmntr->created_on) > time - $INTERVAL ) ) )
85            {
86                $nick = $cmntr->nickname;
87            }
88            else {
89                $nick = $class->get_nickname($vident);
90                $cmntr->nickname($nick);
91                $cmntr->save or return 0;
92            }
93        }
94        else {
95            $nick = $class->get_nickname($vident);
96            $cmntr = $app->_make_commenter(
97                email       => q(),
98                nickname    => $nick,
99                name        => $name,
100                url         => $vident->url,
101                auth_type   => $auth_type,
102                external_id => _url_hash($vident->url),
103            );
104        }
105        return 0 unless $cmntr;
106
107        $nick = $name unless $nick;
108
109        # Signature was valid, so create a session, etc.
110        $session = $app->make_commenter_session($cmntr);
111        unless ($session) {
112            $app->error($app->errstr() || $app->translate("Couldn't save the session"));
113            return 0;
114        }
115
116        if (my $userpic = $cmntr->userpic) {
117            my @stat = stat($userpic->file_path());
118            my $mtime = $stat[9];
119            if ( $mtime > time - $INTERVAL ) {
120                # newer than 7 days ago, don't download the userpic
121                return $cmntr;
122            }
123        }
124
125        if ( my $userpic = $class->get_userpicasset($vident) ) {
126            $userpic->tags('@userpic');
127            $userpic->created_by($cmntr->id);
128            $userpic->save;
129            if (my $userpic = $cmntr->userpic) {
130                # Remove the old userpic thumb so the new userpic's will be generated
131                # in its place.
132                my $thumb_file = $cmntr->userpic_file();
133                my $fmgr = MT::FileMgr->new('Local');
134                if ($fmgr->exists($thumb_file)) {
135                    $fmgr->delete($thumb_file);
136                }
137
138                $userpic->remove;
139            }
140            $cmntr->userpic_asset_id($userpic->id);
141            $cmntr->save;
142        }
143    } else {
144        # If there's no signature, then we trust the cookie.
145        my %cookies = $app->cookies();
146        my $cookie_name = MT::App::COMMENTER_COOKIE_NAME();
147        if ($cookies{$cookie_name}
148            && ($session = $cookies{$cookie_name}->value())) 
149        {
150            require MT::Session;
151            require MT::Author;
152            my $sess = MT::Session->load({id => $session});
153            if ($sess) {
154                $cmntr = MT::Author->load({name => $sess->name,
155                                           type => MT::Author::COMMENTER(),
156                                           auth_type => $auth_type});
157            }
158        }
159    }
160    unless ($cmntr) {
161        return 0;
162    }
163    return $cmntr;
164}
165
166sub _get_ua {
167    return MT->new_ua( { paranoid => 1 } );
168}
169
170sub _get_csr {
171    my ($params, $blog) = @_;
172    my $secret = MT->config->SecretToken;
173    my $ua = _get_ua() or return;
174    require Net::OpenID::Consumer;
175    Net::OpenID::Consumer->new(
176        ua => $ua,
177        args => $params,
178        consumer_secret => $secret,
179    );
180}
181
182sub _get_declared_foaf {
183    my ($vident) = @_;
184    my $req      = MT::Request->instance();
185    my $foaf     = $req->stash( 'foaf:' . _url_hash($vident->url) );
186    return $foaf if $foaf;
187
188    my $ua = _get_ua() or return '';
189
190    if ( my $foaf_url = $vident->declared_foaf ) {
191        my $resp = $ua->get($foaf_url);
192        if ( $resp->is_success ) {
193            $foaf = $resp->content;
194            $req->stash( 'foaf:' . _url_hash($vident->url), $foaf );
195            return $foaf;
196        }
197    }
198
199    q();
200}
201
202sub get_nickname {
203    my $class = shift;
204    my ($vident) = @_;
205    _get_nickname(@_);
206}
207
208sub _get_nickname {
209    my ($vident) = @_;
210
211    ## FOAF
212    if ( my $foaf = _get_declared_foaf($vident) ) {
213        my $name;
214
215        require XML::XPath;
216        my $xml = XML::XPath->new( xml => $foaf );
217        $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
218        $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/');
219        my ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:name');
220        ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:nick')
221            unless $name_el;
222        if ($name_el)
223        {
224            $name = $name_el->string_value;
225        }
226        $xml->cleanup;
227
228        return MT::I18N::utf8_off($name) if $name;
229    }
230
231    ## Atom
232    if(my $atom_url = $vident->declared_atom) {
233        if (my $ua = _get_ua()) {
234            my $resp = $ua->get($atom_url);
235            if($resp->is_success) {
236                my $name;
237
238                require XML::XPath;
239                my $xml = XML::XPath->new( xml => $resp->content );
240                if(my ($name_el) = $xml->findnodes('/feed/author/name')) {
241                    $name = $name_el->string_value;
242                }
243                $xml->cleanup;
244           
245                return MT::I18N::utf8_off($name) if $name;
246            }
247        }
248    }
249
250    return $vident->display ? $vident->display : $vident->url;
251}
252
253sub get_userpicasset {
254    my $class = shift;
255    my ($vident) = @_;
256    my $foaf = _get_declared_foaf($vident);
257    return undef unless $foaf;
258
259    require XML::XPath;
260    my $xml = XML::XPath->new( xml => $foaf );
261    $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
262    $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/');
263    my $resource = $xml->getNodeText('/RDF:RDF/FOAF:Person/FOAF:img/@RDF:resource');
264    my $url;
265    if ($resource) {
266        $url = $resource->value();
267    }
268    $xml->cleanup;
269    return undef unless $url;
270
271    return _asset_from_url($url);
272}
273
274sub _asset_from_url {
275    my ($image_url) = @_;
276    my $ua   = _get_ua() or return;
277    my $resp = $ua->get($image_url);
278    return undef unless $resp->is_success;
279    my $image = $resp->content;
280    return undef unless $image;
281    my $mimetype = $resp->header('Content-Type');
282    my $def_ext = {
283        'image/jpeg' => '.jpg',
284        'image/png'  => '.png',
285        'image/gif'  => '.gif'}->{$mimetype};
286
287    require Image::Size;
288    my ( $w, $h, $id ) = Image::Size::imgsize(\$image);
289
290    require MT::FileMgr;
291    my $fmgr = MT::FileMgr->new('Local');
292
293    my $save_path  = '%s/support/uploads/';
294    my $local_path =
295      File::Spec->catdir( MT->instance->static_file_path, 'support', 'uploads' );
296    $local_path =~ s|/$||
297      unless $local_path eq '/';    ## OS X doesn't like / at the end in mkdir().
298    unless ( $fmgr->exists($local_path) ) {
299        $fmgr->mkpath($local_path);
300    }
301    my $filename = substr($image_url, rindex($image_url, '/'));
302    if ( $filename =~ m!\.\.|\0|\|! ) {
303        return undef;
304    }
305    my ($base, $uploaded_path, $ext) = File::Basename::fileparse($filename, '\.[^\.]*');
306    $ext = $def_ext if $def_ext;  # trust content type higher than extension
307
308    # Find unique name for the file.
309    my $i = 1;
310    my $base_copy = $base;
311    while ($fmgr->exists(File::Spec->catfile($local_path, $base . $ext))) {
312        $base = $base_copy . '_' . $i++;
313    }
314
315    my $local_relative = File::Spec->catfile($save_path, $base . $ext);
316    my $local = File::Spec->catfile($local_path, $base . $ext);
317    $fmgr->put_data( $image, $local, 'upload' );
318
319    require MT::Asset;
320    my $asset_pkg = MT::Asset->handler_for_file($local);
321    return undef if $asset_pkg ne 'MT::Asset::Image';
322
323    my $asset;
324    $asset = $asset_pkg->new();
325    $asset->file_path($local_relative);
326    $asset->file_name($base.$ext);
327    my $ext_copy = $ext;
328    $ext_copy =~ s/\.//;
329    $asset->file_ext($ext_copy);
330    $asset->blog_id(0);
331
332    my $original = $asset->clone;
333    my $url = $local_relative;
334    $url  =~ s!\\!/!g;
335    $asset->url($url);
336    $asset->image_width($w);
337    $asset->image_height($h);
338    $asset->mime_type($mimetype);
339
340    $asset->save
341        or return undef;
342
343    MT->run_callbacks(
344        'api_upload_file.' . $asset->class,
345        File => $local, file => $local,
346        Url => $url, url => $url,
347        Size => length($image), size => length($image),
348        Asset => $asset, asset => $asset,
349        Type => $asset->class, type => $asset->class,
350    );
351    MT->run_callbacks(
352        'api_upload_image',
353        File => $local, file => $local,
354        Url => $url, url => $url,
355        Size => length($image), size => length($image),
356        Asset => $asset, asset => $asset,
357        Height => $h, height => $h,
358        Width => $w, width => $w,
359        Type => 'image', type => 'image',
360        ImageType => $id, image_type => $id,
361    );
362
363    $asset;
364}
365
366sub _url_hash {
367    my ($url) = @_;
368
369    if (eval { require Digest::MD5; 1; }) {
370        return Digest::MD5::md5_hex($url);
371    }
372    return substr $url, 0, 255;
373}
374
375sub check_url_params {
376    my $class = shift;
377    my ( $app, $blog ) = @_;
378    my $q = $app->{query};
379
380    my $path = MT->config->CGIPath;
381    if ($path =~ m!^/!) {
382        # relative path, prepend blog domain
383        my ($blog_domain) = $blog->archive_url =~ m|(.+://[^/]+)|;
384        $path = $blog_domain . $path;
385    }
386    $path .= '/' unless $path =~ m!/$!;
387    $path .= MT->config->CommentScript;
388
389    my $return_to = $path . '?__mode=handle_sign_in'
390        . '&blog_id=' . $q->param('blog_id')
391        . '&static=' . $q->param('static')
392        . '&key=' . $q->param('key');
393    $return_to .= '&entry_id=' . $q->param('entry_id') if $q->param('entry_id');
394    ( trust_root => $path, return_to => $return_to );
395}
396
3971;
398
399__END__
400
401=head1 NAME
402
403MT::Auth::OpenID
404
405Movable Type commenter authentication module via OpenID
406
407=head1 METHODS
408
409=head2 login
410
411This method is called from MT::App::Comments::login_external,
412to initiate process of logging in to a website other than
413Movable Type itself.  You should not have to modify the
414behavior of this method.
415
416=head2 handle_sign_in
417
418This method is called from MT::App::Comments::handle_sign_in
419to accept the result of logging in to an external website.
420You should not have to modify the behavior of this method.
421
422=head2 url_for_userid
423
424This method is called in login method when it needs to construct
425OpenID for the login request.  By default the module accepts
426the identifier entered by the user as OpenID, thus does nothing
427in this method.
428
429You can inherit this class, create your own authentication
430module and override this method to generate OpenID out of
431what user entered in the login form, so it can provide more
432user friendly way of specifying their OpenID.  See MT::Auth::Vox
433and MT::Auth::LiveJournal for examples.
434
435=head2 get_nickname
436
437This method is called in handle_sign_in method, in which it
438tries to grab the user's nickname.  By default, a user who
439is authenticated via OpenID has his/her nickname as the OpenID
440(thus, URL).  It tends to get ugly when it is displayed.
441
442By default, this class tries to load FOAF or Atom from the
443verified OpenID to see if it is able to get more semantic information.
444If it was able to load the semantic info from one of them,
445it uses the information as the user's nickname.
446
447You can inherit this class, create your own authentication
448module and override this method to generate more user friendly
449nickname for a user from the OpenID that does not support
450FOAF or Atom retrieval from the URL.
451
452=head2 get_userpic_asset
453
454This method is called in handle_sign_in method, in which it
455tries to retrieve the user's userpic or avatar.  By default,
456the method sees if the FOAF retrieved from OpenID has the URL
457for userpic.  If it does, the method downloads the userpic and
458saves it as an userpic asset for the user.
459
460You can inherit this class, create your own authentication
461module and override this method to associate a userpic to the user.
462
463=head2 check_url_params
464
465This method is called in login method.  This method must return
466a hash which is passed to I<Net::OpenID::ClaimedIdentity>::check_url.
467Consult I<Net::OpenID::ClaimedIdentity> about what can be specified.
468By default, the class specifies trust_root and return_to parameters.
469
470You can inherit this class, create your own authentication
471module and override this method to specify more parameters, or
472change how to construct trust_root and return_to arguments.
473
474=head1 AUTHOR & COPYRIGHT
475
476Please see L<MT/AUTHOR & COPYRIGHT>.
477
478=cut
Note: See TracBrowser for help on using the browser.