]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - lib/util.php
don't do an end tag for empty elements
[quix0rs-gnu-social.git] / lib / util.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, Controlez-Vous, 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, HTML, user, files) */
21
22 # Show a server error
23
24 function common_server_error($msg, $code=500) {
25         static $status = array(500 => 'Internal Server Error',
26                                                    501 => 'Not Implemented',
27                                                    502 => 'Bad Gateway',
28                                                    503 => 'Service Unavailable',
29                                                    504 => 'Gateway Timeout',
30                                                    505 => 'HTTP Version Not Supported');
31
32         if (!array_key_exists($code, $status)) {
33                 $code = 500;
34         }
35
36         $status_string = $status[$code];
37
38         header('HTTP/1.1 '.$code.' '.$status_string);
39         header('Content-type: text/plain');
40
41         print $msg;
42         print "\n";
43         exit();
44 }
45
46 # Show a user error
47 function common_user_error($msg, $code=400) {
48         static $status = array(400 => 'Bad Request',
49                                                    401 => 'Unauthorized',
50                                                    402 => 'Payment Required',
51                                                    403 => 'Forbidden',
52                                                    404 => 'Not Found',
53                                                    405 => 'Method Not Allowed',
54                                                    406 => 'Not Acceptable',
55                                                    407 => 'Proxy Authentication Required',
56                                                    408 => 'Request Timeout',
57                                                    409 => 'Conflict',
58                                                    410 => 'Gone',
59                                                    411 => 'Length Required',
60                                                    412 => 'Precondition Failed',
61                                                    413 => 'Request Entity Too Large',
62                                                    414 => 'Request-URI Too Long',
63                                                    415 => 'Unsupported Media Type',
64                                                    416 => 'Requested Range Not Satisfiable',
65                                                    417 => 'Expectation Failed');
66
67         if (!array_key_exists($code, $status)) {
68                 $code = 400;
69         }
70
71         $status_string = $status[$code];
72
73         header('HTTP/1.1 '.$code.' '.$status_string);
74
75         common_show_header('Error');
76         common_element('div', array('class' => 'error'), $msg);
77         common_show_footer();
78 }
79
80 $xw = null;
81
82 # Start an HTML element
83 function common_element_start($tag, $attrs=NULL) {
84         global $xw;
85         $xw->startElement($tag);
86         if (is_array($attrs)) {
87                 foreach ($attrs as $name => $value) {
88                         $xw->writeAttribute($name, $value);
89                 }
90         } else if (is_string($attrs)) {
91                 $xw->writeAttribute('class', $attrs);
92         }
93 }
94
95 function common_element_end($tag) {
96         static $empty_tag = array('base', 'meta', 'link', 'hr',
97                                                           'br', 'param', 'img', 'area',
98                                                           'input', 'col'); 
99         global $xw;
100         # TODO check namespace
101         if (in_array($tag, $empty_tag)) {
102                 $xw->endElement();
103         } else {
104                 $xw->fullEndElement();
105         }
106 }
107
108 function common_element($tag, $attrs=NULL, $content=NULL) {
109     common_element_start($tag, $attrs);
110     global $xw;
111     $xw->text($content);
112     common_element_end($tag);
113 }
114
115 function common_start_xml($doc=NULL, $public=NULL, $system=NULL) {
116         global $xw;
117         $xw = new XMLWriter();
118         $xw->openURI('php://output');
119         $xw->setIndent(true);
120         $xw->startDocument('1.0', 'UTF-8');
121         if ($doc) {
122                 $xw->writeDTD($doc, $public, $system);
123         }
124 }
125
126 function common_end_xml() {
127         global $xw;
128         $xw->endDocument();
129         $xw->flush();
130 }
131
132 define('PAGE_TYPE_PREFS', 'text/html,application/xhtml+xml,application/xml;q=0.3,text/xml;q=0.2');
133
134 function common_show_header($pagetitle, $callable=NULL, $data=NULL, $headercall=NULL) {
135         global $config, $xw;
136
137         $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : NULL;
138
139         # XXX: allow content negotiation for RDF, RSS, or XRDS
140
141         $type = common_negotiate_type(common_accept_to_prefs($httpaccept),
142                                                                   common_accept_to_prefs(PAGE_TYPE_PREFS));
143
144         if (!$type) {
145                 common_user_error(_t('This page is not available in a media type you accept'), 406);
146                 exit(0);
147         }
148
149         header('Content-Type: '.$type);
150
151         common_start_xml('html',
152                                          '-//W3C//DTD XHTML 1.0 Strict//EN',
153                                          'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
154
155         # FIXME: correct language for interface
156
157         common_element_start('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
158                                                                            'xml:lang' => 'en',
159                                                                            'lang' => 'en'));
160
161         common_element_start('head');
162         common_element('title', NULL,
163                                    $pagetitle . " - " . $config['site']['name']);
164         common_element('link', array('rel' => 'stylesheet',
165                                                                  'type' => 'text/css',
166                                                                  'href' => theme_path('display.css'),
167                                                                  'media' => 'screen, projection, tv'));
168         foreach (array(6,7) as $ver) {
169                 if (file_exists(theme_file('ie'.$ver.'.css'))) {
170                         # Yes, IE people should be put in jail.
171                         $xw->writeComment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
172                                                           'href="'.theme_path('ie'.$ver.'.css').'" /><![endif]');
173                 }
174         }
175
176         common_element('script', array('type' => 'text/javascript',
177                                                                    'src' => common_path('js/jquery.min.js')),
178                                    ' ');
179         common_element('script', array('type' => 'text/javascript',
180                                                                    'src' => common_path('js/util.js')),
181                                    ' ');
182
183         if ($callable) {
184                 if ($data) {
185                         call_user_func($callable, $data);
186                 } else {
187                         call_user_func($callable);
188                 }
189         }
190         common_element_end('head');
191         common_element_start('body');
192         common_element_start('div', array('id' => 'wrap'));
193         common_element_start('div', array('id' => 'header'));
194         common_nav_menu();
195         if ((is_string($config['site']['logo']) && (strlen($config['site']['logo']) > 0))
196                 || file_exists(theme_file('logo.png')))
197         {
198                 common_element_start('a', array('href' => common_local_url('public')));
199                 common_element('img', array('src' => ($config['site']['logo']) ?
200                                                                         ($config['site']['logo']) : theme_path('logo.png'),
201                                                                         'alt' => $config['site']['name'],
202                                                                         'id' => 'logo'));
203                 common_element_end('a');
204         } else {
205                 common_element_start('p', array('id' => 'branding'));
206                 common_element('a', array('href' => common_local_url('public')),
207                                            $config['site']['name']);
208                 common_element_end('p');
209         }
210
211         common_element('h1', 'pagetitle', $pagetitle);
212
213         if ($headercall) {
214                 if ($data) {
215                         call_user_func($headercall, $data);
216                 } else {
217                         call_user_func($headercall);
218                 }
219         }
220         common_element_end('div');
221         common_element_start('div', array('id' => 'content'));
222 }
223
224 function common_show_footer() {
225         global $xw, $config;
226         common_element_end('div'); # content div
227         common_foot_menu();
228         common_element_start('div', array('id' => 'footer'));
229         common_element_start('div', 'laconica');
230         if (common_config('site', 'broughtby')) {
231                 $instr = _t('**%%site.name%%** is a microblogging service brought to you by [%%site.broughtby%%](%%site.broughtbyurl%%). ');
232         } else {
233                 $instr = _t('**%%site.name%%** is a microblogging service. ');
234         }
235         $instr .= _t('It runs the [Laconica](http://laconi.ca/) ' .
236                          'microblogging software, version ' . LACONICA_VERSION . ', ' .
237                          'available under the ' .
238                          '[GNU Affero General Public License]' .
239                          '(http://www.fsf.org/licensing/licenses/agpl-3.0.html).');
240     $output = common_markup_to_html($instr);
241     common_raw($output);
242         common_element_end('div');
243         common_element('img', array('id' => 'cc',
244                                                                 'src' => $config['license']['image'],
245                                                                 'alt' => $config['license']['title']));
246         common_element_start('p');
247         common_text(_t('Unless otherwise specified, contents of this site are copyright by the contributors and available under the '));
248         common_element('a', array('class' => 'license',
249                                                           'rel' => 'license',
250                                                           href => $config['license']['url']),
251                                    $config['license']['title']);
252         common_text(_t('. Contributors should be attributed by full name or nickname.'));
253         common_element_end('p');
254         common_element_end('div');
255         common_element_end('div');
256         common_element_end('body');
257         common_element_end('html');
258         common_end_xml();
259 }
260
261 function common_text($txt) {
262         global $xw;
263         $xw->text($txt);
264 }
265
266 function common_raw($xml) {
267         global $xw;
268         $xw->writeRaw($xml);
269 }
270
271 function common_nav_menu() {
272         $user = common_current_user();
273         common_element_start('ul', array('id' => 'nav'));
274         if ($user) {
275                 common_menu_item(common_local_url('all', array('nickname' => $user->nickname)),
276                                                  _t('Home'));
277         }
278         common_menu_item(common_local_url('public'), _t('Public'));
279         common_menu_item(common_local_url('doc', array('title' => 'help')),
280                                          _t('Help'));
281         if ($user) {
282                 common_menu_item(common_local_url('profilesettings'),
283                                                  _t('Settings'));
284                 common_menu_item(common_local_url('logout'),
285                                                  _t('Logout'));
286         } else {
287                 common_menu_item(common_local_url('login'), _t('Login'));
288                 common_menu_item(common_local_url('register'), _t('Register'));
289                 common_menu_item(common_local_url('openidlogin'), _t('OpenID'));
290         }
291         common_element_end('ul');
292 }
293
294 function common_foot_menu() {
295         common_element_start('ul', array('id' => 'nav_sub'));
296         common_menu_item(common_local_url('doc', array('title' => 'about')),
297                                          _t('About'));
298         common_menu_item(common_local_url('doc', array('title' => 'faq')),
299                                          _t('FAQ'));
300         common_menu_item(common_local_url('doc', array('title' => 'privacy')),
301                                          _t('Privacy'));
302         common_menu_item(common_local_url('doc', array('title' => 'source')),
303                                          _t('Source'));
304         common_menu_item(common_local_url('doc', array('title' => 'contact')),
305                                          _t('Contact'));
306         common_element_end('ul');
307 }
308
309 function common_menu_item($url, $text, $title=NULL, $is_selected=false) {
310         $lattrs = array();
311         if ($is_selected) {
312                 $lattrs['class'] = 'current';
313         }
314         common_element_start('li', $lattrs);
315         $attrs['href'] = $url;
316         if ($title) {
317                 $attrs['title'] = $title;
318         }
319         common_element('a', $attrs, $text);
320         common_element_end('li');
321 }
322
323 function common_input($id, $label, $value=NULL,$instructions=NULL) {
324         common_element_start('p');
325         common_element('label', array('for' => $id), $label);
326         $attrs = array('name' => $id,
327                                    'type' => 'text',
328                                    'class' => 'input_text',
329                                    'id' => $id);
330         if ($value) {
331                 $attrs['value'] = htmlspecialchars($value);
332         }
333         common_element('input', $attrs);
334         if ($instructions) {
335                 common_element('span', 'input_instructions', $instructions);
336         }
337         common_element_end('p');
338 }
339
340 function common_checkbox($id, $label, $checked=false, $instructions=NULL, $value='true')
341 {
342         common_element_start('p');
343         $attrs = array('name' => $id,
344                                    'type' => 'checkbox',
345                                    'class' => 'checkbox',
346                                    'id' => $id);
347         if ($value) {
348                 $attrs['value'] = htmlspecialchars($value);
349         }
350         if ($checked) {
351                 $attrs['checked'] = 'checked';
352         }
353         common_element('input', $attrs);
354         # XXX: use a <label>
355         common_text(' ');
356         common_element('span', 'checkbox_label', $label);
357         common_text(' ');
358         if ($instructions) {
359                 common_element('span', 'input_instructions', $instructions);
360         }
361         common_element_end('p');
362 }
363
364 function common_hidden($id, $value) {
365         common_element('input', array('name' => $id,
366                                                                   'type' => 'hidden',
367                                                                   'id' => $id,
368                                                                   'value' => $value));
369 }
370
371 function common_password($id, $label, $instructions=NULL) {
372         common_element_start('p');
373         common_element('label', array('for' => $id), $label);
374         $attrs = array('name' => $id,
375                                    'type' => 'password',
376                                    'class' => 'password',
377                                    'id' => $id);
378         common_element('input', $attrs);
379         if ($instructions) {
380                 common_element('span', 'input_instructions', $instructions);
381         }
382         common_element_end('p');
383 }
384
385 function common_submit($id, $label) {
386         global $xw;
387         common_element_start('p');
388         common_element('input', array('type' => 'submit',
389                                                                   'id' => $id,
390                                                                   'name' => $id,
391                                                                   'class' => 'submit',
392                                                                   'value' => $label));
393         common_element_end('p');
394 }
395
396 function common_textarea($id, $label, $content=NULL, $instructions=NULL) {
397         common_element_start('p');
398         common_element('label', array('for' => $id), $label);
399         common_element('textarea', array('rows' => 3,
400                                                                          'cols' => 40,
401                                                                          'name' => $id,
402                                                                          'id' => $id),
403                                    ($content) ? $content : '');
404         if ($instructions) {
405                 common_element('span', 'input_instructions', $instructions);
406         }
407         common_element_end('p');
408 }
409
410 # salted, hashed passwords are stored in the DB
411
412 function common_munge_password($password, $id) {
413         return md5($password . $id);
414 }
415
416 # check if a username exists and has matching password
417 function common_check_user($nickname, $password) {
418         $user = User::staticGet('nickname', $nickname);
419         if (is_null($user)) {
420                 return false;
421         } else {
422                 return (0 == strcmp(common_munge_password($password, $user->id),
423                                                         $user->password));
424         }
425 }
426
427 # is the current user logged in?
428 function common_logged_in() {
429         return (!is_null(common_current_user()));
430 }
431
432 function common_have_session() {
433         return (0 != strcmp(session_id(), ''));
434 }
435
436 function common_ensure_session() {
437         if (!common_have_session()) {
438                 @session_start();
439         }
440 }
441
442 function common_set_user($nickname) {
443         if (is_null($nickname) && common_have_session()) {
444                 unset($_SESSION['userid']);
445                 return true;
446         } else {
447                 $user = User::staticGet('nickname', $nickname);
448                 if ($user) {
449                         common_ensure_session();
450                         $_SESSION['userid'] = $user->id;
451                         return true;
452                 } else {
453                         return false;
454                 }
455         }
456         return false;
457 }
458
459 function common_set_cookie($key, $value, $expiration=0) {
460         $path = common_config('site', 'path');
461         $server = common_config('site', 'server');
462
463         if ($path && ($path != '/')) {
464                 $cookiepath = '/' . $path . '/';
465         } else {
466                 $cookiepath = '/';
467         }
468         return setcookie($key,
469                          $value,
470                                  $expiration,
471                                          $cookiepath,
472                                      $server);
473 }
474
475 define('REMEMBERME', 'rememberme');
476 define('REMEMBERME_EXPIRY', 30 * 24 * 60 * 60);
477
478 function common_rememberme() {
479         $user = common_current_user();
480         if (!$user) {
481                 return false;
482         }
483         $rm = new Remember_me();
484         $rm->code = common_good_rand(16);
485         $rm->user_id = $user->id;
486         $result = $rm->insert();
487         if (!$result) {
488                 common_log_db_error($rm, 'INSERT', __FILE__);
489                 return false;
490         }
491         common_log(LOG_INFO, 'adding rememberme cookie for ' . $user->nickname);
492         common_set_cookie(REMEMBERME,
493                                           implode(':', array($rm->user_id, $rm->code)),
494                                           time() + REMEMBERME_EXPIRY);
495         return true;
496 }
497
498 function common_remembered_user() {
499         $user = NULL;
500         # Try to remember
501         $packed = $_COOKIE[REMEMBERME];
502         if ($packed) {
503                 list($id, $code) = explode(':', $packed);
504                 if ($id && $code) {
505                         $rm = Remember_me::staticGet($code);
506                         if ($rm && ($rm->user_id == $id)) {
507                                 $user = User::staticGet($rm->user_id);
508                                 if ($user) {
509                                         # successful!
510                                         $result = $rm->delete();
511                                         if (!$result) {
512                                                 common_log_db_error($rm, 'DELETE', __FILE__);
513                                                 $user = NULL;
514                                         } else {
515                                                 common_log(LOG_INFO, 'logging in ' . $user->nickname . ' using rememberme code ' . $rm->code);
516                                                 common_set_user($user->nickname);
517                                                 common_real_login(false);
518                                                 # We issue a new cookie, so they can log in
519                                                 # automatically again after this session
520                                                 common_rememberme();
521                                         }
522                                 }
523                         }
524                 }
525         }
526         return $user;
527 }
528
529 # must be called with a valid user!
530
531 function common_forgetme() {
532         common_set_cookie(REMEMBERME, '', 0);
533 }
534
535 # who is the current user?
536 function common_current_user() {
537         if ($_REQUEST[session_name()]) {
538                 common_ensure_session();
539                 $id = $_SESSION['userid'];
540                 if ($id) {
541                         # note: this should cache
542                         $user = User::staticGet($id);
543                         return $user;
544                 }
545         }
546         # that didn't work; try to remember
547         $user = common_remembered_user();
548         return $user;
549 }
550
551 # Logins that are 'remembered' aren't 'real' -- they're subject to
552 # cookie-stealing. So, we don't let them do certain things. New reg,
553 # OpenID, and password logins _are_ real.
554
555 function common_real_login($real=true) {
556         common_ensure_session();
557         $_SESSION['real_login'] = $real;
558 }
559
560 function common_is_real_login() {
561         return common_logged_in() && $_SESSION['real_login'];
562 }
563
564 # get canonical version of nickname for comparison
565 function common_canonical_nickname($nickname) {
566         # XXX: UTF-8 canonicalization (like combining chars)
567         return strtolower($nickname);
568 }
569
570 # get canonical version of email for comparison
571 function common_canonical_email($email) {
572         # XXX: canonicalize UTF-8
573         # XXX: lcase the domain part
574         return $email;
575 }
576
577 define('URL_REGEX', '^|[ \t\r\n])((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal):(([A-Za-z0-9$_.+!*(),;/?:@&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:@&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))');
578
579 function common_render_content($text, $notice) {
580         $r = htmlspecialchars($text);
581         $id = $notice->profile_id;
582         $r = preg_replace('@https?://\S+@', '<a href="\0" class="extlink">\0</a>', $r);
583         $r = preg_replace('/(^|\s+)@([a-z0-9]{1,64})/e', "'\\1@'.common_at_link($id, '\\2')", $r);
584         # XXX: # tags
585         # XXX: machine tags
586         return $r;
587 }
588
589 function common_at_link($sender_id, $nickname) {
590         $sender = Profile::staticGet($sender_id);
591         $recipient = common_relative_profile($sender, $nickname);
592         if ($recipient) {
593                 return '<a href="'.htmlspecialchars($recipient->profileurl).'" class="atlink">'.$nickname.'</a>';
594         } else {
595                 return $nickname;
596         }
597 }
598
599 function common_relative_profile($sender, $nickname, $dt=NULL) {
600         # Try to find profiles this profile is subscribed to that have this nickname
601         $recipient = new Profile();
602         # XXX: use a join instead of a subquery
603         $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender_id.' and subscribed = id)', 'AND');
604         $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND');
605         if ($recipient->find(TRUE)) {
606                 # XXX: should probably differentiate between profiles with
607                 # the same name by date of most recent update
608                 return $recipient;
609         }
610         # Try to find profiles that listen to this profile and that have this nickname
611         $recipient = new Profile();
612         # XXX: use a join instead of a subquery
613         $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender_id.' and subscriber = id)', 'AND');
614         $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND');
615         if ($recipient->find(TRUE)) {
616                 # XXX: should probably differentiate between profiles with
617                 # the same name by date of most recent update
618                 return $recipient;
619         }
620         # If this is a local user, try to find a local user with that nickname.
621         $sender = User::staticGet($sender->id);
622         if ($sender) {
623                 $recipient_user = User::staticGet('nickname', $nickname);
624                 if ($recipient_user) {
625                         return $recipient_user->getProfile();
626                 }
627         }
628         # Otherwise, no links. @messages from local users to remote users,
629         # or from remote users to other remote users, are just
630         # outside our ability to make intelligent guesses about
631         return NULL;
632 }
633
634 // where should the avatar go for this user?
635
636 function common_avatar_filename($id, $extension, $size=NULL, $extra=NULL) {
637         global $config;
638
639         if ($size) {
640                 return $id . '-' . $size . (($extra) ? ('-' . $extra) : '') . $extension;
641         } else {
642                 return $id . '-original' . (($extra) ? ('-' . $extra) : '') . $extension;
643         }
644 }
645
646 function common_avatar_path($filename) {
647         global $config;
648         return INSTALLDIR . '/avatar/' . $filename;
649 }
650
651 function common_avatar_url($filename) {
652         return common_path('avatar/'.$filename);
653 }
654
655 function common_avatar_display_url($avatar) {
656         $server = common_config('avatar', 'server');
657         if ($server) {
658                 return 'http://'.$server.'/'.$avatar->filename;
659         } else {
660                 return $avatar->url;
661         }
662 }
663
664 function common_default_avatar($size) {
665         static $sizenames = array(AVATAR_PROFILE_SIZE => 'profile',
666                                                           AVATAR_STREAM_SIZE => 'stream',
667                                                           AVATAR_MINI_SIZE => 'mini');
668         return theme_path('default-avatar-'.$sizenames[$size].'.png');
669 }
670
671 function common_local_url($action, $args=NULL) {
672         global $config;
673         if ($config['site']['fancy']) {
674                 return common_fancy_url($action, $args);
675         } else {
676                 return common_simple_url($action, $args);
677         }
678 }
679
680 function common_fancy_url($action, $args=NULL) {
681         switch (strtolower($action)) {
682          case 'public':
683                 if ($args && $args['page']) {
684                         return common_path('?page=' . $args['page']);
685                 } else {
686                         return common_path('');
687                 }
688          case 'publicrss':
689                 return common_path('rss');
690          case 'publicxrds':
691                 return common_path('xrds');
692          case 'doc':
693                 return common_path('doc/'.$args['title']);
694          case 'login':
695          case 'logout':
696          case 'register':
697          case 'subscribe':
698          case 'unsubscribe':
699                 return common_path('main/'.$action);
700          case 'remotesubscribe':
701                 if ($args && $args['nickname']) {
702                         return common_path('main/remote?nickname=' . $args['nickname']);
703                 } else {
704                         return common_path('main/remote');
705                 }
706          case 'openidlogin':
707                 return common_path('main/openid');
708          case 'avatar':
709          case 'password':
710                 return common_path('settings/'.$action);
711          case 'profilesettings':
712                 return common_path('settings/profile');
713          case 'openidsettings':
714                 return common_path('settings/openid');
715          case 'newnotice':
716                 return common_path('notice/new');
717          case 'shownotice':
718                 return common_path('notice/'.$args['notice']);
719          case 'xrds':
720          case 'foaf':
721                 return common_path($args['nickname'].'/'.$action);
722          case 'subscriptions':
723          case 'subscribers':
724          case 'all':
725          case 'replies':
726                 if ($args && $args['page']) {
727                         return common_path($args['nickname'].'/'.$action.'?page=' . $args['page']);
728                 } else {
729                         return common_path($args['nickname'].'/'.$action);
730                 }
731          case 'allrss':
732                 return common_path($args['nickname'].'/all/rss');
733          case 'repliesrss':
734                 return common_path($args['nickname'].'/replies/rss');
735          case 'userrss':
736                 return common_path($args['nickname'].'/rss');
737          case 'showstream':
738                 if ($args && $args['page']) {
739                         return common_path($args['nickname'].'?page=' . $args['page']);
740                 } else {
741                         return common_path($args['nickname']);
742                 }
743          case 'confirmaddress':
744                 return common_path('main/confirmaddress/'.$args['code']);
745          case 'userbyid':
746                 return common_path('user/'.$args['id']);
747          case 'recoverpassword':
748             $path = 'main/recoverpassword';
749             if ($args['code']) {
750                 $path .= '/' . $args['code'];
751                 }
752             return common_path($path);
753          case 'imsettings':
754                 return common_path('settings/im');
755          default:
756                 return common_simple_url($action, $args);
757         }
758 }
759
760 function common_simple_url($action, $args=NULL) {
761         global $config;
762         /* XXX: pretty URLs */
763         $extra = '';
764         if ($args) {
765                 foreach ($args as $key => $value) {
766                         $extra .= "&${key}=${value}";
767                 }
768         }
769         return common_path("index.php?action=${action}${extra}");
770 }
771
772 function common_path($relative) {
773         global $config;
774         $pathpart = ($config['site']['path']) ? $config['site']['path']."/" : '';
775         return "http://".$config['site']['server'].'/'.$pathpart.$relative;
776 }
777
778 function common_date_string($dt) {
779         // XXX: do some sexy date formatting
780         // return date(DATE_RFC822, $dt);
781         $t = strtotime($dt);
782         $now = time();
783         $diff = $now - $t;
784
785         if ($now < $t) { # that shouldn't happen!
786                 return common_exact_date($dt);
787         } else if ($diff < 60) {
788                 return _t('a few seconds ago');
789         } else if ($diff < 92) {
790                 return _t('about a minute ago');
791         } else if ($diff < 3300) {
792                 return _t('about ') . round($diff/60) . _t(' minutes ago');
793         } else if ($diff < 5400) {
794                 return _t('about an hour ago');
795         } else if ($diff < 22 * 3600) {
796                 return _t('about ') . round($diff/3600) . _t(' hours ago');
797         } else if ($diff < 37 * 3600) {
798                 return _t('about a day ago');
799         } else if ($diff < 24 * 24 * 3600) {
800                 return _t('about ') . round($diff/(24*3600)) . _t(' days ago');
801         } else if ($diff < 46 * 24 * 3600) {
802                 return _t('about a month ago');
803         } else if ($diff < 330 * 24 * 3600) {
804                 return _t('about ') . round($diff/(30*24*3600)) . _t(' months ago');
805         } else if ($diff < 480 * 24 * 3600) {
806                 return _t('about a year ago');
807         } else {
808                 return common_exact_date($dt);
809         }
810 }
811
812 function common_exact_date($dt) {
813         $t = strtotime($dt);
814         return date(DATE_RFC850, $t);
815 }
816
817 function common_date_w3dtf($dt) {
818         $t = strtotime($dt);
819         return date(DATE_W3C, $t);
820 }
821
822 function common_redirect($url, $code=307) {
823         static $status = array(301 => "Moved Permanently",
824                                                    302 => "Found",
825                                                    303 => "See Other",
826                                                    307 => "Temporary Redirect");
827         header("Status: ${code} $status[$code]");
828         header("Location: $url");
829
830         common_start_xml('a',
831                                          '-//W3C//DTD XHTML 1.0 Strict//EN',
832                                          'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
833         common_element('a', array('href' => $url), $url);
834         common_end_xml();
835 }
836
837 function common_save_replies($notice) {
838         # extract all @messages
839         $cnt = preg_match_all('/(?:^|\s)@([a-z0-9]{1,64})/', $notice->content, $match);
840         if (!$cnt) {
841                 return true;
842         }
843         $sender = Profile::staticGet($notice->profile_id);
844         # store replied only for first @ (what user/notice what the reply directed,
845         # we assume first @ is it)
846         for ($i=0; $i<count($match[1]); $i++) {
847                 $nickname = $match[1][$i];
848                 $recipient = common_relative_profile($sender, $nickname, $notice->created);
849                 if (!$recipient) {
850                         continue;
851                 }
852                 if ($i == 0) {
853                         $reply_for = $recipient;
854                 }
855                 $reply = new Reply();
856                 $reply->notice_id = $notice->id;
857                 $reply->profile_id = $recipient->id;
858                 if ($reply_for) {
859 #                       $recipient_notice = $reply_for->getCurrentNotice($notice->created);
860                         $recipient_notice = $reply_for->getCurrentNotice();
861                         $reply->replied_id = $recipient_notice->id;
862                 }
863                 $id = $reply->insert();
864                 if (!$id) {
865                         $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
866                         common_log(LOG_ERROR, 'DB error inserting reply: ' . $last_error->message);
867                         common_server_error('DB error inserting reply: ' . $last_error->message);
868                         return;
869                 }
870         }
871 }
872
873 function common_broadcast_notice($notice, $remote=false) {
874         if (common_config('queue', 'enabled')) {
875                 # Do it later!
876                 return common_enqueue_notice($notice);
877         } else {
878                 return common_real_broadcast($notice, $remote);
879         }
880 }
881
882 # Stick the notice on the queue
883
884 function common_enqueue_notice($notice) {
885         $qi = new Queue_item();
886         $qi->notice_id = $notice->id;
887         $qi->created = $notice->created;
888         $result = $qi->insert();
889         if (!$result) {
890             $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
891             common_log(LOG_ERROR, 'DB error inserting queue item: ' . $last_error->message);
892             return false;
893         }
894         common_log(LOG_DEBUG, 'complete queueing notice ID = ' . $notice->id);
895         return $result;
896 }
897           
898 function common_real_broadcast($notice, $remote=false) {
899         $success = true;
900         if (!$remote) {
901                 # Make sure we have the OMB stuff
902                 require_once(INSTALLDIR.'/lib/omb.php');
903                 $success = omb_broadcast_remote_subscribers($notice);
904                 if (!$success) {
905                         common_log(LOG_ERROR, 'Error in OMB broadcast for notice ' . $notice->id);
906                 }
907         }
908         if ($success) {
909                 require_once(INSTALLDIR.'/lib/jabber.php');
910                 $success = jabber_broadcast_notice($notice);
911                 if (!$success) {
912                         common_log(LOG_ERROR, 'Error in jabber broadcast for notice ' . $notice->id);
913                 }
914         }
915         // XXX: broadcast notices to SMS
916         // XXX: broadcast notices to other IM
917         return $success;
918 }
919
920 function common_broadcast_profile($profile) {
921         // XXX: optionally use a queue system like http://code.google.com/p/microapps/wiki/NQDQ
922         require_once(INSTALLDIR.'/lib/omb.php');
923         omb_broadcast_profile($profile);
924         // XXX: Other broadcasts...?
925         return true;
926 }
927
928 function common_profile_url($nickname) {
929         return common_local_url('showstream', array('nickname' => $nickname));
930 }
931
932 # Don't call if nobody's logged in
933
934 function common_notice_form($action=NULL, $content=NULL) {
935         $user = common_current_user();
936         assert(!is_null($user));
937         common_element_start('form', array('id' => 'status_form',
938                                                                            'method' => 'post',
939                                                                            'action' => common_local_url('newnotice')));
940         common_element_start('p');
941         common_element('label', array('for' => 'status_textarea',
942                                                                   'id' => 'status_label'),
943                                    _t('What\'s up, ').$user->nickname.'?');
944         common_element('span', array('id' => 'counter', 'class' => 'counter'), '140');
945         common_element('textarea', array('id' => 'status_textarea',
946                                                                          'cols' => 60,
947                                                                          'rows' => 3,
948                                                                          'name' => 'status_textarea'),
949                                    ($content) ? $content : '');
950         if ($action) {
951                 common_hidden('returnto', $action);
952         }
953         common_element('input', array('id' => 'status_submit',
954                                                                   'name' => 'status_submit',
955                                                                   'type' => 'submit',
956                                                                   'value' => _t('Send')));
957         common_element_end('p');
958         common_element_end('form');
959 }
960
961 function common_mint_tag($extra) {
962         global $config;
963         return
964           'tag:'.$config['tag']['authority'].','.
965           $config['tag']['date'].':'.$config['tag']['prefix'].$extra;
966 }
967
968 # Should make up a reasonable root URL
969
970 function common_root_url() {
971         return common_path('');
972 }
973
974 # returns $bytes bytes of random data as a hexadecimal string
975 # "good" here is a goal and not a guarantee
976
977 function common_good_rand($bytes) {
978         # XXX: use random.org...?
979         if (file_exists('/dev/urandom')) {
980                 return common_urandom($bytes);
981         } else { # FIXME: this is probably not good enough
982                 return common_mtrand($bytes);
983         }
984 }
985
986 function common_urandom($bytes) {
987         $h = fopen('/dev/urandom', 'rb');
988         # should not block
989         $src = fread($h, $bytes);
990         fclose($h);
991         $enc = '';
992         for ($i = 0; $i < $bytes; $i++) {
993                 $enc .= sprintf("%02x", (ord($src[$i])));
994         }
995         return $enc;
996 }
997
998 function common_mtrand($bytes) {
999         $enc = '';
1000         for ($i = 0; $i < $bytes; $i++) {
1001                 $enc .= sprintf("%02x", mt_rand(0, 255));
1002         }
1003         return $enc;
1004 }
1005
1006 function common_set_returnto($url) {
1007         common_ensure_session();
1008         $_SESSION['returnto'] = $url;
1009 }
1010
1011 function common_get_returnto() {
1012         common_ensure_session();
1013         return $_SESSION['returnto'];
1014 }
1015
1016 function common_timestamp() {
1017         return date('YmdHis');
1018 }
1019
1020 // XXX: set up gettext
1021
1022 function _t($str) {
1023         return $str;
1024 }
1025
1026 function common_ensure_syslog() {
1027         static $initialized = false;
1028         if (!$initialized) {
1029                 global $config;
1030                 define_syslog_variables();
1031                 openlog($config['syslog']['appname'], 0, LOG_USER);
1032                 $initialized = true;
1033         }
1034 }
1035
1036 function common_log($priority, $msg, $filename=NULL) {
1037         common_ensure_syslog();
1038         syslog($priority, $msg);
1039 }
1040
1041 function common_debug($msg, $filename=NULL) {
1042         if ($filename) {
1043                 common_log(LOG_DEBUG, basename($filename).' - '.$msg);
1044         } else {
1045                 common_log(LOG_DEBUG, $msg);
1046         }
1047 }
1048
1049 function common_log_db_error(&$object, $verb, $filename=NULL) {
1050         $objstr = common_log_objstring($object);
1051         $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
1052         common_log(LOG_ERR, $last_error->message . '(' . $verb . ' on ' . $objstr . ')', $filename);
1053 }
1054
1055 function common_log_objstring(&$object) {
1056         if (is_null($object)) {
1057                 return "NULL";
1058         }
1059         $arr = $object->toArray();
1060         $fields = array();
1061         foreach ($arr as $k => $v) {
1062                 $fields[] = "$k='$v'";
1063         }
1064         $objstring = $object->tableName() . '[' . implode(',', $fields) . ']';
1065         return $objstring;
1066 }
1067
1068 function common_valid_http_url($url) {
1069         return Validate::uri($url, array('allowed_schemes' => array('http', 'https')));
1070 }
1071
1072 function common_valid_tag($tag) {
1073         if (preg_match('/^tag:(.*?),(\d{4}(-\d{2}(-\d{2})?)?):(.*)$/', $tag, $matches)) {
1074                 return (Validate::email($matches[1]) ||
1075                                 preg_match('/^([\w-\.]+)$/', $matches[1]));
1076         }
1077         return false;
1078 }
1079
1080 # Does a little before-after block for next/prev page
1081
1082 function common_pagination($have_before, $have_after, $page, $action, $args=NULL) {
1083
1084         if ($have_before || $have_after) {
1085                 common_element_start('div', array('id' => 'pagination'));
1086                 common_element_start('ul', array('id' => 'nav_pagination'));
1087         }
1088
1089         if ($have_before) {
1090                 $pargs = array('page' => $page-1);
1091                 $newargs = ($args) ? array_merge($args,$pargs) : $pargs;
1092
1093                 common_element_start('li', 'before');
1094                 common_element('a', array('href' => common_local_url($action, $newargs)),
1095                                            _t('« After'));
1096                 common_element_end('li');
1097         }
1098
1099         if ($have_after) {
1100                 $pargs = array('page' => $page+1);
1101                 $newargs = ($args) ? array_merge($args,$pargs) : $pargs;
1102                 common_element_start('li', 'after');
1103                 common_element('a', array('href' => common_local_url($action, $newargs)),
1104                                                    _t('Before »'));
1105                 common_element_end('li');
1106         }
1107
1108         if ($have_before || $have_after) {
1109                 common_element_end('ul');
1110                 common_element_end('div');
1111         }
1112 }
1113
1114 /* Following functions are copied from MediaWiki GlobalFunctions.php
1115  * and written by Evan Prodromou. */
1116
1117 function common_accept_to_prefs($accept, $def = '*/*') {
1118         # No arg means accept anything (per HTTP spec)
1119         if(!$accept) {
1120                 return array($def => 1);
1121         }
1122
1123         $prefs = array();
1124
1125         $parts = explode(',', $accept);
1126
1127         foreach($parts as $part) {
1128                 # FIXME: doesn't deal with params like 'text/html; level=1'
1129                 @list($value, $qpart) = explode(';', $part);
1130                 $match = array();
1131                 if(!isset($qpart)) {
1132                         $prefs[$value] = 1;
1133                 } elseif(preg_match('/q\s*=\s*(\d*\.\d+)/', $qpart, $match)) {
1134                         $prefs[$value] = $match[1];
1135                 }
1136         }
1137
1138         return $prefs;
1139 }
1140
1141 function common_mime_type_match($type, $avail) {
1142         if(array_key_exists($type, $avail)) {
1143                 return $type;
1144         } else {
1145                 $parts = explode('/', $type);
1146                 if(array_key_exists($parts[0] . '/*', $avail)) {
1147                         return $parts[0] . '/*';
1148                 } elseif(array_key_exists('*/*', $avail)) {
1149                         return '*/*';
1150                 } else {
1151                         return NULL;
1152                 }
1153         }
1154 }
1155
1156 function common_negotiate_type($cprefs, $sprefs) {
1157         $combine = array();
1158
1159         foreach(array_keys($sprefs) as $type) {
1160                 $parts = explode('/', $type);
1161                 if($parts[1] != '*') {
1162                         $ckey = common_mime_type_match($type, $cprefs);
1163                         if($ckey) {
1164                                 $combine[$type] = $sprefs[$type] * $cprefs[$ckey];
1165                         }
1166                 }
1167         }
1168
1169         foreach(array_keys($cprefs) as $type) {
1170                 $parts = explode('/', $type);
1171                 if($parts[1] != '*' && !array_key_exists($type, $sprefs)) {
1172                         $skey = common_mime_type_match($type, $sprefs);
1173                         if($skey) {
1174                                 $combine[$type] = $sprefs[$skey] * $cprefs[$type];
1175                         }
1176                 }
1177         }
1178
1179         $bestq = 0;
1180         $besttype = "text/html";
1181
1182         foreach(array_keys($combine) as $type) {
1183                 if($combine[$type] > $bestq) {
1184                         $besttype = $type;
1185                         $bestq = $combine[$type];
1186                 }
1187         }
1188
1189         return $besttype;
1190 }
1191
1192 function common_config($main, $sub) {
1193         global $config;
1194         return $config[$main][$sub];
1195 }
1196
1197 function common_copy_args($from) {
1198         $to = array();
1199         $strip = get_magic_quotes_gpc();
1200         foreach ($from as $k => $v) {
1201                 $to[$k] = ($strip) ? stripslashes($v) : $v;
1202         }
1203         return $to;
1204 }
1205
1206 function common_user_uri(&$user) {
1207         return common_local_url('userbyid', array('id' => $user->id));
1208 }
1209
1210 function common_notice_uri(&$notice) {
1211         return common_local_url('shownotice',
1212                 array('notice' => $notice->id));
1213 }
1214
1215 # 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits
1216
1217 function common_confirmation_code($bits) {
1218         # 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits
1219         static $codechars = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';
1220         $chars = ceil($bits/5);
1221         $code = '';
1222         for ($i = 0; $i < $chars; $i++) {
1223                 # XXX: convert to string and back
1224                 $num = hexdec(common_good_rand(1));
1225                 # XXX: randomness is too precious to throw away almost
1226                 # 40% of the bits we get!
1227                 $code .= $codechars[$num%32];
1228         }
1229         return $code;
1230 }
1231
1232 # convert markup to HTML
1233
1234 function common_markup_to_html($c) {
1235         $c = preg_replace('/%%action.(\w+)%%/e', "common_local_url('\\1')", $c);
1236         $c = preg_replace('/%%doc.(\w+)%%/e', "common_local_url('doc', array('title'=>'\\1'))", $c);
1237         $c = preg_replace('/%%(\w+).(\w+)%%/e', 'common_config(\'\\1\', \'\\2\')', $c);
1238         return Markdown($c);
1239 }