root/branches/release-38/lib/MT/Auth/OpenID.pm @ 2365

Revision 2365, 11.9 kB (checked in by bchoate, 19 months ago)

Revised commenter sessions to include user id (as we do with authors) so we can load by id rather than by name. BugId:79253

  • 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 $root = $class->_get_root($blog);
44    my $return_to = $app->base . $app->uri . '?__mode=handle_sign_in'
45        . '&blog_id=' . $q->param('blog_id')
46        . '&static=' . $q->param('static')
47        . '&key=' . $q->param('key');
48    $return_to .= '&entry_id=' . $q->param('entry_id') if $q->param('entry_id');
49
50    my $check_url = $claimed_identity->check_url(
51        return_to => $return_to,
52        trust_root => $root,
53    );
54
55    return $app->redirect($check_url);
56}
57
58sub handle_sign_in {
59    my $class = shift;
60    my ($app, $auth_type) = @_;
61    my $q = $app->{query};
62    my $INTERVAL = 60 * 60 * 24 * 7;
63
64    $auth_type ||= 'OpenID';
65
66    my $blog = MT::Blog->load($q->param('blog_id'));
67
68    my $cmntr;
69    my $session;
70
71    my %param = $app->param_hash;
72    my $csr = _get_csr(\%param, $blog) or return 0;
73
74    if(my $setup_url = $csr->user_setup_url( post_grant => 'return' )) {
75        return $app->redirect($setup_url);
76    } elsif(my $vident = $csr->verified_identity) {
77        my $name = $vident->url;
78        $cmntr = $app->model('author')->load(
79            {
80                name => $name,
81                type => MT::Author::COMMENTER(),
82                auth_type => $auth_type,
83            }
84        );
85        my $nick;
86        if ( $cmntr ) {
87            if ( ( $cmntr->modified_on
88                && ( ts2epoch($blog, $cmntr->modified_on) > time - $INTERVAL ) )
89              || ( $cmntr->created_on
90                && ( ts2epoch($blog, $cmntr->created_on) > time - $INTERVAL ) ) )
91            {
92                $nick = $cmntr->nickname;
93            }
94            else {
95                $nick = $class->get_nickname($vident);
96                $cmntr->nickname($nick);
97                $cmntr->save or return 0;
98            }
99        }
100        else {
101            $nick = $class->get_nickname($vident);
102            $cmntr = $app->_make_commenter(
103                email       => q(),
104                nickname    => $nick,
105                name        => $name,
106                url         => $vident->url,
107                auth_type   => $auth_type,
108                external_id => _url_hash($vident->url),
109            );
110        }
111        return 0 unless $cmntr;
112
113        $nick = $name unless $nick;
114
115        # Signature was valid, so create a session, etc.
116        $session = $app->make_commenter_session($cmntr);
117        unless ($session) {
118            $app->error($app->errstr() || $app->translate("Couldn't save the session"));
119            return 0;
120        }
121
122        if (my $userpic = $cmntr->userpic) {
123            my @stat = stat($userpic->file_path());
124            my $mtime = $stat[9];
125            if ( $mtime > time - $INTERVAL ) {
126                # newer than 7 days ago, don't download the userpic
127                return $cmntr;
128            }
129        }
130
131        if ( my $userpic = $class->get_userpicasset($vident) ) {
132            $userpic->tags('@userpic');
133            $userpic->created_by($cmntr->id);
134            $userpic->save;
135            if (my $userpic = $cmntr->userpic) {
136                # Remove the old userpic thumb so the new userpic's will be generated
137                # in its place.
138                my $thumb_file = $cmntr->userpic_file();
139                my $fmgr = MT::FileMgr->new('Local');
140                if ($fmgr->exists($thumb_file)) {
141                    $fmgr->delete($thumb_file);
142                }
143
144                $userpic->remove;
145            }
146            $cmntr->userpic_asset_id($userpic->id);
147            $cmntr->save;
148        }
149    } else {
150        # If there's no signature, then we trust the cookie.
151        my %cookies = $app->cookies();
152        my $cookie_name = MT::App::COMMENTER_COOKIE_NAME();
153        if ($cookies{$cookie_name}
154            && ($session = $cookies{$cookie_name}->value())) 
155        {
156            require MT::Session;
157            require MT::Author;
158            my $sess = MT::Session->load({id => $session});
159            if ($sess) {
160                $cmntr = MT::Author->load({name => $sess->name,
161                                           type => MT::Author::COMMENTER(),
162                                           auth_type => $auth_type});
163            }
164        }
165    }
166    unless ($cmntr) {
167        return 0;
168    }
169    return $cmntr;
170}
171
172sub _get_ua {
173    return MT->new_ua( { paranoid => 1 } );
174}
175
176sub _get_csr {
177    my ($params, $blog) = @_;
178    my $secret = MT->config->SecretToken;
179    my $ua = _get_ua() or return;
180    require Net::OpenID::Consumer;
181    Net::OpenID::Consumer->new(
182        ua => $ua,
183        args => $params,
184        consumer_secret => $secret,
185    );
186}
187
188sub _get_declared_foaf {
189    my ($vident) = @_;
190    my $req      = MT::Request->instance();
191    my $foaf     = $req->stash( 'foaf:' . _url_hash($vident->url) );
192    return $foaf if $foaf;
193
194    my $ua = _get_ua() or return '';
195
196    if ( my $foaf_url = $vident->declared_foaf ) {
197        my $resp = $ua->get($foaf_url);
198        if ( $resp->is_success ) {
199            $foaf = $resp->content;
200            $req->stash( 'foaf:' . _url_hash($vident->url), $foaf );
201            return $foaf;
202        }
203    }
204
205    q();
206}
207
208sub get_nickname {
209    my $class = shift;
210    my ($vident) = @_;
211    _get_nickname(@_);
212}
213
214sub _get_nickname {
215    my ($vident) = @_;
216
217    ## FOAF
218    if ( my $foaf = _get_declared_foaf($vident) ) {
219        my $name;
220
221        require XML::XPath;
222        my $xml = XML::XPath->new( xml => $foaf );
223        $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
224        $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/');
225        my ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:name');
226        ($name_el) = $xml->findnodes('/RDF:RDF/FOAF:Person/FOAF:nick')
227            unless $name_el;
228        if ($name_el)
229        {
230            $name = $name_el->string_value;
231        }
232        $xml->cleanup;
233
234        return MT::I18N::utf8_off($name) if $name;
235    }
236
237    ## Atom
238    if(my $atom_url = $vident->declared_atom) {
239        if (my $ua = _get_ua()) {
240            my $resp = $ua->get($atom_url);
241            if($resp->is_success) {
242                my $name;
243
244                require XML::XPath;
245                my $xml = XML::XPath->new( xml => $resp->content );
246                if(my ($name_el) = $xml->findnodes('/feed/author/name')) {
247                    $name = $name_el->string_value;
248                }
249                $xml->cleanup;
250           
251                return MT::I18N::utf8_off($name) if $name;
252            }
253        }
254    }
255
256    return $vident->display ? $vident->display : $vident->url;
257}
258
259sub get_userpicasset {
260    my $class = shift;
261    my ($vident) = @_;
262    my $foaf = _get_declared_foaf($vident);
263    return undef unless $foaf;
264
265    require XML::XPath;
266    my $xml = XML::XPath->new( xml => $foaf );
267    $xml->set_namespace('RDF', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
268    $xml->set_namespace('FOAF', 'http://xmlns.com/foaf/0.1/');
269    my $resource = $xml->getNodeText('/RDF:RDF/FOAF:Person/FOAF:img/@RDF:resource');
270    my $url;
271    if ($resource) {
272        $url = $resource->value();
273    }
274    $xml->cleanup;
275    return undef unless $url;
276
277    return _asset_from_url($url);
278}
279
280sub _asset_from_url {
281    my ($image_url) = @_;
282    my $ua   = _get_ua() or return;
283    my $resp = $ua->get($image_url);
284    return undef unless $resp->is_success;
285    my $image = $resp->content;
286    return undef unless $image;
287    my $mimetype = $resp->header('Content-Type');
288    my $def_ext = {
289        'image/jpeg' => '.jpg',
290        'image/png'  => '.png',
291        'image/gif'  => '.gif'}->{$mimetype};
292
293    require Image::Size;
294    my ( $w, $h, $id ) = Image::Size::imgsize(\$image);
295
296    require MT::FileMgr;
297    my $fmgr = MT::FileMgr->new('Local');
298
299    my $save_path  = '%s/support/uploads/';
300    my $local_path =
301      File::Spec->catdir( MT->instance->static_file_path, 'support', 'uploads' );
302    $local_path =~ s|/$||
303      unless $local_path eq '/';    ## OS X doesn't like / at the end in mkdir().
304    unless ( $fmgr->exists($local_path) ) {
305        $fmgr->mkpath($local_path);
306    }
307    my $filename = substr($image_url, rindex($image_url, '/'));
308    if ( $filename =~ m!\.\.|\0|\|! ) {
309        return undef;
310    }
311    my ($base, $uploaded_path, $ext) = File::Basename::fileparse($filename, '\.[^\.]*');
312    $ext = $def_ext if $def_ext;  # trust content type higher than extension
313
314    # Find unique name for the file.
315    my $i = 1;
316    my $base_copy = $base;
317    while ($fmgr->exists(File::Spec->catfile($local_path, $base . $ext))) {
318        $base = $base_copy . '_' . $i++;
319    }
320
321    my $local_relative = File::Spec->catfile($save_path, $base . $ext);
322    my $local = File::Spec->catfile($local_path, $base . $ext);
323    $fmgr->put_data( $image, $local, 'upload' );
324
325    require MT::Asset;
326    my $asset_pkg = MT::Asset->handler_for_file($local);
327    return undef if $asset_pkg ne 'MT::Asset::Image';
328
329    my $asset;
330    $asset = $asset_pkg->new();
331    $asset->file_path($local_relative);
332    $asset->file_name($base.$ext);
333    my $ext_copy = $ext;
334    $ext_copy =~ s/\.//;
335    $asset->file_ext($ext_copy);
336    $asset->blog_id(0);
337
338    my $original = $asset->clone;
339    my $url = $local_relative;
340    $url  =~ s!\\!/!g;
341    $asset->url($url);
342    $asset->image_width($w);
343    $asset->image_height($h);
344    $asset->mime_type($mimetype);
345
346    $asset->save
347        or return undef;
348
349    MT->run_callbacks(
350        'api_upload_file.' . $asset->class,
351        File => $local, file => $local,
352        Url => $url, url => $url,
353        Size => length($image), size => length($image),
354        Asset => $asset, asset => $asset,
355        Type => $asset->class, type => $asset->class,
356    );
357    MT->run_callbacks(
358        'api_upload_image',
359        File => $local, file => $local,
360        Url => $url, url => $url,
361        Size => length($image), size => length($image),
362        Asset => $asset, asset => $asset,
363        Height => $h, height => $h,
364        Width => $w, width => $w,
365        Type => 'image', type => 'image',
366        ImageType => $id, image_type => $id,
367    );
368
369    $asset;
370}
371
372sub _url_hash {
373    my ($url) = @_;
374
375    if (eval { require Digest::MD5; 1; }) {
376        return Digest::MD5::md5_hex($url);
377    }
378    return substr $url, 0, 255;
379}
380
381sub _get_root {
382    my $class = shift;
383    my ($blog) = @_;
384    my $path = MT->config->CGIPath;
385    if ($path =~ m!^/!) {
386        # relative path, prepend blog domain
387        my ($blog_domain) = $blog->archive_url =~ m|(.+://[^/]+)|;
388        $path = $blog_domain . $path;
389    }
390    $path .= '/' unless $path =~ m!/$!;
391    $path;
392}
393
3941;
Note: See TracBrowser for help on using the browser.