replace) * @return string substituted string */ function replace_macros($s, $r) { $a = get_app(); $arr = array('template' => $s, 'params' => $r); call_hooks('replace_macros', $arr); $t = App::template_engine(); $output = $t->replace_macros($arr['template'],$arr['params']); return $output; } /** * @brief Generates a random string. * * @param number $size * @param int $type * @return string */ function random_string($size = 64, $type = RANDOM_STRING_HEX) { // generate a bit of entropy and run it through the whirlpool $s = hash('whirlpool', (string) rand() . uniqid(rand(),true) . (string) rand(),(($type == RANDOM_STRING_TEXT) ? true : false)); $s = (($type == RANDOM_STRING_TEXT) ? str_replace("\n","",base64url_encode($s,true)) : $s); return(substr($s, 0, $size)); } /** * @brief This is our primary input filter. * * The high bit hack only involved some old IE browser, forget which (IE5/Mac?) * that had an XSS attack vector due to stripping the high-bit on an 8-bit character * after cleansing, and angle chars with the high bit set could get through as markup. * * This is now disabled because it was interfering with some legitimate unicode sequences * and hopefully there aren't a lot of those browsers left. * * Use this on any text input where angle chars are not valid or permitted * They will be replaced with safer brackets. This may be filtered further * if these are not allowed either. * * @param string $string Input string * @return string Filtered string */ function notags($string) { return(str_replace(array("<",">"), array('[',']'), $string)); // High-bit filter no longer used // return(str_replace(array("<",">","\xBA","\xBC","\xBE"), array('[',']','','',''), $string)); } /** * use this on "body" or "content" input where angle chars shouldn't be removed, * and allow them to be safely displayed. * @param string $string * @return string */ function escape_tags($string) { return(htmlspecialchars($string, ENT_COMPAT, 'UTF-8', false)); } function z_input_filter($channel_id,$s,$type = 'text/bbcode') { if($type === 'text/bbcode') return escape_tags($s); if($type === 'text/markdown') return escape_tags($s); if($type == 'text/plain') return escape_tags($s); if($type == 'application/x-pdl') return escape_tags($s); $a = get_app(); if(App::$is_sys) { return $s; } $r = q("select account_id, account_roles, channel_pageflags from account left join channel on channel_account_id = account_id where channel_id = %d limit 1", intval($channel_id) ); if($r) { if(($r[0]['account_roles'] & ACCOUNT_ROLE_ALLOWCODE) || ($r[0]['channel_pageflags'] & PAGE_ALLOWCODE)) { if(local_channel() && (get_account_id() == $r[0]['account_id'])) { return $s; } } } if($type === 'text/html') return purify_html($s); return escape_tags($s); } function purify_html($s) { require_once('library/HTMLPurifier.auto.php'); require_once('include/html2bbcode.php'); /** * @FIXME this function has html output, not bbcode - so safely purify these * $s = html2bb_video($s); * $s = oembed_html2bbcode($s); */ $config = HTMLPurifier_Config::createDefault(); $config->set('Cache.DefinitionImpl', null); $config->set('Attr.EnableID', true); //Allow some custom data- attributes used by built-in libs. //In this way members which do not have allowcode set can still use the built-in js libs in webpages to some extent. $def = $config->getHTMLDefinition(true); //data- attributes used by the foundation library $def->info_global_attr['data-options'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-magellan-expedition'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-magellan-destination'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-magellan-arrival'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-offcanvas'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-topbar'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-orbit'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-orbit-slide-number'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-dropdown'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-dropdown-content'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-reveal-id'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-reveal'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-alert'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-tooltip'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-joyride'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-id'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-text'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-class'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-prev-tex'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-button'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-accordion'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-tab'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-equalizer'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-equalizer-watch'] = new HTMLPurifier_AttrDef_Text; //data- attributes used by the bootstrap library $def->info_global_attr['data-dismiss'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-target'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-toggle'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-backdrop'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-keyboard'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-show'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-spy'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-offset'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-animation'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-container'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-delay'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-placement'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-title'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-trigger'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-content'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-trigger'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-parent'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-ride'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-slide-to'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-slide'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-interval'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-pause'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-wrap'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-offset-top'] = new HTMLPurifier_AttrDef_Text; $def->info_global_attr['data-offset-bottom'] = new HTMLPurifier_AttrDef_Text; //some html5 elements $def->addElement('section', 'Block', 'Flow', 'Common'); $def->addElement('nav', 'Block', 'Flow', 'Common'); $def->addElement('article', 'Block', 'Flow', 'Common'); $def->addElement('aside', 'Block', 'Flow', 'Common'); $def->addElement('header', 'Block', 'Flow', 'Common'); $def->addElement('footer', 'Block', 'Flow', 'Common'); $purifier = new HTMLPurifier($config); return $purifier->purify($s); } /** * @brief generate a string that's random, but usually pronounceable. * * Used to generate initial passwords. * * @param int $len * @return string */ function autoname($len) { if ($len <= 0) return ''; $vowels = array('a','a','ai','au','e','e','e','ee','ea','i','ie','o','ou','u'); if (mt_rand(0, 5) == 4) $vowels[] = 'y'; $cons = array( 'b','bl','br', 'c','ch','cl','cr', 'd','dr', 'f','fl','fr', 'g','gh','gl','gr', 'h', 'j', 'k','kh','kl','kr', 'l', 'm', 'n', 'p','ph','pl','pr', 'qu', 'r','rh', 's','sc','sh','sm','sp','st', 't','th','tr', 'v', 'w','wh', 'x', 'z','zh' ); $midcons = array('ck','ct','gn','ld','lf','lm','lt','mb','mm', 'mn','mp', 'nd','ng','nk','nt','rn','rp','rt'); $noend = array('bl', 'br', 'cl','cr','dr','fl','fr','gl','gr', 'kh', 'kl','kr','mn','pl','pr','rh','tr','qu','wh'); $start = mt_rand(0, 2); if ($start == 0) $table = $vowels; else $table = $cons; $word = ''; for ($x = 0; $x < $len; $x ++) { $r = mt_rand(0,count($table) - 1); $word .= $table[$r]; if ($table == $vowels) $table = array_merge($cons, $midcons); else $table = $vowels; } $word = substr($word,0,$len); foreach ($noend as $noe) { if ((strlen($word) > 2) && (substr($word,-2) == $noe)) { $word = substr($word,0,-1); break; } } if (substr($word, -1) == 'q') $word = substr($word, 0, -1); return $word; } /** * @brief escape text ($str) for XML transport * * @param string $str * @return string Escaped text. */ function xmlify($str) { $buffer = ''; $len = mb_strlen($str); for($x = 0; $x < $len; $x ++) { $char = mb_substr($str,$x,1); switch( $char ) { case "\r" : break; case "&" : $buffer .= '&'; break; case "'" : $buffer .= '''; break; case "\"" : $buffer .= '"'; break; case '<' : $buffer .= '<'; break; case '>' : $buffer .= '>'; break; case "\n" : $buffer .= "\n"; break; default : $buffer .= $char; break; } } $buffer = trim($buffer); return($buffer); } // undo an xmlify // pass xml escaped text ($s), returns unescaped text function unxmlify($s) { $ret = str_replace('&','&', $s); $ret = str_replace(array('<','>','"','''),array('<','>','"',"'"),$ret); return $ret; } /** * Convenience wrapper, reverse the operation "bin2hex" * This is a built-in function in php >= 5.4 * * @FIXME We already have php >= 5.4 requirements, so can we remove this? */ if(! function_exists('hex2bin')) { function hex2bin($s) { if(! (is_string($s) && strlen($s))) return ''; if(strlen($s) & 1) { logger('hex2bin: illegal hex string: ' . $s); return $s; } if(! ctype_xdigit($s)) { return($s); } return(pack("H*",$s)); }} // Automatic pagination. // To use, get the count of total items. // Then call App::set_pager_total($number_items); // Optionally call App::set_pager_itemspage($n) to the number of items to display on each page // Then call paginate($a) after the end of the display loop to insert the pager block on the page // (assuming there are enough items to paginate). // When using with SQL, the setting LIMIT %d, %d => App::$pager['start'],App::$pager['itemspage'] // will limit the results to the correct items for the current page. // The actual page handling is then accomplished at the application layer. function paginate(&$a) { $o = ''; $stripped = preg_replace('/(&page=[0-9]*)/','',App::$query_string); // $stripped = preg_replace('/&zid=(.*?)([\?&]|$)/ism','',$stripped); $stripped = str_replace('q=','',$stripped); $stripped = trim($stripped,'/'); $pagenum = App::$pager['page']; $url = z_root() . '/' . $stripped; if(App::$pager['total'] > App::$pager['itemspage']) { $o .= '
'; if(App::$pager['page'] != 1) $o .= ''."' . t('prev') . ' '; $o .= "" . t('first') . " "; $numpages = App::$pager['total'] / App::$pager['itemspage']; $numstart = 1; $numstop = $numpages; if($numpages > 14) { $numstart = (($pagenum > 7) ? ($pagenum - 7) : 1); $numstop = (($pagenum > ($numpages - 7)) ? $numpages : ($numstart + 14)); } for($i = $numstart; $i <= $numstop; $i++){ if($i == App::$pager['page']) $o .= ''.(($i < 10) ? ' '.$i : $i); else $o .= "".(($i < 10) ? ' '.$i : $i).""; $o .= ' '; } if((App::$pager['total'] % App::$pager['itemspage']) != 0) { if($i == App::$pager['page']) $o .= ''.(($i < 10) ? ' '.$i : $i); else $o .= "".(($i < 10) ? ' '.$i : $i).""; $o .= ' '; } $lastpage = (($numpages > intval($numpages)) ? intval($numpages)+1 : $numpages); $o .= "" . t('last') . " "; if((App::$pager['total'] - (App::$pager['itemspage'] * App::$pager['page'])) > 0) $o .= ''."' . t('next') . ''; $o .= '
'."\r\n"; } return $o; } function alt_pager(&$a, $i, $more = '', $less = '') { if(! $more) $more = t('older'); if(! $less) $less = t('newer'); $stripped = preg_replace('/(&page=[0-9]*)/','',App::$query_string); $stripped = str_replace('q=','',$stripped); $stripped = trim($stripped,'/'); //$pagenum = App::$pager['page']; $url = z_root() . '/' . $stripped; return replace_macros(get_markup_template('alt_pager.tpl'), array( '$has_less' => ((App::$pager['page'] > 1) ? true : false), '$has_more' => (($i > 0 && $i >= App::$pager['itemspage']) ? true : false), '$less' => $less, '$more' => $more, '$url' => $url, '$prevpage' => App::$pager['page'] - 1, '$nextpage' => App::$pager['page'] + 1, )); } /** * @brief Generate a guaranteed unique (for this domain) item ID for ATOM. * * Safe from birthday paradox. * * @return string a unique id */ function item_message_id() { do { $dups = false; $hash = random_string(); $mid = $hash . '@' . App::get_hostname(); $r = q("SELECT id FROM item WHERE mid = '%s' LIMIT 1", dbesc($mid)); if(count($r)) $dups = true; } while($dups == true); return $mid; } /** * @brief Generate a guaranteed unique photo ID. * * Safe from birthday paradox. * * @return string a uniqe hash */ function photo_new_resource() { do { $found = false; $resource = hash('md5', uniqid(mt_rand(), true)); $r = q("SELECT id FROM photo WHERE resource_id = '%s' LIMIT 1", dbesc($resource)); if(count($r)) $found = true; } while($found === true); return $resource; } // for html,xml parsing - let's say you've got // an attribute foobar="class1 class2 class3" // and you want to find out if it contains 'class3'. // you can't use a normal sub string search because you // might match 'notclass3' and a regex to do the job is // possible but a bit complicated. // pass the attribute string as $attr and the attribute you // are looking for as $s - returns true if found, otherwise false function attribute_contains($attr, $s) { $a = explode(' ', $attr); if(count($a) && in_array($s, $a)) return true; return false; } /** * @brief Logging function for Hubzilla. * * Logging output is configured through Hubzilla's system config. The log file * is set in system logfile, log level in system loglevel and to enable logging * set system debugging. * * Available constants for log level are LOGGER_NORMAL, LOGGER_TRACE, LOGGER_DEBUG, * LOGGER_DATA and LOGGER_ALL. * * Since PHP5.4 we get the file, function and line automatically where the logger * was called, so no need to add it to the message anymore. * * @param string $msg Message to log * @param int $level A log level. * @param int $priority - compatible with syslog */ function logger($msg, $level = LOGGER_NORMAL, $priority = LOG_INFO) { require_once('include/dba/dba_driver.php'); // turn off logger in install mode if((App::$module == 'install') || (! (DBA::$dba && DBA::$dba->connected))) return; $debugging = get_config('system', 'debugging'); $loglevel = intval(get_config('system', 'loglevel')); $logfile = get_config('system', 'logfile'); if((! $debugging) || (! $logfile) || ($level > $loglevel)) return; $where = ''; if(version_compare(PHP_VERSION, '5.4.0') >= 0) { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); $where = basename($stack[0]['file']) . ':' . $stack[0]['line'] . ':' . $stack[1]['function'] . ': '; } $s = datetime_convert() . ':' . log_priority_str($priority) . ':' . session_id() . ':' . $where . $msg . PHP_EOL; $pluginfo = array('filename' => $logfile, 'loglevel' => $level, 'message' => $s,'priority' => $priority, 'logged' => false); call_hooks('logger',$pluginfo); if(! $pluginfo['logged']) @file_put_contents($pluginfo['filename'], $pluginfo['message'], FILE_APPEND); } // like logger() but with a function backtrace to pinpoint certain classes // of problems which show up deep in the calling stack function btlogger($msg, $level = LOGGER_NORMAL, $priority = LOG_INFO) { logger($msg, $level, $priority); if(version_compare(PHP_VERSION, '5.4.0') >= 0) { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); if($stack) { for($x = 1; $x < count($stack); $x ++) { logger('stack: ' . basename($stack[$x]['file']) . ':' . $stack[$x]['line'] . ':' . $stack[$x]['function'] . '()',$level, $priority); } } } } function log_priority_str($priority) { $parr = array( LOG_EMERG => 'LOG_EMERG', LOG_ALERT => 'LOG_ALERT', LOG_CRIT => 'LOG_CRIT', LOG_ERR => 'LOG_ERR', LOG_WARNING => 'LOG_WARNING', LOG_NOTICE => 'LOG_NOTICE', LOG_INFO => 'LOG_INFO', LOG_DEBUG => 'LOG_DEBUG' ); if($parr[$priority]) return $parr[$priority]; return 'LOG_UNDEFINED'; } /** * @brief This is a special logging facility for developers. * * It allows one to target specific things to trace/debug and is identical to * logger() with the exception of the log filename. This allows one to isolate * specific calls while allowing logger() to paint a bigger picture of overall * activity and capture more detail. * * If you find dlogger() calls in checked in code, you are free to remove them - * so as to provide a noise-free development environment which responds to events * you are targetting personally. * * @param string $msg Message to log * @param int $level A log level. */ function dlogger($msg, $level = 0) { require_once('include/dba/dba_driver.php'); // turn off logger in install mode if((App::$module == 'install') || (! (DBA::$dba && DBA::$dba->connected))) return; $debugging = get_config('system','debugging'); $loglevel = intval(get_config('system','loglevel')); $logfile = get_config('system','dlogfile'); if((! $debugging) || (! $logfile) || ($level > $loglevel)) return; $where = ''; if(version_compare(PHP_VERSION, '5.4.0') >= 0) { $stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); $where = basename($stack[0]['file']) . ':' . $stack[0]['line'] . ':' . $stack[1]['function'] . ': '; } @file_put_contents($logfile, datetime_convert() . ':' . session_id() . ' ' . $where . $msg . PHP_EOL, FILE_APPEND); } function profiler($t1,$t2,$label) { if(file_exists('profiler.out') && $t1 && t2) @file_put_contents('profiler.out', sprintf('%01.4f %s',$t2 - $t1,$label) . PHP_EOL, FILE_APPEND); } function activity_match($haystack,$needle) { if(($haystack === $needle) || ((basename($needle) === $haystack) && strstr($needle,NAMESPACE_ACTIVITY_SCHEMA))) return true; return false; } // Pull out all #hashtags and @person tags from $s; // We also get @person@domain.com - which would make // the regex quite complicated as tags can also // end a sentence. So we'll run through our results // and strip the period from any tags which end with one. // Returns array of tags found, or empty array. function get_tags($s) { $ret = array(); $match = array(); // ignore anything in a code block $s = preg_replace('/\[code\](.*?)\[\/code\]/sm','',$s); // ignore anything in [style= ] $s = preg_replace('/\[style=(.*?)\]/sm','',$s); // ignore anything in [color= ], because it may contain color codes which are mistaken for tags $s = preg_replace('/\[color=(.*?)\]/sm','',$s); // match any double quoted tags if(preg_match_all('/([@#]\"\;.*?\"\;)/',$s,$match)) { foreach($match[1] as $mtch) { $ret[] = $mtch; } } // Match full names against @tags including the space between first and last // We will look these up afterward to see if they are full names or not recognisable. // The lookbehind is used to prevent a match in the middle of a word // '=' needs to be avoided because when the replacement is made (in handle_tag()) it has to be ignored there // Feel free to allow '=' if the issue with '=' is solved in handle_tag() // added / ? and [ to avoid issues with hashchars in url paths if(preg_match_all('/(?' . "\r\n"; $o .= "\t\t" . '' . "\r\n"; } } return $o; } function contact_block() { $o = ''; $a = get_app(); if(! App::$profile['uid']) return; if(! perm_is_allowed(App::$profile['uid'],get_observer_hash(),'view_contacts')) return; $shown = get_pconfig(App::$profile['uid'],'system','display_friend_count'); if($shown === false) $shown = 25; if($shown == 0) return; $is_owner = ((local_channel() && local_channel() == App::$profile['uid']) ? true : false); $sql_extra = ''; $abook_flags = " and abook_pending = 0 and abook_self = 0 "; if(! $is_owner) { $abook_flags .= " and abook_hidden = 0 "; $sql_extra = " and xchan_hidden = 0 "; } if((! is_array(App::$profile)) || (App::$profile['hide_friends'])) return $o; $r = q("SELECT COUNT(abook_id) AS total FROM abook left join xchan on abook_xchan = xchan_hash WHERE abook_channel = %d $abook_flags and xchan_orphan = 0 and xchan_deleted = 0 $sql_extra", intval(App::$profile['uid']) ); if(count($r)) { $total = intval($r[0]['total']); } if(! $total) { $contacts = t('No connections'); $micropro = null; } else { $randfunc = db_getfunc('RAND'); $r = q("SELECT abook.*, xchan.* FROM abook left join xchan on abook.abook_xchan = xchan.xchan_hash WHERE abook_channel = %d $abook_flags and abook_archived = 0 and xchan_orphan = 0 and xchan_deleted = 0 $sql_extra ORDER BY $randfunc LIMIT %d", intval(App::$profile['uid']), intval($shown) ); if(count($r)) { $contacts = t('Connections'); $micropro = Array(); foreach($r as $rr) { $rr['archived'] = (intval($rr['abook_archived']) ? true : false); $micropro[] = micropro($rr,true,'mpfriend'); } } } $tpl = get_markup_template('contact_block.tpl'); $o = replace_macros($tpl, array( '$contacts' => $contacts, '$nickname' => App::$profile['channel_address'], '$viewconnections' => (($total > $shown) ? sprintf(t('View all %s connections'),$total) : ''), '$micropro' => $micropro, )); $arr = array('contacts' => $r, 'output' => $o); call_hooks('contact_block_end', $arr); return $o; } function chanlink_hash($s) { return z_root() . '/chanview?f=&hash=' . urlencode($s); } function chanlink_url($s) { return z_root() . '/chanview?f=&url=' . urlencode($s); } function chanlink_cid($d) { return z_root() . '/chanview?f=&cid=' . intval($d); } function magiclink_url($observer,$myaddr,$url) { return (($observer) ? z_root() . '/magic?f=&dest=' . $url . '&addr=' . $myaddr : $url ); } function micropro($contact, $redirect = false, $class = '', $textmode = false) { if($contact['click']) $url = '#'; else $url = chanlink_hash($contact['xchan_hash']); return replace_macros(get_markup_template(($textmode)?'micropro_txt.tpl':'micropro_img.tpl'),array( '$click' => (($contact['click']) ? $contact['click'] : ''), '$class' => $class . (($contact['archived']) ? ' archived' : ''), '$url' => $url, '$photo' => $contact['xchan_photo_s'], '$name' => $contact['xchan_name'], '$title' => $contact['xchan_name'] . ' [' . $contact['xchan_addr'] . ']', )); } function search($s,$id='search-box',$url='/search',$save = false) { $a = get_app(); return replace_macros(get_markup_template('searchbox.tpl'),array( '$s' => $s, '$id' => $id, '$action_url' => z_root() . $url, '$search_label' => t('Search'), '$save_label' => t('Save'), '$savedsearch' => feature_enabled(local_channel(),'savedsearch') )); } function searchbox($s,$id='search-box',$url='/search',$save = false) { return replace_macros(get_markup_template('searchbox.tpl'),array( '$s' => $s, '$id' => $id, '$action_url' => z_root() . '/' . $url, '$search_label' => t('Search'), '$save_label' => t('Save'), '$savedsearch' => feature_enabled(local_channel(),'savedsearch') )); } function valid_email_regex($x){ if(preg_match('/^[_a-zA-Z0-9\-\+]+(\.[_a-zA-Z0-9\-\+]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$/',$x)) return true; return false; } function valid_email($x){ if(get_config('system','disable_email_validation')) return true; return valid_email_regex($x); } /** * @brief Replace naked text hyperlink with HTML formatted hyperlink. * * @param string $s * @param boolean $me (optional) default false * @return string */ function linkify($s, $me = false) { $s = preg_replace("/(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\@\~\#\'\%\$\!\+\,\@]*)/", (($me) ? ' $1' : ' $1'), $s); $s = preg_replace("/\<(.*?)(src|href)=(.*?)\&\;(.*?)\>/ism",'<$1$2=$3&$4>',$s); return($s); } /** * @brief Replace media element using http url with https to a local redirector * if using https locally. * * Looks for HTML tags containing src elements that are http when we're viewing an https page * Typically this throws an insecure content violation in the browser. So we redirect them * to a local redirector which uses https and which redirects to the selected content * * @param string $s * @returns string */ function sslify($s) { if (strpos(z_root(),'https:') === false) return $s; // By default we'll only sslify img tags because media files will probably choke. // You can set sslify_everything if you want - but it will likely white-screen if it hits your php memory limit. // The downside is that http: media files will likely be blocked by your browser // Complain to your browser maker $allow = get_config('system','sslify_everything'); $pattern = (($allow) ? "/\<(.*?)src=\"(http\:.*?)\"(.*?)\>/" : "/\/" ); $matches = null; $cnt = preg_match_all($pattern,$s,$matches,PREG_SET_ORDER); if ($cnt) { foreach ($matches as $match) { $filename = basename( parse_url($match[2], PHP_URL_PATH) ); $s = str_replace($match[2],z_root() . '/sslify/' . $filename . '?f=&url=' . urlencode($match[2]),$s); } } return $s; } function get_poke_verbs() { // index is present tense verb // value is array containing past tense verb, translation of present, translation of past if(get_config('system','poke_basic')) { $arr = array( 'poke' => array( 'poked', t('poke'), t('poked')), ); } else { $arr = array( 'poke' => array( 'poked', t('poke'), t('poked')), 'ping' => array( 'pinged', t('ping'), t('pinged')), 'prod' => array( 'prodded', t('prod'), t('prodded')), 'slap' => array( 'slapped', t('slap'), t('slapped')), 'finger' => array( 'fingered', t('finger'), t('fingered')), 'rebuff' => array( 'rebuffed', t('rebuff'), t('rebuffed')), ); call_hooks('poke_verbs', $arr); } return $arr; } function get_mood_verbs() { $arr = array( 'happy' => t('happy'), 'sad' => t('sad'), 'mellow' => t('mellow'), 'tired' => t('tired'), 'perky' => t('perky'), 'angry' => t('angry'), 'stupefied' => t('stupefied'), 'puzzled' => t('puzzled'), 'interested' => t('interested'), 'bitter' => t('bitter'), 'cheerful' => t('cheerful'), 'alive' => t('alive'), 'annoyed' => t('annoyed'), 'anxious' => t('anxious'), 'cranky' => t('cranky'), 'disturbed' => t('disturbed'), 'frustrated' => t('frustrated'), 'depressed' => t('depressed'), 'motivated' => t('motivated'), 'relaxed' => t('relaxed'), 'surprised' => t('surprised'), ); call_hooks('mood_verbs', $arr); return $arr; } // Function to list all smilies, both internal and from addons // Returns array with keys 'texts' and 'icons' function list_smilies() { $a = get_app(); $texts = array( '<3', '</3', '<\\3', ':-)', ';-)', ':-(', ':-P', ':-p', ':-"', ':-"', ':-x', ':-X', ':-D', '8-|', '8-O', ':-O', '\\o/', 'o.O', 'O.o', 'o_O', 'O_o', ":'(", ":-!", ":-/", ":-[", "8-)", ':beer', ':homebrew', ':coffee', ':facepalm', ':like', ':dislike', 'red#matrix', 'red#', 'r#' ); $icons = array( '<3', '</3', '<\\3', ':-)', ';-)', ':-(', ':-P', ':-p', ':-\', ':-\', ':-x', ':-X', ':-D', '8-|', '8-O', ':-O', '\\o/', 'o.O', 'O.o', 'o_O', 'O_o', ':\'(', ':-!', ':-/', ':-[', '8-)', ':beer', ':homebrew', ':coffee', ':facepalm', ':like', ':dislike', 'redmatrix', 'redmatrix', 'redmatrix' ); $params = array('texts' => $texts, 'icons' => $icons); call_hooks('smilie', $params); return $params; } /** * @brief Replaces text emoticons with graphical images. * * It is expected that this function will be called using HTML text. * We will escape text between HTML pre and code blocks, and HTML attributes * (such as urls) from being processed. * * At a higher level, the bbcode [nosmile] tag can be used to prevent this * function from being executed by the prepare_text() routine when preparing * bbcode source for HTML display. * * @param string $s * @param boolean $sample (optional) default false * @return string */ function smilies($s, $sample = false) { if(intval(get_config('system', 'no_smilies')) || (local_channel() && intval(get_pconfig(local_channel(), 'system', 'no_smilies')))) return $s; $s = preg_replace_callback('{<(pre|code)>.*?}ism', 'smile_shield', $s); $s = preg_replace_callback('/<[a-z]+ .*?>/ism', 'smile_shield', $s); $params = list_smilies(); $params['string'] = $s; if ($sample) { $s = '
'; for ($x = 0; $x < count($params['texts']); $x ++) { $s .= '
' . $params['texts'][$x] . '
' . $params['icons'][$x] . '
'; } } else { $params['string'] = preg_replace_callback('/<(3+)/','preg_heart',$params['string']); $s = str_replace($params['texts'],$params['icons'],$params['string']); } $s = preg_replace_callback('//ism', 'smile_unshield', $s); return $s; } /** * @brief * * @param array $m * @return string */ function smile_shield($m) { return ''; } function smile_unshield($m) { return base64url_decode($m[1]); } /** * @brief Expand <3333 to the correct number of hearts. * * @param array $x */ function preg_heart($x) { $a = get_app(); if (strlen($x[1]) == 1) return $x[0]; $t = ''; for($cnt = 0; $cnt < strlen($x[1]); $cnt ++) $t .= '<3'; $r = str_replace($x[0],$t,$x[0]); return $r; } function day_translate($s) { $ret = str_replace(array('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'), array( t('Monday'), t('Tuesday'), t('Wednesday'), t('Thursday'), t('Friday'), t('Saturday'), t('Sunday')), $s); $ret = str_replace(array('January','February','March','April','May','June','July','August','September','October','November','December'), array( t('January'), t('February'), t('March'), t('April'), t('May'), t('June'), t('July'), t('August'), t('September'), t('October'), t('November'), t('December')), $ret); return $ret; } /** * @brief normalises a string. * * @param string $url * @return string */ function normalise_link($url) { $ret = str_replace(array('https:', '//www.'), array('http:', '//'), $url); return(rtrim($ret, '/')); } /** * @brief Compare two URLs to see if they are the same. * * But ignore slight but hopefully insignificant differences such as if one * is https and the other isn't, or if one is www.something and the other * isn't - and also ignore case differences. * * @see normalis_link() * * @param string $a * @param string $b * @return true if the URLs match, otherwise false */ function link_compare($a, $b) { if (strcasecmp(normalise_link($a), normalise_link($b)) === 0) return true; return false; } // Given an item array, convert the body element from bbcode to html and add smilie icons. // If attach is true, also add icons for item attachments function unobscure(&$item) { if(array_key_exists('item_obscured',$item) && intval($item['item_obscured'])) { $key = get_config('system','prvkey'); if($item['title']) $item['title'] = crypto_unencapsulate(json_decode_plus($item['title']),$key); if($item['body']) $item['body'] = crypto_unencapsulate(json_decode_plus($item['body']),$key); if(get_config('system','item_cache')) { q("update item set title = '%s', body = '%s', item_obscured = 0 where id = %d", dbesc($item['title']), dbesc($item['body']), intval($item['id']) ); } } } function unobscure_mail(&$item) { if(array_key_exists('mail_obscured',$item) && intval($item['mail_obscured'])) { if($item['title']) $item['title'] = base64url_decode(str_rot47($item['title'])); if($item['body']) $item['body'] = base64url_decode(str_rot47($item['body'])); } } function theme_attachments(&$item) { $arr = json_decode_plus($item['attach']); if(is_array($arr) && count($arr)) { $attaches = array(); foreach($arr as $r) { $icon = getIconFromType($r['type']); $label = (($r['title']) ? urldecode(htmlspecialchars($r['title'], ENT_COMPAT, 'UTF-8')) : t('Unknown Attachment')); //some feeds provide an attachment where title an empty space if($label == ' ') $label = t('Unknown Attachment'); $title = t('Size') . ' ' . (($r['length']) ? userReadableSize($r['length']) : t('unknown')); require_once('include/identity.php'); if(is_foreigner($item['author_xchan'])) $url = $r['href']; else $url = z_root() . '/magic?f=&hash=' . $item['author_xchan'] . '&dest=' . $r['href'] . '/' . $r['revision']; //$s .= '' . $icon . ''; $attaches[] = array('label' => $label, 'url' => $url, 'icon' => $icon, 'title' => $title); } $s = replace_macros(get_markup_template('item_attach.tpl'), array( '$attaches' => $attaches )); } return $s; } function format_categories(&$item,$writeable) { $s = ''; $terms = get_terms_oftype($item['term'],TERM_CATEGORY); if($terms) { $categories = array(); foreach($terms as $t) { $term = htmlspecialchars($t['term'],ENT_COMPAT,'UTF-8',false) ; if(! trim($term)) continue; $removelink = (($writeable) ? z_root() . '/filerm/' . $item['id'] . '?f=&cat=' . urlencode($t['term']) : ''); $categories[] = array('term' => $term, 'writeable' => $writeable, 'removelink' => $removelink, 'url' => zid($t['url'])); } $s = replace_macros(get_markup_template('item_categories.tpl'),array( '$remove' => t('remove category'), '$categories' => $categories )); } return $s; } /** * @brief Add any hashtags which weren't mentioned in the message body, e.g. community tags * * @param[in] array &$item * @return string HTML link of hashtag */ function format_hashtags(&$item) { $s = ''; $terms = get_terms_oftype($item['term'], array(TERM_HASHTAG,TERM_COMMUNITYTAG)); if($terms) { foreach($terms as $t) { $term = htmlspecialchars($t['term'], ENT_COMPAT, 'UTF-8', false) ; if(! trim($term)) continue; if(strpos($item['body'], $t['url'])) continue; if($s) $s .= ' '; $s .= '#' . $term . ''; } } return $s; } function format_mentions(&$item) { $s = ''; $terms = get_terms_oftype($item['term'],TERM_MENTION); if($terms) { foreach($terms as $t) { $term = htmlspecialchars($t['term'],ENT_COMPAT,'UTF-8',false) ; if(! trim($term)) continue; if(strpos($item['body'], $t['url'])) continue; if($s) $s .= ' '; $s .= '@' . $term . ''; } } return $s; } function format_filer(&$item) { $s = ''; $terms = get_terms_oftype($item['term'],TERM_FILE); if($terms) { $categories = array(); foreach($terms as $t) { $term = htmlspecialchars($t['term'],ENT_COMPAT,'UTF-8',false) ; if(! trim($term)) continue; $removelink = z_root() . '/filerm/' . $item['id'] . '?f=&term=' . urlencode($t['term']); $categories[] = array('term' => $term, 'removelink' => $removelink); } $s = replace_macros(get_markup_template('item_filer.tpl'),array( '$remove' => t('remove from file'), '$categories' => $categories )); } return $s; } function generate_map($coord) { $coord = trim($coord); $coord = str_replace(array(',','/',' '),array(' ',' ',' '),$coord); $arr = array('lat' => trim(substr($coord,0,strpos($coord,' '))), 'lon' => trim(substr($coord,strpos($coord,' ')+1)), 'html' => ''); call_hooks('generate_map',$arr); return (($arr['html']) ? $arr['html'] : $coord); } function generate_named_map($location) { $arr = array('location' => $location, 'html' => ''); call_hooks('generate_named_map',$arr); return (($arr['html']) ? $arr['html'] : $location); } function format_event($jobject) { $event = array(); $object = json_decode($jobject,true); //ensure compatibility with older items - this check can be removed at a later point if(array_key_exists('description', $object)) { $bd_format = t('l F d, Y \@ g:i A'); // Friday January 18, 2011 @ 8:01 AM $event['header'] = replace_macros(get_markup_template('event_item_header.tpl'),array( '$title' => bbcode($object['title']), '$dtstart_label' => t('Starts:'), '$dtstart_title' => datetime_convert('UTC', 'UTC', $object['start'], (($object['adjust']) ? ATOM_TIME : 'Y-m-d\TH:i:s' )), '$dtstart_dt' => (($object['adjust']) ? day_translate(datetime_convert('UTC', date_default_timezone_get(), $object['start'] , $bd_format )) : day_translate(datetime_convert('UTC', 'UTC', $object['start'] , $bd_format))), '$finish' => (($object['nofinish']) ? false : true), '$dtend_label' => t('Finishes:'), '$dtend_title' => datetime_convert('UTC','UTC',$object['finish'], (($object['adjust']) ? ATOM_TIME : 'Y-m-d\TH:i:s' )), '$dtend_dt' => (($object['adjust']) ? day_translate(datetime_convert('UTC', date_default_timezone_get(), $object['finish'] , $bd_format )) : day_translate(datetime_convert('UTC', 'UTC', $object['finish'] , $bd_format ))) )); $event['content'] = replace_macros(get_markup_template('event_item_content.tpl'),array( '$description' => bbcode($object['description']), '$location_label' => t('Location:'), '$location' => bbcode($object['location']) )); } return $event; } function prepare_body(&$item,$attach = false) { require_once('include/identity.php'); call_hooks('prepare_body_init', $item); $s = ''; $photo = ''; $is_photo = ((($item['verb'] === ACTIVITY_POST) && ($item['obj_type'] === ACTIVITY_OBJ_PHOTO)) ? true : false); if($is_photo) { $object = json_decode($item['object'],true); // if original photo width is <= 640px prepend it to item body if($object['link'][0]['width'] && $object['link'][0]['width'] <= 640) { $s .= '
' . $s; } // if original photo width is > 640px make it a cover photo if($object['link'][0]['width'] && $object['link'][0]['width'] > 640) { $scale = ((($object['link'][1]['width'] == 1024) || ($object['link'][1]['height'] == 1024)) ? 1 : 0); $photo = ''; } } $s .= prepare_text($item['body'],$item['mimetype'], false); $event = (($item['obj_type'] === ACTIVITY_OBJ_EVENT) ? format_event($item['object']) : false); $prep_arr = array( 'item' => $item, 'html' => $event ? $event['content'] : $s, 'event' => $event['header'], 'photo' => $photo ); call_hooks('prepare_body', $prep_arr); $s = $prep_arr['html']; $photo = $prep_arr['photo']; $event = $prep_arr['event']; // q("update item set html = '%s' where id = %d", // dbesc($s), // intval($item['id']) // ); if(! $attach) { return $s; } if(strpos($s,'
') !== false && $item['coord']) { $x = generate_map(trim($item['coord'])); if($x) { $s = preg_replace('/\
/','$0' . $x,$s); } } $attachments = theme_attachments($item); $writeable = ((get_observer_hash() == $item['owner_xchan']) ? true : false); $tags = format_hashtags($item); if($item['resource_type']) $mentions = format_mentions($item); $categories = format_categories($item,$writeable); if(local_channel() == $item['uid']) $filer = format_filer($item); $s = sslify($s); // Look for spoiler $spoilersearch = '
'; // Remove line breaks before the spoiler while ((strpos($s, "\n".$spoilersearch) !== false)) $s = str_replace("\n".$spoilersearch, $spoilersearch, $s); while ((strpos($s, "
".$spoilersearch) !== false)) $s = str_replace("
".$spoilersearch, $spoilersearch, $s); while ((strpos($s, $spoilersearch) !== false)) { $pos = strpos($s, $spoilersearch); $rnd = random_string(8); $spoilerreplace = '
'.sprintf(t('Click to open/close')).''. '