<?php
/**
* api: freshmeat
* title: template auxiliary code
* description: A few utility functions and data for the templates
* version: 0.5.2
* license: AGPL
*
* This function asortment prepares some common output.
* While a few are parsing helpers or DB query shortcuts.
*
*/
#-- Additional input filters
// Project names may be alphanumeric, and contain dashes
function proj_name($s) {
return preg_replace("/[^a-z0-9-_.]+|^[^a-z0-9]+|\W+$|\.(?!\w{1,7}$)|[-_.]+(?=[-_.])/", "", strtolower($s));
}
// Tags is a comma-separated list, yet sometimes delimited with something else; normalize..
function f_tags($s) {
return
preg_replace( # exception for "c++" and "c#"
["~[-_.:/]+~", "/(([cflje]#|c\+\+)(?=[\s,-]))?[+#]*/", "/[,;|]+/", "/[^a-z0-9,+#\s-]+/", "/[,\s]+/", "/^\W+|\W+$/"],
[ "-", "$1", ",", " ", ", " , "" ],
strtolower($s)
);
}
// Version numbers, remove non-word characters, compact spaces, strip leftmost "v" prefix
function f_version($s) {
return trim(preg_replace("/(?<=\s)\s+|^[vV](?![a-z])/", "", trim(input::words($s, "~+()#\/@:"))));
}
#-- Template helpers
// Wrap tag list into links
function wrap_tags($tags, $r="") {
foreach (str_getcsv($tags) as $id) {
$id = trim($id);
$r .= "<a class=p-category href=\"/search?tag=$id\">$id </a>";
}
return $r;
}
// Return DAY MONTH and TIME or YEAR for older entries
function date_fmt($time) {
$lastyear = time() - $time > 250*24*3600;
return strftime($lastyear ? "%d %b %Y" : "%d %b %H:%M", $time);
}
/**
* Substitute `$version` placeholders in URLs.
* (Which is one of the most useful features on freshcode.club)
*
* Supported syntax variations:
* โ $version and $version$
* โ %version and %version%
*
* And for substituting $version-number dots:
* โ $-$version for which 1.2.3 becomes 1-2-3
* โ $_$version for which 2.3.4 becomes 2_3_4
*
* Or to interpolate a single version tuple:
* โ $version0 (becomes `7` for `7.1.2` input)
*
*/
function versioned_url($url, $version) {
$rx = "/
([ \$ % ]) # var sigil
( (.?) \\1 )?+ # substitution prefix
(version|Version|VERSION) # 'version'
(\d?) # suffix 0, 1, 2 to access version tuples
(?= \\1 | \b | _ ) # optional ending sigil [%$], wordbreak, or underscore
/x";
// Check for and displace '$version'
return preg_replace_callback(
$rx,
function ($m) use ($version) {
// Optionally substitute dots in version string
if (strlen($m[2])) {
$version = strtr($version, ["." => $m[3]]);
}
// tuple access
if (strlen($m[5])) {
$version = preg_split("/[-._~]/", $version);
return $version[$m[5]];
}
return $version;
},
$url
);
}
/**
* Convert "url1=, url2=, url3=" list into titled hyperlinks.
*
*/
function proj_links($urls, $entry, $r="") {
// unpack and filter
$urls = p_key_value($urls, NULL);
$urls = array_filter(array_map("input::url", $urls));
// join into HTML list
foreach ($urls as $title=>$url) {
// normalize title and substitute $version placeholders
$title = ucwords($title);
$_title = strtolower($title);
$url = input::html(versioned_url($url, $entry["version"]));
//gimmick: append HTML link and <audio> for theme song
$r .= "\t → <a href=\"$url\">$title</a>"
. ($_title == "theme-song" ? "<audio autoplay onclick='this.paused ? this.play() : this.pause()'>โซ<source type=\"audio/ogg\" src=\"$url\"></audio>" : "")
. "<br>\n";
}
return $r;
}
// Project listing output preparation;
// HTML context escapaing, versioned urls, formatted date string
function prepare_output(&$entry) {
// versioned URLs
$entry["download"] = versioned_url($entry["download"], $entry["version"]);
// project screenshots
if (TRUE or empty($entry["image"])) {
if (file_exists($fn = "img/screenshot/$entry[name].jpeg")) {
$entry["image"] = "/$fn?" . filemtime($fn);
}
else {
$entry["image"] = "/img/nopreview.png";
}
}
//
$entry["formatted_date"] = date_fmt($entry["t_published"]);
// HTML context
$entry = array_map("input::_html", $entry);
// user image
$entry["submitter_img"] = submitter_gravatar($entry["submitter_image"]);
}
/**
* Convert email@xyz to gravatar or identicon,
* keep raw URLs, or use default image for empty fields.
*
*/
function submitter_gravatar($img, $size=24) {
// capture+strip email
if (is_int(strpos($img, "@"))) {
$img = "//www.gravatar.com/avatar/" . md5($img) . "?s=$size&d=identicon&r=pg";
}
elseif (empty($img)) {
$img = "/img/user.png";
}
// return html <img> snippet
return "<img src=\"$img\" width=$size height=$size class=gravatar>";
}
/**
* Convert special user@service monikers into URLs,
* keep everything else as supposed gravatar link.
*
*/
function url_user_icon($mail) {
// special project hosters
if (preg_match("/^([\w.-]+)@(github|sourceforge|launchpad)/i", $mail, $user)) {
// user image lookup rules
$map = [
"github" => [
#"page" => ["URL" => "http://api.github.com/users/$user[1]", "USERPWD" => GITHUB_API_PW],
#"rx" => "~\"avatar_url\":\s*\"(.*?)\"~",
"page" => "http://github.com/$user[1]",
"rx" => "~(//avatars\d*.githubusercontent.com/u/\d+[?=\w&;]+)~",
"default" => "//identicons.github.com/$user[1].png",
],
"sourceforge" => [
"page" => "http://sourceforge.net/u/$user[1]/profile",
"rx" => "~(http[:/\w.]+/avatar/[[:xdigit:]]+)~",
"default" => "$user[1]@users.sourceforge.net",
],
"launchpad" => [
"page" => "http://launchpad.net/~$user[1]",
"rx" => "~(//launchpadlibrarian.net/\d+/[^\"\'\>\)]+)~",
"default" => "$user[1]@launchpad.net",
],
];
extract($map[strtolower($user[2])]);
// fetch user page on service, check for gravatar-id or url
if (preg_match($rx, $src = curl($page)->timeout(2.5)->exec(), $url))
{
$mail = $url[1];
}
// fallback
else {
$mail = $default;
}
}
// keep as-is, and have it resolved as standard gravatar
return($mail);
}
// Social media share links
function social_share_links($name, $url) {
$c = array("google"=>0, "facebook"=>0, "twitter"=>0, "reddit"=>0, "linkedin"=>0, "stumbleupon"=>0, "delicious"=>0);
return <<<HTML
<span class=social-share-links>
<a href="https://plus.google.com/share?url=$url" title=google+> g+ </a>
<a href="https://www.facebook.com/sharer/sharer.php?u=$url" title=facebook> fb </a>
<a href="https://twitter.com/intent/tweet?url=$url" title=twitter> tw </a>
<a href="http://reddit.com/submit?url=$url" title=reddit> rd </a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=$url" title=linkedin> in </a>
<a href="https://www.stumbleupon.com/submit?url=$url" title=stumbleupon> su </a>
<a href="https://del.icio.us/post?url=$url" title=delicious> dl </a>
</span>
HTML;
}
function social_share_count($num) {
return empty($num) ? "" : "<var class=social-share-count>$num</var>";
}
/**
* Write out pseudo pagination links.
* This is just appended no matter the actually available entries.
* The db() queries themselves handle the LIMIT/OFFSET, depending on a page param.
*
*/
function pagination($page_no, $GET_param="n") {
print "<p class=pagination-links> ยป";
foreach (range($page_no-2, $page_no+9) as $n) if ($n > 0) {
print " <a " . ($n==$page_no ? "class=current " : ""). "href=\"?n=$n\">$n</a> ";
}
print "ยซ </p>";
}
/**
* Output a list of select <option>s
*
* - Either accepts a option,value,field list.
* - Or an associative array.
*
*/
function form_select_options($names, $value=NULL, $r="") {
// Transform comma-separated string into array
$map = is_string($names) ? array_combine($names = str_getcsv($names), $names) : $names;
// Add currently active value if missing
if ($value and !isset($map[$value]) and $value !== NULL) {
$map[$value] = $value;
}
// Output <option> fields
foreach ($map as $id=>$title) {
// optgroup
if (is_array($title)) {
$r .= "<optgroup label=\"$id\">" . form_select_options($title, $value) . "</optgroup>";
}
// plain value field
else {
$r .= "<option" . ($id == $value ? " selected" : "")
. " value=\"$id\" title=\"$title\">$id</option>";
}
}
return $r;
}
/**
* CSRF token generation/verification.
*
* Is only used for logged-in users though. Here they're mainly to prevent
* remotely initiated requests against other users, not general form nonces.
*/
function csrf($probe=false) {
// Tokens are stored in session, reusable, but only for an hour
$store = & $_SESSION["csrf"];
foreach ($store as $id=>$time) {
if ($time < time()) { unset($store[$id]); }
}
// Test presence
if ($probe) {
if (empty($_SESSION["openid"])) {
return TRUE;
}
if ($id = $_REQUEST->name["_ct"]) {
return isset($store[$id]);
}
}
// Create new entry, output form field for token
else {
// server ENV already contained Apache unique request id etc.
$id = sha1(serialize($_SERVER->__vars));
$store[$id] = time() + 3600; // timeout
return "<input type=hidden name=.ct value=$id>";
}
}
/**
* Detect "AEipUedocbyWuDKj, UKcPXdlZWwRea, bAfqstVUhGImr" garbage submissions in multiple fields.
*
*/
function random_text_spam($release, $count=0) {
$rx = "/^\s* (?=(.*[A-Z\d]+.*){3,}) (?=(.*[a-z]+.*){3,}) \w{5,20} \s*$/x";
$fields = ["name", "title", "description", "tags", "version", "changes", "urls", "autoupdate_regex", "submitter", "lock", "summary"];
foreach ($fields as $field) {
if (preg_match($rx, $release[$field])) {
$count++;
}
}
return $count >= 5;
}
/**
* Trivial check against well-known project spam.
*
*/
function data_blacklisted($release) {
$rules = array(
"summary" => "/wilmix/i",
"submitter" => "/wilmix|jemin/i",
"homepage" => "~fmemodules.com|webixytech.com|wilmix|zeesmovie|softwarereviews|diclofenac|viagra|\/\/buy-|fmeextensions|creditloans|casino|cashapp|quickbooks~i",
"urls" => "~[gjch]dollar|wilmix~i",
"name" => "/DOLLAR|JDollar|Jehovah|Millionaire *s/",
"description" => "/invented by|viagra|levitra|tetracycline*s|quickbooks|wilmix|productcustomization|creditloans|web(site)? development (company|service)|<a href=\"/i",
);
foreach ($rules as $field => $rx) {
if (preg_match($rx, $release[$field])) {
return TRUE;
}
}
return random_text_spam($release) or false;
}
/**
* Store into spam/ folder
*
*/
function log_spam($release, $why="spam") {
$json = json_encode(
[
"release" => $release->__vars,
"server" => $_SERVER,
"reason" => $why,
],
JSON_PRETTY_PRINT
);
file_put_contents("./spam/_".microtime(true).".json", $json);
}
#-- Some string parsing
/**
* Plain comma-separated list
*
*/
function p_csv($str) {
return preg_split("/\s*,\s*/", trim($str));
}
/**
* Extracts key = value list.
* Keys may be wrapped in $, % or []
* Values may not contain spaces
*
*/
function p_key_value($str, $case=CASE_LOWER, $match="\S+") {
preg_match_all(
"@
[[%$]* ([-\w]+) []%$]*
\h* [:=>]+ \h*
($match)
(?<![,.;])
@imsx",
$str, $m
);
$r = array_combine($m[1], $m[2]);
return is_int($case) ? array_change_key_case($r, $case) : $r;
}
?>