]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/util.php
4e809029f9e820c4d304f34d41c08b56d2a40af9
[quix0rs-gnu-social.git] / lib / util.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, 2009, Control Yourself, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /* XXX: break up into separate modules (HTTP, user, files) */
21
22 // Show a server error
23
24 function common_server_error($msg, $code=500)
25 {
26     $err = new ServerErrorAction($msg, $code);
27     $err->showPage();
28 }
29
30 // Show a user error
31 function common_user_error($msg, $code=400)
32 {
33     $err = new ClientErrorAction($msg, $code);
34     $err->showPage();
35 }
36
37 function common_init_locale($language=null)
38 {
39     if(!$language) {
40         $language = common_language();
41     }
42     putenv('LANGUAGE='.$language);
43     putenv('LANG='.$language);
44     return setlocale(LC_ALL, $language . ".utf8",
45                      $language . ".UTF8",
46                      $language . ".utf-8",
47                      $language . ".UTF-8",
48                      $language);
49 }
50
51 function common_init_language()
52 {
53     mb_internal_encoding('UTF-8');
54     $language = common_language();
55     // So we don't have to make people install the gettext locales
56     $locale_set = common_init_locale($language);
57     bindtextdomain("laconica", common_config('site','locale_path'));
58     bind_textdomain_codeset("laconica", "UTF-8");
59     textdomain("laconica");
60     setlocale(LC_CTYPE, 'C');
61     if(!$locale_set) {
62         common_log(LOG_INFO,'Language requested:'.$language.' - locale could not be set:',__FILE__);
63     }
64 }
65
66 function common_timezone()
67 {
68     if (common_logged_in()) {
69         $user = common_current_user();
70         if ($user->timezone) {
71             return $user->timezone;
72         }
73     }
74
75     return common_config('site', 'timezone');
76 }
77
78 function common_language()
79 {
80
81     // If there is a user logged in and they've set a language preference
82     // then return that one...
83     if (_have_config() && common_logged_in()) {
84         $user = common_current_user();
85         $user_language = $user->language;
86         if ($user_language)
87           return $user_language;
88     }
89
90     // Otherwise, find the best match for the languages requested by the
91     // user's browser...
92     $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null;
93     if (!empty($httplang)) {
94         $language = client_prefered_language($httplang);
95         if ($language)
96           return $language;
97     }
98
99     // Finally, if none of the above worked, use the site's default...
100     return common_config('site', 'language');
101 }
102 // salted, hashed passwords are stored in the DB
103
104 function common_munge_password($password, $id)
105 {
106     return md5($password . $id);
107 }
108
109 // check if a username exists and has matching password
110 function common_check_user($nickname, $password)
111 {
112     // NEVER allow blank passwords, even if they match the DB
113     if (mb_strlen($password) == 0) {
114         return false;
115     }
116     $user = User::staticGet('nickname', $nickname);
117     if (is_null($user) || $user === false) {
118         return false;
119     } else {
120         if (0 == strcmp(common_munge_password($password, $user->id),
121                         $user->password)) {
122             return $user;
123         } else {
124             return false;
125         }
126     }
127 }
128
129 // is the current user logged in?
130 function common_logged_in()
131 {
132     return (!is_null(common_current_user()));
133 }
134
135 function common_have_session()
136 {
137     return (0 != strcmp(session_id(), ''));
138 }
139
140 function common_ensure_session()
141 {
142     $c = null;
143     if (array_key_exists(session_name(), $_COOKIE)) {
144         $c = $_COOKIE[session_name()];
145     }
146     if (!common_have_session()) {
147         if (common_config('sessions', 'handle')) {
148             Session::setSaveHandler();
149         }
150         @session_start();
151         if (!isset($_SESSION['started'])) {
152             $_SESSION['started'] = time();
153             if (!empty($c)) {
154                 common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' .
155                            ' is set but started value is null');
156             }
157         }
158     }
159 }
160
161 // Three kinds of arguments:
162 // 1) a user object
163 // 2) a nickname
164 // 3) null to clear
165
166 // Initialize to false; set to null if none found
167
168 $_cur = false;
169
170 function common_set_user($user)
171 {
172
173     global $_cur;
174
175     if (is_null($user) && common_have_session()) {
176         $_cur = null;
177         unset($_SESSION['userid']);
178         return true;
179     } else if (is_string($user)) {
180         $nickname = $user;
181         $user = User::staticGet('nickname', $nickname);
182     } else if (!($user instanceof User)) {
183         return false;
184     }
185
186     if ($user) {
187         common_ensure_session();
188         $_SESSION['userid'] = $user->id;
189         $_cur = $user;
190         return $_cur;
191     }
192     return false;
193 }
194
195 function common_set_cookie($key, $value, $expiration=0)
196 {
197     $path = common_config('site', 'path');
198     $server = common_config('site', 'server');
199
200     if ($path && ($path != '/')) {
201         $cookiepath = '/' . $path . '/';
202     } else {
203         $cookiepath = '/';
204     }
205     return setcookie($key,
206                      $value,
207                      $expiration,
208                      $cookiepath,
209                      $server);
210 }
211
212 define('REMEMBERME', 'rememberme');
213 define('REMEMBERME_EXPIRY', 30 * 24 * 60 * 60); // 30 days
214
215 function common_rememberme($user=null)
216 {
217     if (!$user) {
218         $user = common_current_user();
219         if (!$user) {
220             common_debug('No current user to remember', __FILE__);
221             return false;
222         }
223     }
224
225     $rm = new Remember_me();
226
227     $rm->code = common_good_rand(16);
228     $rm->user_id = $user->id;
229
230     // Wrap the insert in some good ol' fashioned transaction code
231
232     $rm->query('BEGIN');
233
234     $result = $rm->insert();
235
236     if (!$result) {
237         common_log_db_error($rm, 'INSERT', __FILE__);
238         common_debug('Error adding rememberme record for ' . $user->nickname, __FILE__);
239         return false;
240     }
241
242     $rm->query('COMMIT');
243
244     common_debug('Inserted rememberme record (' . $rm->code . ', ' . $rm->user_id . '); result = ' . $result . '.', __FILE__);
245
246     $cookieval = $rm->user_id . ':' . $rm->code;
247
248     common_log(LOG_INFO, 'adding rememberme cookie "' . $cookieval . '" for ' . $user->nickname);
249
250     common_set_cookie(REMEMBERME, $cookieval, time() + REMEMBERME_EXPIRY);
251
252     return true;
253 }
254
255 function common_remembered_user()
256 {
257
258     $user = null;
259
260     $packed = isset($_COOKIE[REMEMBERME]) ? $_COOKIE[REMEMBERME] : null;
261
262     if (!$packed) {
263         return null;
264     }
265
266     list($id, $code) = explode(':', $packed);
267
268     if (!$id || !$code) {
269         common_log(LOG_WARNING, 'Malformed rememberme cookie: ' . $packed);
270         common_forgetme();
271         return null;
272     }
273
274     $rm = Remember_me::staticGet($code);
275
276     if (!$rm) {
277         common_log(LOG_WARNING, 'No such remember code: ' . $code);
278         common_forgetme();
279         return null;
280     }
281
282     if ($rm->user_id != $id) {
283         common_log(LOG_WARNING, 'Rememberme code for wrong user: ' . $rm->user_id . ' != ' . $id);
284         common_forgetme();
285         return null;
286     }
287
288     $user = User::staticGet($rm->user_id);
289
290     if (!$user) {
291         common_log(LOG_WARNING, 'No such user for rememberme: ' . $rm->user_id);
292         common_forgetme();
293         return null;
294     }
295
296     // successful!
297     $result = $rm->delete();
298
299     if (!$result) {
300         common_log_db_error($rm, 'DELETE', __FILE__);
301         common_log(LOG_WARNING, 'Could not delete rememberme: ' . $code);
302         common_forgetme();
303         return null;
304     }
305
306     common_log(LOG_INFO, 'logging in ' . $user->nickname . ' using rememberme code ' . $rm->code);
307
308     common_set_user($user);
309     common_real_login(false);
310
311     // We issue a new cookie, so they can log in
312     // automatically again after this session
313
314     common_rememberme($user);
315
316     return $user;
317 }
318
319 // must be called with a valid user!
320
321 function common_forgetme()
322 {
323     common_set_cookie(REMEMBERME, '', 0);
324 }
325
326 // who is the current user?
327 function common_current_user()
328 {
329     global $_cur;
330
331     if (!_have_config()) {
332         return null;
333     }
334
335     if ($_cur === false) {
336
337         if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
338             common_ensure_session();
339             $id = isset($_SESSION['userid']) ? $_SESSION['userid'] : false;
340             if ($id) {
341                 $_cur = User::staticGet($id);
342                 return $_cur;
343             }
344         }
345
346         // that didn't work; try to remember; will init $_cur to null on failure
347         $_cur = common_remembered_user();
348
349         if ($_cur) {
350             common_debug("Got User " . $_cur->nickname);
351             common_debug("Faking session on remembered user");
352             // XXX: Is this necessary?
353             $_SESSION['userid'] = $_cur->id;
354         }
355     }
356
357     return $_cur;
358 }
359
360 // Logins that are 'remembered' aren't 'real' -- they're subject to
361 // cookie-stealing. So, we don't let them do certain things. New reg,
362 // OpenID, and password logins _are_ real.
363
364 function common_real_login($real=true)
365 {
366     common_ensure_session();
367     $_SESSION['real_login'] = $real;
368 }
369
370 function common_is_real_login()
371 {
372     return common_logged_in() && $_SESSION['real_login'];
373 }
374
375 // get canonical version of nickname for comparison
376 function common_canonical_nickname($nickname)
377 {
378     // XXX: UTF-8 canonicalization (like combining chars)
379     return strtolower($nickname);
380 }
381
382 // get canonical version of email for comparison
383 function common_canonical_email($email)
384 {
385     // XXX: canonicalize UTF-8
386     // XXX: lcase the domain part
387     return $email;
388 }
389
390 function common_render_content($text, $notice)
391 {
392     $r = common_render_text($text);
393     $id = $notice->profile_id;
394     $r = preg_replace('/(^|\s+)@([A-Za-z0-9]{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r);
395     $r = preg_replace('/^T ([A-Z0-9]{1,64}) /e', "'T '.common_at_link($id, '\\1').' '", $r);
396     $r = preg_replace('/(^|\s+)@#([A-Za-z0-9]{1,64})/e', "'\\1@#'.common_at_hash_link($id, '\\2')", $r);
397     $r = preg_replace('/(^|\s)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r);
398     return $r;
399 }
400
401 function common_render_text($text)
402 {
403     $r = htmlspecialchars($text);
404
405     $r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r);
406     $r = common_replace_urls_callback($r, 'common_linkify');
407     $r = preg_replace('/(^|\(|\[|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r);
408     // XXX: machine tags
409     return $r;
410 }
411
412 function common_replace_urls_callback($text, $callback, $notice_id = null) {
413     // Start off with a regex
414     $regex = '#'.
415     '(?:^|\s+)('.
416         '(?:'. //Known protocols
417             '(?:'.
418                 '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://'.
419                 '|'.
420                 '(?:mailto|aim|tel|xmpp):'.
421             ')\S+'.
422         ')'.
423         '|(?:(?: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
424         '|(?:'. //IPv6
425             '(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,6}|'.
426             '(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|'.
427             '(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|'.
428             '(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|'.
429             '(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|'.
430             '(?:[0-9a-f]{1,4}:){1,6}(?::[0-9a-f]{1,4}){1,1}|'.
431             '(?:(?:[0-9a-f]{1,4}:){1,7}|:):|'.
432             ':(?::[0-9a-f]{1,4}){1,7}|'.
433             '(?:(?:(?:[0-9a-f]{1,4}:){6})(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
434             '(?:(?:[0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
435             '(?:[0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
436             '(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
437             '(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,3}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
438             '(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,2}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
439             '(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,1}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
440             '(?:(?:[0-9a-f]{1,4}:){1,5}|:):(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
441             ':(?::[0-9a-f]{1,4}){1,5}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'.
442         ')|(?:'. //DNS
443             '\S+\.(?:museum|travel|onion|local|[a-z]{2,4})'.
444         ')'.
445         '(?:[:/]\S*)?'.
446     ')(?:$|\s+)'.
447     '#ix';
448
449 //preg_match_all($regex,$text,$matches);
450 //print_r($matches);
451 //die("here");
452     return preg_replace_callback($regex, curry(callback_helper,$callback,$notice_id) ,$text);
453 }
454
455 function callback_helper($matches, $callback, $notice_id) {
456     $spaces_left = (strlen($matches[0]) - strlen(ltrim($matches[0])));
457     $spaces_right = (strlen($matches[0]) - strlen(rtrim($matches[0])));
458     if(empty($notice_id)){
459         $result = call_user_func_array($callback,$matches[1]);
460     }else{
461         $result = call_user_func_array($callback, array($matches[1],$notice_id) );
462     }
463     return str_repeat(' ',$spaces_left) . $result . str_repeat(' ',$spaces_right);
464 }
465
466 function curry($fn) {
467     //TODO switch to a PHP 5.3 function closure based approach if PHP 5.3 is used
468     $args = func_get_args();
469     array_shift($args);
470     $id = uniqid('_partial');
471     $GLOBALS[$id] = array($fn, $args);
472     return create_function(
473         '',
474         '
475         $args = func_get_args();
476         return call_user_func_array(
477         $GLOBALS["'.$id.'"][0],
478         array_merge(
479             $args,
480             $GLOBALS["'.$id.'"][1]));
481     ');
482 }
483
484 function common_linkify($url) {
485     // It comes in special'd, so we unspecial it before passing to the stringifying
486     // functions
487     $url = htmlspecialchars_decode($url);
488
489    if(strpos($url, '@')!==false && strpos($url, ':')===false){
490        //url is an email address without the mailto: protocol
491        return XMLStringer::estring('a', array('href' => "mailto:$url", 'rel' => 'external'), $url);
492    }
493
494     $canon = File_redirection::_canonUrl($url);
495
496     $longurl_data = File_redirection::where($url);
497     if (is_array($longurl_data)) {
498         $longurl = $longurl_data['url'];
499     } elseif (is_string($longurl_data)) {
500         $longurl = $longurl_data;
501     } else {
502         throw new ServerException("Can't linkify url '$url'");
503     }
504
505     $attrs = array('href' => $canon, 'rel' => 'external');
506
507     $is_attachment = false;
508     $attachment_id = null;
509     $has_thumb = false;
510
511     // Check to see whether there's a filename associated with this URL.
512     // If there is, it's an upload and qualifies as an attachment
513
514     $localfile = File::staticGet('url', $longurl);
515
516     if (!empty($localfile)) {
517         if (isset($localfile->filename)) {
518             $is_attachment = true;
519             $attachment_id = $localfile->id;
520         }
521     }
522
523     // if this URL is an attachment, then we set class='attachment' and id='attahcment-ID'
524     // where ID is the id of the attachment for the given URL.
525     //
526     // we need a better test telling what can be shown as an attachment
527     // we're currently picking up oembeds only.
528     // I think the best option is another file_view table in the db
529     // and associated dbobject.
530
531     $query = "select file_oembed.file_id as file_id from file join file_oembed on file.id = file_oembed.file_id where file.url='$longurl'";
532     $file = new File;
533     $file->query($query);
534     $file->fetch();
535
536     if (!empty($file->file_id)) {
537         $is_attachment = true;
538         $attachment_id = $file->file_id;
539
540         $query = "select file_thumbnail.file_id as file_id from file join file_thumbnail on file.id = file_thumbnail.file_id where file.url='$longurl'";
541         $file2 = new File;
542         $file2->query($query);
543         $file2->fetch();
544
545         if (!empty($file2)) {
546             $has_thumb = true;
547         }
548     }
549
550     // Add clippy
551     if ($is_attachment) {
552         $attrs['class'] = 'attachment';
553         if ($has_thumb) {
554             $attrs['class'] = 'attachment thumbnail';
555         }
556         $attrs['id'] = "attachment-{$attachment_id}";
557     }
558
559     return XMLStringer::estring('a', $attrs, $url);
560 }
561
562 function common_shorten_links($text)
563 {
564     if (mb_strlen($text) <= 140) return $text;
565     return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
566 }
567
568 function common_xml_safe_str($str)
569 {
570     // Neutralize control codes and surrogates
571         return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str);
572 }
573
574 function common_tag_link($tag)
575 {
576     $canonical = common_canonical_tag($tag);
577     $url = common_local_url('tag', array('tag' => $canonical));
578     $xs = new XMLStringer();
579     $xs->elementStart('span', 'tag');
580     $xs->element('a', array('href' => $url,
581                             'rel' => 'tag'),
582                  $tag);
583     $xs->elementEnd('span');
584     return $xs->getString();
585 }
586
587 function common_canonical_tag($tag)
588 {
589   $tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8");
590   return str_replace(array('-', '_', '.'), '', $tag);
591 }
592
593 function common_valid_profile_tag($str)
594 {
595     return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str);
596 }
597
598 function common_at_link($sender_id, $nickname)
599 {
600     $sender = Profile::staticGet($sender_id);
601     $recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
602     if ($recipient) {
603         $user = User::staticGet('id', $recipient->id);
604         if ($user) {
605             $url = common_local_url('userbyid', array('id' => $user->id));
606         } else {
607             $url = $recipient->profileurl;
608         }
609         $xs = new XMLStringer(false);
610         $attrs = array('href' => $url,
611                        'class' => 'url');
612         if (!empty($recipient->fullname)) {
613             $attrs['title'] = $recipient->fullname . ' (' . $recipient->nickname . ')';
614         }
615         $xs->elementStart('span', 'vcard');
616         $xs->elementStart('a', $attrs);
617         $xs->element('span', 'fn nickname', $nickname);
618         $xs->elementEnd('a');
619         $xs->elementEnd('span');
620         return $xs->getString();
621     } else {
622         return $nickname;
623     }
624 }
625
626 function common_group_link($sender_id, $nickname)
627 {
628     $sender = Profile::staticGet($sender_id);
629     $group = User_group::getForNickname($nickname);
630     if ($group && $sender->isMember($group)) {
631         $attrs = array('href' => $group->permalink(),
632                        'class' => 'url');
633         if (!empty($group->fullname)) {
634             $attrs['title'] = $group->fullname . ' (' . $group->nickname . ')';
635         }
636         $xs = new XMLStringer();
637         $xs->elementStart('span', 'vcard');
638         $xs->elementStart('a', $attrs);
639         $xs->element('span', 'fn nickname', $nickname);
640         $xs->elementEnd('a');
641         $xs->elementEnd('span');
642         return $xs->getString();
643     } else {
644         return $nickname;
645     }
646 }
647
648 function common_at_hash_link($sender_id, $tag)
649 {
650     $user = User::staticGet($sender_id);
651     if (!$user) {
652         return $tag;
653     }
654     $tagged = Profile_tag::getTagged($user->id, common_canonical_tag($tag));
655     if ($tagged) {
656         $url = common_local_url('subscriptions',
657                                 array('nickname' => $user->nickname,
658                                       'tag' => $tag));
659         $xs = new XMLStringer();
660         $xs->elementStart('span', 'tag');
661         $xs->element('a', array('href' => $url,
662                                 'rel' => $tag),
663                      $tag);
664         $xs->elementEnd('span');
665         return $xs->getString();
666     } else {
667         return $tag;
668     }
669 }
670
671 function common_relative_profile($sender, $nickname, $dt=null)
672 {
673     // Try to find profiles this profile is subscribed to that have this nickname
674     $recipient = new Profile();
675     // XXX: use a join instead of a subquery
676     $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender->id.' and subscribed = id)', 'AND');
677     $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND');
678     if ($recipient->find(true)) {
679         // XXX: should probably differentiate between profiles with
680         // the same name by date of most recent update
681         return $recipient;
682     }
683     // Try to find profiles that listen to this profile and that have this nickname
684     $recipient = new Profile();
685     // XXX: use a join instead of a subquery
686     $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender->id.' and subscriber = id)', 'AND');
687     $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND');
688     if ($recipient->find(true)) {
689         // XXX: should probably differentiate between profiles with
690         // the same name by date of most recent update
691         return $recipient;
692     }
693     // If this is a local user, try to find a local user with that nickname.
694     $sender = User::staticGet($sender->id);
695     if ($sender) {
696         $recipient_user = User::staticGet('nickname', $nickname);
697         if ($recipient_user) {
698             return $recipient_user->getProfile();
699         }
700     }
701     // Otherwise, no links. @messages from local users to remote users,
702     // or from remote users to other remote users, are just
703     // outside our ability to make intelligent guesses about
704     return null;
705 }
706
707 function common_local_url($action, $args=null, $params=null, $fragment=null)
708 {
709     static $sensitive = array('login', 'register', 'passwordsettings',
710                               'twittersettings', 'finishopenidlogin',
711                               'finishaddopenid', 'api');
712
713     $r = Router::get();
714     $path = $r->build($action, $args, $params, $fragment);
715
716     $ssl = in_array($action, $sensitive);
717
718     if (common_config('site','fancy')) {
719         $url = common_path(mb_substr($path, 1), $ssl);
720     } else {
721         if (mb_strpos($path, '/index.php') === 0) {
722             $url = common_path(mb_substr($path, 1), $ssl);
723         } else {
724             $url = common_path('index.php'.$path, $ssl);
725         }
726     }
727     return $url;
728 }
729
730 function common_path($relative, $ssl=false)
731 {
732     $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : '';
733
734     if (($ssl && (common_config('site', 'ssl') === 'sometimes'))
735         || common_config('site', 'ssl') === 'always') {
736         $proto = 'https';
737         if (is_string(common_config('site', 'sslserver')) &&
738             mb_strlen(common_config('site', 'sslserver')) > 0) {
739             $serverpart = common_config('site', 'sslserver');
740         } else {
741             $serverpart = common_config('site', 'server');
742         }
743     } else {
744         $proto = 'http';
745         $serverpart = common_config('site', 'server');
746     }
747
748     return $proto.'://'.$serverpart.'/'.$pathpart.$relative;
749 }
750
751 function common_date_string($dt)
752 {
753     // XXX: do some sexy date formatting
754     // return date(DATE_RFC822, $dt);
755     $t = strtotime($dt);
756     $now = time();
757     $diff = $now - $t;
758
759     if ($now < $t) { // that shouldn't happen!
760         return common_exact_date($dt);
761     } else if ($diff < 60) {
762         return _('a few seconds ago');
763     } else if ($diff < 92) {
764         return _('about a minute ago');
765     } else if ($diff < 3300) {
766         return sprintf(_('about %d minutes ago'), round($diff/60));
767     } else if ($diff < 5400) {
768         return _('about an hour ago');
769     } else if ($diff < 22 * 3600) {
770         return sprintf(_('about %d hours ago'), round($diff/3600));
771     } else if ($diff < 37 * 3600) {
772         return _('about a day ago');
773     } else if ($diff < 24 * 24 * 3600) {
774         return sprintf(_('about %d days ago'), round($diff/(24*3600)));
775     } else if ($diff < 46 * 24 * 3600) {
776         return _('about a month ago');
777     } else if ($diff < 330 * 24 * 3600) {
778         return sprintf(_('about %d months ago'), round($diff/(30*24*3600)));
779     } else if ($diff < 480 * 24 * 3600) {
780         return _('about a year ago');
781     } else {
782         return common_exact_date($dt);
783     }
784 }
785
786 function common_exact_date($dt)
787 {
788     static $_utc;
789     static $_siteTz;
790
791     if (!$_utc) {
792         $_utc = new DateTimeZone('UTC');
793         $_siteTz = new DateTimeZone(common_timezone());
794     }
795
796     $dateStr = date('d F Y H:i:s', strtotime($dt));
797     $d = new DateTime($dateStr, $_utc);
798     $d->setTimezone($_siteTz);
799     return $d->format(DATE_RFC850);
800 }
801
802 function common_date_w3dtf($dt)
803 {
804     $dateStr = date('d F Y H:i:s', strtotime($dt));
805     $d = new DateTime($dateStr, new DateTimeZone('UTC'));
806     $d->setTimezone(new DateTimeZone(common_timezone()));
807     return $d->format(DATE_W3C);
808 }
809
810 function common_date_rfc2822($dt)
811 {
812     $dateStr = date('d F Y H:i:s', strtotime($dt));
813     $d = new DateTime($dateStr, new DateTimeZone('UTC'));
814     $d->setTimezone(new DateTimeZone(common_timezone()));
815     return $d->format('r');
816 }
817
818 function common_date_iso8601($dt)
819 {
820     $dateStr = date('d F Y H:i:s', strtotime($dt));
821     $d = new DateTime($dateStr, new DateTimeZone('UTC'));
822     $d->setTimezone(new DateTimeZone(common_timezone()));
823     return $d->format('c');
824 }
825
826 function common_sql_now()
827 {
828     return common_sql_date(time());
829 }
830
831 function common_sql_date($datetime)
832 {
833     return strftime('%Y-%m-%d %H:%M:%S', $datetime);
834 }
835
836 function common_redirect($url, $code=307)
837 {
838     static $status = array(301 => "Moved Permanently",
839                            302 => "Found",
840                            303 => "See Other",
841                            307 => "Temporary Redirect");
842
843     header('HTTP/1.1 '.$code.' '.$status[$code]);
844     header("Location: $url");
845
846     $xo = new XMLOutputter();
847     $xo->startXML('a',
848                   '-//W3C//DTD XHTML 1.0 Strict//EN',
849                   'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
850     $xo->element('a', array('href' => $url), $url);
851     $xo->endXML();
852     exit;
853 }
854
855 function common_broadcast_notice($notice, $remote=false)
856 {
857     return common_enqueue_notice($notice);
858 }
859
860 // Stick the notice on the queue
861
862 function common_enqueue_notice($notice)
863 {
864     static $localTransports = array('omb',
865                                     'twitter',
866                                     'facebook',
867                                     'ping');
868     static $allTransports = array('sms');
869
870     $transports = $allTransports;
871
872     $xmpp = common_config('xmpp', 'enabled');
873
874     if ($xmpp) {
875         $transports[] = 'jabber';
876     }
877
878     if ($notice->is_local == Notice::LOCAL_PUBLIC ||
879         $notice->is_local == Notice::LOCAL_NONPUBLIC) {
880         $transports = array_merge($transports, $localTransports);
881         if ($xmpp) {
882             $transports[] = 'public';
883         }
884     }
885
886     $qm = QueueManager::get();
887
888     foreach ($transports as $transport)
889     {
890         $qm->enqueue($notice, $transport);
891     }
892
893     return true;
894 }
895
896 function common_broadcast_profile($profile)
897 {
898     // XXX: optionally use a queue system like http://code.google.com/p/microapps/wiki/NQDQ
899     require_once(INSTALLDIR.'/lib/omb.php');
900     omb_broadcast_profile($profile);
901     // XXX: Other broadcasts...?
902     return true;
903 }
904
905 function common_profile_url($nickname)
906 {
907     return common_local_url('showstream', array('nickname' => $nickname));
908 }
909
910 // Should make up a reasonable root URL
911
912 function common_root_url($ssl=false)
913 {
914     return common_path('', $ssl);
915 }
916
917 // returns $bytes bytes of random data as a hexadecimal string
918 // "good" here is a goal and not a guarantee
919
920 function common_good_rand($bytes)
921 {
922     // XXX: use random.org...?
923     if (@file_exists('/dev/urandom')) {
924         return common_urandom($bytes);
925     } else { // FIXME: this is probably not good enough
926         return common_mtrand($bytes);
927     }
928 }
929
930 function common_urandom($bytes)
931 {
932     $h = fopen('/dev/urandom', 'rb');
933     // should not block
934     $src = fread($h, $bytes);
935     fclose($h);
936     $enc = '';
937     for ($i = 0; $i < $bytes; $i++) {
938         $enc .= sprintf("%02x", (ord($src[$i])));
939     }
940     return $enc;
941 }
942
943 function common_mtrand($bytes)
944 {
945     $enc = '';
946     for ($i = 0; $i < $bytes; $i++) {
947         $enc .= sprintf("%02x", mt_rand(0, 255));
948     }
949     return $enc;
950 }
951
952 function common_set_returnto($url)
953 {
954     common_ensure_session();
955     $_SESSION['returnto'] = $url;
956 }
957
958 function common_get_returnto()
959 {
960     common_ensure_session();
961     return $_SESSION['returnto'];
962 }
963
964 function common_timestamp()
965 {
966     return date('YmdHis');
967 }
968
969 function common_ensure_syslog()
970 {
971     static $initialized = false;
972     if (!$initialized) {
973         openlog(common_config('syslog', 'appname'), 0,
974             common_config('syslog', 'facility'));
975         $initialized = true;
976     }
977 }
978
979 function common_log_line($priority, $msg)
980 {
981     static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR',
982                                       'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG');
983     return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n";
984 }
985
986 function common_log($priority, $msg, $filename=null)
987 {
988     $logfile = common_config('site', 'logfile');
989     if ($logfile) {
990         $log = fopen($logfile, "a");
991         if ($log) {
992             $output = common_log_line($priority, $msg);
993             fwrite($log, $output);
994             fclose($log);
995         }
996     } else {
997         common_ensure_syslog();
998         syslog($priority, $msg);
999     }
1000 }
1001
1002 function common_debug($msg, $filename=null)
1003 {
1004     if ($filename) {
1005         common_log(LOG_DEBUG, basename($filename).' - '.$msg);
1006     } else {
1007         common_log(LOG_DEBUG, $msg);
1008     }
1009 }
1010
1011 function common_log_db_error(&$object, $verb, $filename=null)
1012 {
1013     $objstr = common_log_objstring($object);
1014     $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
1015     common_log(LOG_ERR, $last_error->message . '(' . $verb . ' on ' . $objstr . ')', $filename);
1016 }
1017
1018 function common_log_objstring(&$object)
1019 {
1020     if (is_null($object)) {
1021         return "null";
1022     }
1023     if (!($object instanceof DB_DataObject)) {
1024         return "(unknown)";
1025     }
1026     $arr = $object->toArray();
1027     $fields = array();
1028     foreach ($arr as $k => $v) {
1029         $fields[] = "$k='$v'";
1030     }
1031     $objstring = $object->tableName() . '[' . implode(',', $fields) . ']';
1032     return $objstring;
1033 }
1034
1035 function common_valid_http_url($url)
1036 {
1037     return Validate::uri($url, array('allowed_schemes' => array('http', 'https')));
1038 }
1039
1040 function common_valid_tag($tag)
1041 {
1042     if (preg_match('/^tag:(.*?),(\d{4}(-\d{2}(-\d{2})?)?):(.*)$/', $tag, $matches)) {
1043         return (Validate::email($matches[1]) ||
1044                 preg_match('/^([\w-\.]+)$/', $matches[1]));
1045     }
1046     return false;
1047 }
1048
1049 /* Following functions are copied from MediaWiki GlobalFunctions.php
1050  * and written by Evan Prodromou. */
1051
1052 function common_accept_to_prefs($accept, $def = '*/*')
1053 {
1054     // No arg means accept anything (per HTTP spec)
1055     if(!$accept) {
1056         return array($def => 1);
1057     }
1058
1059     $prefs = array();
1060
1061     $parts = explode(',', $accept);
1062
1063     foreach($parts as $part) {
1064         // FIXME: doesn't deal with params like 'text/html; level=1'
1065         @list($value, $qpart) = explode(';', trim($part));
1066         $match = array();
1067         if(!isset($qpart)) {
1068             $prefs[$value] = 1;
1069         } elseif(preg_match('/q\s*=\s*(\d*\.\d+)/', $qpart, $match)) {
1070             $prefs[$value] = $match[1];
1071         }
1072     }
1073
1074     return $prefs;
1075 }
1076
1077 function common_mime_type_match($type, $avail)
1078 {
1079     if(array_key_exists($type, $avail)) {
1080         return $type;
1081     } else {
1082         $parts = explode('/', $type);
1083         if(array_key_exists($parts[0] . '/*', $avail)) {
1084             return $parts[0] . '/*';
1085         } elseif(array_key_exists('*/*', $avail)) {
1086             return '*/*';
1087         } else {
1088             return null;
1089         }
1090     }
1091 }
1092
1093 function common_negotiate_type($cprefs, $sprefs)
1094 {
1095     $combine = array();
1096
1097     foreach(array_keys($sprefs) as $type) {
1098         $parts = explode('/', $type);
1099         if($parts[1] != '*') {
1100             $ckey = common_mime_type_match($type, $cprefs);
1101             if($ckey) {
1102                 $combine[$type] = $sprefs[$type] * $cprefs[$ckey];
1103             }
1104         }
1105     }
1106
1107     foreach(array_keys($cprefs) as $type) {
1108         $parts = explode('/', $type);
1109         if($parts[1] != '*' && !array_key_exists($type, $sprefs)) {
1110             $skey = common_mime_type_match($type, $sprefs);
1111             if($skey) {
1112                 $combine[$type] = $sprefs[$skey] * $cprefs[$type];
1113             }
1114         }
1115     }
1116
1117     $bestq = 0;
1118     $besttype = 'text/html';
1119
1120     foreach(array_keys($combine) as $type) {
1121         if($combine[$type] > $bestq) {
1122             $besttype = $type;
1123             $bestq = $combine[$type];
1124         }
1125     }
1126
1127     if ('text/html' === $besttype) {
1128         return "text/html; charset=utf-8";
1129     }
1130     return $besttype;
1131 }
1132
1133 function common_config($main, $sub)
1134 {
1135     global $config;
1136     return isset($config[$main][$sub]) ? $config[$main][$sub] : false;
1137 }
1138
1139 function common_copy_args($from)
1140 {
1141     $to = array();
1142     $strip = get_magic_quotes_gpc();
1143     foreach ($from as $k => $v) {
1144         $to[$k] = ($strip) ? stripslashes($v) : $v;
1145     }
1146     return $to;
1147 }
1148
1149 // Neutralise the evil effects of magic_quotes_gpc in the current request.
1150 // This is used before handing a request off to OAuthRequest::from_request.
1151 function common_remove_magic_from_request()
1152 {
1153     if(get_magic_quotes_gpc()) {
1154         $_POST=array_map('stripslashes',$_POST);
1155         $_GET=array_map('stripslashes',$_GET);
1156     }
1157 }
1158
1159 function common_user_uri(&$user)
1160 {
1161     return common_local_url('userbyid', array('id' => $user->id));
1162 }
1163
1164 function common_notice_uri(&$notice)
1165 {
1166     return common_local_url('shownotice',
1167                             array('notice' => $notice->id));
1168 }
1169
1170 // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits
1171
1172 function common_confirmation_code($bits)
1173 {
1174     // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits
1175     static $codechars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
1176     $chars = ceil($bits/5);
1177     $code = '';
1178     for ($i = 0; $i < $chars; $i++) {
1179         // XXX: convert to string and back
1180         $num = hexdec(common_good_rand(1));
1181         // XXX: randomness is too precious to throw away almost
1182         // 40% of the bits we get!
1183         $code .= $codechars[$num%32];
1184     }
1185     return $code;
1186 }
1187
1188 // convert markup to HTML
1189
1190 function common_markup_to_html($c)
1191 {
1192     $c = preg_replace('/%%action.(\w+)%%/e', "common_local_url('\\1')", $c);
1193     $c = preg_replace('/%%doc.(\w+)%%/e', "common_local_url('doc', array('title'=>'\\1'))", $c);
1194     $c = preg_replace('/%%(\w+).(\w+)%%/e', 'common_config(\'\\1\', \'\\2\')', $c);
1195     return Markdown($c);
1196 }
1197
1198 function common_profile_uri($profile)
1199 {
1200     if (!$profile) {
1201         return null;
1202     }
1203     $user = User::staticGet($profile->id);
1204     if ($user) {
1205         return $user->uri;
1206     }
1207
1208     $remote = Remote_profile::staticGet($profile->id);
1209     if ($remote) {
1210         return $remote->uri;
1211     }
1212     // XXX: this is a very bad profile!
1213     return null;
1214 }
1215
1216 function common_canonical_sms($sms)
1217 {
1218     // strip non-digits
1219     preg_replace('/\D/', '', $sms);
1220     return $sms;
1221 }
1222
1223 function common_error_handler($errno, $errstr, $errfile, $errline, $errcontext)
1224 {
1225     switch ($errno) {
1226
1227      case E_ERROR:
1228      case E_COMPILE_ERROR:
1229      case E_CORE_ERROR:
1230      case E_USER_ERROR:
1231      case E_PARSE:
1232      case E_RECOVERABLE_ERROR:
1233         common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [ABORT]");
1234         die();
1235         break;
1236
1237      case E_WARNING:
1238      case E_COMPILE_WARNING:
1239      case E_CORE_WARNING:
1240      case E_USER_WARNING:
1241         common_log(LOG_WARNING, "[$errno] $errstr ($errfile:$errline)");
1242         break;
1243
1244      case E_NOTICE:
1245      case E_USER_NOTICE:
1246         common_log(LOG_NOTICE, "[$errno] $errstr ($errfile:$errline)");
1247         break;
1248
1249      case E_STRICT:
1250      case E_DEPRECATED:
1251      case E_USER_DEPRECATED:
1252         // XXX: config variable to log this stuff, too
1253         break;
1254
1255      default:
1256         common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [UNKNOWN LEVEL, die()'ing]");
1257         die();
1258         break;
1259     }
1260
1261     // FIXME: show error page if we're on the Web
1262     /* Don't execute PHP internal error handler */
1263     return true;
1264 }
1265
1266 function common_session_token()
1267 {
1268     common_ensure_session();
1269     if (!array_key_exists('token', $_SESSION)) {
1270         $_SESSION['token'] = common_good_rand(64);
1271     }
1272     return $_SESSION['token'];
1273 }
1274
1275 function common_cache_key($extra)
1276 {
1277     $base_key = common_config('memcached', 'base');
1278
1279     if (empty($base_key)) {
1280         $base_key = common_keyize(common_config('site', 'name'));
1281     }
1282
1283     return 'laconica:' . $base_key . ':' . $extra;
1284 }
1285
1286 function common_keyize($str)
1287 {
1288     $str = strtolower($str);
1289     $str = preg_replace('/\s/', '_', $str);
1290     return $str;
1291 }
1292
1293 function common_memcache()
1294 {
1295     static $cache = null;
1296     if (!common_config('memcached', 'enabled')) {
1297         return null;
1298     } else {
1299         if (!$cache) {
1300             $cache = new Memcache();
1301             $servers = common_config('memcached', 'server');
1302             if (is_array($servers)) {
1303                 foreach($servers as $server) {
1304                     $cache->addServer($server);
1305                 }
1306             } else {
1307                 $cache->addServer($servers);
1308             }
1309         }
1310         return $cache;
1311     }
1312 }
1313
1314 function common_compatible_license($from, $to)
1315 {
1316     // XXX: better compatibility check needed here!
1317     return ($from == $to);
1318 }
1319
1320 /**
1321  * returns a quoted table name, if required according to config
1322  */
1323 function common_database_tablename($tablename)
1324 {
1325
1326   if(common_config('db','quote_identifiers')) {
1327       $tablename = '"'. $tablename .'"';
1328   }
1329   //table prefixes could be added here later
1330   return $tablename;
1331 }
1332
1333 function common_shorten_url($long_url)
1334 {
1335     $user = common_current_user();
1336     if (empty($user)) {
1337         // common current user does not find a user when called from the XMPP daemon
1338         // therefore we'll set one here fix, so that XMPP given URLs may be shortened
1339         $svc = 'ur1.ca';
1340     } else {
1341         $svc = $user->urlshorteningservice;
1342     }
1343
1344     $curlh = curl_init();
1345     curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
1346     curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
1347     curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
1348
1349     switch($svc) {
1350      case 'ur1.ca':
1351         require_once INSTALLDIR.'/lib/Shorturl_api.php';
1352         $short_url_service = new LilUrl;
1353         $short_url = $short_url_service->shorten($long_url);
1354         break;
1355
1356      case '2tu.us':
1357         $short_url_service = new TightUrl;
1358         require_once INSTALLDIR.'/lib/Shorturl_api.php';
1359         $short_url = $short_url_service->shorten($long_url);
1360         break;
1361
1362      case 'ptiturl.com':
1363         require_once INSTALLDIR.'/lib/Shorturl_api.php';
1364         $short_url_service = new PtitUrl;
1365         $short_url = $short_url_service->shorten($long_url);
1366         break;
1367
1368      case 'bit.ly':
1369         curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url));
1370         $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
1371         break;
1372
1373      case 'is.gd':
1374         curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url));
1375         $short_url = curl_exec($curlh);
1376         break;
1377      case 'snipr.com':
1378         curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url));
1379         $short_url = curl_exec($curlh);
1380         break;
1381      case 'metamark.net':
1382         curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url));
1383         $short_url = curl_exec($curlh);
1384         break;
1385      case 'tinyurl.com':
1386         curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url));
1387         $short_url = curl_exec($curlh);
1388         break;
1389      default:
1390         $short_url = false;
1391     }
1392
1393     curl_close($curlh);
1394
1395     return $short_url;
1396 }
1397
1398 function common_client_ip()
1399 {
1400     if (!isset($_SERVER) || !array_key_exists('REQUEST_METHOD', $_SERVER)) {
1401         return null;
1402     }
1403
1404     if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {
1405         if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
1406             $proxy = $_SERVER['HTTP_CLIENT_IP'];
1407         } else {
1408             $proxy = $_SERVER['REMOTE_ADDR'];
1409         }
1410         $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
1411     } else {
1412         $proxy = null;
1413         if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) {
1414             $ip = $_SERVER['HTTP_CLIENT_IP'];
1415         } else {
1416             $ip = $_SERVER['REMOTE_ADDR'];
1417         }
1418     }
1419
1420     return array($proxy, $ip);
1421 }