We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

util.php 70.1 KB
Newer Older
1
<?php
Evan Prodromou's avatar
Evan Prodromou committed
2
/*
3
 * StatusNet - the distributed open-source microblogging tool
4
 * Copyright (C) 2008-2011, StatusNet, Inc.
Evan Prodromou's avatar
Evan Prodromou committed
5
 *
6 7 8 9
 * 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.
Evan Prodromou's avatar
Evan Prodromou committed
10
 *
11 12 13 14
 * 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.
Evan Prodromou's avatar
Evan Prodromou committed
15
 *
16 17 18 19
 * 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/>.
 */

20
/* XXX: break up into separate modules (HTTP, user, files) */
21

22 23 24
/**
 * Show a server error.
 */
25 26
function common_server_error($msg, $code=500)
{
27 28
    $err = new ServerErrorAction($msg, $code);
    $err->showPage();
29 30
}

31 32 33
/**
 * Show a user error.
 */
34 35
function common_user_error($msg, $code=400)
{
36 37
    $err = new ClientErrorAction($msg, $code);
    $err->showPage();
38 39
}

40 41 42
/**
 * This should only be used at setup; processes switching languages
 * to send text to other users should use common_switch_locale().
43
 *
44 45 46 47
 * @param string $language Locale language code (optional; empty uses
 *                         current user's preference or site default)
 * @return mixed success
 */
48 49
function common_init_locale($language=null)
{
50 51 52 53 54
    if(!$language) {
        $language = common_language();
    }
    putenv('LANGUAGE='.$language);
    putenv('LANG='.$language);
55
    $ok =  setlocale(LC_ALL, $language . ".utf8",
56 57 58 59
                     $language . ".UTF8",
                     $language . ".utf-8",
                     $language . ".UTF-8",
                     $language);
60 61

    return $ok;
62 63
}

64 65 66
/**
 * Initialize locale and charset settings and gettext with our message catalog,
 * using the current user's language preference or the site default.
67
 *
68 69
 * This should generally only be run at framework initialization; code switching
 * languages at runtime should call common_switch_language().
70
 *
71 72
 * @access private
 */
73 74
function common_init_language()
{
75
    mb_internal_encoding('UTF-8');
76

77 78
    // Note that this setlocale() call may "fail" but this is harmless;
    // gettext will still select the right language.
79 80
    $language = common_language();
    $locale_set = common_init_locale($language);
Evan Prodromou's avatar
Evan Prodromou committed
81

82 83 84 85 86 87 88 89 90 91 92 93
    if (!$locale_set) {
        // The requested locale doesn't exist on the system.
        //
        // gettext seems very picky... We first need to setlocale()
        // to a locale which _does_ exist on the system, and _then_
        // we can set in another locale that may not be set up
        // (say, ga_ES for Galego/Galician) it seems to take it.
        //
        // For some reason C and POSIX which are guaranteed to work
        // don't do the job. en_US.UTF-8 should be there most of the
        // time, but not guaranteed.
        $ok = common_init_locale("en_US");
94 95
        if (!$ok && strtolower(substr(PHP_OS, 0, 3)) != 'win') {
            // Try to find a complete, working locale on Unix/Linux...
96 97 98 99 100 101 102 103 104 105 106
            // @fixme shelling out feels awfully inefficient
            // but I don't think there's a more standard way.
            $all = `locale -a`;
            foreach (explode("\n", $all) as $locale) {
                if (preg_match('/\.utf[-_]?8$/i', $locale)) {
                    $ok = setlocale(LC_ALL, $locale);
                    if ($ok) {
                        break;
                    }
                }
            }
107 108 109
        }
        if (!$ok) {
            common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work.");
110 111 112 113
        }
        $locale_set = common_init_locale($language);
    }

114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
    common_init_gettext();
}

/**
 * @access private
 */
function common_init_gettext()
{
    setlocale(LC_CTYPE, 'C');
    // So we do not have to make people install the gettext locales
    $path = common_config('site','locale_path');
    bindtextdomain("statusnet", $path);
    bind_textdomain_codeset("statusnet", "UTF-8");
    textdomain("statusnet");
}

/**
 * Switch locale during runtime, and poke gettext until it cries uncle.
 * Otherwise, sometimes it doesn't actually switch away from the old language.
 *
 * @param string $language code for locale ('en', 'fr', 'pt_BR' etc)
 */
function common_switch_locale($language=null)
{
    common_init_locale($language);

140
    setlocale(LC_CTYPE, 'C');
Siebrand Mazeland's avatar
Siebrand Mazeland committed
141
    // So we do not have to make people install the gettext locales
142 143
    $path = common_config('site','locale_path');
    bindtextdomain("statusnet", $path);
144 145
    bind_textdomain_codeset("statusnet", "UTF-8");
    textdomain("statusnet");
146 147
}

148 149
function common_timezone()
{
150 151 152 153 154 155
    if (common_logged_in()) {
        $user = common_current_user();
        if ($user->timezone) {
            return $user->timezone;
        }
    }
156

157
    return common_config('site', 'timezone');
158 159
}

160 161 162 163 164 165 166 167 168 169 170 171 172 173
function common_valid_language($lang)
{
    if ($lang) {
        // Validate -- we don't want to end up with a bogus code
        // left over from some old junk.
        foreach (common_config('site', 'languages') as $code => $info) {
            if ($info['lang'] == $lang) {
                return true;
            }
        }
    }
    return false;
}

174 175
function common_language()
{
176 177 178 179 180 181 182 183
    // Allow ?uselang=xx override, very useful for debugging
    // and helping translators check usage and context.
    if (isset($_GET['uselang'])) {
        $uselang = strval($_GET['uselang']);
        if (common_valid_language($uselang)) {
            return $uselang;
        }
    }
184

185 186
    // If there is a user logged in and they've set a language preference
    // then return that one...
187
    if (_have_config() && common_logged_in()) {
188
        $user = common_current_user();
189 190 191

        if (common_valid_language($user->language)) {
            return $user->language;
192
        }
193
    }
194

195 196
    // Otherwise, find the best match for the languages requested by the
    // user's browser...
Brion Vibber's avatar
Brion Vibber committed
197 198 199 200 201 202 203
    if (common_config('site', 'langdetect')) {
        $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null;
        if (!empty($httplang)) {
            $language = client_prefered_language($httplang);
            if ($language)
              return $language;
        }
204
    }
205

206 207
    // Finally, if none of the above worked, use the site's default...
    return common_config('site', 'language');
208
}
209

210 211 212
/**
 * Salted, hashed passwords are stored in the DB.
 */
213
function common_munge_password($password, $id, Profile $profile=null)
214
{
215 216 217 218 219 220 221
    $hashed = null;

    if (Event::handle('StartHashPassword', array(&$hashed, $password, $profile))) {
        Event::handle('EndHashPassword', array(&$hashed, $password, $profile));
    }
    if (empty($hashed)) {
        throw new PasswordHashException();
222
    }
223 224

    return $hashed;
225 226
}

227 228 229
/**
 * Check if a username exists and has matching password.
 */
230 231
function common_check_user($nickname, $password)
{
232 233 234 235 236
    // empty nickname always unacceptable
    if (empty($nickname)) {
        return false;
    }

237 238 239
    $authenticatedUser = false;

    if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) {
240 241

        if (common_is_email($nickname)) {
242
            $user = User::getKV('email', common_canonical_email($nickname));
243
        } else {
244
            $user = User::getKV('nickname', Nickname::normalize($nickname));
245 246
        }

247 248
        if (!empty($user)) {
            if (!empty($password)) { // never allow login with blank password
Craig Andrews's avatar
Craig Andrews committed
249 250 251
                if (0 == strcmp(common_munge_password($password, $user->id),
                                $user->password)) {
                    //internal checking passed
252
                    $authenticatedUser = $user;
Craig Andrews's avatar
Craig Andrews committed
253
                }
Craig Andrews's avatar
Craig Andrews committed
254 255
            }
        }
256
        Event::handle('EndCheckPassword', array($nickname, $password, $authenticatedUser));
257
    }
258 259

    return $authenticatedUser;
260 261
}

262 263 264
/**
 * Is the current user logged in?
 */
265 266
function common_logged_in()
{
267
    return (!is_null(common_current_user()));
268 269
}

270 271
function common_have_session()
{
272
    return (0 != strcmp(session_id(), ''));
273 274
}

275 276
function common_ensure_session()
{
Evan Prodromou's avatar
Evan Prodromou committed
277
    $c = null;
278
    if (array_key_exists(session_name(), $_COOKIE)) {
Evan Prodromou's avatar
Evan Prodromou committed
279 280
        $c = $_COOKIE[session_name()];
    }
281
    if (!common_have_session()) {
282 283 284
        if (common_config('sessions', 'handle')) {
            Session::setSaveHandler();
        }
Evan Prodromou's avatar
Evan Prodromou committed
285 286 287 288 289 290 291 292
	if (array_key_exists(session_name(), $_GET)) {
	    $id = $_GET[session_name()];
	} else if (array_key_exists(session_name(), $_COOKIE)) {
	    $id = $_COOKIE[session_name()];
	}
	if (isset($id)) {
	    session_id($id);
	}
293
        @session_start();
Evan Prodromou's avatar
Evan Prodromou committed
294 295
        if (!isset($_SESSION['started'])) {
            $_SESSION['started'] = time();
Evan Prodromou's avatar
Evan Prodromou committed
296
            if (!empty($id)) {
Evan Prodromou's avatar
Evan Prodromou committed
297 298 299 300
                common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' .
                           ' is set but started value is null');
            }
        }
301
    }
302 303
}

304 305 306 307
// Three kinds of arguments:
// 1) a user object
// 2) a nickname
// 3) null to clear
308

309
// Initialize to false; set to null if none found
310 311
$_cur = false;

312 313
function common_set_user($user)
{
314 315
    global $_cur;

316 317 318 319 320 321
    if (is_null($user) && common_have_session()) {
        $_cur = null;
        unset($_SESSION['userid']);
        return true;
    } else if (is_string($user)) {
        $nickname = $user;
322
        $user = User::getKV('nickname', $nickname);
323
    } else if (!$user instanceof User) {
324 325 326 327
        return false;
    }

    if ($user) {
Craig Andrews's avatar
Craig Andrews committed
328
        if (Event::handle('StartSetUser', array(&$user))) {
329 330
            if (!empty($user)) {
                if (!$user->hasRight(Right::WEBLOGIN)) {
331
                    // TRANS: Authorisation exception thrown when a user a not allowed to login.
332 333
                    throw new AuthorizationException(_('Not allowed to log in.'));
                }
Craig Andrews's avatar
Craig Andrews committed
334 335 336 337 338 339 340
                common_ensure_session();
                $_SESSION['userid'] = $user->id;
                $_cur = $user;
                Event::handle('EndSetUser', array($user));
                return $_cur;
            }
        }
341 342
    }
    return false;
343 344
}

345 346
function common_set_cookie($key, $value, $expiration=0)
{
347 348
    $path = common_config('site', 'path');
    $server = common_config('site', 'server');
349

350 351 352 353 354 355 356 357 358
    if ($path && ($path != '/')) {
        $cookiepath = '/' . $path . '/';
    } else {
        $cookiepath = '/';
    }
    return setcookie($key,
                     $value,
                     $expiration,
                     $cookiepath,
359 360
                     $server,
                     common_config('site', 'ssl')=='always');
361 362 363
}

define('REMEMBERME', 'rememberme');
364
define('REMEMBERME_EXPIRY', 30 * 24 * 60 * 60); // 30 days
365

366 367
function common_rememberme($user=null)
{
368 369 370 371 372 373
    if (!$user) {
        $user = common_current_user();
        if (!$user) {
            return false;
        }
    }
374

375
    $rm = new Remember_me();
376

377
    $rm->code = common_random_hexstr(16);
378
    $rm->user_id = $user->id;
379

380
    // Wrap the insert in some good ol' fashioned transaction code
381 382 383

    $rm->query('BEGIN');

384
    $result = $rm->insert();
385

386 387 388
    if (!$result) {
        common_log_db_error($rm, 'INSERT', __FILE__);
        return false;
389 390
    }

391 392
    $rm->query('COMMIT');

393 394
    $cookieval = $rm->user_id . ':' . $rm->code;

395
    common_log(LOG_INFO, 'adding rememberme cookie "' . $cookieval . '" for ' . $user->nickname);
396

397
    common_set_cookie(REMEMBERME, $cookieval, time() + REMEMBERME_EXPIRY);
398

399
    return true;
400 401
}

402 403
function common_remembered_user()
{
404
    $user = null;
405

406
    $packed = isset($_COOKIE[REMEMBERME]) ? $_COOKIE[REMEMBERME] : null;
407

408 409
    if (!$packed) {
        return null;
410 411 412 413 414
    }

    list($id, $code) = explode(':', $packed);

    if (!$id || !$code) {
415
        common_log(LOG_WARNING, 'Malformed rememberme cookie: ' . $packed);
416
        common_forgetme();
417
        return null;
418 419
    }

420
    $rm = Remember_me::getKV('code', $code);
421 422

    if (!$rm) {
423
        common_log(LOG_WARNING, 'No such remember code: ' . $code);
424
        common_forgetme();
425
        return null;
426 427 428
    }

    if ($rm->user_id != $id) {
429
        common_log(LOG_WARNING, 'Rememberme code for wrong user: ' . $rm->user_id . ' != ' . $id);
430
        common_forgetme();
431
        return null;
432 433
    }

434
    $user = User::getKV('id', $rm->user_id);
435

436
    if (!$user instanceof User) {
437
        common_log(LOG_WARNING, 'No such user for rememberme: ' . $rm->user_id);
438
        common_forgetme();
439
        return null;
440 441
    }

442
    // successful!
443 444 445 446
    $result = $rm->delete();

    if (!$result) {
        common_log_db_error($rm, 'DELETE', __FILE__);
447
        common_log(LOG_WARNING, 'Could not delete rememberme: ' . $code);
448
        common_forgetme();
449
        return null;
450 451 452 453
    }

    common_log(LOG_INFO, 'logging in ' . $user->nickname . ' using rememberme code ' . $rm->code);

454
    common_set_user($user);
455 456
    common_real_login(false);

457 458
    // We issue a new cookie, so they can log in
    // automatically again after this session
459 460 461

    common_rememberme($user);

462
    return $user;
463 464
}

465 466 467
/**
 * must be called with a valid user!
 */
468 469
function common_forgetme()
{
470
    common_set_cookie(REMEMBERME, '', 0);
471 472
}

473 474 475
/**
 * Who is the current user?
 */
476 477
function common_current_user()
{
478 479
    global $_cur;

480 481 482 483
    if (!_have_config()) {
        return null;
    }

484 485
    if ($_cur === false) {

486 487
        if (isset($_COOKIE[session_name()]) || isset($_GET[session_name()])
            || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
488 489 490
            common_ensure_session();
            $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
            if ($id) {
491 492
                $user = User::getKV('id', $id);
                if ($user instanceof User) {
493 494 495
                	$_cur = $user;
                	return $_cur;
                }
496 497 498
            }
        }

499
        // that didn't work; try to remember; will init $_cur to null on failure
500 501 502
        $_cur = common_remembered_user();

        if ($_cur) {
503
            // XXX: Is this necessary?
504 505 506 507
            $_SESSION['userid'] = $_cur->id;
        }
    }

508
    return $_cur;
509 510
}

511 512 513 514 515
/**
 * Logins that are 'remembered' aren't 'real' -- they're subject to
 * cookie-stealing. So, we don't let them do certain things. New reg,
 * OpenID, and password logins _are_ real.
 */
516 517
function common_real_login($real=true)
{
518 519
    common_ensure_session();
    $_SESSION['real_login'] = $real;
520 521
}

522 523
function common_is_real_login()
{
524
    return common_logged_in() && $_SESSION['real_login'];
525 526
}

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
/**
 * Get a hash portion for HTTP caching Etags and such including
 * info on the current user's session. If login/logout state changes,
 * or we've changed accounts, or we've renamed the current user,
 * we'll get a new hash value.
 *
 * This should not be considered secure information.
 *
 * @param User $user (optional; uses common_current_user() if left out)
 * @return string
 */
function common_user_cache_hash($user=false)
{
    if ($user === false) {
        $user = common_current_user();
    }
    if ($user) {
        return crc32($user->id . ':' . $user->nickname);
    } else {
        return '0';
    }
}

550 551 552 553 554
/**
 * get canonical version of nickname for comparison
 *
 * @param string $nickname
 * @return string
555 556 557
 *
 * @throws NicknameException on invalid input
 * @deprecated call Nickname::normalize() directly.
558
 */
559 560
function common_canonical_nickname($nickname)
{
561
    return Nickname::normalize($nickname);
562 563
}

564 565 566 567 568 569 570 571 572
/**
 * get canonical version of email for comparison
 *
 * @fixme actually normalize
 * @fixme reject invalid input
 *
 * @param string $email
 * @return string
 */
573 574
function common_canonical_email($email)
{
575 576 577
    // XXX: canonicalize UTF-8
    // XXX: lcase the domain part
    return $email;
578 579
}

580 581 582 583 584 585 586
/**
 * Partial notice markup rendering step: build links to !group references.
 *
 * @param string $text partially rendered HTML
 * @param Notice $notice in whose context we're working
 * @return string partially rendered HTML
 */
587
function common_render_content($text, Notice $notice)
588
{
589
    $r = common_render_text($text);
590
    $r = common_linkify_mentions($r, $notice);
591
    return $r;
592 593
}

594 595 596 597 598 599 600 601 602 603
/**
 * Finds @-mentions within the partially-rendered text section and
 * turns them into live links.
 *
 * Should generally not be called except from common_render_content().
 *
 * @param string $text partially-rendered HTML
 * @param Notice $notice in-progress or complete Notice object for context
 * @return string partially-rendered HTML
 */
604
function common_linkify_mentions($text, $notice)
605
{
606
    $mentions = common_find_mentions($text, $notice);
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647

    // We need to go through in reverse order by position,
    // so our positions stay valid despite our fudging with the
    // string!

    $points = array();

    foreach ($mentions as $mention)
    {
        $points[$mention['position']] = $mention;
    }

    krsort($points);

    foreach ($points as $position => $mention) {

        $linkText = common_linkify_mention($mention);

        $text = substr_replace($text, $linkText, $position, mb_strlen($mention['text']));
    }

    return $text;
}

function common_linkify_mention($mention)
{
    $output = null;

    if (Event::handle('StartLinkifyMention', array($mention, &$output))) {

        $xs = new XMLStringer(false);

        $attrs = array('href' => $mention['url'],
                       'class' => 'url');

        if (!empty($mention['title'])) {
            $attrs['title'] = $mention['title'];
        }

        $xs->elementStart('span', 'vcard');
        $xs->elementStart('a', $attrs);
648
        $xs->element('span', 'fn nickname mention', $mention['text']);
649 650 651 652 653 654 655 656 657 658 659
        $xs->elementEnd('a');
        $xs->elementEnd('span');

        $output = $xs->getString();

        Event::handle('EndLinkifyMention', array($mention, &$output));
    }

    return $output;
}

660
/**
661 662 663 664 665 666
 * Find @-mentions in the given text, using the given notice object as context.
 * References will be resolved with common_relative_profile() against the user
 * who posted the notice.
 *
 * Note the return data format is internal, to be used for building links and
 * such. Should not be used directly; rather, call common_linkify_mentions().
667 668 669
 *
 * @param string $text
 * @param Notice $notice notice in whose context we're building links
670
 *
671
 * @return array
672 673
 *
 * @access private
674
 */
675
function common_find_mentions($text, $notice)
676
{
677 678 679 680
    try {
        $sender = Profile::getKV('id', $notice->profile_id);
    } catch (NoProfileException $e) {
        return array();
681 682
    }

683 684
    $mentions = array();

685
    if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) {
686
        // Get the context of the original notice, if any
687 688 689
        $origAuthor   = null;
        $origNotice   = null;
        $origMentions = array();
690 691 692

        // Is it a reply?

693 694 695 696
        if ($notice instanceof Notice) {
            try {
                $origNotice = $notice->getParent();
                $origAuthor = $origNotice->getProfile();
697

698
                $ids = $origNotice->getReplies();
699 700

                foreach ($ids as $id) {
701
                    $repliedTo = Profile::getKV('id', $id);
702 703
                    if ($repliedTo instanceof Profile) {
                        $origMentions[$repliedTo->nickname] = $repliedTo;
704 705
                    }
                }
706 707 708 709
            } catch (NoProfileException $e) {
                common_log(LOG_WARNING, sprintf('Notice %d author profile id %d does not exist', $origNotice->id, $origNotice->profile_id));
            } catch (ServerException $e) {
                common_log(LOG_WARNING, __METHOD__ . ' got exception: ' . $e->getMessage());
710 711 712
            }
        }

713
        $matches = common_find_mentions_raw($text);
714 715

        foreach ($matches as $match) {
716 717 718 719 720 721
            try {
                $nickname = Nickname::normalize($match[0]);
            } catch (NicknameException $e) {
                // Bogus match? Drop it.
                continue;
            }
722 723 724 725 726

            // Try to get a profile for this nickname.
            // Start with conversation context, then go to
            // sender context.

727 728 729 730 731
            if ($origAuthor instanceof Profile && $origAuthor->nickname == $nickname) {
                $mentioned = $origAuthor;
            } else if (!empty($origMentions) &&
                       array_key_exists($nickname, $origMentions)) {
                $mentioned = $origMentions[$nickname];
732 733 734
            } else {
                $mentioned = common_relative_profile($sender, $nickname);
            }
735

736
            if ($mentioned instanceof Profile) {
737
                $user = User::getKV('id', $mentioned->id);
738

739
                if ($user instanceof User) {
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
                    $url = common_local_url('userbyid', array('id' => $user->id));
                } else {
                    $url = $mentioned->profileurl;
                }

                $mention = array('mentioned' => array($mentioned),
                                 'text' => $match[0],
                                 'position' => $match[1],
                                 'url' => $url);

                if (!empty($mentioned->fullname)) {
                    $mention['title'] = $mentioned->fullname;
                }

                $mentions[] = $mention;
            }
        }

        // @#tag => mention of all subscriptions tagged 'tag'

        preg_match_all('/(?:^|[\s\.\,\:\;]+)@#([\pL\pN_\-\.]{1,64})/',
761
                       $text, $hmatches, PREG_OFFSET_CAPTURE);
762 763
        foreach ($hmatches[1] as $hmatch) {
            $tag = common_canonical_tag($hmatch[0]);
764
            $plist = Profile_list::getByTaggerAndTag($sender->id, $tag);
765 766 767 768
            if (!$plist instanceof Profile_list || $plist->private) {
                continue;
            }
            $tagged = $sender->getTaggedSubscribers($tag);
769

770 771 772 773 774 775 776 777 778
            $url = common_local_url('showprofiletag',
                                    array('tagger' => $sender->nickname,
                                          'tag' => $tag));

            $mentions[] = array('mentioned' => $tagged,
                                'text' => $hmatch[0],
                                'position' => $hmatch[1],
                                'url' => $url);
        }
779

780 781 782 783 784 785 786 787
        preg_match_all('/(?:^|[\s\.\,\:\;]+)!(' . Nickname::DISPLAY_FMT . ')/',
                       $text, $hmatches, PREG_OFFSET_CAPTURE);
        foreach ($hmatches[1] as $hmatch) {
            $nickname = Nickname::normalize($hmatch[0]);
            $group = User_group::getForNickname($nickname, $sender);

            if (!$group instanceof User_group || !$sender->isMember($group)) {
                continue;
788
            }
789 790 791 792 793 794 795 796

            $profile = $group->getProfile();

            $mentions[] = array('mentioned' => $profile,
                                'text'      => $hmatch[0],
                                'position'  => $hmatch[1],
                                'url'       => $group->permalink,
                                'title'     => $group->getFancyName());
797 798 799 800 801 802 803 804
        }

        Event::handle('EndFindMentions', array($sender, $text, &$mentions));
    }

    return $mentions;
}

805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
/**
 * Does the actual regex pulls to find @-mentions in text.
 * Should generally not be called directly; for use in common_find_mentions.
 *
 * @param string $text
 * @return array of PCRE match arrays
 */
function common_find_mentions_raw($text)
{
    $tmatches = array();
    preg_match_all('/^T (' . Nickname::DISPLAY_FMT . ') /',
                   $text,
                   $tmatches,
                   PREG_OFFSET_CAPTURE);

    $atmatches = array();
    preg_match_all('/(?:^|\s+)@(' . Nickname::DISPLAY_FMT . ')\b/',
                   $text,
                   $atmatches,
                   PREG_OFFSET_CAPTURE);

    $matches = array_merge($tmatches[1], $atmatches[1]);
    return $matches;
}

830 831
function common_render_text($text)
{
832
    $r = htmlspecialchars($text);
833

834
    $r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r);
835
    $r = common_replace_urls_callback($r, 'common_linkify');
mattl's avatar
mattl committed
836
    $r = preg_replace_callback('/(^|\&quot\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/u',
837
                function ($m) { return "{$m[1]}#".common_tag_link($m[2]); }, $r);
838 839
    // XXX: machine tags
    return $r;
Evan Prodromou's avatar
Evan Prodromou committed
840 841
}

842 843 844 845 846 847 848 849
/**
 * Find links in the given text and pass them to the given callback function.
 *
 * @param string $text
 * @param function($text, $arg) $callback: return replacement text
 * @param mixed $arg: optional argument will be passed on to the callback
 */
function common_replace_urls_callback($text, $callback, $arg = null) {
850
    // Start off with a regex
851
    $regex = '#'.
852
    '(?:^|[\s\<\>\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'.
853
    '('.
854
        '(?:'.
855 856
            '(?:'. //Known protocols
                '(?:'.
857
                    '(?:(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://)'.
858
                    '|'.
859 860
                    '(?:(?:mailto|aim|tel|xmpp):)'.
                ')'.
861
                '(?:[\pN\pL\-\_\+\%\~]+(?::[\pN\pL\-\_\+\%\~]+)?\@)?'. //user:pass@
862 863 864 865 866 867
                '(?:'.
                    '(?:'.
                        '\[[\pN\pL\-\_\:\.]+(?<![\.\:])\]'. //[dns]
                    ')|(?:'.
                        '[\pN\pL\-\_\:\.]+(?<![\.\:])'. //dns
                    ')'.
868
                ')'.
869 870
            ')'.
            '|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'. //IPv4
871
            '|(?:'. //IPv6
872
                '\[?(?:(?:(?:[0-9A-Fa-f]{1,4}:){7}(?:(?:[0-9A-Fa-f]{1,4})|:))|(?:(?:[0-9A-Fa-f]{1,4}:){6}(?::|(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})|(?::[0-9A-Fa-f]{1,4})))|(?:(?:[0-9A-Fa-f]{1,4}:){5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){4}(?::[0-9A-Fa-f]{1,4}){0,1}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){3}(?::[0-9A-Fa-f]{1,4}){0,2}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:){2}(?::[0-9A-Fa-f]{1,4}){0,3}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:[0-9A-Fa-f]{1,4}:)(?::[0-9A-Fa-f]{1,4}){0,4}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?::(?::[0-9A-Fa-f]{1,4}){0,5}(?:(?::(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})?)|(?:(?::[0-9A-Fa-f]{1,4}){1,2})))|(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d{1,2})){3})))\]?(?<!:)'.
873
            ')|(?:'. //DNS
874
                '(?:[\pN\pL\-\_\+\%\~]+(?:\:[\pN\pL\-\_\+\%\~]+)?\@)?'. //user:pass@
875 876
                '[\pN\pL\-\_]+(?:\.[\pN\pL\-\_]+)*\.'.
                //tld list from http://data.iana.org/TLD/tlds-alpha-by-domain.txt, also added local, loc, and onion
877
                '(?:AC|AD|AE|AERO|AF|AG|AI|AL|AM|AN|AO|AQ|AR|ARPA|AS|ASIA|AT|AU|AW|AX|AZ|BA|BB|BD|BE|BF|BG|BH|BI|BIZ|BJ|BM|BN|BO|BR|BS|BT|BV|BW|BY|BZ|CA|CAT|CC|CD|CF|CG|CH|CI|CK|CL|CM|CN|CO|COM|COOP|CR|CU|CV|CX|CY|CZ|DE|DJ|DK|DM|DO|DZ|EC|EDU|EE|EG|ER|ES|ET|EU|FI|FJ|FK|FM|FO|FR|GA|GB|GD|GE|GF|GG|GH|GI|GL|GM|GN|GOV|GP|GQ|GR|GS|GT|GU|GW|GY|HK|HM|HN|HR|HT|HU|ID|IE|IL|IM|IN|INFO|INT|IO|IQ|IR|IS|IT|JE|JM|JO|JOBS|JP|KE|KG|KH|KI|KM|KN|KP|KR|KW|KY|KZ|LA|LB|LC|LI|LK|LR|LS|LT|LU|LV|LY|MA|MC|MD|ME|MG|MH|MIL|MK|ML|MM|MN|MO|MOBI|MP|MQ|MR|MS|MT|MU|MUSEUM|MV|MW|MX|MY|MZ|NA|NAME|NC|NE|NET|NF|NG|NI|NL|NO|NP|NR|NU|NZ|OM|ORG|PA|PE|PF|PG|PH|PK|PL|PM|PN|PR|PRO|PS|PT|PW|PY|QA|RE|RO|RS|RU|RW|SA|SB|SC|SD|SE|SG|SH|SI|SJ|SK|SL|SM|SN|SO|SR|ST|SU|SV|SY|SZ|TC|TD|TEL|TF|TG|TH|TJ|TK|TL|TM|TN|TO|TP|TR|TRAVEL|TT|TV|TW|TZ|UA|UG|UK|US|UY|UZ|VA|VC|VE|VG|VI|VN|VU|WF|WS|XN--0ZWM56D|测试|XN--11B5BS3A9AJ6G|परीक्षा|XN--80AKHBYKNJ4F|испытание|XN--9T4B11YI5A|테스트|XN--DEBA0AD|טעסט|XN--G6W251D|測試|XN--HGBK6AJ7F53BBA|آزمایشی|XN--HLCJ6AYA9ESC7A|பரிட்சை|XN--JXALPDLP|δοκιμή|XN--KGBECHTV|إختبار|XN--ZCKZAH|テスト|YE|YT|YU|ZA|ZM|ZW|local|loc|onion)'.
878
            ')(?![\pN\pL\-\_])'.
879
        ')'.
880
        '(?:'.
881
            '(?:\:\d+)?'. //:port
882 883 884
            '(?:/[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@]*)?'. // /path
            '(?:\?[\pN\pL\$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'@\/]*)?'. // ?query string
            '(?:\#[\pN\pL$\,\!\(\)\.\:\-\_\+\/\=\&\;\%\~\*\$\+\'\@/\?\#]*)?'. // #fragment
885
        ')(?<![\?\.\,\#\,])'.
886
    ')'.
887
    '#ixu';
888
    //preg_match_all($regex,$text,$matches);
889
    //print_r($matches);
890
    return preg_replace_callback($regex, curry('callback_helper',$callback,$arg) ,$text);
891
}
892

893 894 895 896 897 898 899 900
/**
 * Intermediate callback for common_replace_links(), helps resolve some
 * ambiguous link forms before passing on to the final callback.
 *
 * @param array $matches
 * @param callable $callback
 * @param mixed $arg optional argument to pass on as second param to callback
 * @return string
901
 *
902 903 904
 * @access private
 */
function callback_helper($matches, $callback, $arg=null) {
905
    $url=$matches[1];
906 907
    $left = strpos($matches[0],$url);
    $right = $left+strlen($url);
908

909 910 911 912 913 914 915 916 917 918 919 920
    $groupSymbolSets=array(
        array(
            'left'=>'(',
            'right'=>')'
        ),
        array(
            'left'=>'[',
            'right'=>']'
        ),
        array(
            'left'=>'{',
            'right'=>'}'
921 922 923 924
        ),
        array(
            'left'=>'<',
            'right'=>'>'
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
        )
    );
    $cannotEndWith=array('.','?',',','#');
    $original_url=$url;
    do{
        $original_url=$url;
        foreach($groupSymbolSets as $groupSymbolSet){
            if(substr($url,-1)==$groupSymbolSet['right']){
                $group_left_count = substr_count($url,$groupSymbolSet['left']);
                $group_right_count = substr_count($url,$groupSymbolSet['right']);
                if($group_left_count<$group_right_count){
                    $right-=1;
                    $url=substr($url,0,-1);
                }
            }
        }
        if(in_array(substr($url,-1),$cannotEndWith)){
            $right-=1;
            $url=substr($url,0,-1);
        }
    }while($original_url!=$url);
946

947
    $result = call_user_func_array($callback, array($url, $arg));
948
    return substr($matches[0],0,$left) . $result . substr($matches[0],$right);
949
}
950

951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
if (version_compare(PHP_VERSION, '5.3.0', 'ge')) {
    // lambda implementation in a separate file; PHP 5.2 won't parse it.
    require_once INSTALLDIR . "/lib/curry.php";
} else {
    function curry($fn) {
        $args = func_get_args();
        array_shift($args);
        $id = uniqid('_partial');
        $GLOBALS[$id] = array($fn, $args);
        return create_function('',
                               '$args = func_get_args(); '.
                               'return call_user_func_array('.
                               '$GLOBALS["'.$id.'"][0],'.
                               'array_merge('.
                               '$args,'.
                               '$GLOBALS["'.$id.'"][1]));');
    }
968 969 970
}

function common_linkify($url) {
Evan Prodromou's avatar
Evan Prodromou committed
971 972 973
    // It comes in special'd, so we unspecial it before passing to the stringifying
    // functions
    $url = htmlspecialchars_decode($url);
974

975 976 977 978 979
    if (strpos($url, '@') !== false && strpos($url, ':') === false && Validate::email($url)) {
        //url is an email address without the mailto: protocol
        $canon = "mailto:$url";
        $longurl = "mailto:$url";
    } else {
980

981
        $canon = File_redirection::_canonUrl($url);
982

983
        $longurl_data = File_redirection::where($canon, common_config('attachments', 'process_links'));
984 985 986 987 988
        if (is_array($longurl_data)) {
            $longurl = $longurl_data['url'];
        } elseif (is_string($longurl_data)) {
            $longurl = $longurl_data;
        } else {
989 990 991 992
            // Unable to reach the server to verify contents, etc
            // Just pass the link on through for now.
            common_log(LOG_ERR, "Can't linkify url '$url'");
            $longurl = $url;
993
        }
994
    }
995 996

    $attrs = array('href' => $canon, 'title' => $longurl);
997

998 999 1000 1001
    $is_attachment = false;
    $attachment_id = null;
    $has_thumb = false;

1002
    // Check to see whether this is a known "attachment" URL.
1003

1004
    $f = File::getKV('url', $longurl);
1005

1006
    if (empty($f)) {
1007 1008 1009 1010
        if (common_config('attachments', 'process_links')) {
            // XXX: this writes to the database. :<
            $f = File::processNew($longurl);
        }
1011 1012
    }

1013
    if (!empty($f)) {
1014
        if ($f->getEnclosure()) {
1015
            $is_attachment = true;
1016
            $attachment_id = $f->id;
1017

1018
            $thumb = File_thumbnail::getKV('file_id', $f->id);
1019 1020
            if (!empty($thumb)) {
                $has_thumb = true;
1021
            }
1022 1023 1024 1025 1026 1027 1028
        }
    }

    // Add clippy
    if ($is_attachment) {
        $attrs['class'] = 'attachment';
        if ($has_thumb) {
1029 1030
            $attrs['class'] = 'attachment thumbnail';
        }
1031
        $attrs['id'] = "attachment-{$attachment_id}";
1032
    }
1033

1034 1035 1036 1037 1038 1039 1040 1041 1042