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

util.php 49.8 KB
Newer Older
1
<?php
Evan Prodromou's avatar
Evan Prodromou committed
2
/*
3 4
 * Laconica - a distributed open-source microblogging tool
 * Copyright (C) 2008, Controlez-Vous, 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
// Show a server error
23

24 25
function common_server_error($msg, $code=500)
{
26 27
    $err = new ServerErrorAction($msg, $code);
    $err->showPage();
28 29
}

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

37 38
function common_init_locale($language=null)
{
39 40 41 42 43 44
    if(!$language) {
        $language = common_language();
    }
    putenv('LANGUAGE='.$language);
    putenv('LANG='.$language);
    return setlocale(LC_ALL, $language . ".utf8",
45 46 47 48
                     $language . ".UTF8",
                     $language . ".utf-8",
                     $language . ".UTF-8",
                     $language);
49 50
}

51 52
function common_init_language()
{
53 54 55 56 57 58 59 60 61 62 63
    mb_internal_encoding('UTF-8');
    $language = common_language();
    // So we don't have to make people install the gettext locales
    $locale_set = common_init_locale($language);
    bindtextdomain("laconica", common_config('site','locale_path'));
    bind_textdomain_codeset("laconica", "UTF-8");
    textdomain("laconica");
    setlocale(LC_CTYPE, 'C');
    if(!$locale_set) {
        common_log(LOG_INFO,'Language requested:'.$language.' - locale could not be set:',__FILE__);
    }
64 65
}

66 67
function common_timezone()
{
68 69 70 71 72 73
    if (common_logged_in()) {
        $user = common_current_user();
        if ($user->timezone) {
            return $user->timezone;
        }
    }
74

75 76
    global $config;
    return $config['site']['timezone'];
77 78
}

79 80
function common_language()
{
81

82 83 84 85 86 87 88 89
    // If there is a user logged in and they've set a language preference
    // then return that one...
    if (common_logged_in()) {
        $user = common_current_user();
        $user_language = $user->language;
        if ($user_language)
          return $user_language;
    }
90

91 92 93 94 95 96 97 98
    // Otherwise, find the best match for the languages requested by the
    // user's browser...
    $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null;
    if (!empty($httplang)) {
        $language = client_prefered_language($httplang);
        if ($language)
          return $language;
    }
99

100 101
    // Finally, if none of the above worked, use the site's default...
    return common_config('site', 'language');
102
}
103
// salted, hashed passwords are stored in the DB
104

105 106
function common_munge_password($password, $id)
{
107
    return md5($password . $id);
108 109
}

110
// check if a username exists and has matching password
111 112
function common_check_user($nickname, $password)
{
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
    // NEVER allow blank passwords, even if they match the DB
    if (mb_strlen($password) == 0) {
        return false;
    }
    $user = User::staticGet('nickname', $nickname);
    if (is_null($user)) {
        return false;
    } else {
        if (0 == strcmp(common_munge_password($password, $user->id),
                        $user->password)) {
            return $user;
        } else {
            return false;
        }
    }
}

// is the current user logged in?
131 132
function common_logged_in()
{
133
    return (!is_null(common_current_user()));
134 135
}

136 137
function common_have_session()
{
138
    return (0 != strcmp(session_id(), ''));
139 140
}

141 142
function common_ensure_session()
{
143 144 145
    if (!common_have_session()) {
        @session_start();
    }
146 147
}

148 149 150 151
// Three kinds of arguments:
// 1) a user object
// 2) a nickname
// 3) null to clear
152

153
// Initialize to false; set to null if none found
154 155 156

$_cur = false;

157 158
function common_set_user($user)
{
159 160 161

    global $_cur;

162 163 164 165 166 167 168 169 170 171 172 173 174 175
    if (is_null($user) && common_have_session()) {
        $_cur = null;
        unset($_SESSION['userid']);
        return true;
    } else if (is_string($user)) {
        $nickname = $user;
        $user = User::staticGet('nickname', $nickname);
    } else if (!($user instanceof User)) {
        return false;
    }

    if ($user) {
        common_ensure_session();
        $_SESSION['userid'] = $user->id;
176
        $_cur = $user;
177 178 179
        return $_cur;
    }
    return false;
180 181
}

182 183
function common_set_cookie($key, $value, $expiration=0)
{
184 185
    $path = common_config('site', 'path');
    $server = common_config('site', 'server');
186

187 188 189 190 191 192 193 194 195 196
    if ($path && ($path != '/')) {
        $cookiepath = '/' . $path . '/';
    } else {
        $cookiepath = '/';
    }
    return setcookie($key,
                     $value,
                     $expiration,
                     $cookiepath,
                     $server);
197 198 199
}

define('REMEMBERME', 'rememberme');
200
define('REMEMBERME_EXPIRY', 30 * 24 * 60 * 60); // 30 days
201

202 203
function common_rememberme($user=null)
{
204 205 206 207 208 209 210
    if (!$user) {
        $user = common_current_user();
        if (!$user) {
            common_debug('No current user to remember', __FILE__);
            return false;
        }
    }
211

212
    $rm = new Remember_me();
213

214 215
    $rm->code = common_good_rand(16);
    $rm->user_id = $user->id;
216

217
    // Wrap the insert in some good ol' fashioned transaction code
218 219 220

    $rm->query('BEGIN');

221
    $result = $rm->insert();
222

223 224 225 226
    if (!$result) {
        common_log_db_error($rm, 'INSERT', __FILE__);
        common_debug('Error adding rememberme record for ' . $user->nickname, __FILE__);
        return false;
227 228
    }

229 230
    $rm->query('COMMIT');

231
    common_debug('Inserted rememberme record (' . $rm->code . ', ' . $rm->user_id . '); result = ' . $result . '.', __FILE__);
232 233 234

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

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

237
    common_set_cookie(REMEMBERME, $cookieval, time() + REMEMBERME_EXPIRY);
238

239
    return true;
240 241
}

242 243
function common_remembered_user()
{
244

245
    $user = null;
246

247
    $packed = isset($_COOKIE[REMEMBERME]) ? $_COOKIE[REMEMBERME] : null;
248

249 250
    if (!$packed) {
        return null;
251 252 253 254 255
    }

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

    if (!$id || !$code) {
256
        common_log(LOG_WARNING, 'Malformed rememberme cookie: ' . $packed);
257
        common_forgetme();
258
        return null;
259 260 261 262 263
    }

    $rm = Remember_me::staticGet($code);

    if (!$rm) {
264
        common_log(LOG_WARNING, 'No such remember code: ' . $code);
265
        common_forgetme();
266
        return null;
267 268 269
    }

    if ($rm->user_id != $id) {
270
        common_log(LOG_WARNING, 'Rememberme code for wrong user: ' . $rm->user_id . ' != ' . $id);
271
        common_forgetme();
272
        return null;
273 274 275 276 277
    }

    $user = User::staticGet($rm->user_id);

    if (!$user) {
278
        common_log(LOG_WARNING, 'No such user for rememberme: ' . $rm->user_id);
279
        common_forgetme();
280
        return null;
281 282
    }

283
    // successful!
284 285 286 287
    $result = $rm->delete();

    if (!$result) {
        common_log_db_error($rm, 'DELETE', __FILE__);
288
        common_log(LOG_WARNING, 'Could not delete rememberme: ' . $code);
289
        common_forgetme();
290
        return null;
291 292 293 294
    }

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

295
    common_set_user($user);
296 297
    common_real_login(false);

298 299
    // We issue a new cookie, so they can log in
    // automatically again after this session
300 301 302

    common_rememberme($user);

303
    return $user;
304 305
}

306
// must be called with a valid user!
307

308 309
function common_forgetme()
{
310
    common_set_cookie(REMEMBERME, '', 0);
311 312
}

313
// who is the current user?
314 315
function common_current_user()
{
316 317 318 319 320 321 322 323 324 325 326 327 328
    global $_cur;

    if ($_cur === false) {

        if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
            common_ensure_session();
            $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
            if ($id) {
                $_cur = User::staticGet($id);
                return $_cur;
            }
        }

329
        // that didn't work; try to remember; will init $_cur to null on failure
330 331 332 333 334
        $_cur = common_remembered_user();

        if ($_cur) {
            common_debug("Got User " . $_cur->nickname);
            common_debug("Faking session on remembered user");
335
            // XXX: Is this necessary?
336 337 338 339
            $_SESSION['userid'] = $_cur->id;
        }
    }

340
    return $_cur;
341 342
}

343 344 345
// 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.
346

347 348
function common_real_login($real=true)
{
349 350
    common_ensure_session();
    $_SESSION['real_login'] = $real;
351 352
}

353 354
function common_is_real_login()
{
355
    return common_logged_in() && $_SESSION['real_login'];
356 357
}

358
// get canonical version of nickname for comparison
359 360
function common_canonical_nickname($nickname)
{
361 362
    // XXX: UTF-8 canonicalization (like combining chars)
    return strtolower($nickname);
363 364
}

365
// get canonical version of email for comparison
366 367
function common_canonical_email($email)
{
368 369 370
    // XXX: canonicalize UTF-8
    // XXX: lcase the domain part
    return $email;
371 372
}

373 374
function common_render_content($text, $notice)
{
375 376 377 378 379
    $r = common_render_text($text);
    $id = $notice->profile_id;
    $r = preg_replace('/(^|\s+)@([A-Za-z0-9]{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r);
    $r = preg_replace('/^T ([A-Z0-9]{1,64}) /e', "'T '.common_at_link($id, '\\1').' '", $r);
    $r = preg_replace('/(^|\s+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r);
380
    $r = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
381
    return $r;
382 383
}

384 385
function common_render_text($text)
{
386
    $r = htmlspecialchars($text);
387

388
    $r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r);
389
    $r = common_replace_urls_callback($r, 'common_linkify');
390
    $r = preg_replace('/(^|\(|\[|\s+)#([A-Za-z0-9_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r);
391 392
    // XXX: machine tags
    return $r;
Evan Prodromou's avatar
Evan Prodromou committed
393 394
}

395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
function common_replace_urls_callback($text, $callback) {
    // Start off with a regex
        preg_match_all('#(?:(?:(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://|(?:mailto|aim|tel):)[^.\s]+\.[^\s]+|(?:[^.\s/]+\.)+(?:museum|travel|[a-z]{2,4})(?:[:/][^\s]*)?)#i', $text, $matches);
    
    // Then clean up what the regex left behind
    $offset = 0;
    foreach($matches[0] as $url) {
        $url = htmlspecialchars_decode($url);
        
        // Make sure we didn't pick up an email address
        if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
        
        // Remove trailing punctuation
        $url = rtrim($url, '.?!,;:\'"`');

        // Remove surrounding parens and the like
        preg_match('/[)\]>]+$/', $url, $trailing);
        if (isset($trailing[0])) {
            preg_match_all('/[(\[<]/', $url, $opened);
            preg_match_all('/[)\]>]/', $url, $closed);
            $unopened = count($closed[0]) - count($opened[0]);

            // Make sure not to take off more closing parens than there are at the end
            $unopened = ($unopened > mb_strlen($trailing[0])) ? mb_strlen($trailing[0]):$unopened;

            $url = ($unopened > 0) ? mb_substr($url, 0, $unopened * -1):$url;
        }
422

423 424 425 426 427 428 429 430 431 432 433 434 435
        // Remove trailing punctuation again (in case there were some inside parens)
        $url = rtrim($url, '.?!,;:\'"`');
        
        // Make sure we didn't capture part of the next sentence
        preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
        
        // Were the parts capitalized any?
        $last_part = (mb_strtolower($url_parts[2]) !== $url_parts[2]) ? true:false;
        $prev_part = (mb_strtolower($url_parts[1]) !== $url_parts[1]) ? true:false;
        
        // If the first part wasn't cap'd but the last part was, we captured too much
        if ((!$prev_part && $last_part)) {
            $url = substr_replace($url, '', mb_strpos($url, '.'.$url_parts[2], 0));
436
        }
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
        
        // Capture the new TLD
        preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
        
        $tlds = array('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', 'ye', 'yt', 'yu', 'za', 'zm', 'zw');

        if (!in_array($url_parts[2], $tlds)) continue;
        
        // Call user specified func
        $modified_url = $callback($url);
        
        // Replace it!
        $start = mb_strpos($text, $url, $offset);
        $text = substr_replace($text, $modified_url, $start, mb_strlen($url));
        $offset = $start + mb_strlen($modified_url);
452
    }
453 454 455 456 457 458 459 460 461
    
    return $text;
}

function common_linkify($url) {
    $display = $url;
    $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url:$url;
    
    if ($longurl = common_longurl($url)) {
462
        $longurl = htmlentities($longurl, ENT_QUOTES, 'UTF-8');
463
        $title = "title=\"$longurl\"";
464 465
    }
    else $title = '';
466 467
    
    return "<a href=\"$url\" $title class=\"extlink\">$display</a>";
468 469
}

470 471
function common_longurl($short_url)
{
472 473 474 475 476
    $long_url = common_shorten_link($short_url, true);
    if ($long_url === $short_url) return false;
    return $long_url;
}

477 478
function common_longurl2($uri)
{
479 480 481 482
    $uri_e = urlencode($uri);
    $longurl = unserialize(file_get_contents("http://api.longurl.org/v1/expand?format=php&url=$uri_e"));
    if (empty($longurl['long_url']) || $uri === $longurl['long_url']) return false;
    return stripslashes($longurl['long_url']);
483 484
}

485 486
function common_shorten_links($text)
{
487
    if (mb_strlen($text) <= 140) return $text;
488 489
    static $cache = array();
    if (isset($cache[$text])) return $cache[$text];
490
    // \s = not a horizontal whitespace character (since PHP 5.2.4)
491
    return $cache[$text] = preg_replace('@https?://[^)\]>\s]+@e', "common_shorten_link('\\0')", $text);
492 493
}

494 495
function common_shorten_link($url, $reverse = false)
{
496
    static $url_cache = array();
497 498
    if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url;

499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 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
    $user = common_current_user();

    $curlh = curl_init();
    curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
    curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
    curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);

    switch($user->urlshorteningservice) {
     case 'ur1.ca':
        $short_url_service = new LilUrl;
        $short_url = $short_url_service->shorten($url);
        break;

     case '2tu.us':
        $short_url_service = new TightUrl;
        $short_url = $short_url_service->shorten($url);
        break;

     case 'ptiturl.com':
        $short_url_service = new PtitUrl;
        $short_url = $short_url_service->shorten($url);
        break;

     case 'bit.ly':
        curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($url));
        $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
        break;

     case 'is.gd':
        curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($url));
        $short_url = curl_exec($curlh);
        break;
     case 'snipr.com':
        curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($url));
        $short_url = curl_exec($curlh);
        break;
     case 'metamark.net':
        curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($url));
        $short_url = curl_exec($curlh);
        break;
     case 'tinyurl.com':
        curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($url));
        $short_url = curl_exec($curlh);
        break;
     default:
        $short_url = false;
    }

    curl_close($curlh);

    if ($short_url) {
550
        $url_cache[(string)$short_url] = $url;
551 552 553
        return (string)$short_url;
    }
    return $url;
554 555
}

556 557
function common_xml_safe_str($str)
{
558
    $xmlStr = htmlentities(iconv('UTF-8', 'UTF-8//IGNORE', $str), ENT_NOQUOTES, 'UTF-8');
Evan Prodromou's avatar
Evan Prodromou committed
559

560 561
    // Replace control, formatting, and surrogate characters with '*', ala Twitter
    return preg_replace('/[\p{Cc}\p{Cf}\p{Cs}]/u', '*', $str);
562 563
}

564 565
function common_tag_link($tag)
{
566 567
    $canonical = common_canonical_tag($tag);
    $url = common_local_url('tag', array('tag' => $canonical));
568
    return '<span class="tag"><a href="' . htmlspecialchars($url) . '" rel="tag">' . htmlspecialchars($tag) . '</a></span>';
569 570
}

571 572
function common_canonical_tag($tag)
{
573
    return strtolower(str_replace(array('-', '_', '.'), '', $tag));
Mike Cochrane's avatar
Mike Cochrane committed
574 575
}

576 577
function common_valid_profile_tag($str)
{
578
    return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str);
579 580
}

581 582
function common_at_link($sender_id, $nickname)
{
583 584 585
    $sender = Profile::staticGet($sender_id);
    $recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
    if ($recipient) {
586
        return '<span class="vcard"><a href="'.htmlspecialchars($recipient->profileurl).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
587 588 589
    } else {
        return $nickname;
    }
590 591
}

592 593 594
function common_group_link($sender_id, $nickname)
{
    $sender = Profile::staticGet($sender_id);
595
    $group = User_group::staticGet('nickname', common_canonical_nickname($nickname));
596 597 598 599 600 601 602
    if ($group && $sender->isMember($group)) {
        return '<span class="vcard"><a href="'.htmlspecialchars($group->permalink()).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
    } else {
        return $nickname;
    }
}

603 604
function common_at_hash_link($sender_id, $tag)
{
605 606 607 608 609 610 611 612 613
    $user = User::staticGet($sender_id);
    if (!$user) {
        return $tag;
    }
    $tagged = Profile_tag::getTagged($user->id, common_canonical_tag($tag));
    if ($tagged) {
        $url = common_local_url('subscriptions',
                                array('nickname' => $user->nickname,
                                      'tag' => $tag));
614
        return '<span class="tag"><a href="'.htmlspecialchars($url).'" rel="tag">'.$tag.'</a></span>';
615 616 617 618 619
    } else {
        return $tag;
    }
}

620 621
function common_relative_profile($sender, $nickname, $dt=null)
{
622 623 624 625 626
    // Try to find profiles this profile is subscribed to that have this nickname
    $recipient = new Profile();
    // XXX: use a join instead of a subquery
    $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender->id.' and subscribed = id)', 'AND');
    $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND');
Evan Prodromou's avatar
TRUE  
Evan Prodromou committed
627
    if ($recipient->find(true)) {
628 629 630 631 632 633 634 635 636
        // XXX: should probably differentiate between profiles with
        // the same name by date of most recent update
        return $recipient;
    }
    // Try to find profiles that listen to this profile and that have this nickname
    $recipient = new Profile();
    // XXX: use a join instead of a subquery
    $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender->id.' and subscriber = id)', 'AND');
    $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND');
Evan Prodromou's avatar
TRUE  
Evan Prodromou committed
637
    if ($recipient->find(true)) {
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
        // XXX: should probably differentiate between profiles with
        // the same name by date of most recent update
        return $recipient;
    }
    // If this is a local user, try to find a local user with that nickname.
    $sender = User::staticGet($sender->id);
    if ($sender) {
        $recipient_user = User::staticGet('nickname', $nickname);
        if ($recipient_user) {
            return $recipient_user->getProfile();
        }
    }
    // Otherwise, no links. @messages from local users to remote users,
    // or from remote users to other remote users, are just
    // outside our ability to make intelligent guesses about
    return null;
654 655 656 657
}

// where should the avatar go for this user?

658 659
function common_avatar_filename($id, $extension, $size=null, $extra=null)
{
660
    global $config;
661

662 663 664 665 666
    if ($size) {
        return $id . '-' . $size . (($extra) ? ('-' . $extra) : '') . $extension;
    } else {
        return $id . '-original' . (($extra) ? ('-' . $extra) : '') . $extension;
    }
667 668
}

669 670
function common_avatar_path($filename)
{
671 672
    global $config;
    return INSTALLDIR . '/avatar/' . $filename;
673 674
}

675 676
function common_avatar_url($filename)
{
677
    return common_path('avatar/'.$filename);
678 679
}

680 681
function common_avatar_display_url($avatar)
{
682 683 684 685 686 687
    $server = common_config('avatar', 'server');
    if ($server) {
        return 'http://'.$server.'/'.$avatar->filename;
    } else {
        return $avatar->url;
    }
688 689
}

690 691
function common_default_avatar($size)
{
692 693 694 695 696 697
    static $sizenames = array(AVATAR_PROFILE_SIZE => 'profile',
                              AVATAR_STREAM_SIZE => 'stream',
                              AVATAR_MINI_SIZE => 'mini');
    return theme_path('default-avatar-'.$sizenames[$size].'.png');
}

698 699
function common_local_url($action, $args=null, $fragment=null)
{
700 701 702 703 704 705 706 707 708 709 710 711
    $url = null;
    if (common_config('site','fancy')) {
        $url = common_fancy_url($action, $args);
    } else {
        $url = common_simple_url($action, $args);
    }
    if (!is_null($fragment)) {
        $url .= '#'.$fragment;
    }
    return $url;
}

712 713
function common_fancy_url($action, $args=null)
{
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
    switch (strtolower($action)) {
     case 'public':
        if ($args && isset($args['page'])) {
            return common_path('?page=' . $args['page']);
        } else {
            return common_path('');
        }
     case 'featured':
        if ($args && isset($args['page'])) {
            return common_path('featured?page=' . $args['page']);
        } else {
            return common_path('featured');
        }
     case 'favorited':
        if ($args && isset($args['page'])) {
            return common_path('favorited?page=' . $args['page']);
        } else {
            return common_path('favorited');
        }
     case 'publicrss':
        return common_path('rss');
     case 'publicatom':
        return common_path("api/statuses/public_timeline.atom");
     case 'publicxrds':
        return common_path('xrds');
     case 'featuredrss':
        return common_path('featuredrss');
     case 'favoritedrss':
        return common_path('favoritedrss');
     case 'opensearch':
        if ($args && $args['type']) {
            return common_path('opensearch/'.$args['type']);
        } else {
            return common_path('opensearch/people');
        }
     case 'doc':
        return common_path('doc/'.$args['title']);
751
     case 'block':
752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777
     case 'login':
     case 'logout':
     case 'subscribe':
     case 'unsubscribe':
     case 'invite':
        return common_path('main/'.$action);
     case 'tagother':
        return common_path('main/tagother?id='.$args['id']);
     case 'register':
        if ($args && $args['code']) {
            return common_path('main/register/'.$args['code']);
        } else {
            return common_path('main/register');
        }
     case 'remotesubscribe':
        if ($args && $args['nickname']) {
            return common_path('main/remote?nickname=' . $args['nickname']);
        } else {
            return common_path('main/remote');
        }
     case 'nudge':
        return common_path($args['nickname'].'/nudge');
     case 'openidlogin':
        return common_path('main/openid');
     case 'profilesettings':
        return common_path('settings/profile');
778 779
     case 'passwordsettings':
        return common_path('settings/password');
780 781 782 783 784 785 786 787 788 789
     case 'emailsettings':
        return common_path('settings/email');
     case 'openidsettings':
        return common_path('settings/openid');
     case 'smssettings':
        return common_path('settings/sms');
     case 'twittersettings':
        return common_path('settings/twitter');
     case 'othersettings':
        return common_path('settings/other');
790 791
     case 'deleteprofile':
        return common_path('settings/delete');
792 793 794 795 796 797 798 799 800 801 802 803 804 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 830 831 832 833 834 835 836 837
     case 'newnotice':
        if ($args && $args['replyto']) {
            return common_path('notice/new?replyto='.$args['replyto']);
        } else {
            return common_path('notice/new');
        }
     case 'shownotice':
        return common_path('notice/'.$args['notice']);
     case 'deletenotice':
        if ($args && $args['notice']) {
            return common_path('notice/delete/'.$args['notice']);
        } else {
            return common_path('notice/delete');
        }
     case 'microsummary':
     case 'xrds':
     case 'foaf':
        return common_path($args['nickname'].'/'.$action);
     case 'all':
     case 'replies':
     case 'inbox':
     case 'outbox':
        if ($args && isset($args['page'])) {
            return common_path($args['nickname'].'/'.$action.'?page=' . $args['page']);
        } else {
            return common_path($args['nickname'].'/'.$action);
        }
     case 'subscriptions':
     case 'subscribers':
        $nickname = $args['nickname'];
        unset($args['nickname']);
        if (isset($args['tag'])) {
            $tag = $args['tag'];
            unset($args['tag']);
        }
        $params = http_build_query($args);
        if ($params) {
            return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : '') . '?' . $params);
        } else {
            return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : ''));
        }
     case 'allrss':
        return common_path($args['nickname'].'/all/rss');
     case 'repliesrss':
        return common_path($args['nickname'].'/replies/rss');
     case 'userrss':
838
        if (isset($args['limit']))
839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
          return common_path($args['nickname'].'/rss?limit=' . $args['limit']);
        return common_path($args['nickname'].'/rss');
     case 'showstream':
        if ($args && isset($args['page'])) {
            return common_path($args['nickname'].'?page=' . $args['page']);
        } else {
            return common_path($args['nickname']);
        }

     case 'usertimeline':
        return common_path("api/statuses/user_timeline/".$args['nickname'].".atom");
     case 'confirmaddress':
        return common_path('main/confirmaddress/'.$args['code']);
     case 'userbyid':
        return common_path('user/'.$args['id']);
     case 'recoverpassword':
        $path = 'main/recoverpassword';
        if ($args['code']) {
            $path .= '/' . $args['code'];
        }
        return common_path($path);
     case 'imsettings':
        return common_path('settings/im');
862 863
     case 'avatarsettings':
        return common_path('settings/avatar');
Robin Millette's avatar
Robin Millette committed
864 865
     case 'groupsearch':
        return common_path('search/group' . (($args) ? ('?' . http_build_query($args)) : ''));
866 867 868 869 870 871 872 873 874
     case 'peoplesearch':
        return common_path('search/people' . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'noticesearch':
        return common_path('search/notice' . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'noticesearchrss':
        return common_path('search/notice/rss' . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'avatarbynickname':
        return common_path($args['nickname'].'/avatar/'.$args['size']);
     case 'tag':
875 876
        $path = 'tag/' . $args['tag'];
        unset($args['tag']);
877
        return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
878 879
     case 'publictagcloud':
        return common_path('tags');
880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
     case 'peopletag':
        $path = 'peopletag/' . $args['tag'];
        unset($args['tag']);
        return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'tags':
        return common_path('tags' . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'favor':
        return common_path('main/favor');
     case 'disfavor':
        return common_path('main/disfavor');
     case 'showfavorites':
        if ($args && isset($args['page'])) {
            return common_path($args['nickname'].'/favorites?page=' . $args['page']);
        } else {
            return common_path($args['nickname'].'/favorites');
        }
     case 'favoritesrss':
        return common_path($args['nickname'].'/favorites/rss');
     case 'showmessage':
        return common_path('message/' . $args['message']);
     case 'newmessage':
        return common_path('message/new' . (($args) ? ('?' . http_build_query($args)) : ''));
     case 'api':
        // XXX: do fancy URLs for all the API methods
        switch (strtolower($args['apiaction'])) {
         case 'statuses':
            switch (strtolower($args['method'])) {
             case 'user_timeline.rss':
                return common_path('api/statuses/user_timeline/'.$args['argument'].'.rss');
             case 'user_timeline.atom':
                return common_path('api/statuses/user_timeline/'.$args['argument'].'.atom');
             case 'user_timeline.json':
                return common_path('api/statuses/user_timeline/'.$args['argument'].'.json');
             case 'user_timeline.xml':
                return common_path('api/statuses/user_timeline/'.$args['argument'].'.xml');
             default: return common_simple_url($action, $args);
            }
         default: return common_simple_url($action, $args);
        }
     case 'sup':
        if ($args && isset($args['seconds'])) {
            return common_path('main/sup?seconds='.$args['seconds']);
        } else {
            return common_path('main/sup');
        }
Evan Prodromou's avatar
Evan Prodromou committed
925 926 927
     case 'newgroup':
        return common_path('group/new');
     case 'showgroup':
Meitar Moscovitz's avatar
Meitar Moscovitz committed
928
        return common_path('group/'.$args['nickname'] . (($args['page']) ? ('?page=' . $args['page']) : ''));
Evan Prodromou's avatar
Evan Prodromou committed
929 930 931 932 933 934 935 936 937 938
     case 'editgroup':
        return common_path('group/'.$args['nickname'].'/edit');
     case 'joingroup':
        return common_path('group/'.$args['nickname'].'/join');
     case 'leavegroup':
        return common_path('group/'.$args['nickname'].'/leave');
     case 'groupbyid':
        return common_path('group/'.$args['id'].'/id');
     case 'grouprss':
        return common_path('group/'.$args['nickname'].'/rss');
Evan Prodromou's avatar
Evan Prodromou committed
939 940
     case 'groupmembers':
        return common_path('group/'.$args['nickname'].'/members');
Evan Prodromou's avatar
Evan Prodromou committed
941 942
     case 'grouplogo':
        return common_path('group/'.$args['nickname'].'/logo');
Evan Prodromou's avatar
Evan Prodromou committed
943
     case 'usergroups':
944 945 946
        $nickname = $args['nickname'];
        unset($args['nickname']);
        return common_path($nickname.'/groups' . (($args) ? ('?' . http_build_query($args)) : ''));
947
     case 'groups':
Evan Prodromou's avatar
Evan Prodromou committed
948
        return common_path('group' . (($args) ? ('?' . http_build_query($args)) : ''));
949 950 951 952 953
     default:
        return common_simple_url($action, $args);
    }
}

954 955
function common_simple_url($action, $args=null)
{
956 957 958 959 960 961 962 963 964
    global $config;
    /* XXX: pretty URLs */
    $extra = '';
    if ($args) {
        foreach ($args as $key => $value) {
            $extra .= "&${key}=${value}";
        }
    }
    return common_path("index.php?action=${action}${extra}");
965 966
}

967 968
function common_path($relative)
{
969 970 971
    global $config;
    $pathpart = ($config['site']['path']) ? $config['site']['path']."/" : '';
    return "http://".$config['site']['server'].'/'.$pathpart.$relative;
972 973
}

974 975
function common_date_string($dt)
{
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
    // XXX: do some sexy date formatting
    // return date(DATE_RFC822, $dt);
    $t = strtotime($dt);
    $now = time();
    $diff = $now - $t;

    if ($now < $t) { // that shouldn't happen!
        return common_exact_date($dt);
    } else if ($diff < 60) {
        return _('a few seconds ago');
    } else if ($diff < 92) {
        return _('about a minute ago');
    } else if ($diff < 3300) {
        return sprintf(_('about %d minutes ago'), round($diff/60));
    } else if ($diff < 5400) {
        return _('about an hour ago');
    } else if ($diff < 22 * 3600) {
        return sprintf(_('about %d hours ago'), round($diff/3600));
    } else if ($diff < 37 * 3600) {
        return _('about a day ago');
    } else if ($diff < 24 * 24 * 3600) {
        return sprintf(_('about %d days ago'), round($diff/(24*3600)));
    } else if ($diff < 46 * 24 * 3600) {
        return _('about a month ago');
    } else if ($diff < 330 * 24 * 3600) {
        return sprintf(_('about %d months ago'), round($diff/(30*24*3600)));
    } else if ($diff < 480 * 24 * 3600) {
        return _('about a year ago');
    } else {
        return common_exact_date($dt);
    }
Evan Prodromou's avatar
Evan Prodromou committed
1007 1008
}

1009 1010
function common_exact_date($dt)
{
Mike Cochrane's avatar
Mike Cochrane committed
1011 1012 1013 1014 1015 1016 1017 1018
    static $_utc;
    static $_siteTz;

    if (!$_utc) {
        $_utc = new DateTimeZone('UTC');
        $_siteTz = new DateTimeZone(common_timezone());
    }

1019 1020 1021 1022
    $dateStr = date('d F Y H:i:s', strtotime($dt));
    $d = new DateTime($dateStr, $_utc);
    $d->setTimezone($_siteTz);
    return $d->format(DATE_RFC850);
1023 1024
}

1025 1026
function common_date_w3dtf($dt)
{
1027 1028 1029 1030
    $dateStr = date('d F Y H:i:s', strtotime($dt));
    $d = new DateTime($dateStr, new DateTimeZone('UTC'));
    $d->setTimezone(new DateTimeZone(common_timezone()));
    return $d->format(DATE_W3C);
1031 1032
}

1033 1034
function common_date_rfc2822($dt)
{
1035 1036 1037 1038
    $dateStr = date('d F Y H:i:s', strtotime($dt));
    $d = new DateTime($dateStr, new DateTimeZone('UTC'));
    $d->setTimezone(new DateTimeZone(common_timezone()));
    return $d->format('r');
zach's avatar
zach committed
1039 1040
}

1041 1042
function common_date_iso8601($dt)
{
1043 1044 1045 1046
    $dateStr = date('d F Y H:i:s', strtotime($dt));
    $d = new DateTime($dateStr, new DateTimeZone('UTC'));
    $d->setTimezone(new DateTimeZone(common_timezone()));
    return $d->format('c');
zach's avatar
zach committed
1047 1048
}

1049 1050
function common_sql_now()
{
1051
    return strftime('%Y-%m-%d %H:%M:%S', time());
1052 1053
}

1054 1055
function common_redirect($url, $code=307)
{
1056 1057 1058 1059
    static $status = array(301 => "Moved Permanently",
                           302 => "Found",
                           303 => "See Other",
                           307 => "Temporary Redirect");
Evan Prodromou's avatar
Evan Prodromou committed
1060

1061 1062 1063
    header("Status: ${code} $status[$code]");
    header("Location: $url");

Evan Prodromou's avatar
Evan Prodromou committed
1064 1065 1066 1067
    $xo = new XMLOutputter();
    $xo->startXML('a',
                  '-//W3C//DTD XHTML 1.0 Strict//EN',
                  'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
1068
    $xo->element('a', array('href' => $url), $url);
Evan Prodromou's avatar
Evan Prodromou committed
1069
    $xo->endXML();
1070
    exit;
1071 1072
}

1073 1074
function common_broadcast_notice($notice, $remote=false)
{
1075

1076 1077 1078
    // Check to see if notice should go to Twitter
    $flink = Foreign_link::getByUserID($notice->profile_id, 1); // 1 == Twitter
    if (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND) {
1079

1080
        // If it's not a Twitter-style reply, or if the user WANTS to send replies...
1081

1082 1083
        if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
            (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY)) {
1084

1085
            $result = common_twitter_broadcast($notice, $flink);
1086

1087 1088 1089 1090 1091
            if (!$result) {
                common_debug('Unable to send notice: ' . $notice->id . ' to Twitter.', __FILE__);
            }
        }
    }
1092

1093 1094 1095 1096 1097 1098
    if (common_config('queue', 'enabled')) {
        // Do it later!
        return common_enqueue_notice($notice);
    } else {
        return common_real_broadcast($notice, $remote);
    }
1099 1100
}

1101 1102
function common_twitter_broadcast($notice, $flink)
{
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
    global $config;
    $success = true;
    $fuser = $flink->getForeignUser();
    $twitter_user = $fuser->nickname;
    $twitter_password = $flink->credentials;
    $uri = 'http://www.twitter.com/statuses/update.json';

    // XXX: Hack to get around PHP cURL's use of @ being a a meta character
    $statustxt = preg_replace('/^@/', ' @', $notice->content);

    $options = array(
                     CURLOPT_USERPWD         => "$twitter_user:$twitter_password",
                     CURLOPT_POST            => true,
                     CURLOPT_POSTFIELDS        => array(
                                                        'status'    => $statustxt,
                                                        'source'    => $config['integration']['source']
                                                        ),
                     CURLOPT_RETURNTRANSFER    => true,
                     CURLOPT_FAILONERROR        => true,
                     CURLOPT_HEADER            => false,
                     CURLOPT_FOLLOWLOCATION    => true,
                     CURLOPT_USERAGENT        => "Laconica",
                     CURLOPT_CONNECTTIMEOUT    => 120,  // XXX: Scary!!!! How long should this be?
1126 1127 1128 1129
                     CURLOPT_TIMEOUT            => 120,

                     # Twitter is strict about accepting invalid "Expect" headers
                     CURLOPT_HTTPHEADER => array('Expect:')
1130 1131 1132
                     );

    $ch = curl_init($uri);
1133 1134 1135 1136
    curl_setopt_array($ch, $options);
    $data = curl_exec($ch);
    $errmsg = curl_error($ch);

1137 1138 1139 1140 1141
    if ($errmsg) {
        common_debug("cURL error: $errmsg - trying to send notice for $twitter_user.",
                     __FILE__);
        $success = false;
    }
1142

1143
    curl_close($ch);
1144

1145 1146 1147 1148 1149
    if (!$data) {
        common_debug("No data returned by Twitter's API trying to send update for $twitter_user",
                     __FILE__);
        $success = false;
    }