ostatussub.php 16.1 KB
Newer Older
1 2 3
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2009-2010, StatusNet, Inc.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *
 * 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/>.
 */

/**
 * @package OStatusPlugin
22
 * @maintainer Brion Vibber <brion@status.net>
23 24
 */

25
if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); }
26

27 28 29 30 31 32 33
/**
 * Key UI methods:
 *
 *  showInputForm() - form asking for a remote profile account or URL
 *                    We end up back here on errors
 *
 *  showPreviewForm() - surrounding form for preview-and-confirm
34
 *    preview() - display profile for a remote user
35
 *
36
 *  success() - redirects to subscriptions page on subscribe
37
 */
38 39
class OStatusSubAction extends Action
{
40 41
    protected $profile_uri; // provided acct: or URI of remote entity
    protected $oprofile; // Ostatus_profile of remote entity, if valid
42

43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
    protected function prepare(array $args=array())
    {
        parent::prepare($args);

        if (!common_logged_in()) {
            // XXX: selfURL() didn't work. :<
            common_set_returnto($_SERVER['REQUEST_URI']);
            if (Event::handle('RedirectToLogin', array($this, null))) {
                common_redirect(common_local_url('login'), 303);
            }
            return false;
        }

        if ($this->pullRemoteProfile()) {
            $this->validateRemoteProfile();
        }
        return true;
    }

    /**
     * Handle the submission.
     */
    protected function handle()
    {
        parent::handle();
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $this->handlePost();
        } else {
            $this->showForm();
        }
    }

75
    /**
76 77
     * Show the initial form, when we haven't yet been given a valid
     * remote profile.
78
     */
79
    function showInputForm()
80 81
    {
        $this->elementStart('form', array('method' => 'post',
82
                                          'id' => 'form_ostatus_sub',
83
                                          'class' => 'form_settings',
84
                                          'action' => $this->selfLink()));
85 86 87 88 89 90

        $this->hidden('token', common_session_token());

        $this->elementStart('fieldset', array('id' => 'settings_feeds'));

        $this->elementStart('ul', 'form_data');
91
        $this->elementStart('li');
92
        $this->input('profile',
Siebrand Mazeland's avatar
Siebrand Mazeland committed
93
                     // TRANS: Field label for a field that takes an OStatus user address.
94
                     _m('Subscribe to'),
95
                     $this->profile_uri,
Siebrand Mazeland's avatar
Siebrand Mazeland committed
96
                     // TRANS: Tooltip for field label "Subscribe to".
Siebrand Mazeland's avatar
Siebrand Mazeland committed
97
                     _m('OStatus user\'s address, like nickname@example.com or http://example.net/nickname.'));
98 99
        $this->elementEnd('li');
        $this->elementEnd('ul');
Siebrand Mazeland's avatar
Siebrand Mazeland committed
100 101
        // TRANS: Button text.
        $this->submit('validate', _m('BUTTON','Continue'));
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

        $this->elementEnd('fieldset');

        $this->elementEnd('form');
    }

    /**
     * Show the preview-and-confirm form. We've got a valid remote
     * profile and are ready to poke it!
     *
     * This controls the wrapper form; actual profile display will
     * be in previewUser() or previewGroup() depending on the type.
     */
    function showPreviewForm()
    {
117
        $ok = $this->preview();
118
        if (!$ok) {
119
            // @todo FIXME maybe provide a cancel button or link back?
120
            return;
121 122 123 124 125
        }

        $this->elementStart('div', 'entity_actions');
        $this->elementStart('ul');
        $this->elementStart('li', 'entity_subscribe');
126 127
        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_ostatus_sub',
128
                                          'class' => 'form_remote_authorize',
129
                                          'action' =>
130
                                          $this->selfLink()));
131
        $this->elementStart('fieldset');
132 133 134
        $this->hidden('token', common_session_token());
        $this->hidden('profile', $this->profile_uri);
        if ($this->oprofile->isGroup()) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
135
            // TRANS: Button text.
136
            $this->submit('submit', _m('Join'), 'submit', null,
Siebrand Mazeland's avatar
Siebrand Mazeland committed
137 138
                         // TRANS: Tooltip for button "Join".
                         _m('BUTTON','Join this group'));
139
        } else {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
140 141 142 143
            // TRANS: Button text.
            $this->submit('submit', _m('BUTTON','Confirm'), 'submit', null,
                         // TRANS: Tooltip for button "Confirm".
                         _m('Subscribe to this user'));
144
        }
145 146
        $this->elementEnd('fieldset');
        $this->elementEnd('form');
147 148 149
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->elementEnd('div');
150 151 152 153
    }

    /**
     * Show a preview for a remote user's profile
154
     * @return boolean true if we're ok to try subscribing
155
     */
156
    function preview()
157
    {
158 159
        // Throws NoProfileException on localProfile when remote user's Profile not found
        $profile = $this->oprofile->localProfile();
160

161
        if ($this->scoped->isSubscribed($profile)) {
162
            $this->element('div', array('class' => 'error'),
163
                           // TRANS: Extra paragraph in remote profile view when already subscribed.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
164
                           _m('You are already subscribed to this user.'));
165 166 167 168 169
            $ok = false;
        } else {
            $ok = true;
        }

170
        $avatarUrl = $profile->avatarUrl(AVATAR_PROFILE_SIZE);
171 172 173 174 175

        $this->showEntity($profile,
                          $profile->profileurl,
                          $avatarUrl,
                          $profile->bio);
176
        return $ok;
177 178
    }

179
    function showEntity($entity, $profile, $avatar, $note)
180 181 182 183 184
    {
        $nickname = $entity->nickname;
        $fullname = $entity->fullname;
        $homepage = $entity->homepage;
        $location = $entity->location;
185

186
        $this->elementStart('div', 'entity_profile vcard');
187
        $this->element('img', array('src' => $avatar,
188
                                    'class' => 'photo avatar entity_depiction',
189 190 191
                                    'width' => AVATAR_PROFILE_SIZE,
                                    'height' => AVATAR_PROFILE_SIZE,
                                    'alt' => $nickname));
192

193
        $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
194 195
        $this->elementStart('a', array('href' => $profile,
                                       'class' => 'url '.$hasFN));
196
        $this->text($nickname);
197 198 199
        $this->elementEnd('a');

        if (!is_null($fullname)) {
200
            $this->elementStart('div', 'fn entity_fn');
201
            $this->text($fullname);
202
            $this->elementEnd('div');
203
        }
204

205
        if (!is_null($location)) {
206
            $this->elementStart('div', 'label entity_location');
207
            $this->text($location);
208
            $this->elementEnd('div');
209 210 211 212
        }

        if (!is_null($homepage)) {
            $this->elementStart('a', array('href' => $homepage,
213
                                           'class' => 'url entity_url'));
214
            $this->text($homepage);
215 216 217
            $this->elementEnd('a');
        }

218
        if (!is_null($note)) {
219
            $this->elementStart('div', 'note entity_note');
220
            $this->text($note);
221
            $this->elementEnd('div');
222 223
        }
        $this->elementEnd('div');
224 225 226 227 228
    }

    /**
     * Redirect on successful remote user subscription
     */
229
    function success()
230
    {
231
        $url = common_local_url('subscriptions', array('nickname' => $this->scoped->nickname));
232 233 234 235 236 237 238 239 240 241
        common_redirect($url, 303);
    }

    /**
     * Pull data for a remote profile and check if it's valid.
     * Fills out error UI string in $this->error
     * Fills out $this->oprofile on success.
     *
     * @return boolean
     */
242
    function pullRemoteProfile()
243
    {
244
        $validate = new Validate();
mmn's avatar
mmn committed
245 246 247 248 249
        try {
            $this->profile_uri = Discovery::normalize($this->trimmed('profile'));
        } catch (Exception $e) {
            $this->profile_uri = null;
        }
250
        try {
251
            if (Discovery::isAcct($this->profile_uri) && $validate->email(mb_substr($this->profile_uri, 5))) {
252
                $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
253
            } else if ($validate->uri($this->profile_uri)) {
254
                $this->oprofile = Ostatus_profile::ensureProfileURL($this->profile_uri);
255
            } else {
256 257
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
258
                $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
259
                common_debug('Invalid address format.', __FILE__);
260 261
                return false;
            }
262 263
            return true;
        } catch (FeedSubBadURLException $e) {
264 265
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
266
            $this->error = _m('Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.');
267
            common_debug('Invalid URL or could not reach server.', __FILE__);
268
        } catch (FeedSubBadResponseException $e) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
269
            // TRANS: Error text.
270
            $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
271
            common_debug('Cannot read feed; server returned error.', __FILE__);
272
        } catch (FeedSubEmptyException $e) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
273
            // TRANS: Error text.
274
            $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
275
            common_debug('Cannot read feed; server returned an empty page.', __FILE__);
276
        } catch (FeedSubBadHTMLException $e) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
277
            // TRANS: Error text.
278
            $this->error = _m('Sorry, we could not reach that feed. Please try that OStatus address again later.');
279
            common_debug('Bad HTML, could not find feed link.', __FILE__);
280
        } catch (FeedSubNoFeedException $e) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
281
            // TRANS: Error text.
282 283
            $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later.");
            common_debug('Could not find a feed linked from this URL.', __FILE__);
284
        } catch (FeedSubUnrecognizedTypeException $e) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
285
            // TRANS: Error text.
286 287
            $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later.");
            common_debug('Not a recognized feed type.', __FILE__);
288 289 290 291
        } catch (FeedSubNoHubException $e) {
            // TRANS: Error text.
            $this->error = _m("Sorry, that feed is not Pubsubhubub enabled.");
            common_debug('No hub found.', __FILE__);
292
        } catch (Exception $e) {
293
            // Any new ones we forgot about
294 295
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
296
            $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname.");
297
            common_debug(sprintf('Bad feed URL: %s %s', get_class($e), $e->getMessage()), __FILE__);
298 299 300 301 302
        }

        return false;
    }

303 304
    function validateRemoteProfile()
    {
Shashi Gowda's avatar
Shashi Gowda committed
305
        // Send us to the respective subscription form for conf
306 307 308
        if ($this->oprofile->isGroup()) {
            $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri));
            common_redirect($target, 303);
Shashi Gowda's avatar
Shashi Gowda committed
309 310 311
        } else if ($this->oprofile->isPeopletag()) {
            $target = common_local_url('ostatuspeopletag', array(), array('profile' => $this->profile_uri));
            common_redirect($target, 303);
312 313 314
        }
    }

315 316 317 318
    /**
     * Attempt to finalize subscription.
     * validateFeed must have been run first.
     *
319
     * Calls showForm on failure or success on success.
320 321 322 323
     */
    function saveFeed()
    {
        // And subscribe the current user to the local profile
324
        $local = $this->oprofile->localProfile();
325
        if ($this->scoped->isSubscribed($local)) {
326 327
            // TRANS: OStatus remote subscription dialog error.
            $this->showForm(_m('Already subscribed!'));
328
        } elseif (Subscription::start($this->scoped, $local)) {
329
            $this->success();
330
        } else {
331 332
            // TRANS: OStatus remote subscription dialog error.
            $this->showForm(_m('Remote subscription failed!'));
333 334 335
        }
    }

336 337 338 339 340 341 342 343 344 345 346
    /**
     * Handle posts to this form
     *
     * @return void
     */

    function handlePost()
    {
        // CSRF protection
        $token = $this->trimmed('token');
        if (!$token || $token != common_session_token()) {
347
            // TRANS: Client error displayed when the session token does not match or is not given.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
348
            $this->showForm(_m('There was a problem with your session token. '.
349 350 351 352
                              'Try again, please.'));
            return;
        }

353
        if ($this->oprofile) {
354
            if ($this->arg('submit')) {
355 356 357
                $this->saveFeed();
                return;
            }
358
        }
359
        $this->showForm();
360 361 362
    }

    /**
363
     * Show the appropriate form based on our input state.
364
     */
365
    function showForm($err=null)
366
    {
367 368
        if ($err) {
            $this->error = $err;
369
        }
370
        if ($this->boolean('ajax')) {
371
            $this->startHTML('text/xml;charset=utf-8');
372
            $this->elementStart('head');
Siebrand Mazeland's avatar
Siebrand Mazeland committed
373
            // TRANS: Form title.
374 375 376 377 378
            $this->element('title', null, _m('Subscribe to user'));
            $this->elementEnd('head');
            $this->elementStart('body');
            $this->showContent();
            $this->elementEnd('body');
379
            $this->endHTML();
380 381
        } else {
            $this->showPage();
382 383 384
        }
    }

385 386 387 388 389
    /**
     * Title of the page
     *
     * @return string Title of the page
     */
390

391 392
    function title()
    {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
393
        // TRANS: Page title for OStatus remote subscription form.
394
        return _m('Confirm');
395
    }
396

397 398 399 400 401
    /**
     * Instructions for use
     *
     * @return instructions for use
     */
402

403 404
    function getInstructions()
    {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
405
        // TRANS: Instructions.
406
        return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
407 408
    }

409
    function showPageNotice()
410
    {
411
        if (!empty($this->error)) {
412
            $this->element('p', 'error', $this->error);
413 414 415
        }
    }

416 417 418 419 420 421 422 423 424
    /**
     * Content area of the page
     *
     * Shows a form for associating a remote OStatus account with this
     * StatusNet account.
     *
     * @return void
     */
    function showContent()
425
    {
426 427 428 429 430
        if ($this->oprofile) {
            $this->showPreviewForm();
        } else {
            $this->showInputForm();
        }
431
    }
432

433 434 435 436 437
    function showScripts()
    {
        parent::showScripts();
        $this->autofocus('feedurl');
    }
438 439 440 441 442

    function selfLink()
    {
        return common_local_url('ostatussub');
    }
443 444 445 446 447 448 449 450 451 452 453 454 455

    /**
     * Disable the send-notice form at the top of the page.
     * This is really just a hack for the broken CSS in the Cloudy theme,
     * I think; copying from other non-notice-navigation pages that do this
     * as well. There will be plenty of others also broken.
     *
     * @fixme fix the cloudy theme
     * @fixme do this in a more general way
     */
    function showNoticeForm() {
        // nop
    }
456
}