X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=lib%2Futil.php;h=f4ee26bbf0ecde3bd6692a2ea21f5bc1e11c0d23;hb=0f9d6f4c82892875f57774f8bcfd1871f8e58cca;hp=8381bc63c0da254706de3129750fc83cbab5b2f6;hpb=82cac2be595953029e61f2e6f21122206b82314e;p=quix0rs-gnu-social.git diff --git a/lib/util.php b/lib/util.php index 8381bc63c0..13966b2dd6 100644 --- a/lib/util.php +++ b/lib/util.php @@ -19,21 +19,32 @@ /* XXX: break up into separate modules (HTTP, user, files) */ -// Show a server error - +/** + * Show a server error. + */ function common_server_error($msg, $code=500) { $err = new ServerErrorAction($msg, $code); $err->showPage(); } -// Show a user error +/** + * Show a user error. + */ function common_user_error($msg, $code=400) { $err = new ClientErrorAction($msg, $code); $err->showPage(); } +/** + * This should only be used at setup; processes switching languages + * to send text to other users should use common_switch_locale(). + * + * @param string $language Locale language code (optional; empty uses + * current user's preference or site default) + * @return mixed success + */ function common_init_locale($language=null) { if(!$language) { @@ -41,28 +52,91 @@ function common_init_locale($language=null) } putenv('LANGUAGE='.$language); putenv('LANG='.$language); - return setlocale(LC_ALL, $language . ".utf8", + $ok = setlocale(LC_ALL, $language . ".utf8", $language . ".UTF8", $language . ".utf-8", $language . ".UTF-8", $language); + + return $ok; } +/** + * Initialize locale and charset settings and gettext with our message catalog, + * using the current user's language preference or the site default. + * + * This should generally only be run at framework initialization; code switching + * languages at runtime should call common_switch_language(). + * + * @access private + */ function common_init_language() { mb_internal_encoding('UTF-8'); - // gettext seems very picky... We first need to setlocale() - // to a locale which _does_ exist on the system, and _then_ - // we can set in another locale that may not be set up - // (say, ga_ES for Galego/Galician) it seems to take it. - common_init_locale("en_US"); - // Note that this setlocale() call may "fail" but this is harmless; // gettext will still select the right language. $language = common_language(); $locale_set = common_init_locale($language); + if (!$locale_set) { + // The requested locale doesn't exist on the system. + // + // gettext seems very picky... We first need to setlocale() + // to a locale which _does_ exist on the system, and _then_ + // we can set in another locale that may not be set up + // (say, ga_ES for Galego/Galician) it seems to take it. + // + // For some reason C and POSIX which are guaranteed to work + // don't do the job. en_US.UTF-8 should be there most of the + // time, but not guaranteed. + $ok = common_init_locale("en_US"); + if (!$ok && strtolower(substr(PHP_OS, 0, 3)) != 'win') { + // Try to find a complete, working locale on Unix/Linux... + // @fixme shelling out feels awfully inefficient + // but I don't think there's a more standard way. + $all = `locale -a`; + foreach (explode("\n", $all) as $locale) { + if (preg_match('/\.utf[-_]?8$/i', $locale)) { + $ok = setlocale(LC_ALL, $locale); + if ($ok) { + break; + } + } + } + } + if (!$ok) { + common_log(LOG_ERR, "Unable to find a UTF-8 locale on this system; UI translations may not work."); + } + $locale_set = common_init_locale($language); + } + + common_init_gettext(); +} + +/** + * @access private + */ +function common_init_gettext() +{ + setlocale(LC_CTYPE, 'C'); + // So we do not have to make people install the gettext locales + $path = common_config('site','locale_path'); + bindtextdomain("statusnet", $path); + bind_textdomain_codeset("statusnet", "UTF-8"); + textdomain("statusnet"); +} + +/** + * Switch locale during runtime, and poke gettext until it cries uncle. + * Otherwise, sometimes it doesn't actually switch away from the old language. + * + * @param string $language code for locale ('en', 'fr', 'pt_BR' etc) + */ +function common_switch_locale($language=null) +{ + common_init_locale($language); + setlocale(LC_CTYPE, 'C'); // So we do not have to make people install the gettext locales $path = common_config('site','locale_path'); @@ -83,40 +157,59 @@ function common_timezone() return common_config('site', 'timezone'); } +function common_valid_language($lang) +{ + if ($lang) { + // Validate -- we don't want to end up with a bogus code + // left over from some old junk. + foreach (common_config('site', 'languages') as $code => $info) { + if ($info['lang'] == $lang) { + return true; + } + } + } + return false; +} + function common_language() { + // Allow ?uselang=xx override, very useful for debugging + // and helping translators check usage and context. + if (isset($_GET['uselang'])) { + $uselang = strval($_GET['uselang']); + if (common_valid_language($uselang)) { + return $uselang; + } + } // If there is a user logged in and they've set a language preference // then return that one... if (_have_config() && common_logged_in()) { $user = common_current_user(); - $user_language = $user->language; - - if ($user->language) { - // Validate -- we don't want to end up with a bogus code - // left over from some old junk. - foreach (common_config('site', 'languages') as $code => $info) { - if ($info['lang'] == $user_language) { - return $user_language; - } - } + + if (common_valid_language($user->language)) { + return $user->language; } } // Otherwise, find the best match for the languages requested by the // user's browser... - $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; - if (!empty($httplang)) { - $language = client_prefered_language($httplang); - if ($language) - return $language; + if (common_config('site', 'langdetect')) { + $httplang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : null; + if (!empty($httplang)) { + $language = client_prefered_language($httplang); + if ($language) + return $language; + } } // Finally, if none of the above worked, use the site's default... return common_config('site', 'language'); } -// salted, hashed passwords are stored in the DB +/** + * Salted, hashed passwords are stored in the DB. + */ function common_munge_password($password, $id) { if (is_object($id) || is_object($password)) { @@ -127,10 +220,16 @@ function common_munge_password($password, $id) return md5($password . $id); } -// check if a username exists and has matching password - +/** + * Check if a username exists and has matching password. + */ function common_check_user($nickname, $password) { + // empty nickname always unacceptable + if (empty($nickname)) { + return false; + } + $authenticatedUser = false; if (Event::handle('StartCheckPassword', array($nickname, $password, &$authenticatedUser))) { @@ -150,7 +249,9 @@ function common_check_user($nickname, $password) return $authenticatedUser; } -// is the current user logged in? +/** + * Is the current user logged in? + */ function common_logged_in() { return (!is_null(common_current_user())); @@ -196,12 +297,10 @@ function common_ensure_session() // 3) null to clear // Initialize to false; set to null if none found - $_cur = false; function common_set_user($user) { - global $_cur; if (is_null($user) && common_have_session()) { @@ -217,7 +316,11 @@ function common_set_user($user) if ($user) { if (Event::handle('StartSetUser', array(&$user))) { - if($user){ + if (!empty($user)) { + if (!$user->hasRight(Right::WEBLOGIN)) { + // TRANS: Authorisation exception thrown when a user a not allowed to login. + throw new AuthorizationException(_('Not allowed to log in.')); + } common_ensure_session(); $_SESSION['userid'] = $user->id; $_cur = $user; @@ -243,7 +346,8 @@ function common_set_cookie($key, $value, $expiration=0) $value, $expiration, $cookiepath, - $server); + $server, + common_config('site', 'ssl')=='always'); } define('REMEMBERME', 'rememberme'); @@ -287,7 +391,6 @@ function common_rememberme($user=null) function common_remembered_user() { - $user = null; $packed = isset($_COOKIE[REMEMBERME]) ? $_COOKIE[REMEMBERME] : null; @@ -349,14 +452,17 @@ function common_remembered_user() return $user; } -// must be called with a valid user! - +/** + * must be called with a valid user! + */ function common_forgetme() { common_set_cookie(REMEMBERME, '', 0); } -// who is the current user? +/** + * Who is the current user? + */ function common_current_user() { global $_cur; @@ -392,10 +498,11 @@ function common_current_user() return $_cur; } -// Logins that are 'remembered' aren't 'real' -- they're subject to -// cookie-stealing. So, we don't let them do certain things. New reg, -// OpenID, and password logins _are_ real. - +/** + * Logins that are 'remembered' aren't 'real' -- they're subject to + * cookie-stealing. So, we don't let them do certain things. New reg, + * OpenID, and password logins _are_ real. + */ function common_real_login($real=true) { common_ensure_session(); @@ -407,14 +514,52 @@ function common_is_real_login() return common_logged_in() && $_SESSION['real_login']; } -// get canonical version of nickname for comparison +/** + * Get a hash portion for HTTP caching Etags and such including + * info on the current user's session. If login/logout state changes, + * or we've changed accounts, or we've renamed the current user, + * we'll get a new hash value. + * + * This should not be considered secure information. + * + * @param User $user (optional; uses common_current_user() if left out) + * @return string + */ +function common_user_cache_hash($user=false) +{ + if ($user === false) { + $user = common_current_user(); + } + if ($user) { + return crc32($user->id . ':' . $user->nickname); + } else { + return '0'; + } +} + +/** + * get canonical version of nickname for comparison + * + * @param string $nickname + * @return string + * + * @throws NicknameException on invalid input + * @deprecated call Nickname::normalize() directly. + */ function common_canonical_nickname($nickname) { - // XXX: UTF-8 canonicalization (like combining chars) - return strtolower($nickname); + return Nickname::normalize($nickname); } -// get canonical version of email for comparison +/** + * get canonical version of email for comparison + * + * @fixme actually normalize + * @fixme reject invalid input + * + * @param string $email + * @return string + */ function common_canonical_email($email) { // XXX: canonicalize UTF-8 @@ -422,18 +567,36 @@ function common_canonical_email($email) return $email; } +/** + * Partial notice markup rendering step: build links to !group references. + * + * @param string $text partially rendered HTML + * @param Notice $notice in whose context we're working + * @return string partially rendered HTML + */ function common_render_content($text, $notice) { $r = common_render_text($text); $id = $notice->profile_id; - $r = common_linkify_mentions($id, $r); - $r = preg_replace('/(^|[\s\.\,\:\;]+)!([A-Za-z0-9]{1,64})/e', "'\\1!'.common_group_link($id, '\\2')", $r); + $r = common_linkify_mentions($r, $notice); + $r = preg_replace('/(^|[\s\.\,\:\;]+)!(' . Nickname::DISPLAY_FMT . ')/e', + "'\\1!'.common_group_link($id, '\\2')", $r); return $r; } -function common_linkify_mentions($profile_id, $text) +/** + * Finds @-mentions within the partially-rendered text section and + * turns them into live links. + * + * Should generally not be called except from common_render_content(). + * + * @param string $text partially-rendered HTML + * @param Notice $notice in-progress or complete Notice object for context + * @return string partially-rendered HTML + */ +function common_linkify_mentions($text, $notice) { - $mentions = common_find_mentions($profile_id, $text); + $mentions = common_find_mentions($text, $notice); // We need to go through in reverse order by position, // so our positions stay valid despite our fudging with the @@ -475,7 +638,7 @@ function common_linkify_mention($mention) $xs->elementStart('span', 'vcard'); $xs->elementStart('a', $attrs); - $xs->element('span', 'fn nickname', $mention['text']); + $xs->element('span', 'fn nickname mention', $mention['text']); $xs->elementEnd('a'); $xs->elementEnd('span'); @@ -487,37 +650,79 @@ function common_linkify_mention($mention) return $output; } -function common_find_mentions($profile_id, $text) +/** + * Find @-mentions in the given text, using the given notice object as context. + * References will be resolved with common_relative_profile() against the user + * who posted the notice. + * + * Note the return data format is internal, to be used for building links and + * such. Should not be used directly; rather, call common_linkify_mentions(). + * + * @param string $text + * @param Notice $notice notice in whose context we're building links + * + * @return array + * + * @access private + */ +function common_find_mentions($text, $notice) { $mentions = array(); - $sender = Profile::staticGet('id', $profile_id); + $sender = Profile::staticGet('id', $notice->profile_id); if (empty($sender)) { return $mentions; } if (Event::handle('StartFindMentions', array($sender, $text, &$mentions))) { + // Get the context of the original notice, if any + $originalAuthor = null; + $originalNotice = null; + $originalMentions = array(); - preg_match_all('/^T ([A-Z0-9]{1,64}) /', - $text, - $tmatches, - PREG_OFFSET_CAPTURE); + // Is it a reply? - preg_match_all('/(?:^|\s+)@(['.NICKNAME_FMT.']{1,64})/', - $text, - $atmatches, - PREG_OFFSET_CAPTURE); + if (!empty($notice) && !empty($notice->reply_to)) { + $originalNotice = Notice::staticGet('id', $notice->reply_to); + if (!empty($originalNotice)) { + $originalAuthor = Profile::staticGet('id', $originalNotice->profile_id); + + $ids = $originalNotice->getReplies(); + + foreach ($ids as $id) { + $repliedTo = Profile::staticGet('id', $id); + if (!empty($repliedTo)) { + $originalMentions[$repliedTo->nickname] = $repliedTo; + } + } + } + } - $matches = array_merge($tmatches[1], $atmatches[1]); + $matches = common_find_mentions_raw($text); foreach ($matches as $match) { + try { + $nickname = Nickname::normalize($match[0]); + } catch (NicknameException $e) { + // Bogus match? Drop it. + continue; + } - $nickname = common_canonical_nickname($match[0]); - $mentioned = common_relative_profile($sender, $nickname); + // Try to get a profile for this nickname. + // Start with conversation context, then go to + // sender context. - if (!empty($mentioned)) { + if (!empty($originalAuthor) && $originalAuthor->nickname == $nickname) { + $mentioned = $originalAuthor; + } else if (!empty($originalMentions) && + array_key_exists($nickname, $originalMentions)) { + $mentioned = $originalMentions[$nickname]; + } else { + $mentioned = common_relative_profile($sender, $nickname); + } + if (!empty($mentioned)) { $user = User::staticGet('id', $mentioned->id); if ($user) { @@ -549,17 +754,19 @@ function common_find_mentions($profile_id, $text) foreach ($hmatches[1] as $hmatch) { $tag = common_canonical_tag($hmatch[0]); - - $tagged = Profile_tag::getTagged($sender->id, $tag); - - $url = common_local_url('subscriptions', - array('nickname' => $sender->nickname, - 'tag' => $tag)); - - $mentions[] = array('mentioned' => $tagged, - 'text' => $hmatch[0], - 'position' => $hmatch[1], - 'url' => $url); + $plist = Profile_list::getByTaggerAndTag($sender->id, $tag); + if (!empty($plist) && !$plist->private) { + $tagged = $sender->getTaggedSubscribers($tag); + + $url = common_local_url('showprofiletag', + array('tagger' => $sender->nickname, + 'tag' => $tag)); + + $mentions[] = array('mentioned' => $tagged, + 'text' => $hmatch[0], + 'position' => $hmatch[1], + 'url' => $url); + } } Event::handle('EndFindMentions', array($sender, $text, &$mentions)); @@ -568,18 +775,50 @@ function common_find_mentions($profile_id, $text) return $mentions; } +/** + * Does the actual regex pulls to find @-mentions in text. + * Should generally not be called directly; for use in common_find_mentions. + * + * @param string $text + * @return array of PCRE match arrays + */ +function common_find_mentions_raw($text) +{ + $tmatches = array(); + preg_match_all('/^T (' . Nickname::DISPLAY_FMT . ') /', + $text, + $tmatches, + PREG_OFFSET_CAPTURE); + + $atmatches = array(); + preg_match_all('/(?:^|\s+)@(' . Nickname::DISPLAY_FMT . ')\b/', + $text, + $atmatches, + PREG_OFFSET_CAPTURE); + + $matches = array_merge($tmatches[1], $atmatches[1]); + return $matches; +} + function common_render_text($text) { $r = htmlspecialchars($text); $r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r); $r = common_replace_urls_callback($r, 'common_linkify'); - $r = preg_replace('/(^|\"\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r); + $r = preg_replace('/(^|\"\;|\'|\(|\[|\{|\s+)#([\pL\pN_\-\.]{1,64})/ue', "'\\1#'.common_tag_link('\\2')", $r); // XXX: machine tags return $r; } -function common_replace_urls_callback($text, $callback, $notice_id = null) { +/** + * Find links in the given text and pass them to the given callback function. + * + * @param string $text + * @param function($text, $arg) $callback: return replacement text + * @param mixed $arg: optional argument will be passed on to the callback + */ +function common_replace_urls_callback($text, $callback, $arg = null) { // Start off with a regex $regex = '#'. '(?:^|[\s\<\>\(\)\[\]\{\}\\\'\\\";]+)(?![\@\!\#])'. @@ -620,10 +859,21 @@ function common_replace_urls_callback($text, $callback, $notice_id = null) { '#ixu'; //preg_match_all($regex,$text,$matches); //print_r($matches); - return preg_replace_callback($regex, curry('callback_helper',$callback,$notice_id) ,$text); + return preg_replace_callback($regex, curry('callback_helper',$callback,$arg) ,$text); } -function callback_helper($matches, $callback, $notice_id) { +/** + * Intermediate callback for common_replace_links(), helps resolve some + * ambiguous link forms before passing on to the final callback. + * + * @param array $matches + * @param callable $callback + * @param mixed $arg optional argument to pass on as second param to callback + * @return string + * + * @access private + */ +function callback_helper($matches, $callback, $arg=null) { $url=$matches[1]; $left = strpos($matches[0],$url); $right = $left+strlen($url); @@ -666,11 +916,7 @@ function callback_helper($matches, $callback, $notice_id) { } }while($original_url!=$url); - if(empty($notice_id)){ - $result = call_user_func_array($callback, array($url)); - }else{ - $result = call_user_func_array($callback, array(array($url,$notice_id)) ); - } + $result = call_user_func_array($callback, array($url, $arg)); return substr($matches[0],0,$left) . $result . substr($matches[0],$right); } @@ -698,24 +944,28 @@ function common_linkify($url) { // functions $url = htmlspecialchars_decode($url); - if(strpos($url, '@') !== false && strpos($url, ':') === false) { - //url is an email address without the mailto: protocol - $canon = "mailto:$url"; - $longurl = "mailto:$url"; - }else{ + if (strpos($url, '@') !== false && strpos($url, ':') === false && Validate::email($url)) { + //url is an email address without the mailto: protocol + $canon = "mailto:$url"; + $longurl = "mailto:$url"; + } else { $canon = File_redirection::_canonUrl($url); - $longurl_data = File_redirection::where($canon); + $longurl_data = File_redirection::where($canon, common_config('attachments', 'process_links')); if (is_array($longurl_data)) { $longurl = $longurl_data['url']; } elseif (is_string($longurl_data)) { $longurl = $longurl_data; } else { - throw new ServerException("Can't linkify url '$url'"); + // Unable to reach the server to verify contents, etc + // Just pass the link on through for now. + common_log(LOG_ERR, "Can't linkify url '$url'"); + $longurl = $url; } } - $attrs = array('href' => $canon, 'title' => $longurl, 'rel' => 'external'); + + $attrs = array('href' => $canon, 'title' => $longurl); $is_attachment = false; $attachment_id = null; @@ -726,25 +976,20 @@ function common_linkify($url) { $f = File::staticGet('url', $longurl); if (empty($f)) { - // XXX: this writes to the database. :< - $f = File::processNew($longurl); + if (common_config('attachments', 'process_links')) { + // XXX: this writes to the database. :< + $f = File::processNew($longurl); + } } if (!empty($f)) { - if ($f->isEnclosure()) { + if ($f->getEnclosure()) { $is_attachment = true; $attachment_id = $f->id; - } else { - $foe = File_oembed::staticGet('file_id', $f->id); - if (!empty($foe)) { - // if it has OEmbed info, it's an attachment, too - $is_attachment = true; - $attachment_id = $f->id; - - $thumb = File_thumbnail::staticGet('file_id', $f->id); - if (!empty($thumb)) { - $has_thumb = true; - } + + $thumb = File_thumbnail::staticGet('file_id', $f->id); + if (!empty($thumb)) { + $has_thumb = true; } } } @@ -758,26 +1003,113 @@ function common_linkify($url) { $attrs['id'] = "attachment-{$attachment_id}"; } + // Whether to nofollow + + $nf = common_config('nofollow', 'external'); + + if ($nf == 'never') { + $attrs['rel'] = 'external'; + } else { + $attrs['rel'] = 'nofollow external'; + } + return XMLStringer::estring('a', $attrs, $url); } -function common_shorten_links($text) +/** + * Find and shorten links in a given chunk of text if it's longer than the + * configured notice content limit (or unconditionally). + * + * Side effects: may save file and file_redirection records for referenced URLs. + * + * Pass the $user option or call $user->shortenLinks($text) to ensure the proper + * user's options are used; otherwise the current web session user's setitngs + * will be used or ur1.ca if there is no active web login. + * + * @param string $text + * @param boolean $always (optional) + * @param User $user (optional) + * + * @return string + */ +function common_shorten_links($text, $always = false, User $user=null) +{ + $user = common_current_user(); + + $maxLength = User_urlshortener_prefs::maxNoticeLength($user); + + if ($always || mb_strlen($text) > $maxLength) { + return common_replace_urls_callback($text, array('File_redirection', 'forceShort'), $user); + } else { + return common_replace_urls_callback($text, array('File_redirection', 'makeShort'), $user); + } +} + +/** + * Very basic stripping of invalid UTF-8 input text. + * + * @param string $str + * @return mixed string or null if invalid input + * + * @todo ideally we should drop bad chars, and maybe do some of the checks + * from common_xml_safe_str. But we can't strip newlines, etc. + * @todo Unicode normalization might also be useful, but not needed now. + */ +function common_validate_utf8($str) { - $maxLength = Notice::maxContent(); - if ($maxLength == 0 || mb_strlen($text) <= $maxLength) return $text; - return common_replace_urls_callback($text, array('File_redirection', 'makeShort')); + // preg_replace will return NULL on invalid UTF-8 input. + // + // Note: empty regex //u also caused NULL return on some + // production machines, but none of our test machines. + // + // This should be replaced with a more reliable check. + return preg_replace('/\x00/u', '', $str); } +/** + * Make sure an arbitrary string is safe for output in XML as a single line. + * + * @param string $str + * @return string + */ function common_xml_safe_str($str) { - // Neutralize control codes and surrogates - return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str); + // Replace common eol and extra whitespace input chars + $unWelcome = array( + "\t", // tab + "\n", // newline + "\r", // cr + "\0", // null byte eos + "\x0B" // vertical tab + ); + + $replacement = array( + ' ', // single space + ' ', + '', // nothing + '', + ' ' + ); + + $str = str_replace($unWelcome, $replacement, $str); + + // Neutralize any additional control codes and UTF-16 surrogates + // (Twitter uses '*') + return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str); } function common_tag_link($tag) { $canonical = common_canonical_tag($tag); - $url = common_local_url('tag', array('tag' => $canonical)); + if (common_config('singleuser', 'enabled')) { + // regular TagAction isn't set up in 1user mode + $nickname = User::singleUserNickname(); + $url = common_local_url('showstream', + array('nickname' => $nickname, + 'tag' => $canonical)); + } else { + $url = common_local_url('tag', array('tag' => $canonical)); + } $xs = new XMLStringer(); $xs->elementStart('span', 'tag'); $xs->element('a', array('href' => $url, @@ -798,20 +1130,27 @@ function common_valid_profile_tag($str) return preg_match('/^[A-Za-z0-9_\-\.]{1,64}$/', $str); } +/** + * + * @param $sender_id + * @param $nickname + * @return + * @access private + */ function common_group_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); - $group = User_group::getForNickname($nickname); + $group = User_group::getForNickname($nickname, $sender); if ($sender && $group && $sender->isMember($group)) { $attrs = array('href' => $group->permalink(), 'class' => 'url'); if (!empty($group->fullname)) { - $attrs['title'] = $group->fullname . ' (' . $group->nickname . ')'; + $attrs['title'] = $group->getFancyName(); } $xs = new XMLStringer(); $xs->elementStart('span', 'vcard'); $xs->elementStart('a', $attrs); - $xs->element('span', 'fn nickname', $nickname); + $xs->element('span', 'fn nickname group', $nickname); $xs->elementEnd('a'); $xs->elementEnd('span'); return $xs->getString(); @@ -820,13 +1159,37 @@ function common_group_link($sender_id, $nickname) } } +/** + * Resolve an ambiguous profile nickname reference, checking in following order: + * - profiles that $sender subscribes to + * - profiles that subscribe to $sender + * - local user profiles + * + * WARNING: does not validate or normalize $nickname -- MUST BE PRE-VALIDATED + * OR THERE MAY BE A RISK OF SQL INJECTION ATTACKS. THIS FUNCTION DOES NOT + * ESCAPE SQL. + * + * @fixme validate input + * @fixme escape SQL + * @fixme fix or remove mystery third parameter + * @fixme is $sender a User or Profile? + * + * @param $sender the user or profile in whose context we're looking + * @param string $nickname validated nickname of + * @param $dt unused mystery parameter; in Notice reply-to handling a timestamp is passed. + * + * @return Profile or null + */ function common_relative_profile($sender, $nickname, $dt=null) { + // Will throw exception on invalid input. + $nickname = Nickname::normalize($nickname); + // Try to find profiles this profile is subscribed to that have this nickname $recipient = new Profile(); // XXX: use a join instead of a subquery - $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender->id.' and subscribed = id)', 'AND'); - $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND'); + $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.intval($sender->id).' and subscribed = id)', 'AND'); + $recipient->whereAdd("nickname = '" . $recipient->escape($nickname) . "'", 'AND'); if ($recipient->find(true)) { // XXX: should probably differentiate between profiles with // the same name by date of most recent update @@ -835,8 +1198,8 @@ function common_relative_profile($sender, $nickname, $dt=null) // Try to find profiles that listen to this profile and that have this nickname $recipient = new Profile(); // XXX: use a join instead of a subquery - $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender->id.' and subscriber = id)', 'AND'); - $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND'); + $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.intval($sender->id).' and subscriber = id)', 'AND'); + $recipient->whereAdd("nickname = '" . $recipient->escape($nickname) . "'", 'AND'); if ($recipient->find(true)) { // XXX: should probably differentiate between profiles with // the same name by date of most recent update @@ -856,29 +1219,41 @@ function common_relative_profile($sender, $nickname, $dt=null) return null; } -function common_local_url($action, $args=null, $params=null, $fragment=null) +function common_local_url($action, $args=null, $params=null, $fragment=null, $addSession=true) { - $r = Router::get(); - $path = $r->build($action, $args, $params, $fragment); + if (Event::handle('StartLocalURL', array(&$action, &$params, &$fragment, &$addSession, &$url))) { + $r = Router::get(); + $path = $r->build($action, $args, $params, $fragment); - $ssl = common_is_sensitive($action); + $ssl = common_is_sensitive($action); - if (common_config('site','fancy')) { - $url = common_path(mb_substr($path, 1), $ssl); - } else { - if (mb_strpos($path, '/index.php') === 0) { - $url = common_path(mb_substr($path, 1), $ssl); + if (common_config('site','fancy')) { + $url = common_path(mb_substr($path, 1), $ssl, $addSession); } else { - $url = common_path('index.php'.$path, $ssl); + if (mb_strpos($path, '/index.php') === 0) { + $url = common_path(mb_substr($path, 1), $ssl, $addSession); + } else { + $url = common_path('index.php'.$path, $ssl, $addSession); + } } + Event::handle('EndLocalURL', array(&$action, &$params, &$fragment, &$addSession, &$url)); } return $url; } function common_is_sensitive($action) { - static $sensitive = array('login', 'register', 'passwordsettings', - 'twittersettings', 'api'); + static $sensitive = array( + 'login', + 'register', + 'passwordsettings', + 'api', + 'ApiOauthRequestToken', + 'ApiOauthAccessToken', + 'ApiOauthAuthorize', + 'ApiOauthPin', + 'showapplication' + ); $ssl = null; if (Event::handle('SensitiveAction', array($action, &$ssl))) { @@ -888,7 +1263,7 @@ function common_is_sensitive($action) return $ssl; } -function common_path($relative, $ssl=false) +function common_path($relative, $ssl=false, $addSession=true) { $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : ''; @@ -912,7 +1287,9 @@ function common_path($relative, $ssl=false) } } - $relative = common_inject_session($relative, $serverpart); + if ($addSession) { + $relative = common_inject_session($relative, $serverpart); + } return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } @@ -956,24 +1333,38 @@ function common_date_string($dt) if ($now < $t) { // that shouldn't happen! return common_exact_date($dt); } else if ($diff < 60) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('a few seconds ago'); } else if ($diff < 92) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('about a minute ago'); } else if ($diff < 3300) { - return sprintf(_('about %d minutes ago'), round($diff/60)); + $minutes = round($diff/60); + // TRANS: Used in notices to indicate when the notice was made compared to now. + return sprintf( _m('about one minute ago', 'about %d minutes ago', $minutes), $minutes); } else if ($diff < 5400) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('about an hour ago'); } else if ($diff < 22 * 3600) { - return sprintf(_('about %d hours ago'), round($diff/3600)); + $hours = round($diff/3600); + // TRANS: Used in notices to indicate when the notice was made compared to now. + return sprintf( _m('about one hour ago', 'about %d hours ago', $hours), $hours); } else if ($diff < 37 * 3600) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('about a day ago'); } else if ($diff < 24 * 24 * 3600) { - return sprintf(_('about %d days ago'), round($diff/(24*3600))); + $days = round($diff/(24*3600)); + // TRANS: Used in notices to indicate when the notice was made compared to now. + return sprintf( _m('about one day ago', 'about %d days ago', $days), $days); } else if ($diff < 46 * 24 * 3600) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('about a month ago'); } else if ($diff < 330 * 24 * 3600) { - return sprintf(_('about %d months ago'), round($diff/(30*24*3600))); + $months = round($diff/(30*24*3600)); + // TRANS: Used in notices to indicate when the notice was made compared to now. + return sprintf( _m('about one month ago', 'about %d months ago',$months), $months); } else if ($diff < 480 * 24 * 3600) { + // TRANS: Used in notices to indicate when the notice was made compared to now. return _('about a year ago'); } else { return common_exact_date($dt); @@ -1069,11 +1460,6 @@ function common_redirect($url, $code=307) exit; } -function common_broadcast_notice($notice, $remote=false) -{ - // DO NOTHING! -} - // Stick the notice on the queue function common_enqueue_notice($notice) @@ -1089,19 +1475,9 @@ function common_enqueue_notice($notice) $transports[] = 'plugin'; } - $xmpp = common_config('xmpp', 'enabled'); - - if ($xmpp) { - $transports[] = 'jabber'; - } - - // @fixme move these checks into QueueManager and/or individual handlers - if ($notice->is_local == Notice::LOCAL_PUBLIC || - $notice->is_local == Notice::LOCAL_NONPUBLIC) { + // We can skip these for gatewayed notices. + if ($notice->isLocal()) { $transports = array_merge($transports, $localTransports); - if ($xmpp) { - $transports[] = 'public'; - } } if (Event::handle('StartEnqueueNotice', array($notice, &$transports))) { @@ -1134,14 +1510,16 @@ function common_broadcast_profile(Profile $profile) function common_profile_url($nickname) { - return common_local_url('showstream', array('nickname' => $nickname)); + return common_local_url('showstream', array('nickname' => $nickname), + null, null, false); } -// Should make up a reasonable root URL - +/** + * Should make up a reasonable root URL + */ function common_root_url($ssl=false) { - $url = common_path('', $ssl); + $url = common_path('', $ssl, false); $i = strpos($url, '?'); if ($i !== false) { $url = substr($url, 0, $i); @@ -1149,9 +1527,10 @@ function common_root_url($ssl=false) return $url; } -// returns $bytes bytes of random data as a hexadecimal string -// "good" here is a goal and not a guarantee - +/** + * returns $bytes bytes of random data as a hexadecimal string + * "good" here is a goal and not a guarantee + */ function common_good_rand($bytes) { // XXX: use random.org...? @@ -1184,12 +1563,38 @@ function common_mtrand($bytes) return $enc; } +/** + * Record the given URL as the return destination for a future + * form submission, to be read by common_get_returnto(). + * + * @param string $url + * + * @fixme as a session-global setting, this can allow multiple forms + * to conflict and overwrite each others' returnto destinations if + * the user has multiple tabs or windows open. + * + * Should refactor to index with a token or otherwise only pass the + * data along its intended path. + */ function common_set_returnto($url) { common_ensure_session(); $_SESSION['returnto'] = $url; } +/** + * Fetch a return-destination URL previously recorded by + * common_set_returnto(). + * + * @return mixed URL string or null + * + * @fixme as a session-global setting, this can allow multiple forms + * to conflict and overwrite each others' returnto destinations if + * the user has multiple tabs or windows open. + * + * Should refactor to index with a token or otherwise only pass the + * data along its intended path. + */ function common_get_returnto() { common_ensure_session(); @@ -1215,7 +1620,7 @@ function common_log_line($priority, $msg) { static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR', 'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG'); - return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n"; + return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . PHP_EOL; } function common_request_id() @@ -1241,6 +1646,7 @@ function common_request_id() function common_log($priority, $msg, $filename=null) { if(Event::handle('StartLog', array(&$priority, &$msg, &$filename))){ + $msg = (empty($filename)) ? $msg : basename($filename) . ' - ' . $msg; $msg = '[' . common_request_id() . '] ' . $msg; $logfile = common_config('site', 'logfile'); if ($logfile) { @@ -1271,7 +1677,12 @@ function common_log_db_error(&$object, $verb, $filename=null) { $objstr = common_log_objstring($object); $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - common_log(LOG_ERR, $last_error->message . '(' . $verb . ' on ' . $objstr . ')', $filename); + if (is_object($last_error)) { + $msg = $last_error->message; + } else { + $msg = 'Unknown error (' . var_export($last_error, true) . ')'; + } + common_log(LOG_ERR, $msg . '(' . $verb . ' on ' . $objstr . ')', $filename); } function common_log_objstring(&$object) @@ -1309,6 +1720,55 @@ function common_valid_tag($tag) return false; } +/** + * Determine if given domain or address literal is valid + * eg for use in JIDs and URLs. Does not check if the domain + * exists! + * + * @param string $domain + * @return boolean valid or not + */ +function common_valid_domain($domain) +{ + $octet = "(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])"; + $ipv4 = "(?:$octet(?:\.$octet){3})"; + if (preg_match("/^$ipv4$/u", $domain)) return true; + + $group = "(?:[0-9a-f]{1,4})"; + $ipv6 = "(?:\[($group(?::$group){0,7})?(::)?($group(?::$group){0,7})?\])"; // http://tools.ietf.org/html/rfc3513#section-2.2 + + if (preg_match("/^$ipv6$/ui", $domain, $matches)) { + $before = explode(":", $matches[1]); + $zeroes = $matches[2]; + $after = explode(":", $matches[3]); + if ($zeroes) { + $min = 0; + $max = 7; + } else { + $min = 1; + $max = 8; + } + $explicit = count($before) + count($after); + if ($explicit < $min || $explicit > $max) { + return false; + } + return true; + } + + try { + require_once "Net/IDNA.php"; + $idn = Net_IDNA::getInstance(); + $domain = $idn->encode($domain); + } catch (Exception $e) { + return false; + } + + $subdomain = "(?:[a-z0-9][a-z0-9-]*)"; // @fixme + $fqdn = "(?:$subdomain(?:\.$subdomain)*\.?)"; + + return preg_match("/^$fqdn$/ui", $domain); +} + /* Following functions are copied from MediaWiki GlobalFunctions.php * and written by Evan Prodromou. */ @@ -1400,12 +1860,26 @@ function common_config($main, $sub) array_key_exists($sub, $config[$main])) ? $config[$main][$sub] : false; } +/** + * Pull arguments from a GET/POST/REQUEST array with first-level input checks: + * strips "magic quotes" slashes if necessary, and kills invalid UTF-8 strings. + * + * @param array $from + * @return array + */ function common_copy_args($from) { $to = array(); $strip = get_magic_quotes_gpc(); foreach ($from as $k => $v) { - $to[$k] = ($strip) ? stripslashes($v) : $v; + if(is_array($v)) { + $to[$k] = common_copy_args($v); + } else { + if ($strip) { + $v = stripslashes($v); + } + $to[$k] = strval(common_validate_utf8($v)); + } } return $to; } @@ -1426,13 +1900,15 @@ function common_remove_magic_from_request() function common_user_uri(&$user) { - return common_local_url('userbyid', array('id' => $user->id)); + return common_local_url('userbyid', array('id' => $user->id), + null, null, false); } function common_notice_uri(&$notice) { return common_local_url('shownotice', - array('notice' => $notice->id)); + array('notice' => $notice->id), + null, null, false); } // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits @@ -1540,21 +2016,6 @@ function common_session_token() return $_SESSION['token']; } -function common_cache_key($extra) -{ - return Cache::key($extra); -} - -function common_keyize($str) -{ - return Cache::keyize($str); -} - -function common_memcache() -{ - return Cache::instance(); -} - function common_license_terms($uri) { if(preg_match('/creativecommons.org\/licenses\/([^\/]+)/', $uri, $matches)) { @@ -1585,7 +2046,6 @@ function common_compatible_license($from, $to) */ function common_database_tablename($tablename) { - if(common_config('db','quote_identifiers')) { $tablename = '"'. $tablename .'"'; } @@ -1596,30 +2056,46 @@ function common_database_tablename($tablename) /** * Shorten a URL with the current user's configured shortening service, * or ur1.ca if configured, or not at all if no shortening is set up. - * Length is not considered. * - * @param string $long_url + * @param string $long_url original URL + * @param User $user to specify a particular user's options + * @param boolean $force Force shortening (used when notice is too long) * @return string may return the original URL if shortening failed * * @fixme provide a way to specify a particular shortener - * @fixme provide a way to specify to use a given user's shortening preferences */ -function common_shorten_url($long_url) +function common_shorten_url($long_url, User $user=null, $force = false) { $long_url = trim($long_url); + $user = common_current_user(); - if (empty($user)) { - // common current user does not find a user when called from the XMPP daemon - // therefore we'll set one here fix, so that XMPP given URLs may be shortened - $shortenerName = 'ur1.ca'; - } else { - $shortenerName = $user->urlshorteningservice; - } - if(Event::handle('StartShortenUrl', array($long_url,$shortenerName,&$shortenedUrl))){ - //URL wasn't shortened, so return the long url + $maxUrlLength = User_urlshortener_prefs::maxUrlLength($user); + + // $force forces shortening even if it's not strictly needed + // I doubt URL shortening is ever 'strictly' needed. - ESP + + if (mb_strlen($long_url) < $maxUrlLength && !$force) { return $long_url; - }else{ + } + + $shortenerName = User_urlshortener_prefs::urlShorteningService($user); + + if (Event::handle('StartShortenUrl', + array($long_url, $shortenerName, &$shortenedUrl))) { + if ($shortenerName == 'internal') { + $f = File::processNew($long_url); + if (empty($f)) { + return $long_url; + } else { + $shortenedUrl = common_local_url('redirecturl', + array('id' => $f->id)); + return $shortenedUrl; + } + } else { + return $long_url; + } + } else { //URL was shortened, so return the result return trim($shortenedUrl); } @@ -1666,7 +2142,7 @@ function common_url_to_nickname($url) $parts = parse_url($url); - # If any of these parts exist, this won't work + // If any of these parts exist, this won't work foreach ($bad as $badpart) { if (array_key_exists($badpart, $parts)) { @@ -1674,15 +2150,15 @@ function common_url_to_nickname($url) } } - # We just have host and/or path + // We just have host and/or path - # If it's just a host... + // If it's just a host... if (array_key_exists('host', $parts) && (!array_key_exists('path', $parts) || strcmp($parts['path'], '/') == 0)) { $hostparts = explode('.', $parts['host']); - # Try to catch common idiom of nickname.service.tld + // Try to catch common idiom of nickname.service.tld if ((count($hostparts) > 2) && (strlen($hostparts[count($hostparts) - 2]) > 3) && # try to skip .co.uk, .com.au @@ -1690,15 +2166,24 @@ function common_url_to_nickname($url) { return common_nicknamize($hostparts[0]); } else { - # Do the whole hostname + // Do the whole hostname return common_nicknamize($parts['host']); } } else { if (array_key_exists('path', $parts)) { - # Strip starting, ending slashes + // Strip starting, ending slashes $path = preg_replace('@/$@', '', $parts['path']); $path = preg_replace('@^/@', '', $path); $path = basename($path); + + // Hack for MediaWiki user pages, in the form: + // http://example.com/wiki/User:Myname + // ('User' may be localized.) + if (strpos($path, ':')) { + $parts = array_filter(explode(':', $path)); + $path = $parts[count($parts) - 1]; + } + if ($path) { return common_nicknamize($path); } @@ -1713,3 +2198,40 @@ function common_nicknamize($str) $str = preg_replace('/\W/', '', $str); return strtolower($str); } + +function common_perf_counter($key, $val=null) +{ + global $_perfCounters; + if (isset($_perfCounters)) { + if (common_config('site', 'logperf')) { + if (array_key_exists($key, $_perfCounters)) { + $_perfCounters[$key][] = $val; + } else { + $_perfCounters[$key] = array($val); + } + if (common_config('site', 'logperf_detail')) { + common_log(LOG_DEBUG, "PERF COUNTER HIT: $key $val"); + } + } + } +} + +function common_log_perf_counters() +{ + if (common_config('site', 'logperf')) { + global $_startTime, $_perfCounters; + + if (isset($_startTime)) { + $endTime = microtime(true); + $diff = round(($endTime - $_startTime) * 1000); + common_log(LOG_DEBUG, "PERF runtime: ${diff}ms"); + } + $counters = $_perfCounters; + ksort($counters); + foreach ($counters as $key => $values) { + $count = count($values); + $unique = count(array_unique($values)); + common_log(LOG_DEBUG, "PERF COUNTER: $key $count ($unique unique)"); + } + } +}