⌈⌋ branch:  freshcode


Check-in Differences

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From b2cdb48c301b1f54 To abc330bf7cbf428b

2014-08-13
16:10
Add db() placeholder documentation ASCII table. check-in: ac49f65add user: mario tags: trunk
16:09
::http filter was too strict (leading numbers in URLs) check-in: abc330bf7c user: mario tags: trunk
16:09
Add curl()->assert() to be run after ->exec() check-in: 063d94349d user: mario tags: trunk
2014-06-30
01:10
curl() wrapper check-in: 11fd88164c user: mario tags: trunk, 0.3
01:10
Populate `tags` table. check-in: b2cdb48c30 user: mario tags: trunk
01:07
Moved utility code to (misnomer) layout_aux. check-in: 1cdf4582fe user: mario tags: trunk

Changes to .htaccess.

1
2
3
4


5
6




7



8

9



10




11
12
13
14













15
16
17
18




19
20
21
22
23





24
25
1
2
3
4
5
6


7
8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
24




25
26
27
28
29
30
31
32
33
34
35
36
37
38



39
40
41
42
43




44
45
46
47
48
49
50




+
+
-
-
+
+
+
+

+
+
+

+
-
+
+
+

+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
+
+
+
+

-
-
-
-
+
+
+
+
+

# encoding: UTF-8
# api: apache
# title: RewriteRules
# description: Map paths onto dispatcher script
# version: 1.0
# depends: mod_rewrite
#
#


Options -MultiViews
RewriteEngine On

#-- Simulate [END] flag
RewriteCond  %{ENV:REDIRECT_STATUS}  =200
RewriteRule  ^                  -                       [L,NS]

#-- Strip www. prefix
RewriteEngine On
RewriteCond  %{REQUEST_METHOD}  ^GET$
RewriteCond  %{HTTP_HOST}       ^ww+\.(\w+\.\w+)\.?$
RewriteRule  ^(.*)$             http://%1/$1            [R,QSA,L]

#-- RSS/Atom aliases
RewriteCond  %{QUERY_STRING}    ^format=(atom|rss)$
RewriteRule  ^$ feed/xfer.%1
RewriteRule  ^(?:projects)\.(atom|rss|json)$  feed/xfer.$1
# strip www. prefix
RewriteCond %{HTTP_HOST} ^ww+\.(\w+\.\w+)\.?$
RewriteCond %{REQUEST_METHOD} ^GET$
RewriteRule ^(.*)$ http://%1/$1 [R,QSA,L]

#-- Freecode API mapping
RewriteCond  %{REQUEST_METHOD}  ^GET$
RewriteRule  ^projects/([\w-_]+)\.json$  index.php?page=api&name=$1&api=query [L,NS,QSA]
RewriteCond  %{REQUEST_METHOD}  ^PUT$
RewriteRule  ^projects/([\w-_]+)\.json$  index.php?page=api&name=$1&api=update_core [L,NS,QSA]
RewriteCond  %{REQUEST_METHOD}  ^POST$
RewriteRule  ^projects/([\w-_]+)/releases\.json$  index.php?page=api&name=$1&api=publish [L,NS,QSA]
RewriteCond  %{REQUEST_METHOD}  ^(GET|DELETE)$
RewriteRule  ^projects/([\w-_]+)/releases/(\w+)\.json$  index.php?page=api&name=$1&api=version_%1&id=$2 [L,NS,QSA]
RewriteCond  %{REQUEST_METHOD}  ^(GET|PUT|POST|PUSH)$
RewriteRule  ^projects/([\w-_]+)/urls\.json$  index.php?page=api&name=$1&api=urls [L,NS,QSA] 


# pages
RewriteRule ^$ index.php?page=index [L,QSA]
RewriteRule ^(projects|submit|flag|tags|feed|login|links|admin)/?(\w+(?:-\w+)*)?/?$ index.php?page=$1&name=$2 [L,QSA]
#-- Page dispatching
RewriteRule  ^$                 index.php?page=index    [L,NS,QSA]
RewriteRule  ^(projects|submit|search|flag|names?|tags?|feed|login|links|rc|admin|drchangelog)\b/?(\w+(?:[-_]\w+)*)?(?:\.(json|atom|rss))?/?$   index.php?page=$1&name=$2&ext=$3   [L,NS,QSA]
RewriteRule  ^(forum|meta)\b/?(\w+)?/?$   page_forum.php?name=$2   [L,NS,QSA]

# deny direct invocations
RewriteRule ^^^freshcode\.db.*$$$ - [F]
RewriteRule ^\. - [F]
RewriteRule ^(?!index\.)\w+\.php($$$|/.*)$ - [F]
#-- Deny direct invocations
RewriteRule  ^freshcode\.db.*$  -                       [F]
RewriteRule  ^\.                -                       [F]
RewriteCond  %{ENV:REDIRECT_STATUS}  !200
RewriteRule  ^\w+\.php(|/.*)$   -                       [F,L,NS]

Added aux.php.










































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshmeat
 * title: template auxiliary code
 * description: A few utility functions and data for the templates
 * version: 0.5
 * 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+$|(?<=[-_])[-_]+/", "", 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)
        );
}


#-- Template helpers

// Wrap tag list into links
function wrap_tags($tags, $r="") {
    foreach (str_getcsv($tags) as $id) {
        $id = trim($id);
        $r .= "<a 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.
 *
 * 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
 *
 */
function versioned_url($url, $version) {
    $rx = "/
        ([ \$ % ])                  # var syntax
        ( (.?) \\1 )?+              # substitution prefix
        (version|Version|VERSION)   # 'version'
        (?= \\1 | \b | _ )          # followed by var syntax, wordbreak, or underscore
    /x";
    // Check for '$version'
    if (preg_match($rx, $url, $m)) {
        // Optionally replace dots in version string
        if (strlen($m[2])) {
            $version = strtr($version, ["." => $m[3]]);
        }
        $url = preg_replace($rx, $version, $url);
    }
    return $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) {
        $title = ucwords($title);
        $url = versioned_url($url, $entry["version"]);
        $r .= "&rarr; <a href=\"$url\">$title</a><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>";
}



// 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&#65122; </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&amp;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] = $map[$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"]) {
            #var_dump($id, $store, isset($store[$id]));
            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>";
    }
}





#-- 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) {
    preg_match_all(
        "@
           [[%$]*  ([-\w]+)  []%$]*
              \h*  [:=>]+  \h*
                   (\S+)
           (?<![,.;])
        @imsx",
        $str, $m
    );
    $r = array_combine($m[1], $m[2]);
    return is_int($case) ? array_change_key_case($r, $case) : $r;
}




?>

Deleted config.orig.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36



































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
/**
 * api: freshcode
 * title: Freshcode.club config
 * description: initialization code
 * version: 0.2
 * plugin-register: include_once("$FN");
 * 
 *
 */

define("INPUT_QUIET", 1);
#define("INPUT_DIRECT", "trim");
include_once("input.php");

include_once("db.php");
db("connect", "sqlite:freshcode.db");
db()->in_clause = 0;
db()->tokens["fields"]
     = "name,title,homepage,description,license,tags,version,state,scope,changes,"
     . "download,urls,autoupdate_module,autoupdate_url,autoupdate_regex,"
     . "t_published,t_changed,flag,deleted,submitter_openid,submitter,lock,hidden,image";

define("LOGIN_REQUIRED", 0);
define("HTTP_HOST", $_SERVER->id["HTTP_HOST"]);
include_once("deferred_openid_session.php");


// List of moderator OpenID handles
$moderator_ids = array(
);

include_once("layout_aux.php");


?>

Added config.php.













































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: Freshcode.club config
 * description: initialization code
 * version: 0.5.0
 * plugin-register: include_once("$FN");
 * 
 *
 * Automatic and manual dependencies.
 * Base configuration.
 *
 */



// autoloader
include("./shared.phar");

// input filter
define("INPUT_QUIET", 1);
define("INPUT_DIRECT", "raw");
include_once("lib/input.php");

// database
include_once("lib/db.php");
db(new PDO("sqlite:freshcode.db"));
db()->in_clause = 0;

// auth+session
define("LOGIN_REQUIRED", 0);
define("CAPTCHA_REQUIRED", 0);
define("HTTP_HOST", $_SERVER->id["HTTP_HOST"]);
include_once("lib/deferred_openid_session.php");

// utility functions
include_once("aux.php");
curl::$defaults["useragent"] = "freshcode/0.6 (Linux x86-64; curl) projects-autoupdate/0.5 (screenshots,changelog,regex,xpath) +http://freshcode.club/";

// List of administrative OpenID handles
$moderator_ids = array();
include("config.local.php");   


?>

Added cron.daily/autoupdate.php.






























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: Autoupdate runner
 * description: Cron job for invoking autoupdates on per-project basis
 * version: 0.5.0
 * depends: curl
 * author: mario
 * license: AGPL
 * 
 *
 * Each project listing can contain:
 *   `autoupdate_module` = none | regex | github | sourceforge | releases.json
 *   `autoupdate_url` = http://...
 *   `autoupdate_regex` = "version=/.../ \n changes=/.../"
 *
 * This module tries to load the mentioned reference URLs, extract version
 * and changelog, scope/state and download link; then updates the database.
 *
 */

// run in cron context
chdir(dirname(__DIR__));
include("config.php");

// go
$run = new Autoupdate();
$run->all();
#print_r($run->test("github", "youtube-dl"));

Added cron.daily/news_feeds.php.






















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * title: Article feeds
 * description: Queries a few online resources for article links
 * version: 0.4
 *
 * Highlights version numbers in news feeds,
 * and populates templates/feed.*.htm for sidebar display.
 *
 */


// switch to webroot
chdir(dirname(__DIR__));


#-- RSS
$feeds = array(
    "reddit" => "http://www.reddit.com/r/linux/.rss",
    "linuxcom" => "http://www.linux.com/news/software?format=feed&type=rss",
    "linuxgames" => "http://www.linuxgames.com/feed",
    "sourceforge" => "http://sourceforge.net/directory/release_feed/",
    "distrowatch" => "http://distrowatch.com/news/dwd.xml",
);
$filter = 
    "/Please 'report' off-topic|namelessrom|machomebrew/"
;

#-- Traverse and collect entries
foreach ($feeds as $name=>$url) {

    // data
    $html = "";
    $x = file_get_contents($url);
    $x = preg_replace("/[^\x20-\x7F\s]/", "", $x);
    $x = simplexml_load_string($x);
    
    // append
    $i = 0;
    foreach ($x->channel->item as $item) {
    
        # pre-filter
        list($title, $link) = array( htmlspecialchars($item->title),  htmlspecialchars($item->link) );
        if (empty($title) or empty($link) or preg_match($filter, $title) or preg_match($filter, $link)) {
            continue;
        }

        # per feed
        switch ($name) {

            // Extract project base names and version numbers
            case "sourceforge":
                if (preg_match("~^(http://sourceforge.net/projects/(\w+))/files/.+?(\d+(\.\d)+).+?/download$~", $item->link, $m)) {
                    $html .= "<a href=\"$m[1]\">$m[2] <em>$m[3]</em></a>\n";
                    $i++;
                }
                break;

            // Extract project base names and version numbers
            case "distrowatch":
                if (preg_match("~^(\d+/\d+)\s(\D+)\s+(.+)$~", $title, $m)) {
                    $html .= "<a href=\"$link\"><small style=color:grey>$m[1]</small> $m[2] <em>$m[3]</em></a>\n";
                }
                break;

            // Titles as is
            default:
            case "reddit":
            case "linuxcom":
            case "linuxgames":
                if (strlen($item->link) and strlen($item->title)) {
                    $title = preg_replace("~(\d+\.[\d-.]+)~", "<em>$0</em>", $title);
                    $html .="<a href=\"$link\">$title</a>\n";
                    $i++;
                }
                break;
        }

        if ($i >= 20) { break; }
    }
    
    // save
    file_put_contents("./template/feed.$name.htm", $html);

}

Added cron.daily/social_links.php.






















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: Social links count
 * description: Queries api.i-o/links for project homepages
 * version: 0.1
 *
 * Retrieve social media sharing site links for project homepages.
 * Stores them in `release`.`social_links`
 *
 * Only updates latest DB entry, so a versioned history of link counts
 * will be retained. (Not that it's needed, ...)
 *
 */

// deps
chdir(dirname(__DIR__)); 
include("config.php");

// use "remotely" for implicit caching
define("IO_LINKS", "http://api.include-once.org/links/social.ajax.php");

// traverse projects
foreach (db("SELECT *, MAX(t_changed) FROM release_versions GROUP BY name ORDER BY t_published DESC") as $project) {

    // homepage
    $url = $project["homepage"];
    
    // request counts
    $counts = curl()
        ->url(IO_LINKS . "?url=$url")
        ->UserAgent("cron.daily/social_links (0.1; http://freshcode.club/)")
        ->exec();
    
    // summarize
    $counts = json_decode($counts, TRUE);
    $counts = array_sum($counts);
    print "$url = $counts\n";
    
    // store
    $project["social_links"] = $counts;
    #$project->store("REPLACE");
    db("UPDATE release SET social_links=? WHERE :&",
        $counts,
        array(
            "name" => $project["name"],
            "t_changed" => $project["t_changed"],
            "t_published" => $project["t_published"],
        )
    );

}


Added cron.daily/spotlight.php.













































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: cron
 * title: Create random picks for project spotlight
 * description: Randomly picks out a few projects for the footer
 * version: 0.1
 *
 *
 */

chdir(dirname(__DIR__)); 
include("config.php");


/**
 * Scan each project,
 * pick random three.
 *
 */
$r = db("
     SELECT name, title, SUBSTR(description, 0, 150) AS description,
     MAX(t_changed) AS t FROM release
     GROUP BY name
     ORDER BY random() 
     LIMIT 3;
"); 

// combine into HTML blob
$html = ""; 
foreach ($r as $entry) {

    $entry = array_map("input::_html", $entry);
#    $entry["description"] = preg_replace("/\.[^.]*$|[,;][^,;]*$|\S*$/", "", $entry["description"]);
    $html .= <<<EOF
   <a class=project-spotlight href="projects/$entry[name]">
      <img src="img/screenshot/$entry[name].jpeg" width=120 height=90 alt=$entry[name]>
      <b> $entry[title] </b>
      <small class=description>$entry[description]</small> 
   </a>

EOF;
}

// store as template
file_put_contents("./template/spotlight.htm", $html);

Changes to cron.daily/tags.php.

1
2
3
4
5
6

7
8
9



10
11
12
13
14
15
16
17
18
19
20
21

22
23
24
25
26

27
28
29
30
31
32
33
1
2
3
4
5

6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27

28
29
30
31
32
33
34
35





-
+

-

+
+
+











-
+




-
+






<?php
/**
 * api: cron
 * title: Update `tags` table
 * description: Splits out tags from according column in project `release`.
 * version: 0.1
 * version: 0.2
 *
 *
 * Manually update tags table.
 *   - Splits up comma separated release.tags field
 *   - Maximum of 7 tags each
 *   - Populates separate tags table with name=>tag list.
 *
 */

chdir(dirname(__DIR__)); 
include("config.php");

/**
 * Scan each project,
 * split up `tags` as CSV and just fille up according tags table.
 *
 */
foreach (db("SELECT * FROM release_versions GROUP BY name") as $entry) {
foreach (db("SELECT *, MAX(t_changed) FROM release_versions GROUP BY name")->into() as $entry) {

    print_r($entry);
    
    $name = $entry->name;
    $tags = array_slice(p_csv($entry->tags), 0, 7);
    $tags = array_slice(array_filter(p_csv($entry->tags)), 0, 7);

    db("DELETE FROM tags WHERE name=?", $name);
    foreach ($tags as $t) {
        db("INSERT INTO tags (name, tag) VALUES (?, ?)", $name, $t);
    }

}

Deleted db.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328







































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
/**
 * title: database
 * description: basic db() interface for parameterized SQL and result folding
 * api: php
 * type: database
 * version: 0.8
 * depends: pdo
 * license: Public Domain
 * author: Mario Salzer
 * url: http://php7framework.sourceforge.net/
 *
 *
 * QUERY
 *
 * Provides simple database queries with enumerated or named parameters. It's
 * flexible in accepting scalar arguments and arrays. Array args get merged,
 * or transcribed when special placeholders are present:
 *
 *   $r = db("SELECT * FROM tbl WHERE a>=? AND b IN (??)", $a, array($b));
 *
 * Two ?? are used for interpolating arrays, which is useful for IN clauses.
 * The placeholder :? interpolates key names (doesn't add values).
 * And :& or :, or :| become a name=:assign list grouped by AND, comma, OR.
 * Whereas :: turns into a simple :named,:value,:list (for IN clauses).
 * Also configurable {TOKENS} are replaced automatically (db()->tokens[]).
 *
 * RESULT
 *
 * The returned result can be accessed as single data row, with $data->column
 * or using $data["column"].
 * Or if it's a result list, foreach() can iterate over all returned rows.
 * And all PDO ->fetch() methods are still available for use on the result obj.
 * ArrayObjects cannot be used like real arrays in all contexts; typecasting
 * the data out is not possible, in string context curly braces "{$a->x}" are
 * necessary, and in sub-loops needed object syntax "foreach ($a->subarray as)"
 *
 * CONNECT  
 *
 * The db() interface utilizes the global "$db" variable. Which could also be
 * instantiated separately or using:
 * db("connect", array("mysql:host=localhost;dbname=test","username","password"));
 *
 * RECORD WRAPPER
 *
 * There's also a simple table data gateway wrapper implemented here. It
 * accepts db() queries for single entries, and allows ->save()ing back, or
 * to ->delete() records.
 * You should only use it in conjuction with sql2php and its simpler wrappers.
 *
 */



/**
 * SQL query.
 *
 */
function db($sql=NULL, $params="...") {
    global $db;
    
    #-- open database
    if ($sql == "connect") {
    
        // DSN
        $params = is_array($params) ? array_values($params) : array($params,"","");
        $db = new PDO($params[0], $params[1], $params[2]);
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
        $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        
        // save settings
        $db->tokens = array("PREFIX"=>""); // or reference global $config
        #$db->in_clause = strstr($params[0], "sqlite");
    }
    
    #-- singleton use
    elseif (empty($sql)) {
        return $db;
    }
    
    #-- reject SQL
    elseif (strpos($sql, "'")) {
        trigger_error("SQL query contained raw data. DO NOT WANT", E_USER_WARNING);
    }
    
    #-- execute SQL
    else {
    
        #-- get $params
        $params2 = array();
        $args = func_get_args();
        array_shift($args);

        #-- flattening sub-arrays (works for ? enumarated and :named params)
        foreach ($args as $i=>$a) {
            if (is_array($a)) {
                $enum = is_int(end(array_keys($a)));

                // subarray corresponds to special syntax placeholder?
                if (preg_match("/\?\?|:\?|::|:&|:,|&\|/", $sql, $uu, PREG_OFFSET_CAPTURE)) {
                    list($token, $pos) = $uu[0];
                    switch ($token) {

                        case "??":  // replace ?? array placeholders
                            $replace = implode(",", array_fill(0, count($a), "?"));
                            break;

                        case ":?":  // and :? name placeholder, transforms list into enumerated params
                            $replace = implode(",", db_identifier($enum ? $a : array_keys($a)), "`");
                            $enum = 1;  $a = array();   // do not actually add values
                            break;

                        case "::":  // inject :named,:value,:list
                            $replace = ":" . implode(",:", db_identifier(array_keys($a)) );
                            break;

                        case ":&":  // associative params - becomes "key=:key AND .."
                        case ":,":  // COMMA-separated
                        case ":|":  // OR-separated
                            $fill = array(":&"=>" AND ", ":,"=>" , ", ":|"=>" OR ");
                            $replace = array(); foreach (db_identifier(array_keys($a)) as $key) { $replace[] = "`$key`=:$key"; }
                            $replace = implode($fill[$token], $replace);

                    }
                    // update SQL string
                    $sql = substr($sql, 0, $pos) . $replace . substr($sql, $pos + strlen($token));
                }

                // unfold
                if ($enum) {
                   $params2 = array_merge($params2, $a);
                } else {
                   $params2 = array_merge($params2, $a);
                }
            }
            else {
                $params2[] = $a;
            }
        }

        #-- placeholders
        if (empty(!$db->tokens) && strpos($sql, "{")) {
            $sql = preg_replace_callback("/\{(\w+)(.*?)\}/e", function($m) use ($db) {
                return isset($db->token["$m[1]"]) ? $db->token["$m[1]"]."$m[2]" : $db->token["$m[1]$m[2]"];
            }, $sql);
        }
        
        #-- SQL incompliance workarounds
        if (!empty($db->in_clause) && strpos($sql, " IN (")) { // only for ?,?,?,? enum params
            $sql = preg_replace_calback("/(\S+)\s+IN\s+\(([?,]+)\)/", function($m) {
               return "($m[1]=" . implode("OR $m[1]=", array_fill(0, 1+strlen("$m[2]")/2, "? ")) . ")";
            }, $sql);
        }

if (isset($db->test)) { print json_encode($params2)." => " . trim($sql) . "\n"; return; }
    
        #-- run
        $s = $db->prepare($sql);
        $s->setFetchMode(PDO::FETCH_ASSOC);
        $r = $s->execute($params2);

        #-- wrap        
        return $r ? new db_result($s) : $s;
    }
}

// This is a restrictive filter function for column/table name identifiers.
function db_identifier($as, $wrap="") {
    return preg_replace("/[^\w\d_.]/", "_", $wrap.$as.$wrap);  // Can only be foregone if it's ensured that none of the passed named db() $arg keys originated from http/user input.
}


/**
 * Allows list access, or fetches first result[0]
 *
 */
class db_result extends ArrayObject implements IteratorAggregate {

    function __construct($results) {
        $this->results = $results;
        parent::__construct(array(), 2);
    }
    
    // single access
    function __get($name) {
    
        // get first result, transfuse into $this
        if ($this->results) {
            foreach ($this->results->fetch(PDO::FETCH_ASSOC) as $key=>$value) {
                $this->{$key} = $value;
            }
            unset($this->results);
        }
        
        // suffice __get
        return $this->{$name};
    }
    
    // used as PDO statement
    function __call($func, $args) {
        return call_user_func_array(array($this->results, $func), $args);
    }
    
    // iterator
    function getIterator() {
        if (isset($this->results)) {
            $this->results->setFetchMode(PDO::FETCH_CLASS, "ArrayObject", array(array(), 2));
            return $this->results;
        }
        else return new ArrayIterator($this);
    }

}



/**
 * Table data gateway. Don't use directly.
 *
 * Keeps ->_meta->table name and ->_meta->fields,
 * uses extendable tables with [ext] field serialization.
 * Doesn't cope with table joins. (yet?)
 *
 * Allows to ->set() and ->save() record back.
 */
class db_record /*resembles db_result*/ extends ArrayObject {

    // this is not purposelessly private, but to not pollute (array) typecasts with decorative data
    private $_meta;

    // initialize from db() result or array
    function __construct($results, $table, $fields, $keys) {
        
        // meta
        $this->_meta = new stdClass();
        $this->_meta->table = $table;
        $this->_meta->fields = array_unique(array_merge(array_keys($fields), array_keys($results)));
        $this->_meta->keys = $keys;
        
        // db query result
        if (is_array($results)) {
            $this->_meta->new = 1;  // instantiate from defaults or given row values
        }
        else {
            //if (is_string($results)) {   // queries are handled in wrapper
            //    $results = db($results);
            //}
            $results = $results->fetch();  // just get first result row
            $this->_meta->new = 0;
        }

        // unfold .ext
        if ($this->_meta->ext = isset($results["ext"])) {
            $results = array_merge($results, unserialize($results["ext"]));
        }

        // copy data
        // and turn object==array
        parent::__construct((array)$results, 2); //ArrayObject::ARRAY_AS_PROPS

        // fluent (hybrid constructor wrapper)
        return $this;
    }
    
    // set field
    function set($key, $val) {
        $this->{$key} = $val;
        return $this;  // fluent
    }

    // store table back to DB
    function save($row=NULL) {
    
        // source
        if (empty($row)) {
            $row = $this->getArrayCopy();
        }
        else {
            $row = array_merge($this->getArrayCopy(), is_array($row) ? $row : $row->getArrayCopy());
        }
    
        // fold .ext
        if ($this->_meta->ext) {
            $ext = array();
            foreach ($row as $key=>$val) {
                if (!in_array($key, $this->_meta->fields)) {
                    $ext[$key] = $val;
                    unset($row[$key]);
                }
            }
            $row["ext"] = serialize($ext);
        }
        
        // store
        if ($this->_meta->new) {
            db("INSERT INTO {$this->_meta->table} (:?) VALUES (??)", $row, $row);
            $this->_meta->new = 0;
        }
        // update
        else {
            $keys = $this->keys($row, 1);
            db("UPDATE {$this->_meta->table} SET :, WHERE :&", $row, $keys);
        }
        
        return $this;  // fluent
    }

    // split $keys from $row/$this
    function keys(&$row, $unset=0) {
        $keys = array();
        foreach ($this->_meta->keys as $key) { 
            $keys[$key] = $row[$key];
            if ($unset) unset($row[$key]);
        } 
        return $keys;
    }
    
    // oh noooes
    function delete() {
        db("DELETE FROM {$this->_meta->table} WHERE :&", $this->keys($this));
        return $this;  // well
    }
}




?>

Changes to db.sql.

1
2
3

4
5
6
7
8
9
10
11
12
13
14
15
16





























17
18
19







20
21
22
23




24
25
26






27
28
29
30
31





32
33
34
35
36
37
38
39









1
2

3
4
5











6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


36
37
38
39
40
41
42
43



44
45
46
47
48


49
50
51
52
53
54
55
56



57
58
59
60
61
62







63
64
65
66
67
68
69
70
71
72


-
+


-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
+
+
+
+
+
+
+

-
-
-
+
+
+
+

-
-
+
+
+
+
+
+


-
-
-
+
+
+
+
+

-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
#
# title: freshcode database schema
# version: 0.5
# version: 0.6
#


CREATE TABLE [release]
  ([name] VARCHAR (100) NOT NULL, [title] TEXT NOT NULL, [homepage] TEXT,
  [description] TEXT NOT NULL, [license] VARCHAR (100), [tags] VARCHAR
  (200), [version] VARCHAR (100) NOT NULL, [state] VARCHAR (20), [scope]
  VARCHAR (20), [changes] TEXT, [download] TEXT, [urls] TEXT,
  [autoupdate_module] VARCHAR (20), [autoupdate_url] TEXT,
  [autoupdate_regex] TEXT, [t_published] INT, [t_changed] INT, [flag] INT
  DEFAULT(0), [deleted] BOOLEAN DEFAULT(0), [submitter_openid] TEXT,
  [submitter] VARCHAR (0, 50), [lock] TEXT, [hidden] BOOLEAN DEFAULT(0),
  [image] TEXT);
CREATE TABLE [release] ( 
    name              VARCHAR( 100 )     NOT NULL,
    title             TEXT               NOT NULL,
    homepage          TEXT,
    description       TEXT               NOT NULL,
    license           VARCHAR( 100 ),
    tags              VARCHAR( 200 ),
    version           VARCHAR( 100 )     NOT NULL,
    state             VARCHAR( 20 ),
    scope             VARCHAR( 20 ),
    changes           TEXT,
    download          TEXT,
    urls              TEXT,
    autoupdate_module VARCHAR( 20 ),
    autoupdate_url    TEXT,
    autoupdate_regex  TEXT,
    t_published       INT,
    t_changed         INT,
    flag              INT                DEFAULT ( 0 ),
    deleted           BOOLEAN            DEFAULT ( 0 ),
    submitter_openid  TEXT,
    submitter         VARCHAR( 0, 100 ),
    lock              TEXT,
    hidden            BOOLEAN            DEFAULT ( 0 ),
    image             TEXT,
    social_links      INT                DEFAULT ( 0 ),
    submitter_image   VARCHAR( 200 ),
    CONSTRAINT 'release_revisions' UNIQUE ( name, version COLLATE 'NOCASE', t_published, t_changed ) 
);

CREATE TABLE flags
  (name TEXT, reason TEXT, note TEXT, submitter_openid TEXT, submitter_ip TEXT);
CREATE TABLE flags ( 
    name             TEXT,
    reason           TEXT,
    note             TEXT,
    submitter_openid TEXT,
    submitter_ip     TEXT 
);

CREATE TABLE [tags]
  ([name] VARCHAR (1, 33), [tag] VARCHAR (1, 33));

CREATE TABLE tags ( 
    name VARCHAR( 1, 33 ),
    tag  VARCHAR( 1, 33 ) 
);

CREATE INDEX [idx_release] ON [release]
  ([name], [version] COLLATE NOCASE, [t_changed] DESC, [t_published] DESC);
CREATE INDEX idx_release ON [release] ( 
    name,
    t_changed                  DESC,
    t_published                DESC,
    version     COLLATE NOCASE 
);


CREATE VIEW [release_ordered] AS
  SELECT * FROM release
  ORDER BY t_published DESC, t_changed DESC;
CREATE VIEW release_ordered AS
       SELECT *
         FROM [release]
        ORDER BY t_published DESC,
                  t_changed DESC;

CREATE VIEW [release_versions] AS
  SELECT *, MAX(t_changed) AS _order
  FROM release_ordered
  WHERE NOT deleted
  GROUP BY name, version
  ORDER BY t_published DESC;
    
CREATE VIEW release_versions AS
       SELECT *,
              MAX( t_changed ) AS _order
         FROM release_ordered
        WHERE NOTdeleted
        GROUP BY name,
                 version
        ORDER BY t_published DESC;


Deleted deferred_openid_session.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
























































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
/**
 * api: php
 * title: Session startup
 * description: Avoids session startup until actual login occured
 * license: MITL
 * version: 0.3
 *
 * Start $_SESSION only if there's already a session cookie present.
 * (Prevent needless cookies and tracking ids for not logged-in users.)
 *
 * The only handler that initiates any login process is `page_login.php`
 *
 */



// Kill off CloudFlare cookie when Do-Not-Track header present
if ($_SERVER->has("HTTP_DNT") and $_SERVER->boolean["HTTP_DNT"]) {
    header("Set-Cookie: __cfduid= ; path=/; domain=.freshcode.club; HttpOnly");
}





// Check for pre-existant cookie before defaulting to initiate session store
if ($_COOKIE->has("USER")) {
    session_fresh();
}
// just populate placeholders
else {
    $_SESSION["openid"] = "";
    $_SESSION["name"] = "";
}


// verify incoming OpenID request
if ($_GET->has("openid_mode")) {

    include_once("openid.php");

    $openid = new LightOpenID(HTTP_HOST);
    if ($openid->mode) {
        if ($openid->validate()) {
            $_COOKIE->no("USER") and session_fresh();
            $_SESSION["openid"] = $openid->identity;
            $_SESSION["name"] = $openid->getAttributes()["namePerson/friendly"];
        }
    }

}



// Prevent some session tampering
function session_fresh() {

    // Initiate with current session identifier
    if ($_COOKIE->has("USER")) {
        session_id($_COOKIE->id["USER"]);
    }
    session_name("USER");
    session_set_cookie_params(0, "/", HTTP_HOST, false, true);
    session_start();

    // Security by obscurity: lock client against User-Agent
    $useragent = $_SERVER->text->length30["HTTP_USER_AGENT"];
    // Security by obscurity: IP subnet lock (or just major route for IPv6)
    $subnet = $_SERVER->ip->length6["REMOTE_ADDR"];
    // Server-side timeout (7 days)
    $expire = time() + 7 * 24 * 3600;

    // New ID for mismatches
    if (empty($_SESSION["state/client"]) or $_SESSION["state/client"] != $useragent
    or  empty($_SESSION["state/subnet"]) or $_SESSION["state/subnet"] != $subnet
    or  empty($_SESSION["state/expire"]) or $_SESSION["state/expire"] < time()
    ) {
        session_destroy();
        session_regenerate_id(true);
        session_start();
    }
    // and Repopulate status fields
    $_SESSION["state/client"] = $useragent;
    $_SESSION["state/subnet"] = $subnet;
    $_SESSION["state/expire"] = $expire;
}


Added doc/cacert.pem.









































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-----BEGIN CERTIFICATE-----
MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290
IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB
IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA
Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO
BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi
MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ
ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ
8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6
zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y
fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7
w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc
G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k
epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q
laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ
QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU
fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826
YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w
ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY
gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe
MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0
IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy
dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw
czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0
dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl
aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC
AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg
b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB
ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc
nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg
18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c
gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl
Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY
sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T
SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF
CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum
GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk
zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW
omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD
-----END CERTIFICATE-----

Added doc/fc-submit.









































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
#!/usr/bin/env python3
"""
freshcode-submit -- script transactions with the Freshcode.club server
"""

import sys, re, requests, json, netrc, email.parser, optparse

version = "3.0"

class Update:
    "Encapsulate dictionaries describing a project metadata update."
    def __init__(self):
        self.name = None
        self.per_project = {}
        self.urlassoc = []
        self.per_release = {}
    def __repr__(self):
        return "Update(" + repr(self.__dict__) + ")"

# The Freecode API implementation is sensitive to the kind of HTTP request
# method you use.  The general rule is:
#
# Reading records:                GET
# Adding new records:             POST
# Updating existing records:      PUT
# Deleting existing records:      DELETE
#
# From http://help.freecode.com/faqs/api-7/data-api-intro:
# 200 OK - Request was successful, the requested content is included
# 201 Created - Creation of a new resource was successful
# 401 Unauthorized - You need to provide an authentication code with this
#     request
# 403 Forbidden - You don't have permission to access this URI
# 404 Not Found - The requested resource was not found
# 409 Conflict - The validation of your submitted data failed, please check
#     the response body for error pointers
# 500 Server Error - The request hit a problem and was aborted, please report
#     as a bug if it persists
# 503 Service Unavailable - You hit your API credit limit

def RequestWithMethod(method, url, **kwargs):
    """requests.request is really a drop-in replacement here; with TLS-SNI support"""
    # Here verify="cacert.pem" would better solve the certificate issue
    return requests.request(method, url, verify=False, **kwargs)

class FreecodeSessionException(Exception):
    "Carry exception state when a session blows up."
    def __init__(self, msg):
        Exception.__init__(self)
        self.msg = msg

class FreecodeSession:
    "Encapsulate the state of a Freecode API session."
    server = "https://%s.freshcode.club/" % "api"

    def __init__(self, auth=None, verbose=0, emit_enable=True):
        "Initialize Freecode session credentials."
        self.auth = auth
        self.verbose = verbose
        self.emit_enable = emit_enable
        self.project = None
        self.permalink = None
        self.id = None
        self.project_data = None
        # If user didn't supply credentials, fetch from ~/.netrc
        if not self.auth:
            try:
                credentials = netrc.netrc()
            except netrc.NetrcParseError as e:
                raise FreecodeSessionException("ill-formed .netrc: %s:%s %s" \
                                               % (e.filename, e.lineno, e.msg))
            except IOError as e:
                raise FreecodeSessionException(("missing .netrc file %s" % \
                                                 str(e).split()[-1]))
            ret = credentials.authenticators("freshcode")
            if not ret:
                raise FreecodeSessionException("no credentials for Freshcode")
            _login, self.auth, _password = ret

    def on_project(self, name):
        "Select project by Freecode shortname."
        if self.verbose:
            print(("Selecting project: %s" % name))
        self.project = name
        pquery = FreecodeSession.server + "projects/%s.json?auth_code=%s" \
                % (self.project, self.auth)
        handle = RequestWithMethod("GET", url=pquery)
        content = json.loads(handle.text)
        self.project_data = content['project']
        #if self.verbose:
        #    print "Project data: %s" % self.project_data
        self.permalink = self.project_data['permalink']
        self.id = self.project_data['id']

    def edit_request(self, url, method="GET", request=None, force=False):
        "Wrap a JSON object with the auth code and ship it as a request"
        if request is None:
            request = {}
        url = FreecodeSession.server + url
        data = {"auth_code" : self.auth}
        data.update(request)
        data = json.dumps(data)
        headers = {"Content-Type" : "application/json"}
        if self.verbose:
            print(("Request URL:", method, url))
        #if self.verbose:
        #    print "Request headers:", headers
        if self.verbose:
            print(("Request data:", data))
        if self.emit_enable or force:
            req = RequestWithMethod(method=method,
                                    url=url,
                                    data=data,
                                    headers=headers)
            if self.verbose:
                print(req.status_code, req.url, req.headers)
            content = req.text
            if self.verbose:
                print(("Response:", content))
            return content

    def publish_release(self, data):
        "Add a new release to the current project."
        if self.verbose:
            print(("Publishing %s release: %s" % (self.project, repr(data))))
        self.edit_request("projects/" + self.permalink + "/releases.json",
                          "POST",
                          {"release": data})

    def withdraw_release(self, dversion):
        "Withdraw a specified release from the current project."
        if self.verbose:
            print("Withdrawing %s release: %s" % (self.project, dversion))
        releases = self.edit_request("projects/%s/releases/pending.json" \
                                     % self.permalink, force=True)
        releases = json.loads(releases)
        for release in releases:
            properties = release["release"]
            if properties.get("version") == dversion:
                vid = properties["id"]
                break
        else:
            raise FreecodeSessionException("couldn't find release %s"%dversion)
        deletelink = "projects/%s/releases/%s.json" % (self.permalink, vid)
        self.edit_request(deletelink, "DELETE", {})

    def update_core(self, coredata):
        "Update the core data for a project."
        if self.verbose:
            print("Core data update for %s is: %s" % (self.project, coredata))
        self.edit_request("projects/" + self.permalink + ".json",
                          "PUT",
                          {"project": coredata})

    def update_urls(self, urlassoc):
        "Update URL list for a project."
        if self.verbose:
            print("URL list update for %s is: %s" % (self.project, urlassoc))
        # First, get permalinks for all existing URLs
        uquery = FreecodeSession.server + "projects/%s/urls.json?auth_code=%s" \
                % (self.permalink, self.auth)
        handle = RequestWithMethod("GET", url=uquery)
        content = json.loads(handle.text)
        permadict = content['urls']
        # Just send the new dict over
        self.edit_request("projects/%s/urls.json" % (self.permalink),
                          "PUSH",
                          {"urls" : dict(urlassoc)})

class FreecodeMetadataFactory:
    "Factory class for producing Freecode records in JSON."
    freecode_field_map = (
        ("Project",          "P", "name"),                   # Project
        ("Summary",          "S", "oneliner"),               # Project
        ("Description",      "D", "description"),            # Project
        ("License-List",     "L", "license_list"),           # Project
        ("Project-Tag-List", "T", "project_tag_list"),       # Project
        ("Version",          "v", "version"),                # Release
        ("Changes",          "c", "changelog"),              # Release
        ("Hide",             "x", "hidden_from_frontpage"),  # Release
        ("Release-Tag-List", "t", "tag_list"),               # Release
        )
    # Which attributes have project scope, all others have release scupe
    projectwide = ('name',
                   'description',
                   'oneliner',
                   'license_list',
                   'project_tag_list')

    def __init__(self):
        self.message_parser = email.parser.Parser()
        self.argument_parser = optparse.OptionParser( \
            usage="usage: %prog [options]")
        for (msg_field, shortopt, rpc_field) in FreecodeMetadataFactory.freecode_field_map:
            self.argument_parser.add_option("-" + shortopt,
                                            "--" + msg_field.lower(),
                                            dest=rpc_field,
                                            help="Set the %s field"%msg_field)
        self.argument_parser.add_option('-q', '--query', dest='query',
                          help='Query metadata for PROJECT',metavar="PROJECT")
        self.argument_parser.add_option('-d', '--delete', dest='delete',
                          default=False, action='store_true',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-n', '--no-stdin', dest='read',
                          default=True, action='store_false',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-N', '--dryrun', dest='dryrun',
                          default=False, action='store_true',
                          help='Suppress reading fields from stdin.')
        self.argument_parser.add_option('-V', '--verbose', dest='verbose',
                          default=False, action='store_true',
                          help='Enable verbose debugging.')
        self.argument_parser.add_option('-?', '--showversion', dest='showversion',
                          default=False, action='store_true',
                          help='Show version and quit.')
    @staticmethod
    def header_to_field(hdr):
        "Map a header name from the job card format to a field."
        lhdr = hdr.lower().replace("-", "_")
        for (alias, _shortopt, field) in FreecodeMetadataFactory.freecode_field_map:
            if lhdr == alias.lower().replace("-", "_").replace("/", "_"):
                return field
        raise FreecodeSessionException("Illegal field name %s" % hdr)

    def getMetadata(self, stream):
        "Return an Update object describing project and release attributes."
        data = {}
        urls = {}
        (options, _args) = self.argument_parser.parse_args()
        # Stuff from stdin if present
        prior_version = data.get("version")
        if not (options.query or options.showversion) and options.read:
            message = self.message_parser.parse(stream)
            for (key, value) in list(message.items()):
                value = re.sub("\n +", " ", value).strip()
                if key.endswith("-URL"):
                    key = key.replace("-", " ")
                    urls.update({key[:-4] : value})
                else:
                    if key.endswith("List"):
                        value = [x.strip() for x in value.split()]
                    data.update({FreecodeMetadataFactory.header_to_field(key) : value})
            if not 'changelog' in data:
                payload = message.get_payload().strip()
                if payload:
                    data['changelog'] = payload + "\n"
            if prior_version and data.get("version") != prior_version:
                raise FreecodeSessionException("Version conflict on stdin.")
        # Merge in options from the command line;
        # they override what's on stdin.
        controls = ('query', 'delete', 'read', 'dryrun', 'verbose', 'showversion')
        prior_version = data.get("version")
        for (key, value) in list(options.__dict__.items()):
            if key not in controls and value != None:
                data[key] = value
                del options.__dict__[key]
        if prior_version and data.get("version") != prior_version and not options.delete:
            raise FreecodeSessionException("Version conflict in options.")
        # Hidden flag special handling
        if "hidden_from_frontpage" in data:
            data["hidden_from_frontpage"] = data["hidden_from_frontpage"] in ("Y", "y")
        # Now merge in the URLs, doing symbol substitution
        urllist = []
        for (label, furl) in list(urls.items()):
            for (k, v) in list(data.items()):
                if type(v) == type(""):
                    furl = furl.replace('${' + k + '}', v)
            urllist.append((label, furl))
        # Sort out what things go where
        update = Update()
        if options.showversion:
            pass
        elif options.query:
            update.name = options.query
        else:
            update.name = data.pop('name')
            update.urlassoc = urllist
            for (k, v) in list(data.items()):
                if k in FreecodeMetadataFactory.projectwide:
                    # Hack to get around a namespace collision
                    if k == "project_release_tag":
                        k = "release_tag"
                    update.per_project[k] = v
                else:
                    update.per_release[k] = v
        # Return this
        return (options, update)

if __name__ == "__main__":
    try:
        # First, gather update data from stdin and command-line switches
        factory = FreecodeMetadataFactory()
        (options, update) = factory.getMetadata(sys.stdin)
        # Some switches shouldn't be passed to the server
        query = 'query' in options.__dict__ and options.query
        verbose = 'verbose' in options.__dict__ and options.verbose
        delete  = 'delete' in options.__dict__ and options.delete
        dryrun  = 'dryrun' in options.__dict__ and options.dryrun
        showversion  = 'showversion' in options.__dict__ and options.showversion
        if showversion:
            print("freshcode-submit", version)
            raise SystemExit(0)
        # Time to ship the update.
        # Establish session
        session = FreecodeSession(verbose=int(verbose), emit_enable=not dryrun)
        try:
            session.on_project(update.name)
        except ValueError as e:
            print(e)
            print("freshcode-submit: looks like a server-side problem at freshcode.club; bailing out.", file=sys.stderr)
            raise SystemExit(1)
        if options.query:
            print("Project: %s" % session.project_data["name"])
            print("Summary: %s" % session.project_data["oneliner"])
            print("Description: %s" % session.project_data["description"].replace("\n", "\n    ").rstrip())
            print("License-List: %s" % ",".join(session.project_data["license_list"]))
            print("Project-Tag-List: %s" % ",".join(session.project_data["tag_list"]))
            for assoc in session.project_data['approved_urls']:
                #print "Querying", assoc["redirector"]
                #req = RequestWithMethod(method="HEAD",
                #                        url=assoc["redirector"],
                #                        data={},
                #                        headers={})
                #handle = urllib2.urlopen(req)
                #print "==="
                #print handle.info()
                #print "==="
                print("%s-URL: %s" % (assoc["label"].replace(" ","-"), assoc["redirector"]))
            if 'recent_releases' in session.project_data and session.project_data['recent_releases']:
                most_recent = session.project_data['recent_releases'][0]
                print("Version: %s" % most_recent['version'])
                print("Tag-List: %s" % ",".join(most_recent['tag_list']))
                if most_recent.get('hidden_from_frontpage'):
                    print("Hide: Y")
                else:
                    print("Hide: N")
                print("")
                print(most_recent["changelog"])
        else:
            # OK, now actually add or delete the release.
            if update.per_project:
                session.update_core(update.per_project)
            if update.urlassoc:
                session.update_urls(update.urlassoc)
            if delete:
                session.withdraw_release(update.per_release['version'])
            elif update.per_release and list(update.per_release.keys())!=["version"]:
                session.publish_release(update.per_release)
    except FreecodeSessionException as e:
        print("freshcode-submit:", e.msg, file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.HTTPError as f:
        print("freshcode-submit: HTTPError %s" %  (f.code), file=sys.stderr)
        print(f.read(), file=sys.stderr)
        sys.exit(1)
    except requests.exceptions.RequestException as f:
        print("freshcode-submit: URLError %s" %  (f.reason,), file=sys.stderr)
        sys.exit(1)

# end

Added doc/logo.svgz.

cannot compute difference between binary files

Added doc/mktrove.php.




































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
#!/usr/bin/php -qC
<?php
/**
 * api: cli
 * title: convert trove.csv
 * description: generates PHP array of trove categories and tag leaves
 *
 *  `soffice --headleass --convert-to csv trove.ods`
 *  does not work, so we need it preconverted in trove.csv
 *
 *  only first 4 columns contain the tree and leaves
 *
 */


#-- read in
$csv = array_map("str_getcsv", file(__DIR__."/trove.csv"));
unset($csv[0]);  // remove head


// target
$TREE = array();
$TMAP = array();

// last tree
$last_path = array();

# loop over lines
foreach ($csv as $row) {

    // Cut out only 5 columns (rest of spreadsheet is documentation)
    $py_trove = $row[5];
    $row = array_map("trim", array_slice($row, 0, 5));


    // merge current leave parts with last path
    if ($row = array_filter($row)) {
        $row = array_slice($last_path, 0, key($row)) + $row;
        $last_path = $row;
    }
    // skip empty
    else {
        continue;
    }
    $TMAP[] = array($py_trove, implode(" :: ", $row));

    
    // append to what we have
    $path = array_filter($row);
    $leaf = array_pop($path);
    $var = enclose("['", $path, "']");
    eval("
        \$TREE{$var}[] = '$leaf';
    ");
#            if ($up_var) unset(\$TREE{$up_var}[array_search('$up_leaf', \$TREE{$up_var})]);

}

// reorder, strip Status and License, then output
print var_export54(array(
    "Topic" => $TREE["Topic"],
    "Programming Language" => $TREE["Programming Language"],
    "Environment" => $TREE["Environment"],
    "Framework" => $TREE["Framework"],
    "Operating System" => $TREE["Operating System"],
    "Audience" => $TREE["Audience"],
#    "Natural" => $TREE["Natural"],
));

// save mapping onto py-trove
file_put_contents(__DIR__."/trove.pypi-map.csv", json_encode($TMAP, JSON_PRETTY_PRINT));


#-- le functions

function enclose($pre, $values, $post) {
    return $values ? $pre . implode("$post$pre", $values) . $post : "";
}


function var_export54($var, $indent="") {
    switch (gettype($var)) {
        case "string":
            return '"' . addcslashes($var, "\\\$\"\r\n\t\v\f") . '"';
        case "array":
          //  $indexed = array_keys($var) === range(0, count($var) - 1);
            $r = [];
            foreach ($var as $key => $value) {
                $indexed = is_numeric($key);
                $r[] = "$indent    "
                     . ($indexed ? "" : var_export54($key) . " => ")
                     . var_export54($value, "$indent    ");
            }
            return "[\n" . implode(",\n", $r) . "\n" . $indent . "]";
        case "boolean":
            return $var ? "TRUE" : "FALSE";
        default:
            return var_export($var, TRUE);
    }
}

Added doc/trove.ods.

cannot compute difference between binary files

Added forum.css.






































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
/**
 * api: css
 * type: stylesheet
 * title: meta forum layout
 * description: Minimalistic forum style
 * version: 0.1
 *
 * Posts are just presented in a nested list,
 * and left-side title banner sticks out.
 *
 */


/**
 * Google fonts
 *
 */
@import url(http://fonts.googleapis.com/css?family=Hind:400,500,700,300);

/**
 * General
 *
 */
html, body { padding: 0; margin: 0; height: 100%; }
body {
/* font: 400 12pt/16pt Kreon;/
   font: 500 12pt/16pt Raleway;
   font: 500 12pt/16pt Numans;
   font: 500 12pt/16pt Inder;
   font: 400 12pt/16pt Voces;
   font: 400 12pt/16pt Magra;*/
   font: 400 12pt/16pt Hind;
}

/**
 * Title border
 *
 */
#title {
   display: block;
   position: fixed;
   left: 0;
   top: 0;
   background: #222;
   width: 150pt;
   float: left;
   height: 100%;
   min-height: 5000px;
   padding: 0;  margin: 0;
}
h1 {
   transform: rotate(270deg);
   color: #fff;
   font-size: 72pt;
   position: relative;
   top: 400pt;
   white-space: nowrap;
   letter-spacing: -0.025em;
}
h1 { font-weight: 300; }
h1 b { font-weight: 700; }
h1 .red { color: #744; }
h1 .grey { color: #444; }

/**
 * Forum tree
 *
 */
.forum, .forum ul {
   padding: 5pt 0 0 30pt;
   list-style: none;
}
ul.forum {
   padding: 5pt;
   padding-left: 180pt;
   list-style: none;
   padding-bottom: 90pt;
}

/**
 * One post block (wrapped in <li>)
 *
 */
.forum .entry {
   display: block;
   width: 600pt;
/*   background: #eee;
   border: 1px solid #ddd; */
   min-height: 30pt;
   padding-left: 5pt;
   margin-top: 15pt;
}

/**
 * Tag / author / time - left-rotated meta info.
 *
 */
.forum .entry .meta {
}
.forum .entry .meta div {
   /* border: 2px dashed #faa; */
}
.forum .entry .meta div > * {
   font-size: 90%;
   line-height: 90%;
   color: #666;
   padding-right: 5pt;
}
.forum .entry .meta .datetime {
   font-size: 5pt;
   color: #ccc;
}
.forum .entry .meta .category {
   color: #85879f;
   background: #fcf9f1;
   border-radius: 3pt;
   font-size: 105%;
   letter-spacing: 0.1em;
}


/**
 * Post content.
 *
 */
.forum .entry .summary {
   display: block;
   margin: 0;
   font-weight: bold;
   font-size: 108%;
   color: #449;
}
.forum .entry .excerpt {
   color: #777;
}
.forum .entry .excerpt.trimmed {
   display: none;
}
.forum .entry .content.trimmed {
   display: none;
}
.forum .entry .funcs.trimmed {
   opacity: 0.1;
}
.forum .entry .content p {
   padding-top: 0; margin-top: 0;
}

label {
   display: block;
   padding: 5pt;
}

.action {
   margin: 1pt;
   padding: 2pt 10pt;
   color: #33c;
   background: #e7e7fc;
   border: #f0f0f9;
   border-radius: 5pt;
   font-size: 10pt;
   opacity: 0.85;
   cursor: pointer;
}
.action.forum-edit {
   opacity: 0.5;
}
.action:hover {
   opacity: 1.0;
}
.action.forum-edit:hover {
   cursor: s-resize;
}
.action.forum-reply:hover {
   cursor: copy;
}


/**
 * Submit form
 *
 */
form.forum-submit label b {
   display: inline-block;
   width: 80pt;
   text-align: right;
}
form.forum-submit label b select {
   font-weight: 900;
   font-size: 105%;
   text-align: right;
}

form.forum-submit .markup-buttons {
   width: 80pt;
   padding: 5pt;
   text-align: right;
   position: relative;
}
.markup-buttons .action {
   display: inline-block;
   margin: 0 10pt 7pt 0;
   cursor: text;
   padding: 3pt 15pt 1pt 15pt;
   line-height: 1em;
   background: #eef0f7;
   opacity: 0.3;
}
.markup-buttons .action:hover {
   opacity: 1.0;
}

.error, .warning {
   background: #eeddaa;
   border: 2px solid #ddcc99;
   border-radius: 5pt;
   padding: 5pt;
}
.error {
   background: #eebbaa;
}


input, textarea, select {
   font-size: 105%;
   padding: 1.5pt;
   border: 1px solid #bbb;
   border-left: 5pt solid #ccc;
   border-radius: 5pt;
}

Changes to freshcode.css.












1
2


3




4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
11


12
13
14
15
16
17
18
19
20
21
22
23
24
25
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+

+
+
+
+






/**
 * api: css
 * type: stylesheet
 * title: freshcode.club layout+style
 * description: Simulates the late freecode.com layout and looks; well mostly.
 * version: 0.6.5
 *
 * Centered two-pane layout. The #main section is usually 33% of the screen width,
 * while the #sidebar floats at the right. They're repositioned only using padding:
 * to the outer html,body{}.
 *
* {
}
 */


/**
 * General HTML rendering presets.
 *
 */
html, body {
    padding: 0;
    margin: 0;
    background: #fff;
    font-family: Verdana, Arial;
    font-size: 11pt;
}
18
19
20
21
22
23
24


25
26

























27
28
29
30
31
32
33
33
34
35
36
37
38
39
40
41


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73







+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






    vertical-align: middle;
    border: 0;
}
h2 { font-size: 13pt; }
h3 { font-size: 12pt; }
h4 { font-size: 11.5pt; }
h5 { font-size: 11pt; }
a[href=""] {
    opacity: 0.20;


}
input, textarea, select {
    font-size: 107%;
}
code, var, kbd {
    background: linear-gradient(90deg,#fafafa,#fafaff);
    box-shadow: 0 0 3px 2px #ccf;
    border-radius: 3px;
    margin: 1px;
}
var { box-shadow: 0 0 3px 2px #fdd; }
kbd { box-shadow: 0 0 3px 2px #dfd; }

table {}
tr, th, td {
   align: left;
   vertical-align: top;
}



/**
 * Page header, info bar and logo box
 *
 */
#topbar {
    background: #555599;
    padding: 3pt 150pt;
    color: #fff;
}
#topbar a {  color: #fec;  }
#topbar a:hover {  color: #fc7;  }
50
51
52
53
54
55
56




57
58
59

60
61
62
63
64
65
66





67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82



83
84
85
































86
87
88

89
90
91
92
93




94
95
96
97
98
99
100
90
91
92
93
94
95
96
97
98
99
100
101
102

103
104
105
106
107
108
109

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184







+
+
+
+


-
+






-
+
+
+
+
+
















+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+





+
+
+
+






    width: 60%;
    max-width: 70%;
    background: #fdfdfd;
    border: 1pt solid #777;
}


/**
 * Main action link box, hovering halfway over header box.
 *
 */
#tools {
    margin: 0 150pt;
    padding: 4pt;
    padding: 3pt 5pt 2pt 5pt;
    border: 1.75pt solid #bbb;
    border-top: 1pt solid #ccc;
    border-radius: 5pt;
    background: #e5e7e9;
    background: linear-gradient(to bottom, #ffffff, #fdfefe, #f5f7f9, #eaecee, #e5e7e9, #e1e3e5, #d0d1d2);
    position: relative;
    top: -15pt;
    top: -14pt;
    font-size: 95%;
}
.absolute {
    position: absolute;
}
#tools a {
    color: #777;
    margin: 0 1pt;
    padding: 2pt 8pt;
    border-radius: 4pt;
}
#tools a.submit {
    background: #79d;
    background: linear-gradient(145deg,#e5e5ef,#d1d3df);
    color: #111;
}
#tools a:hover {
    color: #fff;
    background: #346;
}
#tools .submenu:hover {
    background: #D3D7DE;
    border-radius: 5pt;



}
#tools .submenu a:first-child {
    padding-right: 3pt;
    margin-right: 0;
}
#tools .submenu a:nth-child(2) {
    padding-left: 3pt;
    margin-left: 0;
}
#tools #search_q {
    display: inline;
    border-radius: 4pt;
    padding: 1pt;
}
#tools #search_q input {
    border: 1px solid #999;
    background: #eee;
    height: 11pt;
    border-radius: 3pt;
    margin: 1pt;
}
#tools #search_q:hover {
    background: #D3D7DE;
}
#tools #search_q:hover a {
    color: #eee;
}

/**
 * Sidebar floats right to #main, usually just a fifth of its width.
 *
 */
#sidebar {
    float: right;
    width: 17%;
    width: 15%;
    margin: 25pt 150pt 25pt 25pt;
    min-width: 175pt;
    min-height: 400pt;
    background: #fefdfd;
}
#sidebar.absolute-float {
    position: absolute;
    margin: 25pt 150pt 25pt 68%;
}
#sidebar section {
    border: 1.5pt solid #ccc;
    border-radius: 5.75pt;
    background: #eee linear-gradient(#fcfcfc, #fafafa, #f7f7f7, #f2f2f2, #eeeeee, #dddddd);
    padding: 3pt;
    margin-bottom: 10pt;
}
110
111
112
113
114
115
116


117
118

















119
120
121
122

123
124
125
126
127
128
129

130
131
132

133
134
135
136
137

138
139
140
141
142
143
144

145
146
147
148
149
150
151
152



153

154




155
156
157
158
159
160
161
194
195
196
197
198
199
200
201
202


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222

223
224
225
226
227
228
229

230
231
232

233
234
235
236
237

238
239
240
241
242
243
244

245
246
247
248
249
250
251
252
253
254
255
256

257
258
259
260
261
262
263
264
265
266
267
268
269







+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+






-
+


-
+




-
+






-
+








+
+
+
-
+

+
+
+
+






    display: block;
    background: linear-gradient(#f7f7f7 40%, #eee 70%, #ddd 100%);
}
#sidebar section.article-links.trimmed a {
    height: 1.3em;
    overflow: hidden;
}
#sidebar.community-web {
    font-size: 90%;


}
#sidebar.community-web li {
    margin-left: 5pt;
    list-style: none;
}
#sidebar.community-web li:before {
    content: "→";
    color: #aaa;
}
#sidebar .submitter .gravatar {
    margin: 2pt 5pt 1pt 2pt;
}

/**
 * Main content area (project listings, frontpage, articles, etc.)
 *
 */
#main {
    margin: 20pt 150pt;
    width: 50%;
    min-height: 400pt;
    min-height: 700pt;
}

#main h2, #main h3, #main h4 {
    background: #ddd;
    background: linear-gradient(#f3f3f3,#f0f0f0,#eee,#e7e7e7,#d3d3d3);
    border-radius: 2pt;
    padding: 3.5pt 5pt;
    padding: 3.5pt 5pt 1.5pt 5pt;
    margin-top: 25pt;
}
#main label {
#main label, #sidebar label {
    display: block;
    margin: 10pt 0;
    font-weight: 700;
}
#main label input, #main label textarea {
#main label input, #sidebar label input, #main label textarea {
    display: block;
    font-weight: 400;
}
#main label input[type=radio] {
    display: inline;
}
#main label small {
#main label small, #sidebar label small {
    display: block;
    font-weight: 200;
    font-size: 85%;
    color: #777;
}
#main label.inline {  font-weight: 200; margin: 4pt; }
#main label.inline input {  display: inline;  }

.action {
    color: #697;
    border-bottom: 1px dashed #5d7;

}

/**
 * Login box etc.
 *
 */
#main .box {
    margin: 20pt;
    padding: 50pt;
    border-radius: 5pt;
    border: 3pt solid #357;
    background: #7ae;
    background: linear-gradient(145deg,#7ae,#39d);
178
179
180
181
182
183
184








185
186




187
188
189
190
191
192
193
194
195
196



197
198
199
200
201

202
203
204
205

206
207
208
209
210
211
212
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337







+
+
+
+
+
+
+
+


+
+
+
+










+
+
+





+




+






}
#main .login.box #login_url {
    background: #f7faff;
    background: linear-gradient(99deg, #888 0%, #a0a0a0 1%, #fc7 3%, #bbb 5%, #fff 8%);
    padding-left: 33px;
}


/**
 * Project listing on frontpage and /projects/xyz
 *
 */
#main .project h3 {
    white-space: nowrap;
}
#main .project h3 a {
    color: #000;
    display: inline-block;
    max-width: 400pt;
    overflow: hidden;
    text-overflow: ellipsis;
}
#main .project h3 a:hover, #main .project h3 a:hover .version  {
    color: #237;
}
#main .project .version {
    font-style: normal;
    font-weight: 200;
}
#main .project .links {
    float: right;
}
#main .project .links a[href=""] {
    opacity: 0.15;
}
#main .project .published_date {
    font-weight: 200;
    font-size: 65%;
    color: #777;
    position: relative; top: -3.5pt;
}
#main .project img.preview {
    padding: 3px;
    border: 1px solid #eee;
    box-shadow: 2px 2px 7px 0px #ccc;
    margin: 3pt;
}
#main .project .description {
    padding-bottom: 5pt;
    border-bottom: 1px solid #ccc;
    margin-bottom: 3pt;
    font-size: 95%;
240
241
242
243
244
245
246



























247

248
249
250

251
252
253




254
255
256
257
258
259
260
261
262
263
264
265
266
267

268

269
270



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289




290
291
292
293
294
295
296
297
298
299
300
301

302




303






304
305
306





307
308
309
310
311
312
313
314
315









316
317



















































































































































































318
319
320
321
322
323
324

325































326
327
328
329
330
331
332
333
334
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398

399
400
401

402
403

404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423

424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463

464
465
466
467
468
469

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501


502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686

687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+


-
+

-

+
+
+
+














+
-
+


+
+
+



















+
+
+
+











-
+

+
+
+
+
-
+
+
+
+
+
+



+
+
+
+
+









+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






-
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+








}
#main .project .trimmed div {
    box-shadow: inset 0 5pt 5pt 0px #ffeecc;
    height: 5.55em;
    overflow: hidden;
}


/**
 * Shortened view on search/
 *
 */
#main .project.search {
    clear: both;
    margin-bottom: 15pt;
}
#main .project.search h3 {
    display: inline;
    margin: 2pt;
    padding: 2pt;
    font-size: 95%;
}
#main .project.search img {
    border: 0;
    box-shadow: none;
    margin: 0 2pt 3pt 0;
}
#main .project.search small.description {
    line-height: 80%;
}

/**
 * Variation for /projects/xyz detail view.
 */
#main .project .long-tags span {
#main .project .long-tags th {
    border: 1px solid #ddd;
    background: #eee;
    padding: 1pt 8pt;
    padding: 1pt 12pt;
    border-radius: 4pt;
    margin-right: 30pt;
    font-size: 80%;
    text-align: right;
}
#main .project .long-tags {
    border-spacing: 5pt;
}
#main .project .long-tags a {
    padding: 2pt 5pt;
}
#main .project .long-links a, #sidebar section .long-links {
    border: 1px solid #79f;
    background: #47d;
    padding: 2pt 8pt;
    border-radius: 4pt;
    margin-right: 15pt;
    font-size: 115%;
    color: #fff;
}
#main .project .long-links a:hover {
    text-shadow: 0px 0px 5px #fff;
    color: red;
    color: #eef;
}

/**
 * List of releases below /projects/xyz
 */
#main .release-list div.release-entry {
    margin-bottom: 15pt;
}
#main .release-list .version, #main .release-list .published_date {
    background: #57b;
    padding: 1pt 8pt;
    border-radius: 5pt 0 0 5pt;
    color: #fff;
    font-size: 80%;
}
#main .release-list .published_date {
    background: #666;
    border-radius: 0 5pt 5pt 0;
}
#main .release-list .release-notes {
    display: block;
}


/**
 * Image gallery in /links to other FLOSS directories.
 *
 */
#main .links-entry {
    display: inline-block;
    width: 200px;
    min-height: 250px;
    margin: 10pt;
    float: left;
}
#main .links-entry:nth-child(3n) {
    clear: both;
}
#main .links-entry img {
    margin-bottom: 3pt;
    margin-bottom: 7pt;
    border: 1px solid #f3f3f3;
    box-shadow: 3px 3px 7px #888;
}
#main .links-entry a:hover img {
    border-color: 1px solid #f9f7f3;
    box-shadow: 2px 2px 5px #ccc;
    box-shadow: 2px 2px 5px #777;
}
#main .links-entry a:focus img {
    border-color: 1px solid #ecb;
    position: relative; top: 1px; left: 1px;
    box-shadow: 2px 2px 4px #743;
}


/**
 * Colors and block looks for social media share buttons
 * (per project-homepage and also in freshcode-footer)
 *
 */
.social-share-links a { display: inline-block; opacity: 0.3; border-radius: 4px; padding: 3px; border: 1px solid #ddd; color: #fff !important; box-shadow: 2px 2px 5px 0px #888;}
.social-share-links a:hover { opacity: 1.0; }
.social-share-links a[title="google+"] { background: #DD4B39; font-weight: 100; }
.social-share-links a[title="facebook"] { background: #3B5998; font-weight: 700; }
.social-share-links a[title="reddit"] { background: #C0D7DF; color: #444 !important; }
.social-share-links a[title="twitter"] { background: #55ACEE;  }
.social-share-links a[title="linkedin"] { background: #0e76a8; font-weight: 700; }
.social-share-links a[title="stumbleupon"] { background: #f74425;  }
.social-share-links a[title="delicious"] { background: #557CDE;  }
.social-share-count {
    font-style: normal;
    font-weight: 300;
    font-size: 85%;
    border: 1px solid #eee;
    box-shadow: #999 0px 0px 4px;
    background: linear-gradient(45deg, #fff, #f7f7f7);
    border-radius: 2pt;
    padding: 1pt;


}
.social-share-count:before {
    content: "★";
    color: #779;
    font-size: 150%;
    position: relative;
    top: 1pt;
}

/* */
.pagination-links {
    margin-top: 35pt;
    border: 1px solid #e7e7e7;
    background: #f0f0f0;
    text-align: center;
    padding: 1pt;
}
.pagination-links a {
    background: #d9d9d9;
    border-radius: 4pt;
    padding: 0pt 2pt;
    margin: 0pt 7pt;
    color: #333;
    font-size: 80%;
}
.pagination-links a.current {
    background: #999;
}



/**
 * Submit form: trove select list (<ul> instead of <select>
 *
 */
#trove_tags {
    margin: 0; padding: 0;
    display: inline-block;
    position: relative;
    top: -70px;
    width: 175px;
    height: 170px;
    overflow-y: scroll;
    overflow-x: hidden;
    border: 1px solid #ccc;
    font-size: 85%;
}
#trove_tags.pick-tags {
    width: 220px;
    height: 950px;
    top: 0px;
    border: none;
    font-size: 92%;
}
#trove_tags span {
    display: block;
    margin: 0; padding: 0;
    padding-left: 10px;
    font-weight: 200;
}
#trove_tags > span {
    padding: 0;
}
#trove_tags span > span {
    background: url("img/dotleader.png") no-repeat;
}
#trove_tags span.optgroup b {
    font-weight: 900;
    display: block;
}
#trove_tags span > span.option:hover, #trove_tags span.optgroup > b:hover {
    background: #ccf;
}
#trove_tags .option.selected, #tag_cloud a.selected {
    background: #fba !important;
}

#trove_tags .optgroup[data-tag='topic']   { background: #f3f4f7; }
#trove_tags .optgroup[data-tag='topic'] * { background: #f7f7f9; }
#trove_tags .optgroup[data-tag='programming-language']   { background: #d7dcf0; }
#trove_tags .optgroup[data-tag='programming-language'] * { background: #ecf0fa; }
#trove_tags .optgroup[data-tag='environment']   { background: #f3f0e0; }
#trove_tags .optgroup[data-tag='environment'] * { background: #f7f5eb; }
#trove_tags .optgroup[data-tag='framework']   { background: #f0e0f0; }
#trove_tags .optgroup[data-tag='framework'] * { background: #f7e7f7; }
#trove_tags .optgroup[data-tag='operating-system']   { background: #ffe5e0; }
#trove_tags .optgroup[data-tag='operating-system'] * { background: #fce9e7; }
#trove_tags .optgroup[data-tag='audience']   { background: #e7f7ec; }
#trove_tags .optgroup[data-tag='audience'] * { background: #f0fff3; }




/**
 * Specific pages
 *
 */

/* /names */
.project-name-columns {
   column-count: 6;
   column-gap: 20pt;
   column-break-inside: avoid;
}
.project-name-columns a {
   display: inline-block;
   margin: 11pt;
   white-space: nowrap;
   color: #337;
   font-weight: 700;
   text-align: center;
}
.project-name-columns a img {
   display: block;
   box-shadow: 1px 1px 2px 1px #eee;
}

/* RecentChanges */
table.rc {
   width: 100%;
   margin-top: 5pt;
}
.rc th {
   background: #ddd;
   font-weight: 500;
   padding: 3pt;
   text-align: left;
   border-radius: 5pt 5pt 0 0;
   box-shadow: 0px -2px 3px 1px #eee;
}
.rc td {
   background: #f7f9fc;
}
.rc th a {
   color: #22c;
}
.rc th:first-child {
   font-weight: 600;
}
.rc th:first-child,
.rc td:first-child {
   width: 15%;
}
.rc ins {
   color: #151;
   text-decoration: none;
   border-bottom: 1px dashed green;
   background: #efe;
}
.rc del {
   color: #521;
   background: #fee;
}
.rc.admin ins {
   border: none;
   background: #f1fff3;
}
.rc.admin del {
   color: #210;
   background: #fff5f3;
   text-decoration: none;
}
.rc .funcs {
   float: right;
}
.rc .funcs a {
   font-size: 60%;
   color: #99b;
   background: #e3e3e3;
   border: 1px solid #eee;
   border-radius: 3pt;
   padding: 1pt 3pt;
}


/**
 * Page footer
 *
 */
#spotlight {
    margin-top: 50pt;
    border-top: 3pt solid #B7C0BB;
    background: #E7E7D0;
    padding: 10pt 150pt;
    color: #111;
    height: 70pt;
    height: 120pt;
}
#spotlight a {
    display: inline-block;
    float: left;
    margin: 10pt;
    padding: 7pt;
    width: 200pt;
    height: 85pt;
    overflow: hidden;
    text-overflow: ellipsis;
    border-radius: 10pt;
    background: linear-gradient(to bottom, #F3F3DF, #E7E7D0 70%);
}
#spotlight a b {
    color: #111;
    font-weight: 700;
    display: inline-block;
    max-width: 120px;
    max-height: 15pt;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
#spotlight a img {
    float: left;
    margin-right: 5pt;
    border-radius: 5pt;
}
#spotlight a small {
    color: #444;
}

#bottom {
    border-top: 3pt solid #5F677F;
    background: #444477;
    padding: 10pt 150pt;
    color: #fff;
    height: 50pt;
}
#bottom a { color: #fc9; }

Added gimmicks.js.




























































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
/**
 * api: jquery
 * title: UI behaviour
 * description: Well, just client-side interface features
 * version: 0.4
 * depends: jquery, jquery-ui
 *
 * Collects a few event callbacks to toggle and trigger all the things.
 *
 *  → Compacted entries (.trimmed class)
 *
 *  → Trove tags
 *      → Injection to submit_form input box
 *      → Or appending to links on page_tags cloud
 *
 *  → Action links (green dashed underline)
 *      → Lock entry in submit_form
 *      → Injecting $version placeholder in URLs
 *      → Sidebar box for submit_imports
 *
 *  → Forum action links
 *
 */


// DOM ready
$(document).ready(function(){

    // Make frontpage #main .project descriptions expandable, by undoing .trimmed; animatedly
    $(".project .trimmed").one("click", function(){  
        $(this).animate({"max-height": "20em"});
    });
    
    // Likewise for compacted news feeds in #sidebar
    $(".article-links.trimmed").one("click", function(){
        $(this).toggleClass("trimmed");
    });

    
    /**
     * Trove map and tag cloud.
     *
     */
    
    // Trove tags add to input#tags field
    $("#trove_tags.add-tags .option").click(function(){
        var $tags = $("#tags");
        var prev = $tags.val();
        $tags.val(prev + (prev.length ? ", " : "") + $(this).data("tag"));
    });

    // Trove tags highlight in page_tags cloud
    $("#trove_tags.pick-tags .option").click(function(){

        // highlight in trove box
        $(this).toggleClass("selected");
        var tag = $(this).data("tag");

        // and in tag cloud
        $("#tag_cloud a:contains('"+tag+"')").toggleClass("selected");
    });
    
    // Append trove[]= selection to any clicked links in tag cloud
    $("#tag_cloud a").click(function(){

        // array from selected tags
        var tags = $("#trove_tags b.selected, #trove_tags .option.selected").map(function(){
            return $(this).data("tag");
        }).get();
        
        // append to current link
        if (tags.length) {
            this.href += "&trove[]=" + tags.join("&trove[]=");
        }
    });
 
    
    /**
     * Action links are marked up using <a class="action func-name">
     *
     */
    
    // submit_form: lock entry
    $(".action.lock-entry").click(function(){
        var $lock = $("input[name='lock']");
        if (!$lock.val().length && $lock.attr("placeholder")) {
            $lock.val($lock.attr("placeholder"));
        }
    });

    // submit_form: apply $version placeholder in URLs
    $(".action.version-placeholder").click(function(){
        var $input = $(this).parent().parent().find("input, textarea").eq(0);
        var version = $("input[name='version']").val();
        if (version.length) {
            version = new RegExp(version.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"), "g");
            $input.val($input.val().replace(version, "$version"));
        }
    });

    // submit_form: apply $version placeholder in URLs
    $(".action.submit-import").click(function(){
        $("#sidebar section").eq(0).toggle("medium");
        $("#sidebar .submit-import").fadeToggle("slow");
    });
    
    // Copying some form fields from /submit to /drchangelog
    $(".action.drchangelog").click(function() {
        $(this).attr("href", "/drchangelog?autoupdate_module=" + $("form[method='POST']").serialize());
        // and let default action proceed
    });

    // Append search field in #tools bar
    $("#search_q a").click(function() {
        var q;
        if (q = $("#search_q input[name=q]").val()) {
            $(this).attr("href", "/search?q=" + q);
        }
        // and let default action proceed
    });



    /**
     * Forum actions.
     *
     */

    // Expand forum previews
    $(".forum .entry").one("click", function(){
        $(this).find(".excerpt, .content, .funcs").toggleClass("trimmed");
    });
    
     
    // Post submit button
    $(".forum").delegate(".action", "click", function(){

        // entry/post id
        var id = $(this).data("id");
        var func = this.classList[1];
        var $target = $(this).closest(".entry");

        // new
        if (func == "forum-new") {
            $target.load("/forum/post", { "pid": id }).fadeIn();
        }
        // reply
        if (func == "forum-reply") {
            $target.append("<ul><li></li></ul>");
            $target.find("ul li").load("/forum/post", { "pid": id }).fadeIn();
        }
        // editing
        if (func == "forum-edit") {
            $target.load("/forum/edit", { "id": id });
        }
        // submit
        if (func == "forum-submit"){
            $target = $target.parent();
            $.post("/forum/submit", $target.find("form").serialize(), function(html){
                $target.html(html);
            });
        }
        event.preventDefault();
    });

    // Markup
    $(".forum").delegate(".action.markup", "click", function(){
        var $ta = $(this).parent().parent().parent().find("textarea");
        var content = $ta.val();
        var x = $ta[0].selectionStart;
        var y = $ta[0].selectionEnd;
        var before = $(this).data("before");
        var after = $(this).data("after");
        if (y) {
            $ta.val(content.substr(0, x) + before + content.substr(x, y-x) + after + content.substr(y, content.length - y));
        }
        else {
            $ta.val(content + before + "..." + after);
        }
    });

});






Added handler_api.php.

























































































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: Submit API
 * description: Implements the Freecode JSON Rest API for release updates
 * version: 0.2
 * type: handler
 * category: API
 * doc: http://fossil.include-once.org/freshcode/wiki/Freecode+JSON+API
 * author: mario
 * license: AGPL
 *
 * This utility code emulates the freecode.com API, to support release
 * submissions via freecode-submit and similar tools. The base features
 * fit well with the freshcode.club database scheme.
 *
 * Our RewriteRules map following Freecode API request paths:
 *
 *       GET   projects/<name>.json                query
 *       PUT   projects/<name>.json                update_core
 *      POST   projects/<name>/releases.json       publish
 *       GET   projects/<name>/releases/<w>.json   version_GET, id=
 *    DELETE   projects/<name>/releases/<i>.json   version_DELETE
 *
 * From the ridiculous amount of URL manipulation calls, we just keep:
 *
 *   GET/PUT   projects/<name>/urls.json           urls  (assoc array)
 *
 *
 * Retrieval requests usually come with an ?auth_code= token. For POST
 * or PUT access it's part of the JSON response body. Which comes with
 * varying payloads depending on request type:
 *
 *   { "auth_code": "pw123",
 *     "project": {
 *       "license_list": "GNU GPL",
 *       "project_tag_list": "kernel,operating-system",
 *       "oneliner": "Linux kernel desc",
 *       "description": "Does this and that.."
 *   } }
 *
 * Any crypt(3) password hash in a projects `lock` field will be checked
 * against the plain auth_code.
 *
 * At this point everything went through index.php already; runtime env
 * thus initialized. Therefore API methods can be invoked directly, which
 * either retrieve or store project data, and prepare a JSON response.
 *
 */


/*
 @Test @sh

 @t query
   wget http://freshcode/projects/linux.json?auth_code=unused -O-
   ./fc-submit -q linux

 @t change_core
   ./fc-submit -P linux -D "new proj" -S "oneliner" -L "GNU GPL" -T "kernel,linux" -n -V

 @t publish
   ./fc-submit -P linux -v "3.55.1" -c "Change all the things" -t "major,bugfix" -n -V

 @t delete
   ./fc-submit -P linux -v "3.55.1" -d -n -V

 @t urls
   wget http://freshcode/projects/linux/urls.json?auth_code=0 -O-
*/


// Wraps API methods and utility code
class FreeCode_API {


    // Project name
    var $name;
    
    // API method
    var $api;
    
    // HTTP method
    var $method;
    
    // POST/PUT request body
    var $body;
    
    // Optional auth_code (from URL or JSON body)
    var $auth_code;
    
    // Optional revision ID (just used for releases/; either "pending" or t_published timestamp) 
    var $id;


    // JSON success message    
    var $OK = array("success" => TRUE);



    /**
     * Initialize params from RewriteRule args.
     *
     */
    function __construct() {
    
        // URL params
        $this->name = $_GET->proj_name["name"];
        $this->api = $_GET->id->strtolower->in_array("api", "query,update_core,publish,urls,version_get,version_delete");
        $this->method = $_SERVER->id->strtoupper["REQUEST_METHOD"];
        $this->auth_code = $_REQUEST->text["auth_code"];
        $this->id = $_REQUEST->text["id"];  // optional param
        
        // Request body is only copied, because it comes with varying payloads (release, project, urls)
        if ($_SERVER->int["CONTENT_LENGTH"] && $_SERVER->striposjson->is_int["CONTENT_TYPE"]) {
            $this->body = json_decode(file_get_contents("php://input"), TRUE);
            $this->auth_code = $this->body["auth_code"];
        }
    }


    
    /**
     * Invoke API target function.
     * After retrieving current project data.
     *
     */
    function dispatch() {
    
        // Fetch latest revision
        if (!$project = new release($this->name)) {
            $this->error(NULL, "404 No such project", "Invalid Project ID");
        }
        
        // Run dialed method, then output JSON response.
        $this->json_exit(
            $this->{ $this->api ?: "error" }($project)
        );
    }



    /**
     * GET project description
     * -----------------------
     *
     * @unauthorized
     *
     * Just returns the current releases project description
     * and basic fields.
     * Freecode API clients expect:
     *   → id         (something numeric, which we don't have, so just a CRC32 here)
     *   → permalink  (=name)
     *   → oneliner   (we just extract the first line)
     *   → license_list
     *   → tag_list
     *   → approved_urls
     *
     * Also appends extraneous freshcode fields.
     *
     */
    function query($project) {
    
        // Everyone can access this, but only the owner will see private fields
        $data = $this->auth_filter($project);
    
        // Alias some fields for fc-submit, but append our data scheme intact
        return array(
            "project" => array(
                 "id" => crc32($data["name"]),
                 "permalink" => $data["name"],
                 "oneliner" => substr($data["description"], 0, 100),
                 "license_list" => p_csv($data["license"]),
                 "tag_list" => p_csv($data["tags"]),
                 "approved_urls" => $this->array_uncolumn(p_key_value($data["urls"], NULL))
            ) + $data->getArrayCopy()
        );
    }
    
    // Expand associative URLs into [{label:,redirector:},..] list
    function array_uncolumn($kv, $ind="label", $dat="redirector", $r=array()) {
        foreach ($kv as $key=>$value) {
            $r[] = array($ind=>$key, $dat=>$value);
        }
        return $r;
    }



    /**
     * PUT project base fields
     * -----------------------
     *
     * @auth-required
     *
     * Whereas the project ->body contains:
     *   → license_list
     *   → project_tag_list
     *   → oneliner  (IGNORED)
     *   → description
     * Additionally we'd accept:
     *   → state
     *   → download (URL)
     *   → homepage
     *
     */
    function update_core($project) {
        $core = new input($this->body["project"], "core");
        // extract fields
        $new = array(
            // standard FC API fields
            "license" => tags::map_license(p_csv($core->words["license_list"])[0]),
            "tags" => $core->text->f_tags["project_tag_list"],
            "description" => $core->text["description"],
            // additional overrides
            "homepage" => $core->url->http["homepage"],
            "download" => $core->url->http["download"],
            "state" => tags::state_tag($core->name["state"]),
        );
        return $this->insert($project, $new);
    }


    /**
     * POST release/version
     * --------------------
     *
     * @auth-required
     *
     * Here the release body contains:
     *  → version
     *  → changelog
     *  → tag_list
     *  → hidden_from_frontpage
     * We'd also accept:
     *  → state
     *  → download
     *
     */
    function publish($project) {
        $rel = new input($this->body["release"], "rel");
        // extract fields
        $new = array(
            "version" => $rel->text["version"],
            "changes" => $rel->text["changelog"],
            "scope" => tags::scope_tags($rel->text["tag_list"]),
            "state" => tags::state_tag($rel->text["state"] . $rel->text["tag_list"]),
            "download" => $rel->url->http["download"],
        );
        $flags = array(
            "hidden" => $rel->int["hidden_from_frontpage"],
        );
        return $this->insert($project, $new, $flags);
    }
    

    /**
     * Check for "pending" releases
     * ----------------------------
     *
     * @unauthorized
     *
     * We don't have a pre-approval scheme on Freshcode currently,
     * so this just returns a history of released versions.
     *
     * For the `id` we're just using the `t_published` timestamp.
     * Thus a "withdraw" request could reference it.
     *
     */
    function version_GET($project) {
        assert($this->id === "pending");

        // Query release revisions
        $list = db("
           SELECT name, version, t_published, MAX(t_changed) AS t_changed,
                  scope, hidden, changes
             FROM release
            WHERE name=?
         GROUP BY version
            LIMIT 10", $this->name
        );

        // Assemble oddly nested result array
        $r = [];
        foreach ($list as $row) {
            $r[] = array("release" => array(
                "id" => $row["t_published"],
                "version" => $row["version"],
                "tag_list" => explode(" ", $row["scope"]),
                "hidden_from_frontpage" => (bool)$row["hidden"],
                "approved_at" => gmdate(DateTime::ISO8601, $row["t_changed"]),
                "created_at" =>  gmdate(DateTime::ISO8601, $row["t_published"]),
                "changelog" => $row["changes"],
            ));
        }
        return $r;
    }



    /**
     * "Withdraw" a "pending" release
     * ------------------------------
     *
     * @auth-required
     *
     * We're faking two things here. Firstly that the review process
     * was enabled by default. Secondly that you could delete things.
     * (The database is designed to be somewhat "immutable", we just
     * pile up revisions normally.)
     *
     * So withdrawing a release just means it gets "hidden" and flagged
     * for moderator attention. There's also a "delete" flag; but thats
     * current purpose is terminating a project lifeline (due to VIEW
     * revision grouping).
     *
     * The reasoning being that withdrawn releases are really just
     * authors making last minute fixes; commonly retracted releases
     * are just resent later, or with a different changelog.
     *
     */
    function version_DELETE($project) {

        // Obviously requires a valid `lock` hash
        $project = $this->with_permission($project);
        assert(is_numeric($this->id));

        // Hide all entries for revision
        $r = db([
         " UPDATE release ",
         "    SET :,  " => ["hidden" => 1, "flag" => 0],
         "  WHERE :&  " => ["name" => $this->name, "t_published" => $this->id]
        ]);

        return $r ? $this->OK : $this->error(NULL);
    }


    /**
     * URL editing
     * -----------
     *
     * @auth-required
     *
     * Here we deviate from the overflowing Freecode API with individual
     * URL manipulation. (PUT,POST,DELETE /projects/<name>/urls/targz.json)
     * That's just double the work for client and API.
     *
     * Instead on freshcode there is but one associative URL blob for
     * reading and updating all URLs at once.
     *
     *   GET /projects/name/urls.json   { "urls" : { "src": "http://..",
     *                                       "github": "http://.." }    }
     *   PUT /projects/name/urls.json   { "urls": { "txz":, "doc":.. }  }
     *
     * Our labels use the tag-form, so incoming labels will be adapted.
     * ("Tar/BZ2" becomes "tar-bz2".)
     *
     * Internally the urls are stored in an INI-style key=value text blob.
     * (But the API stays somewhat more RESTy with an associative dict.)
     *
     */
    function urls($project) {
    
        /**
         * For a GET query just mirror "Other URLs" as dict
         *
         * @unauthorized
         *
         */
        if ($this->method == "GET") {
            return array("urls" => p_key_value($project["urls"], NULL));
        }
        
        /**
         * Updates may come as PUT, POST, PUSH request
         *
         * @auth-required
         *
         */
        else {

            // Extract all
            $urls = new input($this->body["urls"], "urls");
            $urls = $urls->list->url[$urls->keys()];

            // Extract homepage and download specifically
            $urls = array_change_key_case($urls, CASE_LOWER);
            $new  = array_intersect_key($urls, array_flip(str_getcsv("homepage,download")));
            $urls = array_diff_key($urls, $new);

            // Join rest into key=value format
            $new["urls"] = "";
            foreach ($urls as $label => $url) {
                $label = trim(preg_replace("/\W+/", "-", $label), "-");
                $new["urls"] .= "$label = $url\r\n";
            }

            // Update DB
            $this->insert($project, $new);
        }
    }


    
    
    /**
     * Perform partial update
     *
     * @auth-required
     *
     */
    function insert($project, $new, $flags=[]) {

        // Write permissions required obviously.
        $project = $this->with_permission($project);

        // Add new fields to $project
        $project->update(array_filter($new, "strlen"), $flags, [], TRUE);

        // Store or return JSON API error.
        return $project->store() and (header("Status: 201 Created") + 1)
             ? $this->OK
             : $this->error(NULL, "500 Internal Issues", "Database mistake");
    }
    
    
    /**
     * Strip down raw project data for absent auth_code
     * in read/GET requests.
     *
     */
    function auth_filter($data) {
        if (!$this->is_authorized($data)) {
            unset(
                $data["lock"], $data["submitter_openid"], $data["submitter"],
                $data["hidden"], $data["deleted"], $data["flag"],
                $data["social_links"],
                $data["autoupdate_regex"], $data["autoupdate_url"],
                $data["t_changed"]
            );
        }
        return $data;
    }

    
    /**
     * Prevent further operations for (write) requests that
     * actually REQUIRE a valid authorization token.
     *
     */
    function with_permission($data) {
        return $this->is_authorized($data)
             ? $data
             : $this->error(NULL, "401 Unauthorized", "No matching API auth_token hash. Add a crypt(3) password in your freshcode.club project entries `lock` field, comma-delimited to your OpenID handle. See http://fossil.include-once.org/freshcode/wiki/Freecode+JSON+API");
    }


    /**
     * The `lock` field usually contains one or more OpenID urls. It's
     * a comma-delimited field.
     *
     * Using the API additionally requires a password hash, as in crypt(3)
     * or `openssl passwd -1` or PHPs password_hash(), to be present.
     *
     * It will simply be compared against the ?auth_code= parameter.
     *
     */
    function is_authorized($data) {
        foreach (preg_grep("/^[^:]+$/", p_csv($data["lock"])) as $hash) {
            if (password_verify($this->auth_code, $hash)) {
                return TRUE;
            }
        }
        return FALSE;
    }


    /**
     * JSON encode and finish.
     *
     */
    function json_exit($data) {
        header("Content-Type2: json/vnd.freecode.com; version=3; charset=UTF-8");
        header("Content-Type: application/json");
        exit(
            json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
        );
    }


    /**
     * Bail with error response.
     *
     */
    function error($data, $http = "503 Unavailable", $json = "unknown method") {
        header("Status: $http");
        $this->json_exit(["error" => "$json"]);
    }

}



?>

Changes to img/changes.png.

cannot compute difference between binary files

Added img/drchangelog.png.

cannot compute difference between binary files

Deleted img/links/advancescripts.com.jpeg.

cannot compute difference between binary files

Deleted img/links/bigresource.com.jpeg.

cannot compute difference between binary files

Deleted img/links/devscripts.com.jpeg.

cannot compute difference between binary files

Added img/links/distrowatch.com.jpeg.

cannot compute difference between binary files

Deleted img/links/fatscripts.com.jpeg.

cannot compute difference between binary files

Added img/links/findbestopensource.com.jpeg.

cannot compute difference between binary files

Added img/links/freeopensourcesoftware.org.jpeg.

cannot compute difference between binary files

Deleted img/links/hotscripts.com.jpeg.

cannot compute difference between binary files

Added img/links/icewalkers.com.jpeg.

cannot compute difference between binary files

Added img/links/libreprojects.net.jpeg.

cannot compute difference between binary files

Added img/links/linuxappfinder.com.jpeg.

cannot compute difference between binary files

Added img/links/linuxgames.com.jpeg.

cannot compute difference between binary files

Added img/links/linuxsoft.cz.jpeg.

cannot compute difference between binary files

Deleted img/links/needscripts.com.jpeg.

cannot compute difference between binary files

Added img/links/openfontlibrary.org.jpeg.

cannot compute difference between binary files

Added img/links/openhatch.org.jpeg.

cannot compute difference between binary files

Added img/links/opensourcearcade.com.jpeg.

cannot compute difference between binary files

Added img/links/opensourcelinux.org.jpeg.

cannot compute difference between binary files

Added img/links/opensourcelist.org.jpeg.

cannot compute difference between binary files

Added img/links/opensourcescripts.com.jpeg.

cannot compute difference between binary files

Added img/links/osalt.com.jpeg.

cannot compute difference between binary files

Deleted img/links/scripts.com.jpeg.

cannot compute difference between binary files

Deleted img/links/scripts20.com.jpeg.

cannot compute difference between binary files

Added img/links/thechangelog.com.jpeg.

cannot compute difference between binary files

Added img/links/zwodnik.com.jpeg.

cannot compute difference between binary files

Added img/logo.png.

cannot compute difference between binary files

Changes to img/nopreview.png.

cannot compute difference between binary files

Added img/screenshot/.empty.

1
+
This directory gets populated by cron.daily/screenshots.php

Added img/user.png.

cannot compute difference between binary files

Changes to index.php.

1
2
3

4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22



23
24
25
26
27



28
29
30





31
32
33
34

35
36
37
38





39
40
41
42
43
44
45
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62



+


-
+
















+
+
+





+
+
+



+
+
+
+
+



-
+




+
+
+
+
+






<?php
/**
 * api: php
 * type: main
 * title: Freshcode.club
 * description: FLOSS software release tracking website
 * version: 0.3
 * version: 0.6.5
 * author: mario
 * license: AGPL
 * 
 * Implements a freshmeat/freecode-like directory for open source
 * release publishing / tracking.
 *
 */


#-- init
include("config.php");


#-- dispatch
switch ($page = $_GET->id["page"]) {

    case "name":
    case "names":
        $page = "names";
    case "index":
    case "projects":
    case "feed":
    case "links":
    case "tags":
    case "search":
    case "rc":
    case "drchangelog":
    case "login":
        include("page_$page.php");
        break;

    case "forum":
    case "meta":
        include("page_forum.php");
        break;

    case "flag":
    case "submit":
        if (LOGIN_REQUIRED and empty($_SESSION["openid"])) {
        if ((LOGIN_REQUIRED or $page === "flag") and empty($_SESSION["openid"])) {
            exit(include("page_login.php"));
        }
        include("page_$page.php");
        break;

    case "api":
        $api = new FreeCode_API();
        $api->dispatch();
        break;

    case "admin":
        if (!in_array($_SESSION["openid"], $moderator_ids)) {
            exit(include("page_login.php"));
        }
        include("page_admin.php");
        break;

Deleted input.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149




























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
 /**
  * api: php
  * title: Input $_REQUEST wrappers
  * type: interface
  * description: Encapsulates input variable superglobals for easy sanitization access
  * version: 2.7
  * revision: $Id$
  * license: Public Domain
  * depends: php:filter, php >5.0, html_purifier
  * config: <const name="INPUT_DIRECT" type="multi" value="disallow" multi="disallow|raw|log" description="filter method for direct $_REQUEST[var] access" />
  *         <const name="INPUT_QUIET" type="bool" value="0" multi="0=report all|1=no notices|2=no warnings" description="suppress access and behaviour notices" />
  * throws: E_USER_NOTICE, E_USER_WARNING, OutOfBoundsException
  *
  * Using these object wrappers ensures a single entry point and verification
  * spot for all user data. They wrap all superglobals and filter HTTP input
  * through sanitizing methods. For auditing casual and unverified access.
  *
  * Standardly they wrap: $_REQUEST, $_GET, $_POST, $_SERVER, $_COOKIE
  * And provide convenient access to filtered data:
  *   $_GET->int("page_num")                      // method
  *   $_POST->text["commentfield"]                // array syntax
  *   $_REQUEST->text->ascii->strtolower["xyz"]   // filter chains
  *
  * Available filter methods are:
  *   ->int
  *   ->float
  *   ->boolean
  *   ->name
  *   ->id
  *   ->words
  *   ->text
  *   ->ascii
  *   ->nocontrol
  *   ->spaces
  *   ->q
  *   ->escape
  *   ->regex
  *   ->in_array
  *   ->html
  *   ->purify
  *   ->json
  *   ->length
  *   ->range
  *   ->default
  * And most useful methods of the php filter extension.
  *   ->email
  *   ->url ->uri ->http
  *   ->ip
  *   ->ipv4->public
  * PHP string modification functions:
  *   ->urlencode
  *   ->strip_tags
  *   ->htmlentities
  *   ->strtolower
  * Automatic filter chain handler:
  *   ->array
  * Fetch multiple variables at once:
  *   ->list["var1,key,name"]
  * Process multiple entries as array:
  *   ->multi->http_build_query["id,token"]
  * Possible but should be used very consciously:
  *   ->xss
  *   ->sql
  *   ->mysql
  *   ->log
  *   ->raw
  *
  * You can also pre-define a standard filter-chain for all following calls:
  *   $_GET->nocontrol->iconv->utf7->xss->always();
  *
  * Using $__rules[] a set of filter rules can be preset per variable name.
  *
  * Parameterized filters can alternatively use the ellipsis … symbol (AltGr+:)
  * instead of the terminating method access syntax.
  *   $_GET->int->range…0…59["minutes"]
  *
  * Some filters are a mixture of sanitizing and validation. Basically
  * all can also be used independently of the superglobals with their
  * underscore name, $str = input::_text($str);
  *
  * For the superglobals it's also possible to count($_GET); or check with
  * just $_POST() if there are contents. (Use this in lieu of empty() test.)
  * There are also the three functions ->has ->no ->keys() for array lookup.
  *
  * Defining new filters can be done as methods, or since those are picked
  * up too, as plain global functions. It's also possible to assign function
  * aliases or attach closures:
  *   $_POST->_newfilter = function($s) { return modified($s); }
  * Note that the assigned filter name must have an underscore prefixed.
  *
  * --
  *
  * Input validation of course is no substitute for secure application logic,
  * parameterized sql and proper output encoding. But this methodology is a
  * good base and streamlines input data handling.
  *
  * Filter methods can be bypassed in any number of ways. There's no effort
  * made at prevention here. But it's recommended to simply use ->raw() when
  * needed - not all input can be filtered anyway. This way an audit handler
  * could always attach there - when desired.
  *
  * The goal is not to coerce, but encourage security via API *simplicity*.
  *
  */


/**
 * Handler name for direct $_REQUEST["array"] access.
 *
 *   "raw" = reports with warning,
 *   "disallow" = throws an exception
 */
defined("INPUT_DIRECT") or
define("INPUT_DIRECT", "raw");

/**
 * Notice suppression.
 *
 *   0 = report all,
 *   1 = no notices,
 *   2 = ignore non-existant filter
 */
defined("INPUT_QUIET") or
define("INPUT_QUIET", 0);


/**
 * @class Request variable input wrapper.
 *
 * The methods are defined with underscore prefix, but supposed to be used without
 * when invoked on the superglobal arrays:
 *
 * @method int   int[$field] converts input into integer
 * @method float float[$field]
 * @method name  name[$field] removes any non-alphanumeric characters
 * @method id    id[$field] alphanumeric string with dots
 * @method text  text[$field] textual data with interpunction
 *
 */  
class input implements ArrayAccess, Countable, Iterator {



    /**
     * Data filtering functions.
     *
     * These methods are usually not to be called directly. Instead use
     * $_GET->filtername["varname"] syntax without preceeding underscore
     * to access variable content.
     *
     * Categories: [e]=escape, [w]=whitelist, [b]=blacklist, [s]=sanitize, [v]=validate
     *
     */


    
    /**
     * [w]
     * Integer.
     *
     */
    function _int($data) {
        return (int)$data;
    }

    /**
     * [w]
     * Float.
     *
     */
    function _float($data) {
        return (float)$data;
    }
    
    /**
     * [w]
     * Alphanumeric strings.
     * (e.g. var names, letters and numbers, may contain international letters)
     *
     */
    function _name($data) {
        return preg_replace("/\W+/u", "", $data);
    }

    /**
     * [w]
     * Identifiers with underscores and dots,
     * like "xvar.1_2.x"
     *
     */
    function _id($data) {
        return preg_replace("#(^[^a-z_]+)|[^\w\d_.]+|([^\w_]$)#i", "", $data);
    }
    
    /**
     * [w]
     * Flow text with whitespace,
     * minimal interpunction allowed.
     *
     */
    function _words($data, $extra="") {
        return preg_replace("/[^\w\d\s,._\-+$extra]+/u", " ", strip_tags($data));
    }

    /**
     * [w]
     * Human-readable text with many special/interpunction characters:
     *  " and ' allowed, but no <, > or \
     *
     */
    function _text($data) {
        return preg_replace("/[^\w\d\s,._\-+?!;:\"\'\/`´()*=#]+/u", " ", strip_tags($data));
    }
    
    /**
     * Acceptable filename characters.
     *
     * Alphanumerics and dot (but not as first character).
     * You should use `->basename` as primary filter anyway.
     *
     * @t whitelist
     *
     */
    function _filename($data) {
        return preg_replace("/^[.\s]|[^\w._+-]/", "_", $data);
    }
    
    
    /**
     * [b]
     * Filter non-ascii text out.
     * Does not remove control characters. (Similar to FILTER_FLAG_STRIP_HIGH.)
     * 
     */
    function _ascii($data) {
        return preg_replace("/[\\200-\\377]+/", "", $data);
    }

    /**
     * [b]
     * Remove control characters. (Similar to FILTER_FLAG_STRIP_LOW.)
     * 
     */
    function _nocontrol($data) {
        return preg_replace("/[\\000-\\010\\013\\014\\016-\\037\\177\\377]+/", "", $data); // all except \r \n \t
    }
    
    /**
     * [e] 
     * Exchange \r \n \t and \f \v \0 for normal spaces.
     * 
     */
    function _spaces($data) {
        return strtr($data, "\r\n\t\f\v\0", "      ");
    }

    /**
     * [x]
     * Regular expression filter.
     *
     * This either extracts (preg_match) data if you have a () capture group,
     * or functions as filter (pref_replace) if there's a [^ character class.
     * 
     */
    function _regex($data, $rx="", $match=1) {
        # validating
        if (strpos($rx, "(")) {
            if (preg_match($rx, $data, $result)) {
                return($result[$match]);
            }
        }
        # cropping
        elseif (strpos($rx, "[^")) {
            return preg_replace($rx, "", $data);
        }
    }
    
    /**
     * [w]
     * Miximum and minimum string length.
     *
     */
    function _length($data, $max=65535, $cut=NULL) {
        // just the length limit
        if (is_null($cut)) {
            return substr($data, 0, $max);
        }
        // require minimum string length, else drop value completely
        else {
            return strlen($data) >= $max ? substr($data, 0, $cut)  : NULL;
        }
    }
    
    /**
     * [w]
     * Range ensures value is between given minimum and maximum.
     * (Does not convert to integer itself.)
     *
     */
    function _range($data, $min, $max) {
        return ($data > $max) ? $max : (($data < $min) ? $min : $data);
    }

    /**
     * [b]
     * Fallback value for absent/falsy values.
     *
     */
    function _default($data, $default) {
        return empty($data) ? $default : $data;
    }

    /**
     * [w] 
     * Boolean recognizes 1 or 0 and textual values like "false" and "off" or "no".
     *
     */
    function _boolean($data) {
        if (empty($data) || $data==="0" || in_array(strtolower($data), array("false", "off", "no", "n", "wrong", "not", "-"))) {
            return false;
        }
        elseif ($data==="1" || in_array(strtolower($data), array("true", "on", "yes", "right", "y", "ok"))) {
            return true;
        }
        else return NULL;
    }

    /**
     * [w]
     * Ensures field is in array of allowed values.
     *
     * Works with arrays, but also string list. If you supply a "list,list,list" then
     * the comparison is case-insensitive.
     *
     */
    function _in_array($data, $array) {
        if (is_array($array) ? in_array($data, $array) : in_array(strtolower($data), explode(",", strtolower($array)))) {
            return $data;
        }
    }
    
    
    ###### filter_var() wrappers #########

    /**
     * [w]
     * Common case email syntax.
     *
     * (Close to RFC2822 but disallows square brackets or double quotes, no verification of TLDs,
     * doesn't restrict underscores in domain names, ignores i@an and anyone@localhost.)
     *
     */
    function _email($data, $validate=1) {
        $data = preg_replace("/[^\w!#$%&'*+/=?_`{|}~@.\[\]\-]/", "", $data);  // like filter_var
        if (!$validate || preg_match("/^(?!\.)[.\w!#$%&'*+/=?^_`{|}~-]+@(?:(?!-)[\w-]{2,}\.)+[\w]{2,6}$/i", trim($data))) {
            return $data;
        }
    }

    /**
     * [s] 
     * URI characters. (Actually IRI)
     *
     */
    function _uri($data) {
    # we should encode all-non chars
        return preg_replace("/[^-\w\d\$.+!*'(),{}\|\\~\^\[\]\`<>#%\";\/?:@&=]+/u", "", $data);  // same as SANITIZE_URL
    }
    
    /**
     * [w]
     * URL syntax
     *
     * This is an alias for FILTER_VALIDATE_URL. Beware that it lets a few unwanted schemes
     * through (file:// and mailto:) and what you'd consider misformed URLs (http://http://whatever).
     *
     */
    function _url($data) {
        return filter_var($data, FILTER_VALIDATE_URL);
    }

    /**
     * [v]
     * More restrictive HTTP/FTP url syntax.
     * No usernames allowed, no empty port, pathnames/qs/anchor are not checked.
     *
     # see also http://internet.ls-la.net/folklore/url-regexpr.html
     */
    function _http($data) {
        return preg_match("~
            (?(DEFINE)  (?<byte> 2[0-4]\d |25[0-5] |1\d\d |[1-9]?\d)  (?<ip>(?&byte)(\.(?&byte)){3})  (?<hex>[0-9a-fA-F]{1,4})  )
        ^   (?<proto>https?|ftps?)  ://
            # (?<user> \w+(:\w+)?@ )?
            ( (?<host>  (?:[a-z][a-z\d_\-\$]*\.?)+)
             |(?<ipv6>  \[     (?! [:\w]*:::          # ASSERT: no triple ::: colons
                                 |(:\w+){8}|(\w+:){8} # not more than 7 : colons
                                 |(\w*:){7}\w+\.      # not more than 6 : if there's a .
                                 | [:\w]*::[:\w]+:: ) # double :: colon must be unique
                               (?= [:\w]*::           # don't count if there is one ::
                                 |(\w+:){6}\w+[:.])   # else require six : and one . or :
              (?: :|(?&hex):)+((?&hex)|(?&ip)|:) \])  # MATCH: combinations of HEX : IP
             |(?<ipv4>  (?&ip) )     )
            (?<port> :\d{1,5} )?   # the integer isn't optional if port : colon present (unlike FILTER_VALIDATE_URL)
            (?<path> [/][^?#\s]* )?
            (?<qury> [?][^#\s]* )?
            (?<frgm> [#]\S* )?
        \z~ix", $data, $uu) ? $data : NULL;#(print_r($uu) ? $data : $data)
    }
    
    /**
     * [w] 
     * IP address
     *
     */
    function _ip($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6) ? $data : NULL;
    }

    /**
     * [w]
     * IPv4 address
     *
     */
    function _ipv4($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $data : NULL;
    }
    
    /**
     * [w]
     * must be public IP address
     *
     */
    function _public($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE) ? $data : NULL;
    }


    ###### format / representation #########
    
    
    /**
     * [v]
     * HTML5 datetime / datetime_local, date, time
     *
     */
    function _datetime($data) {
        return preg_match("/^\d{0}\d\d\d\d -(0\d|1[012]) -([012]\d|3[01]) T ([01]\d|2[0-3]) :[0-5]\d :[0-5]\d   (Z|[+-]\d\d:\d\d|\.\d\d)$/x", $data) ? $data : NULL;
    }
    function _date($data) {
        return preg_match("/^\d\d\d\d -(0\d|1[012]) -([012]\d|3[01])$/x", $data) ? $data : NULL;
    }
    function _time($data) {
        return preg_match("/^([01]\d|2[0-3]) :[0-5]\d :[0-5]\d  (\.\d\d)?$/x", $data) ? $data : NULL;
    }

    /**
     * [v]
     * HTML5 color
     *
     */
    function _color($data) {
        return preg_match("/^#[0-9A-F]{6}$/i", $data) ? strtoupper($data) : NULL;
    }


    /**
     * [w]
     * Telephone numbers (HTML5 <input type=tel> makes no constraints.)
     *
     */
    function _tel($data) {
        $data = preg_replace("#[/.\s]+#", "-", $data);
        if (preg_match("/^(\+|00)?(-?\d{2,}|\(\d+\)){2,}(-\d{2,}){,3}(\#\d+)?$/", $data)) {
            return trim($data, "-");
        }
    }


    /**
     * [v]
     * Verify minimum consistency (RFC4627 regex) and decode json data.
     *
     */
    function _json($data) { 
        if (!preg_match('/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/', preg_replace('/"(\\.|[^"\\])*"/g', '', $data))) {
            return json_decode($data);
        }
    }
    
    /**
     * [v]
     * XML tree.
     *
     */
    function _xml($data) {
        return simplexml_load_string($data);
    }

    /**
     * [w]
     * Clean html string via HTMLPurifier.
     *
     */
    function _purify($data) {
        $h = new HTMLPurifier;
        return $h->purify( $data );
    }

    /**
     * [e]
     * HTML escapes.
     *
     * This is actually an output filter. But might be useful to mirror input back into
     * form fields instantly `<input name=field value="<?= $_GET->html["field"] ?>">`
     *
     * @param $data string
     * @return string
     */
    function _html($data) {

        return htmlspecialchars($data, ENT_QUOTES, "UTF-8", false);
    }

    /**
     * [e]
     * Escape all significant special chars.
     *
     */
    function _escape($data) {
        return preg_replace("/[\\\\\[\]\{\}\[\]\'\"\`\´\$\!\&\?\/\>\<\|\*\~\;\^]/", "\\$1", $data);
    }

    /**
     * [e]
     * Addslashes
     *
     */
    function _q($data) {
        return addslashes($data);
    }

    /**
     * [b]
     * Minimal XSS detection.
     * Attempts no cleaning, just bails if anything looks suspicious.
     *
     * If something is XSS contaminated, it's spam and not worth to process further.
     * WEAK filters are better than no filters, but you should ultimatively use ->html purifier instead.
     *
     */
    function _xss($data) {
        if (preg_match("/[<&>]/", $data)) {   // looks remotely like html
            $html = $data;
            
            // remove filler
            $html = preg_replace("/&#(\d);*/e", "ord('$1')", $html);   // escapes
            $html = preg_replace("/&#x(\w);*/e", "ord(dechex('$1'))", $html);   // escapes
            $html = preg_replace("/[\x00-\x20\"\'\`\´]/", "", $html);   //  whitespace + control characters, also any quotes
            $html .= preg_replace("#/\*[^<>]*\*/#", "", $html);   // in-JS obfuscation comments

            // alert patterns
            if (preg_match("#[<<]/*(\?import|applet|embed|object|script|style|!\[CDATA\[|title|body|link|meta|base|i?frame|frameset|i?layer)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(javascript|ecmascript|vbscript|jscript|python|actionscript|livescript):)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(on(mouse\w+|key\w+|focus\w*|blur|click|dblclick|reset|select|change|submit|load|unload|error|abort|drag|Abort|Activate|AfterPrint|AfterUpdate|BeforeActivate|BeforeCopy|BeforeCut|BeforeDeactivate|BeforeEditFocus|BeforePaste|BeforePrint|BeforeUnload|Begin|Blur|Bounce|CellChange|Change|Click|ContextMenu|ControlSelect|Copy|Cut|DataAvailable|DataSetChanged|DataSetComplete|DblClick|Deactivate|Drag|DragEnd|DragLeave|DragEnter|DragOver|DragDrop|Drop|End|Error|ErrorUpdate|FilterChange|Finish|Focus|FocusIn|FocusOut|Help|KeyDown|KeyPress|KeyUp|LayoutComplete|Load|LoseCapture|MediaComplete|MediaError|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|MouseWheel|Move|MoveEnd|MoveStart|OutOfSync|Paste|Pause|Progress|PropertyChange|ReadyStateChange|Repeat|Reset|Resize|ResizeEnd|ResizeStart|Resume|Reverse|RowsEnter|RowExit|RowDelete|RowInserted|Scroll|Seek|Select|SelectionChange|SelectStart|Start|Stop|SyncRestored|Submit|TimeError|TrackChange|Unload|URLFlip)[^<\w=>]*[=:]+)[^<>]{3,}#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(-moz-binding:))#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(style[=:]+[^<>]*(expression\(|behaviour:|script:))#iUu", $html, $uu))
            {
                $this->_log($data, "DETECTED XSS PATTERN ({$uu['1']}),");
                $data = "";
                die("input::_xss");
            }
        }
        return $data;
    }

    /**
     * [w]
     * Cleans utf-8 from invalid sequences and alternative representations.
     * (BEWARE: performance drain)
     *
     */
    function _iconv($data) {
        return iconv("UTF-8", "UTF-8//IGNORE", $data);
    }

    /**
     * [b]
     * Few dangerous UTF-7 sequences
     * (only necessary if output pages don't have a charset specified)
     *
     */
    function _utf7($data) {
        return preg_replace("/[+]A(D[w40]|C[IYQU])(-|$)?/", "", $data);;
    }

    /**
     * [e] 
     * Escape for concatenating data into sql query.
     * (suboptimal, use parameterized queries instead)
     *
     */
    function _sql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return db()->quote($data);  // global object
    }
    function _mysql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return mysql_real_escape_string($data);
    }
    



    ###### pseudo filters ##############


    /**
     * [x]
     * Unfiltered access should obviously be avoided. But it's not always possible,
     * so this method exists and will just trigger a notice in debug mode.
     *
     */
    function _raw($data) {
        INPUT_QUIET or trigger_error("Unfiltered input variable \${$this->__title}['{$this->__varname}'] accessed.", E_USER_NOTICE);
        return $data;
    }
    /**
     * [x]
     * Unfiltered access, but logs variable name and value.
     *
     */
    function _log($data, $reason="manual log") {
        syslog(LOG_NOTICE, "php7://input:_log@{$_SERVER['SERVER_NAME']} accessing \${$this->__title}['{$this->__varname}'] variable, $reason, content=" . substr($this->_id(json_encode($data)), 0, 48));
        return $data;
    }

    /**
     * [b]
     * Abort with fatal error. (Used as fallback for INPUT_DIRECT access.)
     *
     */
    function _disallow($data) {
        throw new OutOfBoundsException("Direct \$_REQUEST[\"$this->__varname\"] is not allowed, add ->filter method, or change INPUT_DIRECT if needed.");
    }
    
    
    
    /**
     * Validate instead of sanitize.
     *
     */
    function _is($data) {
        $this->__filter[] = array("is_still", array());
        return $data;
    }
    function _is_still($data) {
        return $data === $this->__vars[$this->__varname];
    }



   

    ######  implementation  ################################################




    /**
     * Array data from previous superglobal.
     *
     * (It's pointless to make this a priv/proteced attribute, as raw data could be accessed
     * in any number of ways. Not the least would be to just not use the input filters.)
     *
     */
    var $__vars = array();

    
    /**
     * Name of superarray this filter object wraps.
     * (e.g. "_GET" or "_SERVER")
     *
     */
    var $__title = "";


    /**
     * Currently accessed array key.
     *
     */
    var $__varname = "";


    /**
     * Amassed filter list.
     * Each ->method->chain name will be appended here. Gets automatically reset after
     * a succesful variable access.
     *
     * Each entry is in `array("methodname", array("param1", 2, 3))` format.
     *
     */
    var $__filter = array();  // filterchain method stack


    /**
     * Automatically appended filter list. (Simply combined with current `$__filter`s).
     *
     */
    var $__always = array();


    /**
     * Currently accessed array keys.
     *
     */
    var $__rules = array(     // pre-defined varname filters
        // "varname.." => array(  array("length",array(256), array("nocontrol",array())  ),
    );


    /**
     * Initialize object from
     *
     * @param array  one of $_REQUEST, $_GET or $_POST etc.
     */
    function __construct($_INPUT, $title="") {
        $this->__vars = $_INPUT;   // stripslashes on magic_quotes_gpc might go here, but we have no word if we actually receive a superglobal or if it wasn't already corrected
        $this->__title = $title;
    }

    
    /**
     * Sets default filter chain.
     * These are ALWAYS applied in CONJUNCTION to ->manually->specified->filters.
     *
     */
    function always() {
        $this->__always = $this->__filter;
        $this->__filter = array();
    }
    
    
    /**
     * Normalize array keys (to UPPER case), for e.g. $_SERVER vars.
     *
     */
    function key_case($case=CASE_UPPER) {
        $this->__vars = array_change_key_case($this->__vars, $case);
    }
    
    
    /**
     * Executes current filter or filter chain on given $varname.
     *
     */
    function filter($varname, $reset_filter_afterwards=NULL) {

        // single array [["name"]] becomes varname
        if (is_array($varname) and count($varname)===1) {
            $varname = current($varname);
        }
        $this->__varname = $varname;

        // direct/raw access without any ->filtername prior offsetGet[] or plain filter() call
        if (empty($this->__filter)) {
            if (isset($this->__rules[$varname])) {
                $this->__filter = $this->rules[$varname];  // use a predefined filterset
            } else {
                $this->__filter = array( INPUT_DIRECT );  // direct access handler
            }
        }
        $first_handler = reset($this->__filter);

        // retrieve value for selected input variable
        $data = NULL;
        if (!is_scalar($varname) or $first_handler == "list" or $first_handler == "multi") {
            // one of the multiplex handlers supposedly picks it up
        }
        elseif (isset($this->__vars[$varname])) {
            // entry exists
            $data = $this->__vars[$varname];
        }
        elseif (!INPUT_QUIET) {
            trigger_error("Undefined input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
            // run through filters anyway (for ->log)
        }
        
        // implicit ->array filter handling
        if (is_array($data) and $first_handler != "array") {
            array_unshift($this->__filter, "array");
        }
        
        // apply filters (we're building an ad-hoc merged array here, because ->apply works on the reference, and some filters expect ->__filter to contain the complete current list)
        $this->__filter = array_merge($this->__filter, $this->__always);
        $data = $this->apply($data, $this->__filter);
        
        // the Traversable array interface resets the filter list after each request, see ->current()
        if ($reset_filter_afterwards) {
            $this->__filter = $reset_filter_afterwards;
        }

        // done
        return $data;
    }


    /**
     * Runs list of filters on data. Uses either methods, bound closures, or global functions
     * if the filter name matches.
     *
     * It works on an array reference and with array_shift() because filters are allowed to
     * modify the list at runtime (->array requires to).
     *
     */
    function apply($data, &$filterchain) {
        while ($f = array_shift($filterchain)) {
            list($filtername, $args) = is_array($f) ? $f : array($f, array());

            // an override function name or closure exists
            if (isset($this->{"_$filtername"})) {
                $filtername = $this->{"_$filtername"};
            }
            // call _filter method
            if (is_string($filtername) && method_exists($this, "_$filtername")) {
                $data = call_user_func_array(array($this, "_$filtername"), array_merge(array($data), (array)$args));
            }
            // ordinary php function, or closure, or rebound method
            elseif (is_callable($filtername)) {
                $data = call_user_func_array($filtername, array_merge(array($data), $args));
            }
            else {
                INPUT_QUIET>=2 or trigger_error("unknown filter '" . (is_scalar($filtername) ? $filtername : "closure") . "', falling back on wiping non-alpha characters from '{$this->__varname}'", E_USER_WARNING);
                $data = $this->_name($data);
            }
        }
        return $data;
    }


    /**
     * @multiplex
     *
     * List data / array value handling.
     *
     * This virtual filter hijacks the original filter chain, and applies it
     * to sub values.
     *
     */
    function _array($data) {

        // save + swap out the current filter chain
        list($multiplex, $this->__filter) = array($this->__filter, array());

        // iteratively apply original filter chain on each array entry
        $data = (array) $data;
        foreach (array_keys($data) as $i) {
            $chain = $multiplex;
            $data[$i] = $this->apply($data[$i], $chain);
        }

        return $data;
    }


    /**
     * @multiplex
     *
     * Grab a collection of input variables, names delimited by comma.
     * Implicitly makes it an ->_array() list.
     * The _array handler is implicitly used for indexed values. _list
     * and _multi can be used for associative arrays, given a key list.
     * 
     * @example  extract($_GET->list->text["name,title,date,email,comment"]);
     * @php5.4+  $_GET->list[['key1','key2','key3']];
     *
     * @bugs  hacky, improper way to intercept, fetches from $__vars[] directly,
     *        uses $__varname instead of $data, may prevent replays,
     *        main will trigger a notice anyway as VAR1,VAR2,.. is undefined
     *
     */
    function _list($keys, $pass_array=FALSE) {

        // get key list
        if (is_array($this->__varname)) {
            $keys = $this->__varname;
        }
        else {
            $keys = explode(",", $this->__varname);
        }

        // slice out named values from ->__vars
        $data = array_intersect_key($this->__vars, array_flip($keys));

        // chain to _array multiplex handler
        if (!$pass_array) {
            return $this->_array($data);
        }
        // process fetched list as array (for user-land functions like http_build_query)
        else {
            return $data;
        }
    }
    
    /**
     * Processes collection of input variables.
     * Passes list on as array to subsequent filters.
     *
     */
    function _multi($keys) {
        return $this->_list($keys, "process_as_array");
    }


    
    /**
     * @hide
     *
     * Ordinary method calls are captured here. Any ->method("name") will trigger
     * the filter and return variable data, just like ->method["name"] would. It
     * just allows to supply additional method parameters.
     *
     */
    function __call($filtername, $args) {  // can have arguments
        $this->__filter[] = array($filtername, array_slice($args, 1));
        return $this->filter($args[0]);
    }

    
    /**
     * @hide
     *
     * Wrapper to capture ->name->chain accesses.
     *
     */
    function __get($filtername) {
        //
        // we could do some heuristic chaining here,
        // if the last entry in the ->attrib->attrib list is not a valid method name,
        // but a valid varname, we should execute the filter chain rather than add.
        //
        
        // Unpack parameterized filter attributes, use U+2022 ellipsis … as delimiter
        if (strpos($filtername, "…")) {
            $args = explode("…", $filtername);
            $filtername = array_shift($args);
        }
        else {
            $args=array();
        }

        // Add filter to list
        $this->__filter[] = count($args) ? array($filtername, $args) : $filtername;

        // fluent interface
        return $this;
    }
    


    /**
     * @hide ArrayAccess
     *
     * Normal $_ARRAY["name"] syntax access is redirected through the filter here.
     *
     */
    function offsetGet($varname) {
        // never chains
        return $this->filter($varname);
    }

    /**
     * @hide ArrayAccess
     *
     * Needed for commonplace isset($_POST["var"]) checks.
     *
     */
    function offsetExists($name) {
        return isset($this->__vars[$name]);
    }

    /**
     * @hide
     * Sets value in array. Note that it only works for array["xy"]= top-level
     * assignments. Subarrays are always retrieved by value (due to filtering)
     * and cannot be set item-wise.
     *
     * @discouraged
     * Manipulating the input array is indicative for state tampering and thus
     * throws a notice per default.
     *
     */
    function offsetSet($name, $value) {
        INPUT_QUIET or trigger_error("Manipulation of input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        $this->__vars[$name] = $value;
    }
    
    /**
     * @hide
     * Removes entry.
     *
     * @discouraged
     * Triggers a notice per default.
     *
     */
    function offsetUnset($name) {
        INPUT_QUIET or trigger_error("Removed input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        unset($this->__vars[$name]);
    }



    /**
     * Generic array features.
     * Due to being reserved words `isset` and `empty` need custom method names here:
     *
     *  ->has(isset)
     *  ->no(empty)
     *  ->keys()
     * 
     * Has No Keys seems easy to remember.
     *
     */

    /**
     * isset/array_key_exists check;
     * may list multiple keys to check.
     *
     */
    function has($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_intersect_key($this->__vars, array_flip($args))) == count($args);
    }
    
    /**
     * empty() probing,
     * Tests if named keys are absent or falsy.
     *
     */
    function no($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_filter(array_intersect_key($this->__vars, array_flip($args)))) == 0;
    }
    
    /**
     * Returns list of all contained keys.
     *
     */
    function keys() {
        return array_keys($this->__vars);
    }


    /**
     * @hide Traversable
     *
     * Allows to loop over all array entries in a foreach loop. A supplied filterlist
     * is reapplied for each iteration.
     *
     * - Basically all calls are mirrored onto ->__vars` internal array pointer.
     * 
     */
    function current() {
        return $this->filter(key($this->__vars), /*reset_filter_afterwards=to what it was before*/$this->__filter);
    }
    function key() {
        return key($this->__vars);
    }
    function next() {
        return next($this->__vars);
    }
    function rewind() {
        return reset($this->__vars);
    }
    function valid() {
        if (key($this->__vars) !== NULL) {
            return TRUE;
        }
        else {
            // also reset the filter list after we're done traversing all entries
            $this->__filters=array();
            return FALSE;
        }
    }


    /**
     * @hide Countable
     * 
     */
    function count() {
        return count($this->__vars);
    }


    /**
     * @hide Make filtering functions available as static methods
     *       without underscore prefix for external invocation.
     */ 
    static function __callStatic($func, $args) {
        static $filter = "input";

        // Also eschew E_STRICT notices
        if (!is_object($filter)) {
            $filter = new input(array(), "\$_INLINE_INPUT");
        }

        return isset($filter->{"_$func"})
             ? call_user_func_array($filter->{"_$func"}, $args)
             : call_user_func_array(array($filter, "_$func"), $args);
    }

    /**
     * @hide Allows testing variable presence with e.g. if ( $_POST() )
     *       Alternatively $_POST("var") is an alias to $_POST["var"].
     *
     */
    function __invoke($varname=NULL) {
    
        // treat it as variable access
        if (!is_null($varname)) {
            return $this->offsetGet($varname);
        }
        
        // do the count() call
        else {
            return $this->count();
        }
    }

}



/**
 * @autorun
 *
 */
$_SERVER = new input($_SERVER, "_SERVER");
$_REQUEST = new input($_REQUEST, "_REQUEST");
$_GET = new input($_GET, "_GET");
$_POST = new input($_POST, "_POST");
$_COOKIE = new input($_COOKIE, "_COOKIE");
#$_SESSION
#$_ENV
#$_FILES required a special handler


?>

Deleted layout_aux.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230





































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
/**
 * api: freshmeat
 * title: template auxiliary code
 * description: A few utility functions and data for the templates
 * version: 0.2
 * license: AGPL
 *
 * This function asortment prepares some common output.
 * While a few are parsing helpers or DB query shortcuts.
 *
 */


// Abbreviated and full license names
$licenses = array (
  "Apache" => "Apache License 2.0",
  "Artistic" => "Artistic license 2.0",
  "BSDL" => "BSD 3-Clause 'New/Revised' License",
  "BSDL-2" => "BSD 2-Clause 'Simplified/FreeBSD' License",
  "CDDL" => "Common Development and Distribution License 1.0",
  "MITL" => "MIT license",
  "MPL" => "Mozilla Public License 2.0",
  "Public Domain" => "Public Domain (no copyright)",
  "Python" => "Python License",
  "PHPL" => "PHP License 3.0",
  "GNU GPL" => "GNU General Public License 2.0",
  "GNU GPLv3" => "GNU General Public License 3.0",
  "GNU LPGL" => "GNU Library/Lesser General Public License 2.1",
  "GNU LPGLv3" => "GNU Library/Lesser General Public License 3.0",
  "Affero GPL" => "Affero GNU Public License 2.0",
  "Affero GPLv3" => "GNU Affero General Public License v3",
  "AFL" => "Academic Free License 3.0",
  "APL" => "Adaptive Public License",
  "APSL" => "Apple Public Source License",
  "AAL" => "Attribution Assurance Licenses",
  "BSL" => "Boost Software License",
  "CECILL" => "CeCILL License 2.1",
  "CATOSL" => "Computer Associates Trusted Open Source License 1.1",
  "CDDL" => "Common Development and Distribution License 1.0",
  "CPAL" => "Common Public Attribution License 1.0",
  "CUA" => "CUA Office Public License Version 1.0",
  "EUDatagrid" => "EU DataGrid Software License",
  "EPL" => "Eclipse Public License 1.0",
  "ECL" => "Educational Community License, Version 2.0",
  "EFL" => "Eiffel Forum License V2.0",
  "Entessa" => "Entessa Public License",
  "EUPL" => "European Union Public License, Version 1.1 (EUPL-1.1)",
  "Fair" => "Fair License",
  "Frameworx" => "Frameworx License",
  "HPND" => "Historical Permission Notice and Disclaimer",
  "IPL" => "IBM Public License 1.0",
  "IPA" => "IPA Font License",
  "ISC" => "ISC License",
  "LPPL" => "LaTeX Project Public License 1.3c",
  "LPL" => "Lucent Public License Version 1.02",
  "MirOS" => "MirOS Licence",
  "MS" => "Microsoft Reciprocal License",
  "MIT" => "MIT license",
  "Motosoto" => "Motosoto License",
  "Multics" => "Multics License",
  "NASA" => "NASA Open Source Agreement 1.3",
  "NTP" => "NTP License",
  "Naumen" => "Naumen Public License",
  "NGPL" => "Nethack General Public License",
  "Nokia" => "Nokia Open Source License",
  "NPOSL" => "Non-Profit Open Software License 3.0",
  "OCLC" => "OCLC Research Public License 2.0",
  "OFL" => "Open Font License 1.1",
  "OGTSL" => "Open Group Test Suite License",
  "OSL" => "Open Software License 3.0",
  "PostgreSQL" => "The PostgreSQL License",
  "CNRI" => "CNRI Python license (CNRI-Python)",
  "QPL" => "Q Public License",
  "RPSL" => "RealNetworks Public Source License V1.0",
  "RPL" => "Reciprocal Public License 1.5",
  "RSCPL" => "Ricoh Source Code Public License",
  "SimPL" => "Simple Public License 2.0",
  "Sleepycat" => "Sleepycat License",
  "SPL" => "Sun Public License 1.0",
  "Watcom" => "Sybase Open Watcom Public License 1.0",
  "NCSA" => "University of Illinois/NCSA Open Source License",
  "VSL" => "Vovida Software License v. 1.0",
  "W3C" => "W3C License",
  "WXwindows" => "wxWindows Library License",
  "Xnet" => "X.Net License",
  "ZPL" => "Zope Public License 2.0",
  "Zlib" => "zlib/libpng license",
  "Other" => "Other License",
); // todo: Dicuss entry for Commercial/Proprietary code anyhow.
   // hint: Separation usually works better than prohibition.
   //       (Filtering instead of cleanups)




#-- Additional input filters

// Length of strings in arrays > 100
function min_length_100($a) {
    return array_sum(array_map("strlen", $a)) >= 100;
}




#-- Template helpers

// Wrap tag list into links
function wrap_tags($tags, $r="") {
    foreach (str_getcsv($tags) as $id) {
        $id = trim($id);
        $r .= "<a href=\"/tags/$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
function versioned_url($url, $version) {
    return preg_replace("/([\$%])(version|Version|VERSION)\b\\1?/", $version, $url);
}


// Project listing output preparation;
// HTML context escapaing, versioned urls, formatted date string
function prepare_output(&$entry) {
    $entry["download"] = versioned_url($entry["download"], $entry["version"]);
    if (TRUE or empty($entry["image"])) {
        if (file_exists("./img/screenshot/$entry[name].jpeg")) {
            $entry["image"] = "/img/screenshot/$entry[name].jpeg";
        }
        else {
            $entry["image"] = "/img/nopreview.png";
        }
    }
    $entry["formatted_date"] = date_fmt($entry["t_published"]);
    $entry = array_map("input::_html", $entry);
}



// 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&#65122; </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&amp;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;
}




#-- 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) {
    preg_match_all(
        "@
           [[%$]*  (\w+)  []%$]*
              \s*  [:=>]+  \s*
                   (\S+)
           (?<![,.;])
        @imsx",
        $str, $m
    );
    return array_combine($m[1], $m[2]);
}



/**
 *  Extracts key = /regex/ list.  Regex delimiters are always required,
 *  but keys may be in multiple formats (version=, [version]=>, $version:=..)
 *
 */
function p_key_value_rx($str) {
    preg_match_all(
        "@
           [[%$]*  (\w+)  []%$]*
              \s*  [:=>]+  \s*
           (
              ([^\s\w])  (?> (?!\\3|\\\\). |  \\\\. )+  \\3 [umixUs]* [*]?
           )
        @msx",
        $str, $m
    );
    return array_combine($m[1], $m[2]);
}


#-- database check
function project_version_exists($name, $version) {
    return intval(
        db("SELECT 1 FROM release WHERE name=? AND version=?", $name, $version)->fetch()
    );
}



?>

Deleted layout_bottom.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
</section>

<footer id=spotlight>
&nbsp;
</footer>

<footer id=bottom>
<a href="http://fossil.include-once.org/freshcode/wiki/About">About</a> |
<a href="http://fossil.include-once.org/freshcode/wiki/Privacy">Privacy / Policy</a> |
<a href="http://fossil.include-once.org/freshcode/wiki/Contribute">Contribute</a> |
<small>
   <a href="http://fossil.include-once.org/freshcode/wiki/Contribute"><i>optional</i> Login</a>
</small>
<small style=float:right>
<?php print social_share_links("freshcode", "http://freshcode.club/"); ?>
</small>
<br>
<small style="font-size:90%">
This is a non-commercial project.
<br>
All project entries are licensed as CC-BY-SA. There will be <a href="/feed">/atom+json feeds</a>..
</small>
</footer>

</html>

Deleted layout_header.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45












































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<!DOCTYPE html>
<html>
<head> 
    <title>freshcode.club</title>
    <meta charset=UTF-8>
    <link rel=stylesheet href="/freshcode.css?0.5.1">
    <base href="http://<?= HTTP_HOST ?>/">
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script>
       $(function(){
          $(".project .trimmed").one("click", function(){  
              $(this).animate({"max-height": "20em"});
          });
          $(".article-links.trimmed").one("click", function(){
              $(this).toggleClass("trimmed");
          });
       });
    </script>
</head>
<body>

<nav id=topbar>
Open source community software release tracking.
<span style=float:right>
<a href="//freshmeat.club/">freshmeat.club</a> |
<a href="//freecode.club/">freecode.club</a> |
<b><a href="//freshcode.club/">freshcode.club</a></b>
</span>
</nav>

<footer id=logo>
<a href="/" title="freshcode.club"><img src=logo.png width=200 height=110 alt=freshcode border=0></a>
<div class=empty-box>&nbsp;</div>
</footer>

<nav id=tools>
   <a href="/">Home</a>
   <a href="/submit" class=submit>Submit</a>
   <a href="/tags">Browse Projects by Tag</a>
   <a href="http://fossil.include-once.org/freshcode/wiki/About">About</a>
   <a href="/links">Links</a>
   <a href="http://www.opensourcestore.org/">Forum</a>
</nav>


Added lib/curl.php.



























































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: php7
 * title: fluent curl
 * description: simple wrapper around curl functions
 * version: 0.3
 * license: Public Domain
 *
 *
 * This simple wrapper class and its factory function allow
 * compact invocations of curl. You can pass either just an
 * URL, an option array, a previously wrapped curl() or a
 * raw curl resource handle.
 * Methods and options (CURL_ constants) lose their prefix
 * once wrapped, and can be easily chained:
 *
 *   curl($url)->followlocation(1)->verbose(1)->exec();
 *
 * Option names are case-insensitive too. The exec() and any
 * setting retrieval terminate the fluent call.
 *
 *
 * Individual curl() wrappers can also be combined into a
 * parallel-request curl_multi() instance:
 *
 *   curl_multi($url1, $url2, $url3)->verbose(1)->exec();
 *
 * Here the handles may be passed as associative array or
 * indexed list. exec() will wait until all finish, then
 * directly return an array of result contents.
 * In-between option calls are applied to all bound handles.
 *
 */



# hybrid constructor
function curl($url=NULL) {
    return is_a($url, "curl") ? $url : new curl($url);
}



/**
 * Fluent curl wrapper which obviates CURL_ prefixes.
 *
 * Makes curl_functions available as methods. Previous constants
 * are easily accessible through chainable method calls as well.
 * Only getinfo() options are mapped as virtual properties.
 *
 */
class curl {


    /**
     * resource / curl handle
     *
     */
    public $handle = NULL;
    
    
    /**
     * Setup parameters.
     *
     */
    static $defaults = array(
        "returntransfer" => 1,
        "httpget" => 1,
        "http200aliases" => array(200, 201, 202, 203),
        "header" => 0,
    );


    /**
     * Some checks before returning the resulting content.
     *
     */
    public $assert = array();


    /**
     * Initialize, where $params is usually just an $url, or an [OPT=>val] list
     *
     */
    function __construct($params) {
    
        // create
        $this->handle = is_object($params) ? $params : curl_init();
        
        // merge options
        $params = array_merge(
            curl::$defaults,
            array("followlocation" => !ini_get("safe_mode")),
            is_array($params) ? array_change_key_case($params) : array(),
            is_string($params) ? array("url" => $params) : array()
        );

        // multiple [url=>$url, post=>$bool, ..] options
        if (is_array($params)) foreach ($params as $cmd=>$opt) {
            $this->__call($cmd, array($opt));
        }
    }

    
    /**
     * Most invocations are mapped onto CURL_CONST settings,
     * while some are just plain function calls:
     *
     * @method exec()
     * @method errno()
     * @method error()
     * @method getinfo()
     */
    function __call($opt, $args) {

        # CURLOPT_*** constant
        if (defined($const = "CURLOPT_" . strtoupper($opt))) {
            curl_setopt($this->handle, constant($const), $args[0]);
        }
        # curl_action function
        elseif (function_exists($func = "curl_$opt")) {
            return call_user_func_array($func, array_merge(array($this->handle), $args));
        }
        # neither exists
        else {
            trigger_error("curl: unknown '$opt' option", E_USER_ERROR);
        }
        
        return $this;
    }
    
    
    /**
     * Append result-checks such as ["HTTP_CODE" => [200, 201]].
     *
     */
    function assert($list) {
        $this->assert += $list;
        return $this;
    }


    /**
     * Wrap exec() to test result handle for HTTP or CURL properties.
     *
     */
    function exec() {
        $args = func_get_args();
        $content = $this->__call("exec", $args);
        foreach ($this->assert as $option => $values) {
            if (!in_array($this->$option, $values)) {
                return NULL;
            }
        }
        return $content;
    }

    
    /**
     * retrieve constants 
     *
     */
    function __get($opt) {
        // @implicit error message from constant() for non-existant symbols
        return curl_getinfo($this->handle, constant($const = "CURLINFO_" . strtoupper($opt)));
    }
    
    
}




# hybrid constructor
function curl_multi($url=NULL) {
    return new curl_multi($url);
}


/**
 * Associative handler for multiple concurrent curl requests.
 * Not so fluent.
 *
 */
class curl_multi {


    /**
     * Indexed or associative list
     *
     */
    public $list = array();

    
    /**
     * Multi-handle
     *
     */
    public $mh = NULL;


    /**
     * Bind list of curl handles.
     *
     * An associative array can be passed here, either of prepared curl() handles,
     * just URLs, or an curl option array each.
     *
     */
    function __construct($list) {
    
        // optionally get indexed params as list
        if (func_num_args() >= 2) {
           $list = func_get_args();
        }
        
        // auto-wrap curl() handlers
        $list = array_map("curl", $list);

        // copy handle list
        $this->list = $list;
        
        // bind actual curl handles
        $this->mh = curl_multi_init();
        foreach ($list as $cw) {
            curl_multi_add_handle($this->mh, $cw->handle);
        }
    }
    
    
    /**
     * Run until all individual curl() handles are finished.
     *
     * Implicitly returns a list of result contents (associative array as well).
     *
     */
    function exec($timeout=1.50, $t=0.0, $running=1) {

        // process within timeframe
        while (($timeout > $t += $timeout/50) and $running) {

            curl_multi_exec($this->mh, $running);
            curl_multi_select($this->mh, $timeout/50);
        }

        // fetch results, then close handles
        return $this->close( $this->getcontent() );
    }

    
    /**
     * Retrieve results, associative array.
     *
     */
    function getcontent() {
        return array_map("curl_multi_getcontent", array_map("current",/*retrieve first property (->handle) each*/ $this->list));
    }
    

    /**
     * Close all curl() handles in $list, and the curl-multi wrapper.
     *
     */
    function close($r=NULL) {
        $this->__call("close", array());
        curl_multi_close($this->mh);
        return $r;
    }


    /**
     * Applies options on each curl handle.
     *
     */
    function __call($opt, $args) {
        foreach ($this->list as $cw) {
            $cw->__call($opt, $args);
        }
    }

}


?>

Added lib/db.php.
































































































































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * title: PDO wrapper
 * description: Hybrid db() interface for extended SQL parameterization and result folding
 * api: php
 * type: database
 * version: 0.9.9
 * depends: php:pdo
 * license: Public Domain
 * author: Mario Salzer
 * doc: http://fossil.include-once.org/hybrid7/wiki/db
 *
 *
 * QUERY
 *
 * Provides simple database queries with enumerated / named parameters. It's
 * flexible in accepting plain PDO scalar arguments or arrays. Array args get
 * merged, or transcribed when special placeholders are present:
 *
 *   $r = db("SELECT * FROM tbl WHERE a>=? AND b IN (??)", $a, array($b, $c));
 *
 * Extended placeholder syntax:
 *
 *      ??    Interpolation of indexed arrays - useful for IN clauses.
 *      ::    Turns associative arrays into a :named, :value, :list.
 *      :?    Interpolates key names (ignores values).
 *
 *      :&    Becomes a `name`=:value list, joined by AND - for WHERE clauses.
 *      :|    Becomes a `name`=:value list, joined by OR - for WHERE clauses.
 *      :,    Becomes a `name`=:value list, joined by , commas - for UPDATEs.
 *
 *      :*    Expression placeholder, where the associated argument should
 *            contain an array ["AND foo IN (??)", $params] - which only
 *            interpolates if $params contains any value.  Can be nested.
 *
 * Configurable {TOKENS} from db()->tokens[] are also substituted..
 *
 *
 * RESULT
 *
 * The returned result can be accessed as single data row, when fetching just
 * one:
 *       $result->column
 *       $result["column"]
 *
 * Or just traversed row-wise as usual by iteration
 *
 *       foreach (db("...") as $row)
 *
 * Alternatively by object-wrapping (unlike plain PDO->fetchObject() this
 * hydrates the object using its normal constructor) the result set with:
 *
 *       foreach ($result->into("ArrayObject") as $row)
 *
 * Yet all PDO ->fetch() methods are still available for use on the result obj.
 *
 *
 * CONNECT  
 *
 * The db() interface binds the global "$db" variable. It ought to be
 * initialized with:
 *
 *       db(new PDO(...));
 *
 * It's wrapped PDO handle can also be retrieved with just $pdo = db(); then.
 * 
 *
 * RECORD WRAPPER
 *
 * There's also a simple table data gateway wrapper implemented here. It
 * accepts db() queries for single entries, and allows ->save()ing back, or
 * to ->delete() records.
 * You should only use it in conjuction with sql2php and its simpler wrappers.
 *
 */



/**
 * Hybrid instantiation / query function.
 * Couples `$db` in the shared/global scope.
 *
 */
function db($sql=NULL, $params=NULL) {

    #-- shared PDO handle
    $db = & $GLOBALS["db"];   // Alternatively could be just static to hide it behind db()
    
    #-- open database
    if (is_object($sql)) {
    
        // use passed param
        $db = new db_wrap($sql);
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
        $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
        $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, is_int(stripos($db->getAttribute(PDO::ATTR_DRIVER_NAME), "mysql")));
        $db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
        $db->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
    
        // save settings
        $db->tokens = array("PREFIX"=>""); // or reference global $config
        $db->in_clause = $db->getAttribute(PDO::ATTR_DRIVER_NAME) == "sqlite"
                     and $db->getAttribute(PDO::ATTR_CLIENT_VERSION) < 3.6;
    }
    
    #-- return PDO handle
    elseif (empty($sql)) {
        return $db;
    }
    
    #-- just dispatch to the wrapper
    else {
        $args = array_slice(func_get_args(), 1);
        return $db($sql, $args);
    }

}


/**
 * Binds PDO handle, allows original calls and extended placeholder use.
 *
 */
class db_wrap {


    /**
     * Keep PDO handle.
     *
     */
    public $pdo = NULL;
    function __construct($pdo) {
        $this->pdo = $pdo;
    }


    /**
     * Chain to plain PDO if any other method invoked.
     *
     */
    function __call($func, $args) {
        return call_user_func_array(array($this->pdo, $func), $args);
    }


    /**
     * Prepares and executes query after extended placeholders and parameter unpacking.
     *
     */
    function __invoke($sql, $args=array()) {

        // $sql may contain associative SQL parts and parameters
        if (is_array($sql)) {
            list($sql, $args) = $this->join($sql);
        }

        // reject plain strings in SQL
        if (strpos($sql, "'")) {
            trigger_error("SQL query contained raw data. DO NOT WANT", E_USER_WARNING);
            return NULL;
        }

        // flatten array arguments and extended placeholders
        list($sql, $args) = $this->fold($sql, $args);
        
        // placeholders
        if (!empty($this->tokens) && strpos($sql, "{")) {
            $sql = preg_replace_callback("/\{(\w+)(.*?)\}/", array($this, "token"), $sql);
        }
        // older SQLite workaround
        if (!empty($this->in_clause) && strpos($sql, " IN (")) { // only for ?,?,?,? enum params
            $sql = preg_replace_callback("/(\S+)\s+IN\s+\(([?,]+)\)/", array($this, "in_clause"), $sql);
        }
        // just debug output
        if (!empty($this->test)) { 
            print json_encode($args)." => " . trim($sql) . "\n"; return;
        }
    
        // run
        $s = $this->prepare($sql)
        and
        $r = $s->execute($args);

        // wrap        
        return $s && $r ? new db_result($s) : $s;
    }


    /**
     * Expands the extended placeholders and flattens arrays from parameter list.
     *
     */
    function fold($sql, $args) {
    
        // output parameter list
        $flat_params = array();
        
        #-- flattening sub-arrays (works for ? enumarated and :named params)
        foreach ($args as $i=>$a) {

            // subarray that corresponds to special syntax placeholder?
            if (is_array($a)
            and preg_match("/  \?\?  |  : [?:*  &,|]  /x", $sql, $capture, PREG_OFFSET_CAPTURE))
            {
                list($token, $pos) = current($capture);

                // placeholder substitution, possibly changing $a params
                $replace = $this->{self::$expand[$token]}($a);

                // update SQL string
                $sql = substr($sql, 0, $pos) . $replace . substr($sql, $pos + strlen($token));
            }

            // unfold into plain parameter list
            if (is_array($a)) {
                $flat_params = array_merge($flat_params, $a);
            }
            else {
                $flat_params[] = $a;
            }
        }
        
        return array($sql, $flat_params);
    }


    /**
     * Syntax expansion callbacks
     *
     */
    static $expand = array(
        "??" => "expand_list",
        ":?" => "expand_keys",
        "::" => "expand_named",
        ":," => "expand_assoc_comma",
        ":&" => "expand_assoc_and",
        ":|" => "expand_assoc_or",
        ":*" => "expand_expr",
    );

    // ?? array placeholders
    function expand_list($a) {
        return implode(",", array_fill(0, count($a), "?"));
    }

    // :? name placeholders, transforms list into enumerated params
    function expand_keys(&$a) {
        $enum = array_keys($a) === range(0, count($a) - 1);
        $r = implode(",", $this->db_identifier($enum ? $a : array_keys($a), "`"));
        $a = array();
        return $r;
    }

    // :: becomes :named,:value,:list
    function expand_named($a) {
        return ":" . implode(",:", $this->db_identifier(array_keys($a)) );
    }

    // for :, expand COMMA-separated key=:key,bar=:bar associative array
    function expand_assoc_comma($a, $fill = ", ", $replace=array()) {
        foreach ($this->db_identifier(array_keys($a)) as $key) {
            $replace[] = "`$key` = :$key";
        }
        return implode($fill, $replace);
    }
    // for :& AND-chained assoc foo=:foo AND bar=:bar
    function expand_assoc_and($a) {
        return $this->expand_assoc_comma($a, " AND ");
    }
    // for :| OR-chained assoc foo=:foo OR bar=:bar
    function expand_assoc_or($a) {
        return $this->expand_assoc_comma($a, " OR ");
    }

    /**
     * While :* holds an optional expression and subvalue list. Which only gets
     * interpolated if the params list is non-empty.
     * which may be provided as alternative pairs ["AND :&", $and, "OR :|", $or].
     * 
     * While each value list should be a list itself, it's common to just pass
     * one array param for a single ::/?? extended placeholder. (Which then will
     * be auto-wrapped.)
     *
     */
    function expand_expr(&$a) {
        foreach (array_chunk($a, 2) as $pair) if (list($sql, $args) = $pair) {
            // substitute subexpression as if it were a regular SQL string
            if (is_array($args) && count($args)) {
                // rewrap simple value lists into param-args list
                $args = array_sum(array_map("is_array", $args)) ? $args : array($args);
                list ($replace, $a) = $this->fold($sql, $args);
                return $replace;
            }
        }
        $a = array();  // else replace with nothing and omit current data for flattened $params2
    }

    
    
    /**
     * For readability input SQL may come as associative clause => params list.
     *   ["SELECT ?" => $num,
     *    "FROM :?"  => [$tbl],
     *    "WHERE :&" => $match
     *   ]
     * Which is separated here into keys as $sql string and $args from values.
     *
     */
    function join($sql_args, $sql="", $args=array()) {
        foreach ($sql_args as $key=>$val) {
            // Key itself is not an SQL part
            if (is_int($key)) {
                // Value then can be an SQL string, or a param
                if (is_string($val)) {
                    $sql .= $val;
                }
                else {
                    $args[] = $val;
                }
            }
            // Plain SQL => Value
            else {
                $sql .= $key . "\n  ";
                $args[] = $val;
            }
        }
        return array($sql, $args);
    }



    // This is a restrictive filter function for column/table name identifiers.
    // Can only be foregone if it's ensured that none of the passed named db() $arg keys originated from http/user input.
    function db_identifier($as, $wrap="") {
        return preg_replace(array("/[^\w\d_.]/", "/^|$/"), array("_", $wrap), $as);
    }

    
    // Regex callbacks
    function token($m) {
        list($m, $tok, $ext) = $m;
        return isset($this->token[$tok]) ? $this->token[$tok].$ext : $this->token["$tok$ext"];
    }
    function in_clause($m) {
        list($m, $key, $vals) = $m;
        $num = substr_count($vals, "?");
        return "($key=" . implode("OR $key=", array_fill(0, $num, "? ")) . ")";
    }

}



/**
 * Allows traversing result sets as arrays or hydrated objects,
 * or fetches only first result row on ->column_name accesses.
 *
 */
class db_result extends ArrayObject implements IteratorAggregate {

    protected $results = NULL;

    function __construct($results) {
        parent::__construct(array(), 2);
        $this->results = $results;
    }
    // used as PDO statement
    function __call($func, $args) {
        return call_user_func_array(array($this->results, $func), $args);
    }

    // Single column access
    function offsetGet($name) {
    
        // get first result, transfuse into $this
        if (is_object($this->results)) {
            $this->exchangeArray($this->results->fetch());
            unset($this->results);
        }
        
        // suffice __get
        return parent::offsetGet($name);
    }

    // Just let PDOStatement handle the Traversable
    function getIterator() {
        return isset($this->results)
             ? $this->results
             : new ArrayIterator($this);
    }

    // Or hydrate specific result objects ourselves
    function into() {
        $into = func_get_args() ?: array("ArrayObject", 2);
        return new db_result_iter($this->results, $into);
    }
}


/**
 * More wrapping for hydrated iteration.
 *
 */
class db_result_iter implements Iterator {

    // Again keep PDOStatement and class specifier
    protected $results = NULL;
    protected $into = array();
    function __construct($results, $into) {
        $this->results = $results;
        $this->into = $into;
    }
    
    // Iterator just fetches and converts on traversal
    protected $row = NULL;
    public function current()
    {
        list($class, $a2, $a3, $a4, $a5) = array_merge($this->into, [NULL, NULL, NULL, NULL]);
        return new $class($this->row, $a2);
    }
    function valid() {
        return !empty($this->row = $this->results->fetch());
    }
    
    // unused for normal `foreach` operation
    function next() { return NULL; }
    function rewind() { return NULL; }
    function key() { return NULL; }
}



/**
 * Table data gateway. Don't use directly.
 *
 * Keeps ->_meta->table name and ->_meta->fields,
 * uses extendable tables with [ext] field serialization.
 * Doesn't cope with table joins. (yet?)
 *
 * Allows to ->set() and ->save() record back.
 */
class db_record /*resembles db_result*/ extends ArrayObject {

    // this is not purposelessly private, but to not pollute (array) typecasts with decorative data
    private $_meta;

    // initialize from db() result or array
    function __construct($results, $table, $fields, $keys) {
        
        // meta
        $this->_meta = new stdClass();
        $this->_meta->table = $table;
        $this->_meta->fields = array_unique(array_merge(array_keys($fields), array_keys($results)));
        $this->_meta->keys = $keys;
        
        // db query result
        if (is_array($results)) {
            $this->_meta->new = 1;  // instantiate from defaults or given row values
        }
        else {
            //if (is_string($results)) {   // queries are handled in wrapper
            //    $results = db($results);
            //}
            $results = $results->fetch();  // just get first result row
            $this->_meta->new = 0;
        }

        // unfold .ext
        if ($this->_meta->ext = isset($results["ext"])) {
            $results = array_merge($results, unserialize($results["ext"]));
        }

        // copy data
        // and turn object==array
        parent::__construct((array)$results, 2); //ArrayObject::ARRAY_AS_PROPS

        // fluent (hybrid constructor wrapper)
        return $this;
    }
    
    // set field
    function set($key, $val) {
        $this->{$key} = $val;
        return $this;  // fluent
    }

    // store table back to DB
    function save($row=NULL) {
    
        // source
        if (empty($row)) {
            $row = $this->getArrayCopy();
        }
        else {
            $row = array_merge($this->getArrayCopy(), is_array($row) ? $row : $row->getArrayCopy());
        }
    
        // fold .ext
        if ($this->_meta->ext) {
            $ext = array();
            foreach ($row as $key=>$val) {
                if (!in_array($key, $this->_meta->fields)) {
                    $ext[$key] = $val;
                    unset($row[$key]);
                }
            }
            $row["ext"] = serialize($ext);
        }
        
        // store
        if ($this->_meta->new) {
            db("INSERT INTO {$this->_meta->table} (:?) VALUES (??)", $row, $row);
            $this->_meta->new = 0;
        }
        // update
        else {
            $keys = $this->keys($row, 1);
            db("UPDATE {$this->_meta->table} SET :, WHERE :&", $row, $keys);
        }
        
        return $this;  // fluent
    }

    // split $keys from $row/$this
    function keys(&$row, $unset=0) {
        $keys = array();
        foreach ($this->_meta->keys as $key) { 
            $keys[$key] = $row[$key];
            if ($unset) unset($row[$key]);
        } 
        return $keys;
    }
    
    // oh noooes
    function delete() {
        db("DELETE FROM {$this->_meta->table} WHERE :&", $this->keys($this));
        return $this;  // well
    }
}




?>

Added lib/deferred_openid_session.php.


























































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: php
 * title: Session startup
 * description: Avoids session startup until actual login occured
 * license: MITL
 * version: 0.3.1
 *
 * Start $_SESSION only if there's already a session cookie present.
 * (Prevent needless cookies and tracking ids for not logged-in users.)
 *
 * The only handler that initiates any login process is `page_login.php`
 *
 */



// Kill off CloudFlare cookie when Do-Not-Track header present
if ($_SERVER->has("HTTP_DNT") and $_SERVER->boolean["HTTP_DNT"]) {
    header("Set-Cookie: __cfduid= ; path=/; domain=.freshcode.club; HttpOnly");
}





// Check for pre-existant cookie before defaulting to initiate session store
if ($_COOKIE->has("USER")) {
    session_fresh();
}
// just populate placeholders
else {
    $_SESSION["openid"] = "";
    $_SESSION["name"] = "";
    $_SESSION["csrf"] = array();
}


// verify incoming OpenID request
if ($_GET->has("openid_mode") and empty($_SESSION["openid"])) {

    include_once("lib/openid.php");

    $openid = new LightOpenID(HTTP_HOST);
    if ($openid->mode) {
        if ($openid->validate()) {
            $_COOKIE->no("USER") and session_fresh();
            $_SESSION["openid"] = $openid->identity;
            $_SESSION["name"] = $openid->getAttributes()["namePerson/friendly"];
        }
    }

}



// Prevent some session tampering
function session_fresh() {

    // Initiate with current session identifier
    if ($_COOKIE->has("USER")) {
        session_id($_COOKIE->id["USER"]);
    }
    session_name("USER");
    session_set_cookie_params(0, "/", HTTP_HOST, false, true);
    session_start();

    // Security by obscurity: lock client against User-Agent
    $useragent = $_SERVER->text->length30["HTTP_USER_AGENT"];
    // Security by obscurity: IP subnet lock (or just major route for IPv6)
    $subnet = $_SERVER->ip->length6["REMOTE_ADDR"];
    // Server-side timeout (7 days)
    $expire = time() + 7 * 24 * 3600;

    // New ID for mismatches
    if (empty($_SESSION["state/client"]) or $_SESSION["state/client"] != $useragent
    or  empty($_SESSION["state/subnet"]) or $_SESSION["state/subnet"] != $subnet
    or  empty($_SESSION["state/expire"]) or $_SESSION["state/expire"] < time()
    ) {
        session_destroy();
        session_regenerate_id(true);
        session_start();
    }
    // and Repopulate status fields
    $_SESSION["state/client"] = $useragent;
    $_SESSION["state/subnet"] = $subnet;
    $_SESSION["state/expire"] = $expire;
}


Added lib/diff.php.
















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/*
    Paul's Simple Diff Algorithm v 0.1
    (C) Paul Butler 2007 <http://www.paulbutler.org/>
    May be used and distributed under the zlib/libpng license.
    
    This code is intended for learning purposes; it was written with short
    code taking priority over performance. It could be used in a practical
    application, but there are a few ways it could be optimized.
    
    Given two arrays, the function diff will return an array of the changes.
    I won't describe the format of the array, but it will be obvious
    if you use print_r() on the result of a diff on some test data.
    
    htmlDiff is a wrapper for the diff command, it takes two strings and
    returns the differences in HTML. The tags used are <ins> and <del>,
    which can easily be styled with CSS.  
*/

class pdiff {

    // 
    static function diff($old, $new){
        $matrix = array();
        $maxlen = 0;
        foreach($old as $oindex => $ovalue){
            $nkeys = array_keys($new, $ovalue);
            foreach($nkeys as $nindex){
                $matrix[$oindex][$nindex] = isset($matrix[$oindex - 1][$nindex - 1]) ?
                    $matrix[$oindex - 1][$nindex - 1] + 1 : 1;
                if($matrix[$oindex][$nindex] > $maxlen){
                    $maxlen = $matrix[$oindex][$nindex];
                    $omax = $oindex + 1 - $maxlen;
                    $nmax = $nindex + 1 - $maxlen;
                }
            }   
        }
        if($maxlen == 0) return array(array('d'=>$old, 'i'=>$new));
        return array_merge(
            pdiff::diff(array_slice($old, 0, $omax), array_slice($new, 0, $nmax)),
            array_slice($new, $nmax, $maxlen),
            pdiff::diff(array_slice($old, $omax + $maxlen), array_slice($new, $nmax + $maxlen)));
    }

    // markup <ins> and <del> between old and new text blob
    static function htmlDiff($old, $new){
        $ret = '';
        $diff = pdiff::diff(preg_split("/[\s]+/", $old), preg_split("/[\s]+/", $new));
        foreach($diff as $k){
            if(is_array($k))
                $ret .=
                    (!empty($k['d']) ? "<del>" . input::html(implode(' ',$k['d'])) . "</del> " : '').
                    (!empty($k['i']) ? "<ins>" . input::html(implode(' ',$k['i'])) . "</ins> " : '');
            else $ret .= $k . ' ';
        }
        return $ret;
    }

    // Just compare word-wise without between three revisions, without honoring order
    static function triDiff($prev, $curr, $next){
        $ret = '';
        $prev = preg_split("/[\s]+/", $prev);
        $curr = preg_split("/[\s]+/", $curr);
        $next = preg_split("/[\s]+/", $next);
        foreach($curr as $word){
            if (!in_array($word, $prev)) {
               $ret .= "<ins>$word</ins> ";
            }
            elseif (!in_array($word, $next)) {
               $ret .= "<del>$word</del> ";
            }
            else {
               $ret .= "$word ";
            }
        }
        return $ret;
    }

}
?>

Added lib/feeder.phar.

cannot compute difference between binary files

Added lib/forum.php.



























































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: php
 * type: handler
 * title: Follow The Thread
 * description: Straightforward threaded discussion forum
 * version: 0.2
 * category: discussion
 * depends: HTMLPurifier
 * config:
 *    <var name="forum_cfg[categories]" type="list" default="discussion,documentation" help="Comma-separated list of thread classifiers"/>
 *
 *
 * Implements a minimalistic web forum.
 * Primary goals are:
 *   → Threaded discussions in place of bulletin board blabber.
 *   → Contemporary security over restriction gimmicks.
 *   → Usability instead of UI featuritis.
 *   → Open access in lieu of accounteritis.
 *
 * Single table database:
 *    [id] INT PRIMARY KEY NOT NULL UNIQUE,
 *    [pid] INT NOT NULL DEFAULT(0) REFERENCES [forum] ([id]),
 *    [gid] INT NOT NULL REFERENCES [forum] ([id]),
 *    [tag] VARCHAR (0, 32) NOT NULL DEFAULT('discussion'),
 *    [summary] VARCHAR (0, 200) NOT NULL,
 *    [source] TEXT,
 *    [html] TEXT,
 *    [excerpt] TEXT,
 *    [author] VARCHAR (0, 80),
 *    [miniature] TEXT,
 *    [t_published] INT NOT NULL,
 *    [edit_token] VARCHAR (16, 64)
 *
 * Templating
 * Behaviour
 * Configuration
 *
 *
 *
 */


/**
 * Decorative classification/grouping of threads.
 *
 */
global $forum_cfg;
$forum_cfg["categories"] = "discussion,projects,announcement,code,documentation,autoupdate";



/**
 * Callbacks, dispatcher and handler.
 *
 */
class forum {


    /**
     * Can be set externally, depending on application logic.
     *
     */
    var $is_admin = 0;
    var $can_edit = 1;



    /**
     * NOP
     *
     */
    function __construct() {
    }


    /**
     * Show forum listing
     *
     */
    function index($page=0) {
    
        // Fetch thread groups (attached to root gid=0)
        $entries = db("
            SELECT *
              FROM forum
             WHERE gid
                IN ( SELECT id
                       FROM forum
                      WHERE pid = 0
                   ORDER BY t_published DESC
                      LIMIT 50
                     OFFSET ?*50 )
          ORDER BY gid DESC,
                   t_published ASC
        ", $page);
        
        // Iterate over groups
        $last = 0;
        $group = array();
        foreach ($entries as $e) {
            if ($e["gid"] != $last) {
                $this->show_thread($group);
                $group = array();
            }
            $group[] = $e;
            $last = $e["gid"];
        }
        $this->show_thread($group);
    }


    /**
     * Iterate over grouped entry list
     * and recursively output posts.
     *
     */
    function show_thread($group, $pid=0) {
    
        #-- find available parent ids
        $parents = array_column($group, "pid");
    
        #-- step throuh
        foreach ($group as $entry) {
        
            #-- show if associated
            if ($entry["pid"] == $pid) {
                $entry["miniature"] or $entry["miniature"] = "/img/user.png";
                include("template/forum_entry.php");
                
                #-- Nest its children
                if (in_array($entry["id"], $parents)) {
                    print "    <ul>\n";
                    $this->show_thread($group, $entry["id"]);
                    print "    </ul>\n";
                }

                print "       </li>\n";
            }
        }
    }

    
    
    /**
     * Load a single entry.
     *
     */
    function entry($id) {
    }



    /**
     * Accept POST input and populate new forum post.
     * Adds an reply if ?pid= is not zero.
     * Doubles as edit function if ?id= is present.
     *
     */
    function submit($INSERT="INSERT") {
    
        #-- Prepare some fields
        $data = array(
            "id" => NULL,
            "pid" => $_POST->int["pid"],
            "gid" => NULL,
            "author" => $_POST->text->length30->html["author"],
            "miniature" => $_POST->text->length200->html["image"],
            "tag" => $_POST->text->length20->html["tag"],
            "summary" => $_POST->text->length120->html["summary"],
            "source" => $_POST->nocontrol->length12000["source"],
            "html" => "",
            "excerpt" => "",
            "t_published" => time(),
            "edit_token" => $this->edit_token(),
        );

        #-- Source to HTML
        $data = $this->prepare_output($data);
        
        #-- Reject too minor submisions
        if (strlen("$data[source]$data[summary]") < 100) {
            exit("<p class=warning>Your post was a little too coarse. Please elaborate to keep discussions going.</p>");
        }

        #-- Edit
        if ($id = $_POST->int["id"]) {
            $prev = $this->edit_keep($this->edit_entry($id));
            $data = array_merge($data, $prev);
            $INSERT = "REPLACE";
#            var_dump($INSERT, $data, $prev);
        }
        #-- Reply
        elseif ($data["pid"]) {
            $data["gid"] = $this->group_id($data["pid"]);
        }

        /**
         * Store entry
         *  → Find maximum ID
         *  → Use as new id, and group id if new
         *  → Else keep previous pid and gid/id
         *
         * $data and $ids are split up before, so the :? and ::
         * placeholders don't consume all fields.
         *
         */
        $ids = array_splice($data, 0, 3);  // extract id,pid,gid
        $ok = db("
            $INSERT INTO forum (id, pid, gid, :?)
            VALUES (
                IFNULL(:id, (SELECT IFNULL(MAX(id), 0) + 1 AS id FROM forum)),
                IFNULL(:pid, 0),
                COALESCE(:gid, :id, (SELECT IFNULL(MAX(id), 0) + 1 AS id FROM forum)),
                ::
            );
        ", $data, $data, $ids);
        
        #-- return rendered
        if ($ok) {
            $data["id"] = db()->lastInsertId();
            $data["pid"] = 0;
            $this->show_thread([$data], 0);
        }
    }


    #-- Editing timeout / permission
    function edit_permission($prev) {
        return
            $this->is_admin
        or
            $_COOKIE->name["edit_token"] == $prev["edit_token"]
        and
            $prev["t_published"] + 48*3600 > time();
    }


    #-- Retrieves or sets edit_token
    function edit_token() {
        if (empty($token = $_COOKIE->name["edit_token"])) {
            setcookie("edit_token", $token = sha1(serialize($_SERVER)), time()+7*24*3600);
        }
        return $token;
    }


    /**
     * Retrieve post entry, check edit permission, and/or set defaults
     *
     *  → For forum/edit requests fetches the existing post content.
     *  → Also checks editing permissions (token),
     *    or e.g. existing OpenID logon ($this->can_edit is set from main site).
     *
     */
    function edit_entry($id) {
        if ($prev = db("SELECT * FROM forum WHERE id=?", $id)->fetch()) {

            if (!$this->can_edit) {
                exit("<p class=warning>You aren't logged in on the main site. Please associate an OpenID account.</p>");
            }
            if (!$this->edit_permission($prev)) {
                exit("<p class=warning>Entry not editable. (The edit token does not match, or the article is too old for editing.)</p>");
            }
            
            return $prev;
        }
        exit("<p class=error>Post #$id does not exist.</p>");
    }

    
    #-- Unsets a few fields for replying.
    function edit_keep($prev) {
        $copy = array_flip(str_getcsv("id,pid,gid,miniature,t_published,edit_token"));
        return array_intersect_key($prev, $copy);
    }


    #-- Copy thread/group id from parent post etc.
    function group_id($parent_id) {
        if ($prev = db("SELECT gid FROM forum WHERE id = ?", $parent_id)) {
            return $prev->gid;
        }
        return 0;
    }    


    #-- Convert Markdown/BB source into HTML, create excerpt, prepare user avatar
    function prepare_output($data) {

        #-- Content
        define("HTMLPURIFIER_PREFIX", "phar://lib/htmlpurifier.phar/standalone/");
        $md = new Parsedown();
        $data["html"] = input::purify($md->parse($data["source"]));
        $data["excerpt"] = input::html(substr(strip_tags($data["html"]), 0, 320));
        
        #-- Author
        if (strlen($data["miniature"]) and $type = $_POST->in_array("img_type", "gravatar,identicon,monsterid,wavatar,retro"))
        {
            $data["miniature"] = "http://www.gravatar.com/avatar/"
                               . md5($data["miniature"])
                               . ".jpeg?s=16" . ($type == "gravatar" ? "" : "&d=$type");
        }
        return $data;
    }




    /**
     * Output submit <form>
     *
     *  → Provides a few blank fields.
     *  → Injects cookie defaults if fresh submission / not an edit.
     *  → Escapes previous content for edit_form() calls.
     *
     */
    function submit_form($pid=0, $id=0, $data=array()) {
        global $forum_cfg;

        extract(array_merge(
            array_fill_keys(str_getcsv("author,miniature,tag,summary,source"), ""),
            $data ? array() : $_COOKIE->list->text["author,miniature"],
            array_map("htmlspecialchars", $data)
        ));

        include("template/forum_submit_form.php");
    }


    /**
     * Show editing <form> instead with prefilled previous data.
     *
     *  → Retrieves previous post content, checks permissions at that.
     *  → Outputs edit form (replied for AJAX $.load() request)
     *
     */
    function edit_form($pid=0, $id=0) {
        $data = $this->edit_entry($id);
        $this->submit_form(0, 0, $data);
    }

}




Added lib/htmlpurifier.phar.

cannot compute difference between binary files

Added lib/input.php.



















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
 /**
  * api: php
  * title: Input $_REQUEST wrappers
  * type: interface
  * description: Encapsulates input variable superglobals for easy sanitization access
  * version: 2.8
  * revision: $Id$
  * license: Public Domain
  * depends: php:filter, php >5.0, html_purifier
  * config: <const name="INPUT_DIRECT" type="multi" value="disallow" multi="disallow|raw|log" description="filter method for direct $_REQUEST[var] access" />
  *         <const name="INPUT_QUIET" type="bool" value="0" multi="0=report all|1=no notices|2=no warnings" description="suppress access and behaviour notices" />
  * throws: E_USER_NOTICE, E_USER_WARNING, OutOfBoundsException
  *
  * Using these object wrappers ensures a single entry point and verification
  * spot for all user data. They wrap all superglobals and filter HTTP input
  * through sanitizing methods. For auditing casual and unverified access.
  *
  * Standardly they wrap: $_REQUEST, $_GET, $_POST, $_SERVER, $_COOKIE
  * And provide convenient access to filtered data:
  *   $_GET->int("page_num")                      // method
  *   $_POST->text["commentfield"]                // array syntax
  *   $_REQUEST->text->ascii->strtolower["xyz"]   // filter chains
  *
  * Available filter methods are:
  *   ->int
  *   ->float
  *   ->boolean
  *   ->name
  *   ->id
  *   ->words
  *   ->text
  *   ->ascii
  *   ->nocontrol
  *   ->spaces
  *   ->q
  *   ->escape
  *   ->regex
  *   ->in_array
  *   ->html
  *   ->purify
  *   ->json
  *   ->length
  *   ->range
  *   ->default
  * And most useful methods of the php filter extension.
  *   ->email
  *   ->url ->uri ->http
  *   ->ip
  *   ->ipv4->public
  * PHP string modification functions:
  *   ->urlencode
  *   ->strip_tags
  *   ->htmlentities
  *   ->strtolower
  * Automatic filter chain handler:
  *   ->array
  * Fetch multiple variables at once:
  *   ->list["var1,key,name"]
  * Process multiple entries as array:
  *   ->multi->http_build_query["id,token"]
  * Possible but should be used very consciously:
  *   ->xss
  *   ->sql
  *   ->mysql
  *   ->log
  *   ->raw
  *
  * You can also pre-define a standard filter-chain for all following calls:
  *   $_GET->nocontrol->iconv->utf7->xss->always();
  *
  * Using $__rules[] a set of filter rules can be preset per variable name.
  *
  * Parameterized filters can alternatively use the ellipsis … symbol (AltGr+:)
  * instead of the terminating method access syntax.
  *   $_GET->int->range…0…59["minutes"]
  *
  * Some filters are a mixture of sanitizing and validation. Basically
  * all can also be used independently of the superglobals with their
  * underscore name, $str = input::_text($str);
  *
  * For the superglobals it's also possible to count($_GET); or check with
  * just $_POST() if there are contents. (Use this in lieu of empty() test.)
  * There are also the three functions ->has ->no ->keys() for array lookup.
  *
  * Defining new filters can be done as methods, or since those are picked
  * up too, as plain global functions. It's also possible to assign function
  * aliases or attach closures:
  *   $_POST->_newfilter = function($s) { return modified($s); }
  * Note that the assigned filter name must have an underscore prefixed.
  *
  * --
  *
  * Input validation of course is no substitute for secure application logic,
  * parameterized sql and proper output encoding. But this methodology is a
  * good base and streamlines input data handling.
  *
  * Filter methods can be bypassed in any number of ways. There's no effort
  * made at prevention here. But it's recommended to simply use ->raw() when
  * needed - not all input can be filtered anyway. This way an audit handler
  * could always attach there - when desired.
  *
  * The goal is not to coerce, but encourage security via API *simplicity*.
  *
  */


/**
 * Project-specific identifiers.
 *
 * Since `input` is an overly generic name, one might wish to wrap it up if
 * there's an identifier conflict. (Remember; namespaces aren't taxonomies.)
 *
 */
#namespace filter;
#use \ArrayObject, \Countable, \Iterator, \OutOfBoundsException;
#use \HTMLPurifier;


/**
 * Handler name for direct $_REQUEST["array"] access.
 *
 *   "raw" = reports with warning,
 *   "disallow" = throws an exception
 */
defined("INPUT_DIRECT") or
define("INPUT_DIRECT", "raw");

/**
 * Notice suppression.
 *
 *   0 = report all,
 *   1 = no notices,
 *   2 = ignore non-existant filter
 */
defined("INPUT_QUIET") or
define("INPUT_QUIET", 0);


/**
 * @class Request variable input wrapper.
 *
 * The methods are defined with underscore prefix, but supposed to be used without
 * when invoked on the superglobal arrays:
 *
 * @method int   int[$field] converts input into integer
 * @method float float[$field]
 * @method name  name[$field] removes any non-alphanumeric characters
 * @method id    id[$field] alphanumeric string with dots
 * @method text  text[$field] textual data with interpunction
 *
 */  
class input implements ArrayAccess, Countable, Iterator {



    /**
     * Data filtering functions.
     *
     * These methods are usually not to be called directly. Instead use
     * $_GET->filtername["varname"] syntax without preceeding underscore
     * to access variable content.
     *
     * Categories: [e]=escape, [w]=whitelist, [b]=blacklist, [s]=sanitize, [v]=validate
     *
     */


    
    /**
     * @type    cast
     * @sample  22
     *
     * Integer.
     *
     */
    function _int($data) {
        return (int)$data;
    }

    /**
     * @type    cast
     * @sample  3.14159
     *
     * Float.
     *
     */
    function _float($data) {
        return (float)$data;
    }
    
    /**
     * @type    white, strip
     * @sample  "_foo123"
     *
     * Alphanumeric strings.
     * (e.g. var names, letters and numbers, may contain international letters)
     *
     */
    function _name($data) {
        return preg_replace("/\W+/u", "", $data);
    }

    /**
     * @type    white, strip
     * @sample  "xvar.1_2.x"
     *
     * Identifiers with underscores and dots,
     *
     */
    function _id($data) {
        return preg_replace("#(^[^a-z_]+)|[^\w\d_.]+|([^\w_]$)#i", "", $data);
    }

    /**
     * @type    white, strip
     * @sample  "tag-name-123"
     *
     * Alphanumeric strings with dashes. Commonly used for slugs.
     * Consecutive dashes and underscores are compacted.
     *
     */
    function _slug($data) {
        return preg_replace("/[^\w-]+|^[^a-z]+|[^\w]+$|(?<=[-_])[-_]+/", "", strtolower($s));
    }
    
    /**
     * @type    white, strip
     * @sample  "Hello - World. Foo + 3, Bar."
     * 
     * Flow text with whitespace,
     * with minimal interpunction allowed (optional parameter).
     *
     */
    function _words($data, $extra="") {
        return preg_replace("/[^\w\d\s,._\-+$extra]+/u", " ", strip_tags($data));
    }

    /**
     * [w]
     * Human-readable text with many special/interpunction characters:
     *  " and ' allowed, but no <, > or \
     *
     */
    function _text($data) {
        return preg_replace("/[^\w\d\s,._\-+?!;:\"\'\/`´()*=#@]+/u", " ", strip_tags($data));
    }
    
    /**
     * Acceptable filename characters.
     *
     * Alphanumerics and dot (but not as first character).
     * You should use `->basename` as primary filter anyway.
     *
     * @t whitelist
     *
     */
    function _filename($data) {
        return preg_replace("/^[.\s]|[^\w._+-]/", "_", $data);
    }
    
    
    /**
     * [b]
     * Filter non-ascii text out.
     * Does not remove control characters. (Similar to FILTER_FLAG_STRIP_HIGH.)
     * 
     */
    function _ascii($data) {
        return preg_replace("/[\\200-\\377]+/", "", $data);
    }

    /**
     * [b]
     * Remove control characters. (Similar to FILTER_FLAG_STRIP_LOW.)
     * 
     */
    function _nocontrol($data) {
        return preg_replace("/[\\000-\\010\\013\\014\\016-\\037\\177\\377]+/", "", $data); // all except \r \n \t
    }
    
    /**
     * [e] 
     * Exchange \r \n \t and \f \v \0 for normal spaces.
     * 
     */
    function _spaces($data) {
        return strtr($data, "\r\n\t\f\v\0", "      ");
    }

    /**
     * [x]
     * Regular expression filter.
     *
     * This either extracts (preg_match) data if you have a () capture group,
     * or functions as filter (pref_replace) if there's a [^ character class.
     * 
     */
    function _regex($data, $rx="", $match=1) {
        # validating
        if (strpos($rx, "(")) {
            if (preg_match($rx, $data, $result)) {
                return($result[$match]);
            }
        }
        # cropping
        elseif (strpos($rx, "[^")) {
            return preg_replace($rx, "", $data);
        }
        # replacing
        else {
            return preg_replace($rx, $match, $data);
        }
    }
    
    /**
     * [w]
     * Miximum and minimum string length.
     *
     */
    function _length($data, $max=65535, $cut=NULL) {
        // just the length limit
        if (is_null($cut)) {
            return substr($data, 0, $max);
        }
        // require minimum string length, else drop value completely
        else {
            return strlen($data) >= $max ? substr($data, 0, $cut)  : NULL;
        }
    }
    
    /**
     * [w]
     * Range ensures value is between given minimum and maximum.
     * (Does not convert to integer itself.)
     *
     */
    function _range($data, $min, $max) {
        return ($data > $max) ? $max : (($data < $min) ? $min : $data);
    }

    /**
     * [b]
     * Fallback value for absent/falsy values.
     *
     */
    function _default($data, $default) {
        return empty($data) ? $default : $data;
    }

    /**
     * [w] 
     * Boolean recognizes 1 or 0 and textual values like "false" and "off" or "no".
     *
     */
    function _boolean($data) {
        if (empty($data) || $data==="0" || in_array(strtolower($data), array("false", "off", "no", "n", "wrong", "not", "-"))) {
            return false;
        }
        elseif ($data==="1" || in_array(strtolower($data), array("true", "on", "yes", "right", "y", "ok"))) {
            return true;
        }
        else return NULL;
    }

    /**
     * [w]
     * Ensures field is in array of allowed values.
     *
     * Works with arrays, but also string list. If you supply a "list,list,list" then
     * the comparison is case-insensitive.
     *
     */
    function _in_array($data, $array) {
        if (is_array($array) ? in_array($data, $array) : in_array(strtolower($data), explode(",", strtolower($array)))) {
            return $data;
        }
    }
    
    
    ###### filter_var() wrappers #########

    /**
     * [w]
     * Common case email syntax.
     *
     * (Close to RFC2822 but disallows square brackets or double quotes, no verification of TLDs,
     * doesn't restrict underscores in domain names, ignores i@an and anyone@localhost.)
     *
     */
    function _email($data, $validate=1) {
        $data = preg_replace("/[^\w!#$%&'*+/=?_`{|}~@.\[\]\-]/", "", $data);  // like filter_var
        if (!$validate || preg_match("/^(?!\.)[.\w!#$%&'*+/=?^_`{|}~-]+@(?:(?!-)[\w-]{2,}\.)+[\w]{2,6}$/i", trim($data))) {
            return $data;
        }
    }

    /**
     * [s] 
     * URI characters. (Actually IRI)
     *
     */
    function _uri($data) {
    # we should encode all-non chars
        return preg_replace("/[^-\w\d\$.+!*'(),{}\|\\~\^\[\]\`<>#%\";\/?:@&=]+/u", "", $data);  // same as SANITIZE_URL
    }
    
    /**
     * [w]
     * URL syntax
     *
     * This is an alias for FILTER_VALIDATE_URL. Beware that it lets a few unwanted schemes
     * through (file:// and mailto:) and what you'd consider misformed URLs (http://http://whatever).
     *
     */
    function _url($data) {
        return filter_var($data, FILTER_VALIDATE_URL);
    }

    /**
     * [v]
     * More restrictive HTTP/FTP url syntax.
     * No usernames allowed, no empty port, pathnames/qs/anchor are not checked.
     *
     # see also http://internet.ls-la.net/folklore/url-regexpr.html
     */
    function _http($data) {
        return preg_match("~
            (?(DEFINE)  (?<byte> 2[0-4]\d |25[0-5] |1\d\d |[1-9]?\d)  (?<ip>(?&byte)(\.(?&byte)){3})  (?<hex>[0-9a-fA-F]{1,4})  )
        ^   (?<proto>https?|ftps?)  ://
            # (?<user> \w+(:\w+)?@ )?
            ( (?<host>  (?:[a-z\d][a-z\d_\-\$]*\.?)+)
             |(?<ipv6>  \[     (?! [:\w]*:::          # ASSERT: no triple ::: colons
                                 |(:\w+){8}|(\w+:){8} # not more than 7 : colons
                                 |(\w*:){7}\w+\.      # not more than 6 : if there's a .
                                 | [:\w]*::[:\w]+:: ) # double :: colon must be unique
                               (?= [:\w]*::           # don't count if there is one ::
                                 |(\w+:){6}\w+[:.])   # else require six : and one . or :
              (?: :|(?&hex):)+((?&hex)|(?&ip)|:) \])  # MATCH: combinations of HEX : IP
             |(?<ipv4>  (?&ip) )     )
            (?<port> :\d{1,5} )?   # the integer isn't optional if port : colon present (unlike FILTER_VALIDATE_URL)
            (?<path> [/][^?#\s]* )?
            (?<qury> [?][^#\s]* )?
            (?<frgm> [#]\S* )?
        \z~ix", $data, $uu) ? $data : NULL;#(print_r($uu) ? $data : $data)
    }
    
    /**
     * [w] 
     * IP address
     *
     */
    function _ip($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6) ? $data : NULL;
    }

    /**
     * [w]
     * IPv4 address
     *
     */
    function _ipv4($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? $data : NULL;
    }
    
    /**
     * [w]
     * must be public IP address
     *
     */
    function _public($data) {
        return filter_var($data, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE) ? $data : NULL;
    }


    ###### format / representation #########
    
    
    /**
     * [v]
     * HTML5 datetime / datetime_local, date, time
     *
     */
    function _datetime($data) {
        return preg_match("/^\d{0}\d\d\d\d -(0\d|1[012]) -([012]\d|3[01]) T ([01]\d|2[0-3]) :[0-5]\d :[0-5]\d   (Z|[+-]\d\d:\d\d|\.\d\d)$/x", $data) ? $data : NULL;
    }
    function _date($data) {
        return preg_match("/^\d\d\d\d -(0\d|1[012]) -([012]\d|3[01])$/x", $data) ? $data : NULL;
    }
    function _time($data) {
        return preg_match("/^([01]\d|2[0-3]) :[0-5]\d :[0-5]\d  (\.\d\d)?$/x", $data) ? $data : NULL;
    }

    /**
     * [v]
     * HTML5 color
     *
     */
    function _color($data) {
        return preg_match("/^#[0-9A-F]{6}$/i", $data) ? strtoupper($data) : NULL;
    }


    /**
     * [w]
     * Telephone numbers (HTML5 <input type=tel> makes no constraints.)
     *
     */
    function _tel($data) {
        $data = preg_replace("#[/.\s]+#", "-", $data);
        if (preg_match("/^(\+|00)?(-?\d{2,}|\(\d+\)){2,}(-\d{2,}){,3}(\#\d+)?$/", $data)) {
            return trim($data, "-");
        }
    }


    /**
     * [v]
     * Verify minimum consistency (RFC4627 regex) and decode json data.
     *
     */
    function _json($data) { 
        if (!preg_match('/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/', preg_replace('/"(\\.|[^"\\])*"/g', '', $data))) {
            return json_decode($data);
        }
    }
    
    /**
     * [v]
     * XML tree.
     *
     */
    function _xml($data) {
        return simplexml_load_string($data);
    }

    /**
     * [w]
     * Clean html string via HTMLPurifier.
     *
     */
    function _purify($data) {
        $h = new HTMLPurifier;
        return $h->purify( $data );
    }

    /**
     * [e]
     * HTML escapes.
     *
     * This is actually an output filter. But might be useful to mirror input back into
     * form fields instantly `<input name=field value="<?= $_GET->html["field"] ?>">`
     *
     * @param $data string
     * @return string
     */
    function _html($data) {

        return htmlspecialchars($data, ENT_QUOTES, "UTF-8", false);
    }

    /**
     * [b]
     * Removes residual and certain HTML tags.
     * WARNING: This is no `strip_tags()` substitute, but purposefully leaves
     * lonesome < angle brackets > alone. Only strips coherent and well-known
     * presentational markup.
     *
     */
    function _strip_markup($data, $with="") {
        return preg_replace("~(<\s*/?\s*(a|b|i|em|strong|sup|sub|ins|del|br|hr|big|small|font|span|p|div|table|tr|td|ol|ul|li|dl|dd|dt|abbr|tt|code|pre|h[1-6])\b".
               "(?>[^>\"\']+|\"[^\"]*\"|'[^']*')*>)+~", $with, $data);
    }


    /**
     * [e]
     * Escape all significant special chars.
     *
     */
    function _escape($data) {
        return preg_replace("/[\\\\\[\]\{\}\[\]\'\"\`\´\$\!\&\?\/\>\<\|\*\~\;\^]/", "\\$1", $data);
    }

    /**
     * [e]
     * Addslashes
     *
     */
    function _q($data) {
        return addslashes($data);
    }

    /**
     * [b]
     * Minimal XSS detection.
     * Attempts no cleaning, just bails if anything looks suspicious.
     *
     * If something is XSS contaminated, it's spam and not worth to process further.
     * WEAK filters are better than no filters, but you should ultimatively use ->html purifier instead.
     *
     */
    function _xss($data) {
        if (preg_match("/[<&>]/", $data)) {   // looks remotely like html
            $html = $data;
            
            // remove filler
            $html = preg_replace("/&#(\d);*/e", "ord('$1')", $html);   // escapes
            $html = preg_replace("/&#x(\w);*/e", "ord(dechex('$1'))", $html);   // escapes
            $html = preg_replace("/[\x00-\x20\"\'\`\´]/", "", $html);   //  whitespace + control characters, also any quotes
            $html .= preg_replace("#/\*[^<>]*\*/#", "", $html);   // in-JS obfuscation comments

            // alert patterns
            if (preg_match("#[<<]/*(\?import|applet|embed|object|script|style|!\[CDATA\[|title|body|link|meta|base|i?frame|frameset|i?layer)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(javascript|ecmascript|vbscript|jscript|python|actionscript|livescript):)#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(on(mouse\w+|key\w+|focus\w*|blur|click|dblclick|reset|select|change|submit|load|unload|error|abort|drag|Abort|Activate|AfterPrint|AfterUpdate|BeforeActivate|BeforeCopy|BeforeCut|BeforeDeactivate|BeforeEditFocus|BeforePaste|BeforePrint|BeforeUnload|Begin|Blur|Bounce|CellChange|Change|Click|ContextMenu|ControlSelect|Copy|Cut|DataAvailable|DataSetChanged|DataSetComplete|DblClick|Deactivate|Drag|DragEnd|DragLeave|DragEnter|DragOver|DragDrop|Drop|End|Error|ErrorUpdate|FilterChange|Finish|Focus|FocusIn|FocusOut|Help|KeyDown|KeyPress|KeyUp|LayoutComplete|Load|LoseCapture|MediaComplete|MediaError|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|MouseWheel|Move|MoveEnd|MoveStart|OutOfSync|Paste|Pause|Progress|PropertyChange|ReadyStateChange|Repeat|Reset|Resize|ResizeEnd|ResizeStart|Resume|Reverse|RowsEnter|RowExit|RowDelete|RowInserted|Scroll|Seek|Select|SelectionChange|SelectStart|Start|Stop|SyncRestored|Submit|TimeError|TrackChange|Unload|URLFlip)[^<\w=>]*[=:]+)[^<>]{3,}#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(\w{3,}[=:]+(-moz-binding:))#iUu", $html, $uu)
             or preg_match("#[<>]\w[^>]*(style[=:]+[^<>]*(expression\(|behaviour:|script:))#iUu", $html, $uu))
            {
                $this->_log($data, "DETECTED XSS PATTERN ({$uu['1']}),");
                $data = "";
                die("input::_xss");
            }
        }
        return $data;
    }

    /**
     * [w]
     * Cleans utf-8 from invalid sequences and alternative representations.
     * (BEWARE: performance drain)
     *
     */
    function _iconv($data) {
        return iconv("UTF-8", "UTF-8//IGNORE", $data);
    }

    /**
     * [b]
     * Few dangerous UTF-7 sequences
     * (only necessary if output pages don't have a charset specified)
     *
     */
    function _utf7($data) {
        return preg_replace("/[+]A(D[w40]|C[IYQU])(-|$)?/", "", $data);;
    }

    /**
     * [e] 
     * Escape for concatenating data into sql query.
     * (suboptimal, use parameterized queries instead)
     *
     */
    function _sql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return db()->quote($data);  // global object
    }
    function _mysql($data) {
        INPUT_QUIET or trigger_error("SQL escaping of input variable '$this->__varname'.", E_USER_NOTICE);
        return mysql_real_escape_string($data);
    }
    



    ###### pseudo filters ##############


    /**
     * [x]
     * Unfiltered access should obviously be avoided. But it's not always possible,
     * so this method exists and will just trigger a notice in debug mode.
     *
     */
    function _raw($data) {
        INPUT_QUIET or trigger_error("Unfiltered input variable \${$this->__title}['{$this->__varname}'] accessed.", E_USER_NOTICE);
        return $data;
    }
    /**
     * [x]
     * Unfiltered access, but logs variable name and value.
     *
     */
    function _log($data, $reason="manual log") {
        syslog(LOG_NOTICE, "php7://input:_log@{$_SERVER['SERVER_NAME']} accessing \${$this->__title}['{$this->__varname}'] variable, $reason, content=" . substr($this->_id(json_encode($data)), 0, 48));
        return $data;
    }

    /**
     * [b]
     * Abort with fatal error. (Used as fallback for INPUT_DIRECT access.)
     *
     */
    function _disallow($data) {
        throw new OutOfBoundsException("Direct \$_REQUEST[\"$this->__varname\"] is not allowed, add ->filter method, or change INPUT_DIRECT if needed.");
    }
    
    
    
    /**
     * Validate instead of sanitize.
     *
     */
    function _is($data) {
        $this->__filter[] = array("is_still", array());
        return $data;
    }
    function _is_still($data) {
        return $data === $this->__vars[$this->__varname];
    }



   

    ######  implementation  ################################################




    /**
     * Array data from previous superglobal.
     *
     * (It's pointless to make this a priv/proteced attribute, as raw data could be accessed
     * in any number of ways. Not the least would be to just not use the input filters.)
     *
     */
    var $__vars = array();

    
    /**
     * Name of superarray this filter object wraps.
     * (e.g. "_GET" or "_SERVER")
     *
     */
    var $__title = "";


    /**
     * Currently accessed array key.
     *
     */
    var $__varname = "";


    /**
     * Amassed filter list.
     * Each ->method->chain name will be appended here. Gets automatically reset after
     * a succesful variable access.
     *
     * Each entry is in `array("methodname", array("param1", 2, 3))` format.
     *
     */
    var $__filter = array();  // filterchain method stack


    /**
     * Automatically appended filter list. (Simply combined with current `$__filter`s).
     *
     */
    var $__always = array();


    /**
     * Currently accessed array keys.
     *
     */
    var $__rules = array(     // pre-defined varname filters
        // "varname.." => array(  array("length",array(256), array("nocontrol",array())  ),
    );


    /**
     * Initialize object from
     *
     * @param array  one of $_REQUEST, $_GET or $_POST etc.
     */
    function __construct($_INPUT, $title="") {
        $this->__vars = $_INPUT;   // stripslashes on magic_quotes_gpc might go here, but we have no word if we actually receive a superglobal or if it wasn't already corrected
        $this->__title = $title;
    }

    
    /**
     * Sets default filter chain.
     * These are ALWAYS applied in CONJUNCTION to ->manually->specified->filters.
     *
     */
    function always() {
        $this->__always = $this->__filter;
        $this->__filter = array();
    }
    
    
    /**
     * Normalize array keys (to UPPER case), for e.g. $_SERVER vars.
     *
     */
    function key_case($case=CASE_UPPER) {
        $this->__vars = array_change_key_case($this->__vars, $case);
    }
    
    
    /**
     * Executes current filter or filter chain on given $varname.
     *
     */
    function filter($varname, $reset_filter_afterwards=NULL) {

        // single array [["name"]] becomes varname
        if (is_array($varname) and count($varname)===1) {
            $varname = current($varname);
        }
        $this->__varname = $varname;

        // direct/raw access without any ->filtername prior offsetGet[] or plain filter() call
        if (empty($this->__filter)) {
            if (isset($this->__rules[$varname])) {
                $this->__filter = $this->rules[$varname];  // use a predefined filterset
            } else {
                $this->__filter = array( INPUT_DIRECT );  // direct access handler
            }
        }
        $first_handler = reset($this->__filter);

        // retrieve value for selected input variable
        $data = NULL;
        if (!is_scalar($varname) or $first_handler == "list" or $first_handler == "multi") {
            // one of the multiplex handlers supposedly picks it up
        }
        elseif (isset($this->__vars[$varname])) {
            // entry exists
            $data = $this->__vars[$varname];
        }
        elseif (!INPUT_QUIET) {
            trigger_error("Undefined input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
            // run through filters anyway (for ->log)
        }
        
        // implicit ->array filter handling
        if (is_array($data) and $first_handler != "array") {
            array_unshift($this->__filter, "array");
        }
        
        // apply filters (we're building an ad-hoc merged array here, because ->apply works on the reference, and some filters expect ->__filter to contain the complete current list)
        $this->__filter = array_merge($this->__filter, $this->__always);
        $data = $this->apply($data, $this->__filter);
        
        // the Traversable array interface resets the filter list after each request, see ->current()
        if ($reset_filter_afterwards) {
            $this->__filter = $reset_filter_afterwards;
        }

        // done
        return $data;
    }


    /**
     * Runs list of filters on data. Uses either methods, bound closures, or global functions
     * if the filter name matches.
     *
     * It works on an array reference and with array_shift() because filters are allowed to
     * modify the list at runtime (->array requires to).
     *
     */
    function apply($data, &$filterchain) {
        while ($f = array_shift($filterchain)) {
            list($filtername, $args) = is_array($f) ? $f : array($f, array());

            // an override function name or closure exists
            if (isset($this->{"_$filtername"})) {
                $filtername = $this->{"_$filtername"};
            }
            // call _filter method
            if (is_string($filtername) && method_exists($this, "_$filtername")) {
                $data = call_user_func_array(array($this, "_$filtername"), array_merge(array($data), (array)$args));
            }
            // ordinary php function, or closure, or rebound method
            elseif (is_callable($filtername)) {
                $data = call_user_func_array($filtername, array_merge(array($data), $args));
            }
            else {
                INPUT_QUIET>=2 or trigger_error("unknown filter '" . (is_scalar($filtername) ? $filtername : "closure") . "', falling back on wiping non-alpha characters from '{$this->__varname}'", E_USER_WARNING);
                $data = $this->_name($data);
            }
        }
        return $data;
    }


    /**
     * @multiplex
     *
     * List data / array value handling.
     *
     * This virtual filter hijacks the original filter chain, and applies it
     * to sub values.
     *
     */
    function _array($data) {

        // save + swap out the current filter chain
        list($multiplex, $this->__filter) = array($this->__filter, array());

        // iteratively apply original filter chain on each array entry
        $data = (array) $data;
        foreach (array_keys($data) as $i) {
            $chain = $multiplex;
            $data[$i] = $this->apply($data[$i], $chain);
        }

        return $data;
    }


    /**
     * @multiplex
     *
     * Grab a collection of input variables, names delimited by comma.
     * Implicitly makes it an ->_array() list.
     * The _array handler is implicitly used for indexed values. _list
     * and _multi can be used for associative arrays, given a key list.
     * 
     * @example  extract($_GET->list->text["name,title,date,email,comment"]);
     * @php5.4+  $_GET->list[['key1','key2','key3']];
     *
     * @bugs  hacky, improper way to intercept, fetches from $__vars[] directly,
     *        uses $__varname instead of $data, may prevent replays,
     *        main will trigger a notice anyway as VAR1,VAR2,.. is undefined
     *
     */
    function _list($keys, $pass_array=FALSE) {

        // get key list
        if (is_array($this->__varname)) {
            $keys = $this->__varname;
        }
        else {
            $keys = explode(",", $this->__varname);
        }

        // slice out named values from ->__vars
        $data = array_merge(
            array_fill_keys($keys, NULL),
            array_intersect_key($this->__vars, array_flip($keys))
        );

        // chain to _array multiplex handler
        if (!$pass_array) {
            return $this->_array($data);
        }
        // process fetched list as array (for user-land functions like http_build_query)
        else {
            return $data;
        }
    }
    
    /**
     * Processes collection of input variables.
     * Passes list on as array to subsequent filters.
     *
     */
    function _multi($keys) {
        return $this->_list($keys, "process_as_array");
    }


    
    /**
     * @hide
     *
     * Ordinary method calls are captured here. Any ->method("name") will trigger
     * the filter and return variable data, just like ->method["name"] would. It
     * just allows to supply additional method parameters.
     *
     */
    function __call($filtername, $args) {  // can have arguments
        $this->__filter[] = array($filtername, array_slice($args, 1));
        return $this->filter($args[0]);
    }

    
    /**
     * @hide
     *
     * Wrapper to capture ->name->chain accesses.
     *
     */
    function __get($filtername) {
        //
        // we could do some heuristic chaining here,
        // if the last entry in the ->attrib->attrib list is not a valid method name,
        // but a valid varname, we should execute the filter chain rather than add.
        //
        
        // Unpack parameterized filter attributes, use U+2022 ellipsis … as delimiter
        if (strpos($filtername, "…")) {
            $args = explode("…", $filtername);
            $filtername = array_shift($args);
        }
        else {
            $args=array();
        }

        // Add filter to list
        $this->__filter[] = count($args) ? array($filtername, $args) : $filtername;

        // fluent interface
        return $this;
    }
    


    /**
     * @hide ArrayAccess
     *
     * Normal $_ARRAY["name"] syntax access is redirected through the filter here.
     *
     */
    function offsetGet($varname) {
        // never chains
        return $this->filter($varname);
    }

    /**
     * @hide ArrayAccess
     *
     * Needed for commonplace isset($_POST["var"]) checks.
     *
     */
    function offsetExists($name) {
        return isset($this->__vars[$name]);
    }

    /**
     * @hide
     * Sets value in array. Note that it only works for array["xy"]= top-level
     * assignments. Subarrays are always retrieved by value (due to filtering)
     * and cannot be set item-wise.
     *
     * @discouraged
     * Manipulating the input array is indicative for state tampering and thus
     * throws a notice per default.
     *
     */
    function offsetSet($name, $value) {
        INPUT_QUIET or trigger_error("Manipulation of input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        $this->__vars[$name] = $value;
    }
    
    /**
     * @hide
     * Removes entry.
     *
     * @discouraged
     * Triggers a notice per default.
     *
     */
    function offsetUnset($name) {
        INPUT_QUIET or trigger_error("Removed input variable \${$this->__title}['{$this->__varname}']", E_USER_NOTICE);
        unset($this->__vars[$name]);
    }



    /**
     * Generic array features.
     * Due to being reserved words `isset` and `empty` need custom method names here:
     *
     *  ->has(isset)
     *  ->no(empty)
     *  ->keys()
     * 
     * Has No Keys seems easy to remember.
     *
     */

    /**
     * isset/array_key_exists check;
     * may list multiple keys to check.
     *
     */
    function has($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_intersect_key($this->__vars, array_flip($args))) == count($args);
    }
    
    /**
     * empty() probing,
     * Tests if named keys are absent or falsy.
     *
     */
    function no($args) {
        $args = (func_num_args() > 1) ? func_get_args() : explode(",", $args);
        return count(array_filter(array_intersect_key($this->__vars, array_flip($args)))) == 0;
    }
    
    /**
     * Returns list of all contained keys.
     *
     */
    function keys() {
        return array_keys($this->__vars);
    }


    /**
     * @hide Traversable
     *
     * Allows to loop over all array entries in a foreach loop. A supplied filterlist
     * is reapplied for each iteration.
     *
     * - Basically all calls are mirrored onto ->__vars` internal array pointer.
     * 
     */
    function current() {
        return $this->filter(key($this->__vars), /*reset_filter_afterwards=to what it was before*/$this->__filter);
    }
    function key() {
        return key($this->__vars);
    }
    function next() {
        return next($this->__vars);
    }
    function rewind() {
        return reset($this->__vars);
    }
    function valid() {
        if (key($this->__vars) !== NULL) {
            return TRUE;
        }
        else {
            // also reset the filter list after we're done traversing all entries
            $this->__filters=array();
            return FALSE;
        }
    }


    /**
     * @hide Countable
     * 
     */
    function count() {
        return count($this->__vars);
    }


    /**
     * @hide Make filtering functions available as static methods
     *       without underscore prefix for external invocation.
     */ 
    static function __callStatic($func, $args) {
        static $filter = "input";

        // Also eschew E_STRICT notices
        if (!is_object($filter)) {
            $filter = new input(array(), "\$_INLINE_INPUT");
        }

        return isset($filter->{"_$func"})
             ? call_user_func_array($filter->{"_$func"}, $args)
             : call_user_func_array(array($filter, "_$func"), $args);
    }

    /**
     * @hide Allows testing variable presence with e.g. if ( $_POST() )
     *       Alternatively $_POST("var") is an alias to $_POST["var"].
     *
     */
    function __invoke($varname=NULL) {
    
        // treat it as variable access
        if (!is_null($varname)) {
            return $this->offsetGet($varname);
        }
        
        // do the count() call
        else {
            return $this->count();
        }
    }

}



/**
 * @autorun
 *
 */
$_SERVER = new input($_SERVER, "_SERVER");
$_REQUEST = new input($_REQUEST, "_REQUEST");
$_GET = new input($_GET, "_GET");
$_POST = new input($_POST, "_POST");
$_COOKIE = new input($_COOKIE, "_COOKIE");
#$_SESSION
#$_ENV
#$_FILES required a special handler


?>

Added lib/openid.phar.

cannot compute difference between binary files

Deleted logo.png.

cannot compute difference between binary files

Deleted logo.svgz.

cannot compute difference between binary files

Deleted openid.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831






























































































































































































































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
<?php
/**
 * This class provides a simple interface for OpenID (1.1 and 2.0) authentication.
 * Supports Yadis discovery.
 * The authentication process is stateless/dumb.
 *
 * Usage:
 * Sign-on with OpenID is a two step process:
 * Step one is authentication with the provider:
 * <code>
 * $openid = new LightOpenID('my-host.example.org');
 * $openid->identity = 'ID supplied by user';
 * header('Location: ' . $openid->authUrl());
 * </code>
 * The provider then sends various parameters via GET, one of them is openid_mode.
 * Step two is verification:
 * <code>
 * $openid = new LightOpenID('my-host.example.org');
 * if ($openid->mode) {
 *     echo $openid->validate() ? 'Logged in.' : 'Failed';
 * }
 * </code>
 *
 * Change the 'my-host.example.org' to your domain name. Do NOT use $_SERVER['HTTP_HOST']
 * for that, unless you know what you are doing.
 *
 * Optionally, you can set $returnUrl and $realm (or $trustRoot, which is an alias).
 * The default values for those are:
 * $openid->realm     = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'];
 * $openid->returnUrl = $openid->realm . $_SERVER['REQUEST_URI'];
 * If you don't know their meaning, refer to any openid tutorial, or specification. Or just guess.
 *
 * AX and SREG extensions are supported.
 * To use them, specify $openid->required and/or $openid->optional before calling $openid->authUrl().
 * These are arrays, with values being AX schema paths (the 'path' part of the URL).
 * For example:
 *   $openid->required = array('namePerson/friendly', 'contact/email');
 *   $openid->optional = array('namePerson/first');
 * If the server supports only SREG or OpenID 1.1, these are automaticaly
 * mapped to SREG names, so that user doesn't have to know anything about the server.
 *
 * To get the values, use $openid->getAttributes().
 *
 *
 * The library requires PHP >= 5.1.2 with curl or http/https stream wrappers enabled.
 * @author Mewp
 * @copyright Copyright (c) 2010, Mewp
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 */
class LightOpenID
{
    public $returnUrl
         , $required = array()
         , $optional = array()
         , $verify_peer = null
         , $capath = null
         , $cainfo = null
         , $data;
    private $identity, $claimed_id;
    protected $server, $version, $trustRoot, $aliases, $identifier_select = false
            , $ax = false, $sreg = false, $setup_url = null, $headers = array();
    static protected $ax_to_sreg = array(
        'namePerson/friendly'     => 'nickname',
        'contact/email'           => 'email',
        'namePerson'              => 'fullname',
        'birthDate'               => 'dob',
        'person/gender'           => 'gender',
        'contact/postalCode/home' => 'postcode',
        'contact/country/home'    => 'country',
        'pref/language'           => 'language',
        'pref/timezone'           => 'timezone',
        );

    function __construct($host)
    {
        $this->trustRoot = (strpos($host, '://') ? $host : 'http://' . $host);
        if ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off')
            || (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
            && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')
        ) {
            $this->trustRoot = (strpos($host, '://') ? $host : 'https://' . $host);
        }

        if(($host_end = strpos($this->trustRoot, '/', 8)) !== false) {
            $this->trustRoot = substr($this->trustRoot, 0, $host_end);
        }

        $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?');
        $this->returnUrl = $this->trustRoot . $uri;

        $this->data = ($_SERVER['REQUEST_METHOD'] === 'POST') ? $_POST : $_GET;

        if(!function_exists('curl_init') && !in_array('https', stream_get_wrappers())) {
            throw new ErrorException('You must have either https wrappers or curl enabled.');
        }
    }

    function __set($name, $value)
    {
        switch ($name) {
        case 'identity':
            if (strlen($value = trim((String) $value))) {
                if (preg_match('#^xri:/*#i', $value, $m)) {
                    $value = substr($value, strlen($m[0]));
                } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) {
                    $value = "http://$value";
                }
                if (preg_match('#^https?://[^/]+$#i', $value, $m)) {
                    $value .= '/';
                }
            }
            $this->$name = $this->claimed_id = $value;
            break;
        case 'trustRoot':
        case 'realm':
            $this->trustRoot = trim($value);
        }
    }

    function __get($name)
    {
        switch ($name) {
        case 'identity':
            # We return claimed_id instead of identity,
            # because the developer should see the claimed identifier,
            # i.e. what he set as identity, not the op-local identifier (which is what we verify)
            return $this->claimed_id;
        case 'trustRoot':
        case 'realm':
            return $this->trustRoot;
        case 'mode':
            return empty($this->data['openid_mode']) ? null : $this->data['openid_mode'];
        }
    }

    /**
     * Checks if the server specified in the url exists.
     *
     * @param $url url to check
     * @return true, if the server exists; false otherwise
     */
    function hostExists($url)
    {
        if (strpos($url, '/') === false) {
            $server = $url;
        } else {
            $server = @parse_url($url, PHP_URL_HOST);
        }

        if (!$server) {
            return false;
        }

        return !!gethostbynamel($server);
    }

    protected function request_curl($url, $method='GET', $params=array(), $update_claimed_id)
    {
        $params = http_build_query($params, '', '&');
        $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : ''));
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($curl, CURLOPT_HEADER, false);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*'));

        if($this->verify_peer !== null) {
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer);
            if($this->capath) {
                curl_setopt($curl, CURLOPT_CAPATH, $this->capath);
            }

            if($this->cainfo) {
                curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo);
            }
        }

        if ($method == 'POST') {
            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $params);
        } elseif ($method == 'HEAD') {
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_NOBODY, true);
        } else {
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_HTTPGET, true);
        }
        $response = curl_exec($curl);

        if($method == 'HEAD' && curl_getinfo($curl, CURLINFO_HTTP_CODE) == 405) {
            curl_setopt($curl, CURLOPT_HTTPGET, true);
            $response = curl_exec($curl);
            $response = substr($response, 0, strpos($response, "\r\n\r\n"));
        }

        if($method == 'HEAD' || $method == 'GET') {
            $header_response = $response;

            # If it's a GET request, we want to only parse the header part.
            if($method == 'GET') {
                $header_response = substr($response, 0, strpos($response, "\r\n\r\n"));
            }

            $headers = array();
            foreach(explode("\n", $header_response) as $header) {
                $pos = strpos($header,':');
                if ($pos !== false) {
                    $name = strtolower(trim(substr($header, 0, $pos)));
                    $headers[$name] = trim(substr($header, $pos+1));
                }
            }

            if($update_claimed_id) {
                # Updating claimed_id in case of redirections.
                $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
                if($effective_url != $url) {
                    $this->identity = $this->claimed_id = $effective_url;
                }
            }

            if($method == 'HEAD') {
                return $headers;
            } else {
                $this->headers = $headers;
            }
        }

        if (curl_errno($curl)) {
            throw new ErrorException(curl_error($curl), curl_errno($curl));
        }

        return $response;
    }

    protected function parse_header_array($array, $update_claimed_id)
    {
        $headers = array();
        foreach($array as $header) {
            $pos = strpos($header,':');
            if ($pos !== false) {
                $name = strtolower(trim(substr($header, 0, $pos)));
                $headers[$name] = trim(substr($header, $pos+1));

                # Following possible redirections. The point is just to have
                # claimed_id change with them, because the redirections
                # are followed automatically.
                # We ignore redirections with relative paths.
                # If any known provider uses them, file a bug report.
                if($name == 'location' && $update_claimed_id) {
                    if(strpos($headers[$name], 'http') === 0) {
                        $this->identity = $this->claimed_id = $headers[$name];
                    } elseif($headers[$name][0] == '/') {
                        $parsed_url = parse_url($this->claimed_id);
                        $this->identity =
                        $this->claimed_id = $parsed_url['scheme'] . '://'
                                          . $parsed_url['host']
                                          . $headers[$name];
                    }
                }
            }
        }
        return $headers;
    }

    protected function request_streams($url, $method='GET', $params=array(), $update_claimed_id)
    {
        if(!$this->hostExists($url)) {
            throw new ErrorException("Could not connect to $url.", 404);
        }

        $params = http_build_query($params, '', '&');
        switch($method) {
        case 'GET':
            $opts = array(
                'http' => array(
                    'method' => 'GET',
                    'header' => 'Accept: application/xrds+xml, */*',
                    'ignore_errors' => true,
                ), 'ssl' => array(
                    'CN_match' => parse_url($url, PHP_URL_HOST),
                ),
            );
            $url = $url . ($params ? '?' . $params : '');
            break;
        case 'POST':
            $opts = array(
                'http' => array(
                    'method' => 'POST',
                    'header'  => 'Content-type: application/x-www-form-urlencoded',
                    'content' => $params,
                    'ignore_errors' => true,
                ), 'ssl' => array(
                    'CN_match' => parse_url($url, PHP_URL_HOST),
                ),
            );
            break;
        case 'HEAD':
            # We want to send a HEAD request,
            # but since get_headers doesn't accept $context parameter,
            # we have to change the defaults.
            $default = stream_context_get_options(stream_context_get_default());
            stream_context_get_default(
                array(
                    'http' => array(
                        'method' => 'HEAD',
                        'header' => 'Accept: application/xrds+xml, */*',
                        'ignore_errors' => true,
                    ), 'ssl' => array(
                        'CN_match' => parse_url($url, PHP_URL_HOST),
                    ),
                )
            );

            $url = $url . ($params ? '?' . $params : '');
            $headers = get_headers ($url);
            if(!$headers) {
                return array();
            }

            if(intval(substr($headers[0], strlen('HTTP/1.1 '))) == 405) {
                # The server doesn't support HEAD, so let's emulate it with
                # a GET.
                $args = func_get_args();
                $args[1] = 'GET';
                call_user_func_array(array($this, 'request_streams'), $args);
                return $this->headers;
            }

            $headers = $this->parse_header_array($headers, $update_claimed_id);

            # And restore them.
            stream_context_get_default($default);
            return $headers;
        }

        if($this->verify_peer) {
            $opts['ssl'] += array(
                'verify_peer' => true,
                'capath'      => $this->capath,
                'cafile'      => $this->cainfo,
            );
        }

        $context = stream_context_create ($opts);
        $data = file_get_contents($url, false, $context);
        # This is a hack for providers who don't support HEAD requests.
        # It just creates the headers array for the last request in $this->headers.
        if(isset($http_response_header)) {
            $this->headers = $this->parse_header_array($http_response_header, $update_claimed_id);
        }

        return $data;
    }

    protected function request($url, $method='GET', $params=array(), $update_claimed_id=false)
    {
        if (function_exists('curl_init')
            && (!in_array('https', stream_get_wrappers()) || !ini_get('safe_mode') && !ini_get('open_basedir'))
        ) {
            return $this->request_curl($url, $method, $params, $update_claimed_id);
        }
        return $this->request_streams($url, $method, $params, $update_claimed_id);
    }

    protected function build_url($url, $parts)
    {
        if (isset($url['query'], $parts['query'])) {
            $parts['query'] = $url['query'] . '&' . $parts['query'];
        }

        $url = $parts + $url;
        $url = $url['scheme'] . '://'
             . (empty($url['username'])?''
                 :(empty($url['password'])? "{$url['username']}@"
                 :"{$url['username']}:{$url['password']}@"))
             . $url['host']
             . (empty($url['port'])?'':":{$url['port']}")
             . (empty($url['path'])?'':$url['path'])
             . (empty($url['query'])?'':"?{$url['query']}")
             . (empty($url['fragment'])?'':"#{$url['fragment']}");
        return $url;
    }

    /**
     * Helper function used to scan for <meta>/<link> tags and extract information
     * from them
     */
    protected function htmlTag($content, $tag, $attrName, $attrValue, $valueName)
    {
        preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1);
        preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2);

        $result = array_merge($matches1[1], $matches2[1]);
        return empty($result)?false:$result[0];
    }

    /**
     * Performs Yadis and HTML discovery. Normally not used.
     * @param $url Identity URL.
     * @return String OP Endpoint (i.e. OpenID provider address).
     * @throws ErrorException
     */
    function discover($url)
    {
        if (!$url) throw new ErrorException('No identity supplied.');
        # Use xri.net proxy to resolve i-name identities
        if (!preg_match('#^https?:#', $url)) {
            $url = "https://xri.net/$url";
        }

        # We save the original url in case of Yadis discovery failure.
        # It can happen when we'll be lead to an XRDS document
        # which does not have any OpenID2 services.
        $originalUrl = $url;

        # A flag to disable yadis discovery in case of failure in headers.
        $yadis = true;

        # We'll jump a maximum of 5 times, to avoid endless redirections.
        for ($i = 0; $i < 5; $i ++) {
            if ($yadis) {
                $headers = $this->request($url, 'HEAD', array(), true);

                $next = false;
                if (isset($headers['x-xrds-location'])) {
                    $url = $this->build_url(parse_url($url), parse_url(trim($headers['x-xrds-location'])));
                    $next = true;
                }

                if (isset($headers['content-type'])
                    && (strpos($headers['content-type'], 'application/xrds+xml') !== false
                        || strpos($headers['content-type'], 'text/xml') !== false)
                ) {
                    # Apparently, some providers return XRDS documents as text/html.
                    # While it is against the spec, allowing this here shouldn't break
                    # compatibility with anything.
                    # ---
                    # Found an XRDS document, now let's find the server, and optionally delegate.
                    $content = $this->request($url, 'GET');

                    preg_match_all('#<Service.*?>(.*?)</Service>#s', $content, $m);
                    foreach($m[1] as $content) {
                        $content = ' ' . $content; # The space is added, so that strpos doesn't return 0.

                        # OpenID 2
                        $ns = preg_quote('http://specs.openid.net/auth/2.0/', '#');
                        if(preg_match('#<Type>\s*'.$ns.'(server|signon)\s*</Type>#s', $content, $type)) {
                            if ($type[1] == 'server') $this->identifier_select = true;

                            preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
                            preg_match('#<(Local|Canonical)ID>(.*)</\1ID>#', $content, $delegate);
                            if (empty($server)) {
                                return false;
                            }
                            # Does the server advertise support for either AX or SREG?
                            $this->ax   = (bool) strpos($content, '<Type>http://openid.net/srv/ax/1.0</Type>');
                            $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
                                       || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');

                            $server = $server[1];
                            if (isset($delegate[2])) $this->identity = trim($delegate[2]);
                            $this->version = 2;

                            $this->server = $server;
                            return $server;
                        }

                        # OpenID 1.1
                        $ns = preg_quote('http://openid.net/signon/1.1', '#');
                        if (preg_match('#<Type>\s*'.$ns.'\s*</Type>#s', $content)) {

                            preg_match('#<URI.*?>(.*)</URI>#', $content, $server);
                            preg_match('#<.*?Delegate>(.*)</.*?Delegate>#', $content, $delegate);
                            if (empty($server)) {
                                return false;
                            }
                            # AX can be used only with OpenID 2.0, so checking only SREG
                            $this->sreg = strpos($content, '<Type>http://openid.net/sreg/1.0</Type>')
                                       || strpos($content, '<Type>http://openid.net/extensions/sreg/1.1</Type>');

                            $server = $server[1];
                            if (isset($delegate[1])) $this->identity = $delegate[1];
                            $this->version = 1;

                            $this->server = $server;
                            return $server;
                        }
                    }

                    $next = true;
                    $yadis = false;
                    $url = $originalUrl;
                    $content = null;
                    break;
                }
                if ($next) continue;

                # There are no relevant information in headers, so we search the body.
                $content = $this->request($url, 'GET', array(), true);

                if (isset($this->headers['x-xrds-location'])) {
                    $url = $this->build_url(parse_url($url), parse_url(trim($this->headers['x-xrds-location'])));
                    continue;
                }

                $location = $this->htmlTag($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content');
                if ($location) {
                    $url = $this->build_url(parse_url($url), parse_url($location));
                    continue;
                }
            }

            if (!$content) $content = $this->request($url, 'GET');

            # At this point, the YADIS Discovery has failed, so we'll switch
            # to openid2 HTML discovery, then fallback to openid 1.1 discovery.
            $server   = $this->htmlTag($content, 'link', 'rel', 'openid2.provider', 'href');
            $delegate = $this->htmlTag($content, 'link', 'rel', 'openid2.local_id', 'href');
            $this->version = 2;

            if (!$server) {
                # The same with openid 1.1
                $server   = $this->htmlTag($content, 'link', 'rel', 'openid.server', 'href');
                $delegate = $this->htmlTag($content, 'link', 'rel', 'openid.delegate', 'href');
                $this->version = 1;
            }

            if ($server) {
                # We found an OpenID2 OP Endpoint
                if ($delegate) {
                    # We have also found an OP-Local ID.
                    $this->identity = $delegate;
                }
                $this->server = $server;
                return $server;
            }

            throw new ErrorException("No OpenID Server found at $url", 404);
        }
        throw new ErrorException('Endless redirection!', 500);
    }

    protected function sregParams()
    {
        $params = array();
        # We always use SREG 1.1, even if the server is advertising only support for 1.0.
        # That's because it's fully backwards compatibile with 1.0, and some providers
        # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com
        $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1';
        if ($this->required) {
            $params['openid.sreg.required'] = array();
            foreach ($this->required as $required) {
                if (!isset(self::$ax_to_sreg[$required])) continue;
                $params['openid.sreg.required'][] = self::$ax_to_sreg[$required];
            }
            $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']);
        }

        if ($this->optional) {
            $params['openid.sreg.optional'] = array();
            foreach ($this->optional as $optional) {
                if (!isset(self::$ax_to_sreg[$optional])) continue;
                $params['openid.sreg.optional'][] = self::$ax_to_sreg[$optional];
            }
            $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']);
        }
        return $params;
    }

    protected function axParams()
    {
        $params = array();
        if ($this->required || $this->optional) {
            $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0';
            $params['openid.ax.mode'] = 'fetch_request';
            $this->aliases  = array();
            $counts   = array();
            $required = array();
            $optional = array();
            foreach (array('required','optional') as $type) {
                foreach ($this->$type as $alias => $field) {
                    if (is_int($alias)) $alias = strtr($field, '/', '_');
                    $this->aliases[$alias] = 'http://axschema.org/' . $field;
                    if (empty($counts[$alias])) $counts[$alias] = 0;
                    $counts[$alias] += 1;
                    ${$type}[] = $alias;
                }
            }
            foreach ($this->aliases as $alias => $ns) {
                $params['openid.ax.type.' . $alias] = $ns;
            }
            foreach ($counts as $alias => $count) {
                if ($count == 1) continue;
                $params['openid.ax.count.' . $alias] = $count;
            }

            # Don't send empty ax.requied and ax.if_available.
            # Google and possibly other providers refuse to support ax when one of these is empty.
            if($required) {
                $params['openid.ax.required'] = implode(',', $required);
            }
            if($optional) {
                $params['openid.ax.if_available'] = implode(',', $optional);
            }
        }
        return $params;
    }

    protected function authUrl_v1($immediate)
    {
        $returnUrl = $this->returnUrl;
        # If we have an openid.delegate that is different from our claimed id,
        # we need to somehow preserve the claimed id between requests.
        # The simplest way is to just send it along with the return_to url.
        if($this->identity != $this->claimed_id) {
            $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id;
        }

        $params = array(
            'openid.return_to'  => $returnUrl,
            'openid.mode'       => $immediate ? 'checkid_immediate' : 'checkid_setup',
            'openid.identity'   => $this->identity,
            'openid.trust_root' => $this->trustRoot,
            ) + $this->sregParams();

        return $this->build_url(parse_url($this->server)
                               , array('query' => http_build_query($params, '', '&')));
    }

    protected function authUrl_v2($immediate)
    {
        $params = array(
            'openid.ns'          => 'http://specs.openid.net/auth/2.0',
            'openid.mode'        => $immediate ? 'checkid_immediate' : 'checkid_setup',
            'openid.return_to'   => $this->returnUrl,
            'openid.realm'       => $this->trustRoot,
        );
        if ($this->ax) {
            $params += $this->axParams();
        }
        if ($this->sreg) {
            $params += $this->sregParams();
        }
        if (!$this->ax && !$this->sreg) {
            # If OP doesn't advertise either SREG, nor AX, let's send them both
            # in worst case we don't get anything in return.
            $params += $this->axParams() + $this->sregParams();
        }

        if ($this->identifier_select) {
            $params['openid.identity'] = $params['openid.claimed_id']
                 = 'http://specs.openid.net/auth/2.0/identifier_select';
        } else {
            $params['openid.identity'] = $this->identity;
            $params['openid.claimed_id'] = $this->claimed_id;
        }

        return $this->build_url(parse_url($this->server)
                               , array('query' => http_build_query($params, '', '&')));
    }

    /**
     * Returns authentication url. Usually, you want to redirect your user to it.
     * @return String The authentication url.
     * @param String $select_identifier Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1.
     * @throws ErrorException
     */
    function authUrl($immediate = false)
    {
        if ($this->setup_url && !$immediate) return $this->setup_url;
        if (!$this->server) $this->discover($this->identity);

        if ($this->version == 2) {
            return $this->authUrl_v2($immediate);
        }
        return $this->authUrl_v1($immediate);
    }

    /**
     * Performs OpenID verification with the OP.
     * @return Bool Whether the verification was successful.
     * @throws ErrorException
     */
    function validate()
    {
        # If the request was using immediate mode, a failure may be reported
        # by presenting user_setup_url (for 1.1) or reporting
        # mode 'setup_needed' (for 2.0). Also catching all modes other than
        # id_res, in order to avoid throwing errors.
        if(isset($this->data['openid_user_setup_url'])) {
            $this->setup_url = $this->data['openid_user_setup_url'];
            return false;
        }
        if($this->mode != 'id_res') {
            return false;
        }

        $this->claimed_id = isset($this->data['openid_claimed_id'])?$this->data['openid_claimed_id']:$this->data['openid_identity'];
        $params = array(
            'openid.assoc_handle' => $this->data['openid_assoc_handle'],
            'openid.signed'       => $this->data['openid_signed'],
            'openid.sig'          => $this->data['openid_sig'],
            );

        if (isset($this->data['openid_ns'])) {
            # We're dealing with an OpenID 2.0 server, so let's set an ns
            # Even though we should know location of the endpoint,
            # we still need to verify it by discovery, so $server is not set here
            $params['openid.ns'] = 'http://specs.openid.net/auth/2.0';
        } elseif (isset($this->data['openid_claimed_id'])
            && $this->data['openid_claimed_id'] != $this->data['openid_identity']
        ) {
            # If it's an OpenID 1 provider, and we've got claimed_id,
            # we have to append it to the returnUrl, like authUrl_v1 does.
            $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?')
                             .  'openid.claimed_id=' . $this->claimed_id;
        }

        if ($this->data['openid_return_to'] != $this->returnUrl) {
            # The return_to url must match the url of current request.
            # I'm assuing that noone will set the returnUrl to something that doesn't make sense.
            return false;
        }

        $server = $this->discover($this->claimed_id);

        foreach (explode(',', $this->data['openid_signed']) as $item) {
            # Checking whether magic_quotes_gpc is turned on, because
            # the function may fail if it is. For example, when fetching
            # AX namePerson, it might containg an apostrophe, which will be escaped.
            # In such case, validation would fail, since we'd send different data than OP
            # wants to verify. stripslashes() should solve that problem, but we can't
            # use it when magic_quotes is off.
            $value = $this->data['openid_' . str_replace('.','_',$item)];
            $params['openid.' . $item] = function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc() ? stripslashes($value) : $value;

        }

        $params['openid.mode'] = 'check_authentication';

        $response = $this->request($server, 'POST', $params);

        return preg_match('/is_valid\s*:\s*true/i', $response);
    }

    protected function getAxAttributes()
    {
        $alias = null;
        if (isset($this->data['openid_ns_ax'])
            && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0'
        ) { # It's the most likely case, so we'll check it before
            $alias = 'ax';
        } else {
            # 'ax' prefix is either undefined, or points to another extension,
            # so we search for another prefix
            foreach ($this->data as $key => $val) {
                if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_'
                    && $val == 'http://openid.net/srv/ax/1.0'
                ) {
                    $alias = substr($key, strlen('openid_ns_'));
                    break;
                }
            }
        }
        if (!$alias) {
            # An alias for AX schema has not been found,
            # so there is no AX data in the OP's response
            return array();
        }

        $attributes = array();
        foreach (explode(',', $this->data['openid_signed']) as $key) {
            $keyMatch = $alias . '.value.';
            if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
                continue;
            }
            $key = substr($key, strlen($keyMatch));
            if (!isset($this->data['openid_' . $alias . '_type_' . $key])) {
                # OP is breaking the spec by returning a field without
                # associated ns. This shouldn't happen, but it's better
                # to check, than cause an E_NOTICE.
                continue;
            }
            $value = $this->data['openid_' . $alias . '_value_' . $key];
            $key = substr($this->data['openid_' . $alias . '_type_' . $key],
                          strlen('http://axschema.org/'));

            $attributes[$key] = $value;
        }
        return $attributes;
    }

    protected function getSregAttributes()
    {
        $attributes = array();
        $sreg_to_ax = array_flip(self::$ax_to_sreg);
        foreach (explode(',', $this->data['openid_signed']) as $key) {
            $keyMatch = 'sreg.';
            if (substr($key, 0, strlen($keyMatch)) != $keyMatch) {
                continue;
            }
            $key = substr($key, strlen($keyMatch));
            if (!isset($sreg_to_ax[$key])) {
                # The field name isn't part of the SREG spec, so we ignore it.
                continue;
            }
            $attributes[$sreg_to_ax[$key]] = $this->data['openid_sreg_' . $key];
        }
        return $attributes;
    }

    /**
     * Gets AX/SREG attributes provided by OP. should be used only after successful validaton.
     * Note that it does not guarantee that any of the required/optional parameters will be present,
     * or that there will be no other attributes besides those specified.
     * In other words. OP may provide whatever information it wants to.
     *     * SREG names will be mapped to AX names.
     *     * @return Array Array of attributes with keys being the AX schema names, e.g. 'contact/email'
     * @see http://www.axschema.org/types/
     */
    function getAttributes()
    {
        if (isset($this->data['openid_ns'])
            && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0'
        ) { # OpenID 2.0
            # We search for both AX and SREG attributes, with AX taking precedence.
            return $this->getAxAttributes() + $this->getSregAttributes();
        }
        return $this->getSregAttributes();
    }
}

Added page_admin.php.


















































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * type: page
 * title: admin interface
 * description: Showcase user flags and allow to delete or hide project entries/revisions.
 * version: 0.1
 * depends: db
 *
 * User flags are collected in a separate `flags` table. Yet each project
 * entry contains a `flags` counter column as well (this is used by front-end
 * code to automatically hide too frequently flagged submissions)..
 *
 * CREATE TABLE flags
 *     (name TEXT, reason TEXT, note TEXT, submitter_openid TEXT, submitter_ip TEXT);
 *
 * The admin page lists from the flags table. Then allows to "delete" certain
 * revisions, or just mark them as hidden.
 *
 */



// Moderator authorization already handled by dispatcher (index.php)
include("template/header.php");
?>
  <section id=main>
<?php
$name = $_REQUEST->proj_name["name"];


// Just list flags+projects
if (empty($name)) {
    print "<h3>Flagged entries</h3> <dl>";

    // query flags table, but associate data from last release
    $flags = db("SELECT * FROM flags
                 LEFT JOIN release_versions ON flags.name=release_versions.name");
   
    // just output admin/PROJID links
    while ($row = $flags->fetch()) {
        $row = array_map("input::html", $row);
        print <<<HTML
           <dt><a href="admin/$row[name]">$row[name]</a> (<em>$row[flag]</em> flags on #$row[t_published])</dt>
           <dd><b>$row[reason]</b>
               $row[note]
           </dd>
HTML;
    }
    
}

// Show entry + respond to actions
else {


    /**
     * Apply actions
     *
     *   → Actions in `action[field][]=name and action[value][]=value`
     *   → Revisions are  lists of `select[t_published][] = t_changed`
     *
     */
    if ($_POST->has("action")) {
    
        // Merge action keys and values
        $action = $_POST->raw["action"];
        $action = array_combine(
            array_intersect_key($action["field"], $action["value"]),
            array_intersect_key($action["value"], $action["field"])
        );
        var_dump($action);

        // Run trough actions
        foreach ($action as $field=>$value) if (strlen($field)) {
            // Update DB for each revision in select[][]
            foreach ($_POST->raw["select"] as $t_published => $t_changed) {
                db("UPDATE release
                    SET :? = ?
                    WHERE name=? AND t_published=? AND t_changed IN (??)",
                    array($field), $value,
                    $name, $t_published, $t_changed
                );
            }
        }
        
        // Manually empty `flags` table
        if ($action["flag"] === 0) {
            db("DELETE FROM flags WHERE name=?", $name);
        }
    }



    /**
     * Get all revisions and flags for project name
     *
     *
     */
    $entries = db("SELECT * FROM release WHERE name=? ORDER BY t_published ASC, t_changed ASC", $name)->fetchAll();
    $flags = db("SELECT * FROM flags WHERE name=?", $name)->fetchAll();


    // Start <form>
    print "<h3>Edit '$name' revisions</h3>  Oldest to newest.
           <form action='admin/$name' method=POST>         
          ";

    // Show all flagging notes
    foreach ($flags as $row) {
        $row = array_map("input::html", $row);
        print "<li>Flag: <b>$row[reason]</b><br>Note: <em>$row[note]</em><br>By: <u>$row[submitter_openid]</u></li><br>";
    }

    
    // Print each revision;
    foreach ($entries as $rev=>$row) {
    
        // current, last, and next row
        $row = array_map("input::html", $row);
        $last = isset($entries[$rev-1]) ? array_map("input::html", $entries[$rev-1]) : $row;
        $next = isset($entries[$rev+1]) ? array_map("input::html", $entries[$rev+1]) : $row;

        // Version header
        $date = strftime("%Y-%m-%d %T", $row["t_changed"]);
        print "
        <br>
        <table class='rc admin'>
        <tr>
          <th>
             <input type=checkbox name='select[$row[t_published]][]' value='$row[t_changed]'>
             rev=$rev
          </th>
          <th>
             pub=$row[t_published]
             chg=$row[t_changed] <small>($date)</small>
          </th>
        </tr>";
        
        // Fields
        foreach ($row as $f=>$v) {
            $v = pdiff::tridiff($last[$f], $v, $next[$f]);
            if (in_array($f, ["t_published", "t_changed"])) {
                $v .= " <small>(" . strftime("%Y-%m-%d %T", $row[$f]) . ")</small>";
            }
            if (in_array($f, ["hidden","flag","deleted","name","t_changed","version"])) {
                $f = "<em>$f</em>";
            }
            print "<tr><td>$f</td><td>$v</td>";
        }
        print "</table>";
    }

    
    /**
     * Print `action` form fields
     *
     *  → Assocciatively as `action[field][] = dbfield`
     *                  and `action[value][] = vakue`
     *  → Applying depends an the field being non-empty
     *    (for unchecked checkboxes they are).
     *
     */
    print <<<HTML
    <h4>Actions</h4>
    <table class=rc>
    <tr>
      <td><label>
        <input type=checkbox name="action[field][0]" value="hidden">
        <input type=hidden name="action[value][0]" value="1">
        <b>Set hidden</b>
      </label></td>
      <td>so the revision will no longer show up on the frontpage. </td>
    </tr>
    <tr>
      <td><label>
        <input type=checkbox name="action[field][1]" value="deleted">
        <input type=hidden name="action[value][1]" value="1">
        <b>Mark deleted</b>
      </label></td>
      <td> to terminate a project revision line </td>
    </tr>
    <tr>
      <td><label>
        <input type=checkbox name="action[field][2]" value="flag" checked>
        <input type=hidden name="action[value][2]" value="0">
        <b>Unset flags</b>
      </label></td>
      <td> to clear flagging history when finished. </td>
    </tr>
    <tr><th>Update field</th><th>with value</th></tr>
    <tr>
      <td> <input type=text name="action[field][3]" value=""> </td>
      <td> <input type=text name="action[value][3]" value="" size=40> </td>
    </tr>
    <tr>
      <td> <input type=text name="action[field][4]" value=""> </td>
      <td> <input type=text name="action[value][4]" value="" size=40> </td>
    </tr>
    </table>
    <br>
    <input type=submit value=Apply>
    <br>
    <br>
HTML;
}



?>

Added page_drchangelog.php.






































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: page
 * title: Dr. Changelog
 * description: Tool to experiment and try out Autoupdate modules
 * version: 0.1
 * license: AfferoLGPL
 *
 * Reuses fields from /submit form to start a live check run with
 * actual Autoupdate modules.
 *
 */


$header_add = "<meta name=robots content=noindex>";
include("template/header.php");
?>
<aside id=sidebar>
 <section>
  <h5>Know your audience</h5>
  <small>
  <p> Whatever source you choose for release announcements, try to keep them <b>user-friendly</b>. </p>
  <p> End users aren't fond of commit logs. While "merged pull request XY" might be technically
      highly relevant (for e.g. libraries), it's gibberish to most everyone else.</p>
  <p> So be careful with the <em>GitHub</em> module in particular. If you're not using githubs
      /release tool, a commit log may be used still. Only basic filtering is applied.</p>
  <p> Likewise write <em>Changelogs</em> as <b>summaries</b>. (They're better and more correctly called NEWS
      or RELEASE-NOTES files actually.)</p>
  </small>
 </section>
</aside>
<section id=main> <?php


#-- Output formatted results
class TestProject extends ArrayObject {
    function update($result) {
        #-- output formatted
        print "<dl>\n";
        foreach ($result as $key=>$value) {
            print "<dt><b>$key</b></dt>\n<dd>" . input::html($value) . "</dd>\n";
        }
        print "</dl>";
    }
}


// run test
if ($_REQUEST->has("test")) {

    #-- prepare
    $run = new Autoupdate();
    $run->debug = 1;
    $project = new TestProject(array(
         "name" => "testproject",
         "version" => "0.0.0.0.0.0.1",
         "t_published" => 0,
         "homepage" => "",
         "download" => "",
         "urls" => "",
         "autoupdate_module" => $_REQUEST->id->in_array("autoupdate_module", "none,release.json,changelog,regex,github"),
         "autoupdate_url" => $_REQUEST->url["autoupdate_url"],
         "autoupdate_regex" => $_REQUEST->raw["autoupdate_regex"],
    ));
    
    #-- exec
    $method = $run->map[$project["autoupdate_module"]];
    print "<h3>Results for <em>$method</em> extraction</h3>\n";
    $result = $run->$method($project);
    $result = new TestProject((array)$result);
    $result->update($result);
}


// display form
else {

   $data = $_REQUEST->list->html["name,autoupdate_module,autoupdate_url,autoupdate_regex"];
   $data["autoupdate_regex"] or $data["autoupdate_regex"] = "\n\nversion = /Version ([\d.]+)/\n\nchanges = http://example.org/news.html\nchanges = $('article pre#release')\nchanges = ~ ((add|fix|change) \V+) ~mix*";
   $current_date = strftime("%Y-%m-%d", time());

   $select = "form_select_options";
   print<<<FORM

<style>
/**
 * page-specific layout
 *
 */
.autoupdate-alternatives { border-spacing: 5pt; }
.autoupdate-alternatives td {
    padding: 3pt;
    width: 25%;
    vertical-align: top;
    background: #fcfcfc linear-gradient(to bottom, #f7f0e9, #fff);
    box-shadow: 2px 2px 3px 1px #f9f5f1;
    border-radius: 10pt;
    font-size: 95%;
}
.autoupdate-alternatives td .hidden {
    position: absolute;
    display: none;
}
.autoupdate-alternatives td:hover .hidden {
    display: block;
}
.autoupdate-alternatives td .hidden pre {
    position: relative; top: -30pt; left: -30pt;
    padding: 7pt;
    border: 7px solid #111;
    border-radius: 7pt;
    background: #f7f7f7;
    background-image: radial-gradient(circle at 50% 50%, rgb(255,255,255), rgb(244,244,244));
}
li {
    padding: 1.5pt;
}
</style>
   
   <h3>Dr. Changelog</h3> 
   <form action=drchangelog method=POST>
        <img src=img/drchangelog.png align=right alt="birdy big eyes" title="Don't ask me, I'm just a pictogram.">
        <p>
           Freshcode.club can automatically track your software releases. There are
           <a href="http://fossil.include-once.org/freshcode/wiki/Autoupdate">various
           alternatives for</a> uncovering them. Try them out.

           <label>
               Retrieval method
               <select name=autoupdate_module>
                   {$select("release.json,changelog,regex,github", $data["autoupdate_module"])}
               </select>
           </label>

           <table class=autoupdate-alternatives><tr>
           <td>
             <a href="http://fossil.include-once.org/freshcode/wiki/releases.json"><em>releases.json</em></a>
             defines a concrete scheme for publishing version and release notes.
<span class=hidden><pre>
{
  "version": "1.0.0",
  "changes": "Fixes and adds lots
              of new functions ..",
  "state": "stable",
  "scope": "major feature",
  "download": "http://exmpl.org/"
}
</pre></span>
             </td>
           <td>While a <a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateChangelog"><em>Changelog</em></a>
             text file is likely the easiest, given a coherent format and style.
<span class=hidden><pre>
1.0.0 ($current_date)
------------------
 * Changes foo and bar.
 + Adds baz baz.
 - Some more bugs removed.
 
0.9.9 (2014-02-27)
------------------
 * Now uses Qt5
 - Removed all the bugs

0.9.1 (2014-01-20)
------------------
 * Initial release with
</pre></span>
             </td>
           <td><a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateGithub"><em>Github</em></a>
            extraction prefers <nobr>/releases</nobr> notes. But may as last resort condense a git commit log.
<span class=hidden><pre><a href="https://github.com/blog/1547-release-your-software"><img src="https://camo.githubusercontent.com/9f23f54df9e2f69047fb0f9f80b2e33c8339606f/68747470733a2f2f662e636c6f75642e6769746875622e636f6d2f6173736574732f32312f3733373136362f62643163623637652d653332392d313165322d393064312d3361656365653930373339662e6a7067" width=400 height=200></a></pre></span>
            </td>
           <td>Using <a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateRegex"><em>regex/xpath</em></a>
             is however the most universal way to extract from project websites.
<span class=hidden><pre>
<span style=color:gray># load page</span>
changes = http://exmpl/news

<span style=color:gray># jQuery</span>
changes = $("body .release")
 
<span style=color:gray># RegExp</span>
version = /Version \d+\.\d+/

</pre></span>
             </td>
           </tr></table>

        </p>
        <p>
           <label>
               Autoupdate URL
               <input name=autoupdate_url type=url size=80 value="$data[autoupdate_url]" placeholder="https://github.com/user/repo/tags.atom" maxlength=250>
           </label>
           Add the URL to your Changelog, releases.json, or GitHub project here. For the regex method
           this will also be the first page to be extracted from.
        </p>

        <p>
           <h4>Content Scraping</h4>
           Picking out from your own project website can be surprisingly simple. Define a list for at
           least <code>version = ...</code> and <code>changes = ...</code> - Add source URLs
           and apply
           <a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateRegex">
           RegExp, XPath, or jQuery</a> selectors for extraction.
           <label>
               Extraction Rules <em>(URLs, Regex, Xpath, jQuery)</em>
               <textarea cols=67 rows=10 name=autoupdate_regex placeholder="version = /-(\d+\.\d+\.\d+)\.txz/" maxlength=2500>$data[autoupdate_regex]</textarea>
               <small>
               <li>Assigning new URLs is only necessary when there's different data to extract from.</li>
               <li>RegExps like <code>version = /Changes for ([\d.]+)/</code> often match headlines well.</li>
               <li>A common XPath rule for extracting the first bullet point list is <code>changes = (//ul)[1]/li</code>.</li>
               <li>While <code>changes = $("section#main article .release")</code> narrows it down
                   for HTML pages.</li>
               <li>You often can mix extractors, first an XPath/jQuery expression, then a RegExp.</li>
               <li>Rules for state=, scope= and download= are optional.</li>
               </small>
           </label>
        </p>
        <p>
          <input type=submit name=test value=Test-Run>
        </p>
   </form>
FORM;
}


include("template/bottom.php");

?>

Added page_error.php.


































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: page
 * title: Error info
 * description: Generic error page
 * version: 0.1
 * license: -
 *
 * Frontpage.
 * Just shows the most recent projects and their released versions.
 *
 * Shows:
 *   → Recent projects and their released versions.
 *   → Visually trimmed descriptions and changelogs.
 *   → Small boxed tags.
 * Sidebar:
 *   → Newsfeeds (e.g. linux.com, /r/linux)
 * HTML:
 *   → RSS/Atom links for update feed comprised of all projects.
 *
 */


include("template/header.php");
?> <section id=main> <?php

print "<h2>Error</h2>\n";

print isset($error) ? "<p>$error</p>" : "<p>Some problem occured (entry not accessible etc.)</p>";


include("template/bottom.php");

?>

Changes to page_feed.php.

1
2
3
4
5
6

7
8

9
10














11

12
13
14
15
16
17








18
19
20
21
22
23
24
1
2
3
4
5

6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25


26



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41




-
+

-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-

-
-
-
+
+
+
+
+
+
+
+







<?php
/**
 * api: freshcode
 * title: json feeds
 * description: exchange protocol and per-project feeds
 * version: 1.0
 * version: 1.1
 * license: CC-BY-SA
 * depends: json, db
 * depends: php:json, feeder
 *
 * Generates /xfer stream and per-/project release feeds.
 * Returns JSON (interchange format) and RSS or Atom feeds.
 *
 * The URL schemes:
 *    http://freshcode.club/feed/projectname    (.json optional)
 *    http://freshcode.club/feed/projectname.rss
 *    http://freshcode.club/feed/projectname.atom
 * For the complete site update list:
 *    http://freshcode.club/feed/xfer         (.json/.atom/.rss)
 *
 * No Content-Negotiation here, as nobody is even bothering
 * anymore. The .htaccess dispatching adds the ?ext=rss if an
 * extension (.json / .atom / .rss) was appended.
 *
 *
 * Both only JSON for now.
 * JSON FORMAT
 * (No content-negotiation, as for RSS/ATOM a different
 * content summary probably made more sense.)
 *
 * This is still susceptible to changes. Currently freshcode.club
 * seems the only FM-reimplementation. But obviously the data
 * format should converge to facilitate proper synchronization.
 *   Is still susceptible to changes. Currently freshcode.club
 *   seems the only FM-reimplementation. But obviously the data
 *   format should converge to facilitate proper synchronization.
 *
 *   /feed/xfer also doesn't provide the raw DB contents. OpenID
 *   handles are stripped, and personally identifyable infos 
 *   dropped (e.g. gravatar email).
 *   Otherwise it's similar to the internal database structure.
 *
 */


/**
 * group and rename internal columns into feed structure
 *
43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
66
67
68
69
70
71
72

73
74
75
76
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103



104
105
106



107
108







































109
110
111

112
113
114
115
116









117
118
119






120



121
122

123
124
60
61
62
63
64
65
66

67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82
83
84
85
86
87
88

89
90
91
92
93
94
95
96
97
98
99
100

101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123



124
125
126
127

128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

169
170
171
172
173

174
175
176
177
178
179
180
181
182
183
184

185
186
187
188
189
190
191
192
193
194
195

196
197
198






-
+







-
+













-
+











-
+



















+
+
+
-
-
-
+
+
+

-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+




-
+
+
+
+
+
+
+
+
+


-
+
+
+
+
+
+

+
+
+

-
+


function feed_release($row) {
    return array(
        "version" => $row["version"],
        "state" => $row["state"],
        "scope" => $row["scope"],
        "changes" => $row["changes"],
        "download" => versioned_url($row["download"], $row["version"]),
        "published" => date(DateTime::ISO8601, $row["t_published"]),
        "published" => gmdate(DateTime::ISO8601, $row["t_published"]),
    );
}

#-- exchange data
function feed_xfer($row) {
    return array(
        "hidden" => $row["hidden"],
        "changed" => date(DateTime::ISO8601, $row["t_published"]),
        "changed" => gmdate(DateTime::ISO8601, $row["t_published"]),
        "autoupdate_module" => $row["autoupdate_module"],
        "autoupdate_url" => $row["autoupdate_url"],
        "autoupdate_regex" => $row["autoupdate_regex"],
        // following fields will not be transferred for privacy reasons
       # "submitter_openid" => $row["submitter_openid"],
       # "lock" => $row["submitter_lock"],
    );
}




#-- something was requested
if ($name = $_GET->id["name"]) {
if ($name = $_GET->proj_name["name"]) {

    $feed = array(
        "\$feed-origin" => "http://freshcode.club/",
        "\$feed-license" => "CC-BY-SA 3.0",
    );


    #-- exchange data
    if ($name == "xfer") {
        $feed["releases"] = array();
        
        $r = db("SELECT * FROM release_versions LIMIT 100");
        $r = db("SELECT * FROM release_versions LIMIT ?", $_GET->int->default…100->range…5…1000["num"]);
        while ( $row = $r->fetch() ) {
            $feed["releases"][] = feed_project($row) + feed_release($row) + feed_xfer($row);
        }
    }

    
    #-- per project
    else {
        $r = db("SELECT * FROM release_versions WHERE name=? LIMIT 10", $name);
        while ( $row = $r->fetch() ) {

            // project description
            isset($feed["releases"]) or $feed += feed_project($row) + array("releases" => array());

            // versions
            $feed["releases"][] = feed_release($row);
        }
    }


    #-- Output JSON
    if ($ext = $_GET->name->default…json["ext"] == "json") {
    header("Content-Type: json/vnd.freshcode.club; charset=UTF-8");
    exit(json_encode($feed, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
}
        header("Content-Type: json/vnd.freshcode.club; charset=UTF-8");
        exit(json_encode($feed, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES));
    }


    
    #-- Else convert into RSS or Atom
    else {

        /**
         * It's obviously super long-winded to restructure the JSON xfer
         * or per-project data into RSS/Atom snippets here afterwards.
         *
         * @todo: restructure
         *
         */

        $f = new Feeder();
        $f->channel()->setfromarray(array(
            "title"       => "$name",
            "description" => "Open Source project updates",
            "author"      => "freshcode.club",
            "license"     => $feed["\$feed-license"],
            "icon"        => "http://freshcode.club/img/changes.png",
            "logo"        => "http://freshcode.club/logo.png",
        ));
        
        foreach ($feed["releases"] as $i=>$row) {
            $f->entry($i, new FeedEntry(@array(
                "title"   => ($row["title"] ?: $feed["title"]) . " $row[version]",
                "published" => $row["published"],
                "author"  => $row["submitter"] ?: $feed["submitter"],
                "content" => $row["changes"],
                "permalink" => $row["homepage"] ?: $feed["homepage"],
            )));
        }

        #-- Output
        $o = ($ext == "atom") ? new AtomFeed() : new Rss20Feed();
        $o->output($f);
    }
}


#-- else print an info page
else {
    include("layout_header.php");
    include("template/header.php");
    ?>
    <section id=main>
       <h4>Feeds</h4>
       <p>
          You can get any projects <b>releases.json</b> feed using<br><tt>http://freshcode.org/feed/<i>projectname</i></tt>.
          You can get any projects <b>releases.json</b> feed using
          <ul>
             <li> <tt>http://freshcode.club/feed/<em>projectname</em><var style="color: #ccc">.json</var></tt>
          </ul>
          Alternatively as RSS/Atom feed
          <ul>
             <li> <tt>http://freshcode.club/feed/<em>projectname</em>.rss</tt>
             <li> <tt>http://freshcode.club/feed/<em>projectname</em>.atom</tt>
          </ul>
       </p>
       <p>
          Whereas using <i>xfer</i> will return the whole recent changes list.
          To get all project updates instead use
          <ul>
             <li> <tt>http://freshcode.club/feed/<b>xfer</b><var style="color: #ccc">.json</var></tt>
             <li> <tt>http://freshcode.club/projects.rss</tt>
             <li> <tt>http://freshcode.club/projects.atom</tt>
          </ul>
       </p>
       <p>
          JSON feeds are using a post-1.0 MIME type of <em>json/vnd.freshcode.club</em> for now.
       </p>
    <?php
    include("layout_bottom.php");
    include("template/bottom.php");
}

Changes to page_flag.php.

1
2
3
4
5
6
7
8
9
10
11
12
13

14
15
16

17
18
19
20
21
22
23
1
2
3
4
5
6
7
8
9
10
11
12

13
14
15

16
17
18
19
20
21
22
23











-
+


-
+







<?php
/**
 * type: page
 * title: Flagging
 * description: Allows users to flag project listings for moderator attention.
 *
 * A submission here will both increase the `release`.`flag` counter,
 * as well as leave a documentation entry in the `flags` table.
 *
 */


include("layout_header.php");
include("template/header.php");
?> <section id=main> <?php

$name = $_REQUEST->name["name"];
$name = $_REQUEST->proj_name["name"];

// submit
if ($_REQUEST->has("reason", "note", "name")) {

    
    // exists
    if (db("SELECT note FROM flags WHERE name=? and submitter_openid=?", $name, $_SESSION["openid"])->fetch()) {
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
36
37
38
39
40
41
42

43
44
45
46
47
48
49
50






-
+







            (reason, note, name, submitter_openid, submitter_ip)
            VALUES (?,?,?,?,?)",
            $reason, $note, $name, $_SESSION["openid"], $_SERVER->ip["REMOTE_ADDR"]
        );
       
        // Increase `release`.`flag` column (just the last entry)
        db("UPDATE release SET flag=flag+1
            WHERE name=?  ORDER BY t_changed DESC  LIMIT 1",
            WHERE name=?  ORDER BY t_published DESC, t_changed DESC  LIMIT 1",
            $name
        );

        print "<h2>Thank you</h2> <p>We'll investigate. Thanks for your time and attention!</p>";

    }
}
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

107
108
78
79
80
81
82
83
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

106
107
108






-
+




















-
+


             </label>

             <label>
                <input type=radio name=reason value=urls> URLs are no longer working.
             </label>

             <label>
                <input checked type=radio name=reason value=inappropriate> Other (use the note box below in either case).
                <input checked type=radio name=reason value=other> Other (use the note box below in either case).
             </label>

             <label>
                <textarea name=note cols=50 rows=5 placeholder="Moderators, I summon thee, because ..."></textarea>
             </label>

             <label>
                <input type=submit value=Submit>
             </label>

         </form>
      </p>
      
      <p>This is also a reasonable contact mechanism if you want to report another type
      of bug. For reclaiming a lost OpenID logon please preferrably contact us per mail.</p>
      
HTML;
}


include("layout_bottom.php");
include("template/bottom.php");

?>

Added page_forum.php.
















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * type: main
 * title: meta/forum
 * description: Simple threaded discussion / documentation forum.
 * version: 0.2
 *
 * Distinct layout from main site and harbours its own dispatcher.
 * Editing/post features. CSS is melted in, as there's no subpaging.
 *
 */


#-- custom config
include_once("./shared.phar");  // autoloader
define("INPUT_QUIET", 1) and
include_once("lib/input.php");  // input filter
define("HTTP_HOST", $_SERVER->id["HTTP_HOST"]);
include_once("lib/deferred_openid_session.php");  // auth+session
include_once("aux.php");        // utility functions
include_once("config.local.php");
include_once("lib/db.php");     // database API
db(new PDO("sqlite:forum.db")); // separate storage


#-- set up forum handling
$f = new forum();
$f->is_admin = in_array($_SESSION["openid"], $moderator_ids);


#-- dispatch functions
switch ($name = $_GET->id["name"]) {

    case "submit":
        exit( $f->submit() );

    case "post":
        exit( $f->submit_form($_REQUEST->int["pid"], 0) );

    case "edit":
        exit( $f->edit_form(0, $_REQUEST->int["id"]) );

    case "index":
    case "":
    default:
        // handled below per default
}
   
?>
<!DOCTYPE html>
<html>
<head>
    <title>freshcode.club forum</title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src=gimmicks.js></script>
    <meta charset=UTF-8>
    <?= "<style>\n"
      . file_get_contents("forum.css")
      . "</style>";
    ?>
</head>
<body>
<div id=title>
   <h1><b>fresh</b>(code)<b class=red>.</b><span class=grey>club</span></h1>
</div>
<br>
<ul class=forum>

   <li>
      <div class=entry>
         <a class="action forum-new" data-id=0>New Thread</a>
      </div>
   </li>
   <?php
      $f->index();
    ?>
</ul>
</body>
</html>

Changes to page_index.php.

1
2
3
4
5
6

7
8

9
10









11
12
13

14
15


16
17
18
19

20
21
22
23

24
25
26



27
28
29
30
31


32
33
34
35

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56



57
58

59
60
61

62
63
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24


25
26
27
28
29
30
31
32
33
34
35
36



37
38
39
40


41

42
43
44



45





















46
47
48


49
50
51

52
53
54




-
+


+


+
+
+
+
+
+
+
+
+



+
-
-
+
+




+




+
-
-
-
+
+
+

-
-

-
+
+

-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+


-
+


<?php
/**
 * type: page
 * title: Project release listing
 * description: Front page for listing recently submitted projects and their releases
 * version: 0.2
 * version: 0.3
 * license: AGPL
 *
 * Frontpage.
 * Just shows the most recent projects and their released versions.
 *
 * Shows:
 *   → Recent projects and their released versions.
 *   → Visually trimmed descriptions and changelogs.
 *   → Small boxed tags.
 * Sidebar:
 *   → Newsfeeds (e.g. linux.com, /r/linux)
 * HTML:
 *   → RSS/Atom links for update feed comprised of all projects.
 *
 */


$header_add = "<link rel=alternate type=application/rss+xml href=/feed/xfer.rss>\n<link rel=alternate type=application/atom+xml href=/feed/xfer.atom>";
include("layout_header.php");
include("layout_index_sidebar.php");
include("template/header.php");
include("template/index_sidebar.php");
?> <section id=main> <?php


// query projects
$page_no = $_GET->int->range…1…100["n"];
$releases = db("
    SELECT *
      FROM release_versions
     WHERE flag < 5
       AND NOT hidden
     LIMIT 50
    OFFSET ?
", $_GET->int->max…0['n']);
     LIMIT 40
    OFFSET 40*?
", $page_no - 1);

// show
foreach ($releases->fetchAll() as $entry) {

    // HTML escaping and some autogenerated fields
// Convert entries to HTML output
foreach ($releases as $entry) {
    prepare_output($entry);
    // Callback for varexpression function calls in heredoc
    $_ = "trim";
    
    include("template/index_project.php");
    // Output
    print <<<HTML
      <article class=project>
        <h3>
            <a href="projects/$entry[name]">$entry[title]
            <em class=version>$entry[version]</em></a>
            <span class=links>
                <span class=published_date>$entry[formatted_date]</span>
                <a href="$entry[homepage]"><img src="img/home.png" width=20 height=20 border=0 align=middle></a>
                <a href="$entry[download]"><img src="img/disk.png" width=20 height=20 border=0 align=middle></a>
            </span>
        </h3>
        <a href="$entry[homepage]"><img class=preview src="$entry[image]" align=right width=120 height=90 border=0></a>
        <p class="description trimmed">$entry[description]</p>
        <p class="release-notes trimmed"><b>$entry[scope]:</b> $entry[changes]</p>
        <p class=tags><img src="img/tag.png" width=30 align=middle height=22 border=0><a class=license>$entry[license]</a>{$_(wrap_tags($entry["tags"]))}</p>
      </article>
HTML;

}

}

// Add pseudo pagination for further pages.
include("layout_bottom.php");

pagination($page_no, "n");



include("template/bottom.php");

?>

Changes to page_links.php.

1
2
3
4
5

6
7
8
9
10
11
12
13




14


15
16
17




18

19

20








































































































21

22
23
24
25
26
27
28







29
30
31
32
33





34

35

36
37
38
39
40













41

42







43

44
45
46

47
48





49
50
51
52
53
54


55
56
57
58
59
60


61
62
63
64

65
66
67
68
69
70
71
72



73

74

75
76

77
78
79


80
81
82
83


84
85

86

87
88




89




90
91


92
93
94
95


96
97
98


99
100

101

102

103
104




105

106

107
108

109



110
111
112

113

114
115
116
117
118



119
120
121


122
123

124
125
126
127

128
129
130
1
2
3
4

5
6
7
8





9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148


149
150
151
152
153

154
155
156
157
158
159


160
161
162
163
164
165
166
167
168
169
170
171
172

173
174
175
176
177
178
179
180
181

182
183
184

185


186
187
188
189
190
191
192
193
194


195
196

197




198
199




200
201
202
203
204
205



206
207
208
209
210

211


212
213


214
215

216


217
218

219
220

221


222
223
224
225
226
227
228
229
230


231
232

233


234
235



236
237


238
239
240

241


242
243
244
245
246
247

248


249
250
251
252
253
254
255
256
257

258
259
260



261
262
263
264


265
266
267
268
269
270
271
272

273
274
275
276



-
+



-
-
-
-
-
+
+
+
+

+
+



+
+
+
+

+
-
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+







+
+
+
+
+
+
+



-
-
+
+
+
+
+
-
+

+



-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+

+
+
+
+
+
+
+
-
+


-
+
-
-
+
+
+
+
+




-
-
+
+
-

-
-
-
-
+
+
-
-
-
-
+





-
-
-
+
+
+

+
-
+
-
-
+

-
-
+
+
-

-
-
+
+
-

+
-
+
-
-
+
+
+
+

+
+
+
+
-
-
+
+
-

-
-
+
+
-
-
-
+
+
-
-
+

+
-
+
-
-
+
+
+
+

+
-
+
-
-
+

+
+
+



+
-
+


-
-
-
+
+
+

-
-
+
+


+



-
+



<?php
/**
 * title: Links to other directories
 * description: Collection/overview of other software tracking / link lists.
 * version: 0.1
 * version: 0.4
 *
 *
 * ToDo
 *  + http://opensourcelist.org/
 *  + http://www.linuxsoft.cz/en/
 *  + http://www.osalt.com/
 *  + http://www.datamation.com/open-source/open-source-software-the-mega-list-1.html
 *  + http://distrowatch.com/
 *  - http://www.datamation.com/open-source/open-source-software-the-mega-list-1.html
 *  - http://www.datamation.com/osrc/article.php/3925806/Open-Source-Software-Top-59-Sites.htm
 *  - http://sourceforge.net/new/
 *  + http://fossies.org/linux/misc/
 *
 *  - http://www.krugle.org/projects/
 *  - http://flossmetrics.org/
 *
 */

#-- preferred languages
header("Vary: Accept-Language");
preg_match_all("/\b(\w\w)([-,\s;]\w*|$)/", $_SERVER->text["HTTP_ACCEPT_LANGUAGE"], $langs)
and $langs = $langs[1];


include("layout_header.php");
include("template/header.php");
?>

 <style>
    #sidebar dl, #sidebar dl dd, #sidebar ul { margin: 0; padding: 0; }
    #sidebar dl dt { font-weight: 700; }
 </style>

 <aside id=sidebar class="absolute-float community-web">
    <section><h5>Ecosystem</h5></section>
    <p>
      Open Source development is more than just software and coding. User enthusiasm
      and interaction have an even larger stake in its progress.
    </p>
    <p>
      The interaction playground is technically comprised of:
    </p>
    <section>
      <b>News sites / Blogs</b>
      <li> <a href="http://slashdot.org/">slashdot</a>
      <li> <a href="http://lxer.com/">LXer</a>
      <li> <a href="http://lwn.net/">LWN</a>
      <li> <a href="http://osdir.com/">OSdir</a>
      <li> <a href="http://www.linuxtoday.com/">LinuxToday</a>
      <li> <a href="http://www.phoronix.com/">Phoronix</a>
      <li> <a href="http://www.osnews.com/">OSnews</a>
      <li> <a href="http://www.omgubuntu.co.uk/">OMG!Ubuntu</a>
      <li> <a href="http://www.webupd8.org/">Web Upd8</a>
      <li> <a href="http://ostatic.com/">OStatic</a>
    </section>
    <section>
      <b>Boards / Forums</b>
      <li> <a href="http://www.linuxquestions.org/">LinuxQuestions</a>
      <li> <a href="http://www.linuxforums.org/forum/">LinuxBoards</a>
    </section>
    <section>
      <b>Support / Q&amp;A</b>
      <li> <a onclick="return confirm('If you want to find something specific, don\'t be vague. Instead of asking for \'best\' software, provide a constrained set of feature requirements.');" href="http://softwarerecs.stackexchange.com/">Software Recommendations</a>
      <li> <a onclick="return confirm('AskUbuntu is for technical questions, not a Google substitute for finding software.');" href="http://askubuntu.com/">askubuntu.com</a>
    </section>
    <section>
      <b>Wikis / Howtos</b>
      <li> <a href="https://wiki.archlinux.org/">Arch Linux Wiki</a>
      <li> <a href="https://wiki.ubuntu.com/">Ubuntu Wiki</a>
      <li> <a href="http://www.howtoforge.com/">HowtoForge</a>
    </section>
    <section>
      <b>Chat channels</b>
      <li> <a href="http://freenode.net/">Freenode</a><br> &nbsp;&nbsp; and its <a href="https://webchat.freenode.net/">web chat</a>
    </section>
    <p>
      Programming-language specific developer hubs and package repositories
      often allow uncovering new software as well.
    </p>
    <section>
      <dl>
        <dt>Python</dt>
        <dd>
          <li> <a href="https://pypi.python.org/" title="Python Package Index">PyPI</a>
        </dd>
        <dt>Perl</dt>
        <dd>
          <li> <a href="http://www.cpan.org/" title="Comprehensive Perl Archive Network">CPAN</a>
          <li> <a href="http://perltricks.com/">Perl Tricks</a>
        </dd>
        <dt>PHP</dt>
        <dd>
          <li> <a href="https://packagist.org/explore/" title="Composers Package Repo">Packagegist</a>
          <li> <a href="http://phptrends.com/">PHP Trends</a>
        </dd>
        <dt>Ruby</dt>
        <dd>
          <li> <a href="https://rubygems.org/gems">Gems</a>
        </dd>
        <dt>Vala</dt>
        <dd>
          <li> <a href="https://bbs.archlinux.org/viewtopic.php?id=173563" title="Arch Linux Bulletin Board about Vala projects">Arch BBS</a>
        </dd>
        <dt>Javascript</dt>
        <dd>
          <li> <a href="http://plugins.jquery.com/">jQuery Plugins</a>
        </dd>
      </dl>
    </section>
    <section>
      <b>Windows software</b>
      <li> <a href="https://help.ubuntu.com/community/ListOfOpenSourcePrograms">LOOP</a>
      <li> <a href="http://eos.osbf.eu/start/">EOS directory</a>
      <li> <a href="http://opensourcewindows.org/">OpenSource Windows</a>
      <li> <a href="http://osswin.sourceforge.net/">OSSWin</a>
    </section>

    <?php  if (in_array("de", $langs)): ?>
    <p>Local websites</p>
    <section>
      <b>.de</b>
      <li> <a href="http://www.pro-linux.de/">Pro Linux</a> + <a href="http://www.pro-linux.de/cgi-bin/DBApp/check.cgi">DBApp</a>
      <li> <a href="http://www.linux-magazin.de/">Linux Magazin</a>
      <li> <a href="http://www.heise.de/open/">Heise Open</a> + <a href="http://www.heise.de/download/top-downloads-50000505000/?f=5s">SW-Cat</a>
      <li> <a href="http://www.linux-community.de/">Linux Community</a>
      <li> <a href="http://wiki.ubuntuusers.de/Software">Ubuntu Users: Software</a>
      <li> <a href="http://ubuntunews.de/">Ubuntu News</a>
    </section>
    <?php endif; ?>
    
 </aside>
 <section id=main style="height:2000pt">
 <section id=main style="height: 2600pt; min-width: 700px;">

 <h4>Other FLOSS/Linux software directories</h4>
   <p>
 <?php

  $links = [

#      ["http://freshcode.club/", "freshcode.club.jpeg", "freshcode.club",
#       "and <a href=\"http://freecode.club/\">freecode</a>/<a href=\"http://freshmeat.club/\">freshmeat.club</a>
#        are supposed to become substitutes with differing views on shared data sets."
#      ],


   //1
      ["http://freecode.com/", "freecode.com.jpeg", "Freecode.com",
       "(AKA Freshmeat) was the original software release tracker, and is still available as archive."
      ],
      ["http://freshcode.club/", "freshcode.club.jpeg", "freshcode.club",
       "and <a href=\"http://freecode.club/\">freecode</a>/<a href=\"http://freshmeat.club/\">freshmeat.club</a>
      ["http://sourceforge.net/", "sourceforge.net.jpeg", "Sourceforge.net",
       "Is the classic open source development service
        and still home to and primary hub for many projects."
      ],
      ["http://fossies.org/linux/misc/", "fossies.org.jpeg", "Fossies.org",
        are supposed to become substitutes with differing views on shared data sets."
       "tracks popular open source packages; and is quite feature-rich underneath its classic interface.",
      ],
   //2
      ["http://directory.fsf.org/", "directory.fsf.org.jpeg", "Free Software directory",
       "is the FSFs Wiki to summarize FLOSS packages and projects."
      ],
      ["http://sourceforge.net/", "sourceforge.net.jpeg", "Sourceforge.net",
       "The original open source development plattform
      ["http://github.com/", "github.com.jpeg", "GitHub",
       "is a prettier frontend onto git source control.
        Less suited for end users, but still allows searching for software."
      ],
      ["http://www.icewalkers.com/", "icewalkers.com.jpeg", "Ice Walkers",
       "is also a software release tracker and news blog, with its own software directory.",
      ],
   //3
      ["http://www.opensourcesoftwaredirectory.com/", "opensourcesoftwaredirectory.com.jpeg", "Open Source Software Directory",
       "Lists only stable and well-known Linux software, as it's intended for end users."
      ],
      ["https://launchpad.net/", "launchpad.net.jpeg", "Launchpad",
       "is the development hub for Ubuntu and also lists a few things that
        is still home to and primary notification hub for many projects."
        haven't made it into the package managers yet."
      ],
      ["http://www.linuxgames.com/", "linuxgames.com.jpeg", "Linux Games",
       "Captures progress and newly released gaming software for Linux.",
      ],
   //4
      ["http://www.osalt.com/", "osalt.com.jpeg", "OS as Alternative",
       "Lists commercial/prioprietary software and the Free or Linux alternatives in usage categories."
      ],
      ["http://www.ohloh.net/", "ohloh.net.jpeg", "Ohloh.net",
      ["http://www.ohloh.net/", "ohloh.net.jpeg", "OpenHUB (Ohloh)",
       "statistically tracks open source project development."
      ],
      ["http://github.com/", "github.com.jpeg", "GitHub",
      ["http://www.zwodnik.com/", "zwodnik.com.jpeg", "Zwodnik",
       "Is a frontend onto a distributed version control system.
        It's less suited for end users, but still allows searching for software."
       "provides a pretty overview, categorization, description and reviews for open source packages.",
      ],
   //5
      ["http://www.linuxalt.com/", "linuxalt.com.jpeg", "Linux Alternatives",
       "Curates a list of Linux software alternatives for migrating newcomers."
      ],
      ["http://savannah.nongnu.org/", "savannah.nongnu.org.jpeg", "Savannah",
       "Provides an alternative Free software development plattform."
      ],
      ["https://launchpad.net/", "launchpad.net.jpeg", "Launchpad",
       "Is the development hub for Ubuntu and also lists a few things that
      ["http://www.reddit.com/r/coolgithubprojects", "coolgithubprojects.jpeg", "CoolGitHubProjects",
       "is a subreddit discussing interesting finds from (otherwise opaque) GitHub repos.",
        haven't made it into the package managers yet."
      ],
      ["http://www.opensourcesoftwaredirectory.com/", "opensourcesoftwaredirectory.com.jpeg", "Open Source Software Directory",
       "Lists only stable and well-known Linux software, as it's intended for end users."
      ],
      ["http://www.linuxalt.com/", "linuxalt.com.jpeg", "Linux Alternatives",
   //6
      ["http://www.linuxsoft.cz/en/", "linuxsoft.cz.jpeg", "LinuxSoft.cz",
       "Curates a list of Linux software alternatives for migrating newcomers."
      ],
      ["http://www.libe.net/version/index.php", "libe.net.jpeg", "Libe.net",
       "Is an archive and version tracker for various Linux and open source packages."
       "Provides a comprehensive and searchable software list divided into categories.",
      ],
      ["http://en.wikipedia.org/wiki/List_of_free_and_open-source_software_packages", "wikipedia.org.jpeg",
       "Wikipedia: List of free and open source software packages",
       "summarizes a few common names."
      ],
      ["http://osliving.com/", "osliving.com.jpeg", "Open Source Living",
       "is an odd outlier, as they expect open source projects to pay for listings;
        thrives on click thru ads, etc."
      ["http://distrowatch.com/", "distrowatch.com.jpeg",
       "DistroWatch",
       "Does as it says and tracks new and upcoming BSD / Linux / GNU / Solaris distribution releases."
      ],
   //7
      ["http://www.hotscripts.com/", "hotscripts.com.jpeg", "hotscripts.com",
      ["http://linuxappfinder.com/all", "linuxappfinder.com.jpeg", "Linux AppFinder",
       "is a script directory, mixed open source or non-free and commercial listings,
        many entries somewhat outdated"
       "Provides vast categories and application options, alternative lists, web feeds, news, and a community forum.",
      ],
      ["http://www.devscripts.com/", "devscripts.com.jpeg", "devscripts.com",
       "is a script directory, mixed open source or non-free and commercial listings,
      ["http://www.opensourcescripts.com/", "opensourcescripts.com.jpeg", "Open Source Scripts",
       "Collects web applications and web service scripts.",
        many entries somewhat outdated"
      ],
      ["http://www.developertutorials.com/scripts/", "developertutorials.com.jpeg", "developer&shy;tutorials.com",
       "is a script directory, mixed open source or non-free and commercial listings,
      ["http://www.libe.net/version/index.php", "libe.net.jpeg", "Libe.net",
       "is an archive and version tracker for various Linux and open source packages."
        many entries outdated"
      ],
   //8
      ["http://www.bigresource.com/scripts/", "bigresource.com.jpeg", "bigresource.com",
      ["http://opensourcelist.org/", "opensourcelist.org.jpeg", "OpenSourceList.org",
       "is a script directory, mixed open source or non-free and commercial listings,
        many entries somewhat outdated"
       "Collection of best-per-category software; also includes MacOS and Windowsware."
      ],
      ["http://opensourcelinux.org/", "opensourcelinux.org.jpeg", "Open Source List",
       "Provides a summary list of common applications.",
      ],
      ["http://opensourcearcade.com/", "opensourcearcade.com.jpeg", "Open Source Arcade",
       "is an assemblage of games categorized per programming language or genre.",
      ],
   //9
      ["http://www.scripts.com/", "scripts.com.jpeg", "scripts.com",
       "is a script directory, mixed open source or non-free and commercial listings,
      ["http://libreprojects.net/", "libreprojects.net.jpeg", "Libre Projects",
       "is itself a meta directory to various open source and open content directories and community hubs.",
        many entries somewhat outdated"
      ],
 #     ["http://www.fatscripts.com/", "fatscripts.com.jpeg", "fatscripts.com",
 #      "is a script directory, mixed open source or non-free and commercial listings,
      ["http://openfontlibrary.org/", "openfontlibrary.org.jpeg", "Open Font Library",
       "Helps to easily uncover new and nicely categorized true or open type fonts.",
 #       many entries somewhat outdated"
 #     ],
      ["http://www.scripts20.com/", "scripts20.com.jpeg", "scripts20.com",
      ],
      ["http://www.findbestopensource.com/home/", "findbestopensource.com.jpeg", "Find Best OpenSource",
       "is a script directory, mixed open source or non-free and commercial listings,
        many entries somewhat outdated"
       "is a well curated news and application category blog.",
      ],
   //10
      ["http://www.needscripts.com/", "needscripts.com.jpeg", "needscripts.com",
      ["http://freeopensourcesoftware.org/", "freeopensourcesoftware.org.jpeg", "Free Open Source Software",
       "is a script directory, mixed open source or non-free and commercial listings,
        many entries somewhat outdated"
       "A Wiki which doesn't host a software directory itself, but provides various resources to uncover them.",
      ],
      ["https://openhatch.org/", "openhatch.org.jpeg", "OpenHatch",
       "enables matchmaking for projects and their developers and interested users and contributions.",
      ],
   //11
      ["http://www.advancescripts.com/", "advancescripts.com.jpeg", "advancescripts.com",
      ["http://thechangelog.com/", "thechangelog.com.jpeg", "the changelog",
       "is a script directory, mixed open source or non-free and commercial listings,
        many entries somewhat outdated"
       "Is a blog and weekly podcast on open source development and interesting projects."
      ],
#      ["", "", "",
#       "",
#      ],
  ];
  
  
  // Write out our gallery  
  foreach ($links as $row) {
  foreach ($links as $entry) {
      print <<<HTML
      <div class=links-entry>
         <a href="$row[0]">
            <img src="/img/links/$row[1]" width=200 height=150 align=bottom border=0>
            <b>$row[2]</b>
         <a href="$entry[0]">
            <img src="/img/links/$entry[1]" width=200 height=150 align=bottom border=0>
            <b>$entry[2]</b>
         </a>
         $row[3]
      </div>
         $entry[3]
      </div>\n
HTML;
  }
  



include("layout_bottom.php");
include("template/bottom.php");


?>

Changes to page_login.php.

12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
28
29

30
31
32
33
34
35
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
38
39
40
41
42

43
44
45
46
47
48
49
50






-
+









-
+













-
+







 *
 */


// initiate verification
if ($_POST->has("login_url")) {

    include_once("openid.php");
    include_once("lib/openid.php");

    $openid = new LightOpenID(HTTP_HOST);
    $openid->identity = $_POST->uri["login_url"];
    $openid->optional = array("namePerson/friendly");
    exit(header("Location: " . $openid->authUrl()));
}


// else
include("layout_header.php");
include("template/header.php");
?> <section id=main> <?php


// display login form
if (empty($_SESSION["openid"])) {

    print<<<HTML
    <h3>Login</h3>

    <p>Please provide an <a href="http://en.wikipedia.org/wiki/OpenID">OpenID</a> handle.</p>

    <p>
    <form action="" method=POST class="login box">
      <input type=text id=login_url name=login_url size=50 value="" placeholder="http://name.openid.xy/">
      <input type=url id=login_url name=login_url size=50 value="" placeholder="http://name.openid.xy/">
      <br>
      <input type=password style=display:none value=dummy>
      <input type=submit value=Login>
      <span class="service-logins">
         Or use your <a onclick="$('#login_url').val('http://facebook-openid.appspot.com/YourFaceBookLogin').focus().prop({selectionStart:35, selectionEnd:52});">Facebook</a>
               | <a onclick="$('#login_url').val('http://me.yahoo.com/#yourname').focus().prop({selectionStart:21, selectionEnd:29});">Yahoo</a> <br>
               | <a onclick="$('#login_url').val('http://launchpad.net/~yourname').focus().prop({selectionStart:22, selectionEnd:30});">Launchpad</a>
69
70
71
72
73
74
75
76

77
78
79
80
81

82
69
70
71
72
73
74
75

76
77
78
79
80

81
82






-
+




-
+

// a previous login was already successful
else {

    print "<h3>Already logged in</h3>";
    
    print isset($login_hint)
        ? "<p>$login_hint</p>"
        : "<p>You have already associated an OpenID name.
        : "<p>You have already associated an OpenID name (<var>$_SESSION[openid]</var>).
           <form action='/login/logout' method=POST><button>Logout</button></form></p>";
    
}

include("layout_bottom.php");
include("template/bottom.php");
?>

Added page_names.php.


















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: page
 * title: Browse Projects by Name
 * description: Alphabetical project lists
 * version: 0.1
 *
 * Needs some styling, too early for splitting up letter ranges.
 *
 */


include("template/header.php");
?><section id=main><article class=project-name-columns><?php


// Letter slicing (AZ or 09)
$letters = $_GET->name->length…2->strtolower->default("name", "ae");
$letters = range($letters[0], $letters[1]);

// Fetch project names from letter group
$names = db("
   SELECT DISTINCT name
     FROM release
    WHERE substr(name, 1, 1) IN (??)
 ORDER BY name
", $letters);

// Show
foreach ($names as $id) {
    print "<a href=/projects/$id[name]><img src='img/screenshot/$id[name].jpeg' width=100 height=75 align=top> $id[name]</a> <br> ";
}


?>
</article>
<aside class=pagination-links>
  <a href="names/AE">A-E</a>
  <a href="names/FH">F-H</a>
  <a href="names/IN">I-N</a>
  <a href="names/OT">O-T</a>
  <a href="names/UZ">U-Z</a>
  <a href="names/09">0-9</a>
</aside>
<?php

include("template/bottom.php");


?>

Changes to page_projects.php.

1
2
3
4
5
6
7

8
9

10
11
12


13
14

15
16
17

18
19
20
21

22
23
24

25
26
27

28
29
30
31


32
33

34
35
36
37
38
39
40
41



42
43




44
45



46
47
48
49
50
51
52
53
54
55
56
57
58


59
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76


77
78





79
80
81
82
83
84
85
86
87
88
89

90
91
92


93

94
95
96
97

98
99

100
101
102
103
104


105
106
107



108
109
110
111



112
113
114

115
116

117
118
119
120

121
122
123
124
1
2
3
4
5
6

7
8

9



10
11


12



13




14



15



16




17
18


19







20
21
22
23


24
25
26
27


28
29
30
31






32





33
34


35
















36
37


38
39
40
41
42
43








44

45


46
47
48

49


50

51


52





53
54



55
56
57
58



59
60
61
62
63

64


65




66
67
68
69
70





-
+

-
+
-
-
-
+
+
-
-
+
-
-
-
+
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
-
+
+
-
-
+
-
-
-
-
-
-
-

+
+
+
-
-
+
+
+
+
-
-
+
+
+

-
-
-
-
-
-

-
-
-
-
-
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
+

-
-
-
-
-
-
-
-

-
+
-
-

+
+
-
+
-
-

-
+
-
-
+
-
-
-
-
-
+
+
-
-
-
+
+
+

-
-
-
+
+
+


-
+
-
-
+
-
-
-
-
+




<?php
/**
 * type: page
 * title: Project detail view
 * description: List project entry with all URLs and releases
 * license: AGPL
 * version 0.2
 * version 0.5
 * 
 *
 * Shows:
 */


 *   → General project description
 *   → Sidebar with project links, submitter, management links, social share count
include("layout_header.php");

 *   → Release history and changelogs
// query projects
$releases = db("
    SELECT *
 * Adds:
      FROM release_versions
     WHERE name = ?
", $_REQUEST->name["name"]);

 *   → RSS/Atom links to header template
// show
if ($entry = $releases->fetch()) {

 *
    // HTML preparation and some auto-generated fields
    prepare_output($entry);
    
 */
    // callback for varexpression function calls in heredoc
    $_ = "trim";
    
    // output

// Current project id
    print <<<HTML

$name = $_REQUEST->proj_name["name"];
      <aside id=sidebar>
         <section>
           <h5>Links</h5>
           <a href="$entry[homepage]"><img src="img/home.png" width=11 height=11> Project Website</a><br>
           <a href="$entry[download]"><img src="img/disk.png" width=11 height=11> Download</a><br>
           {$_(proj_links($entry["urls"], $entry))} 
         </section>

#-- Fetch project/release entries
$releases = db("
        SELECT *, MAX(t_changed)
         <section>
           <h5>Submitted by</h5>
          FROM release
         WHERE name = ?
           AND flag < 5
           AND NOT deleted
           <a href="/?user=$entry[submitter]">$entry[submitter]</a><br>
         </section>
      GROUP BY version
      ORDER BY t_published DESC, t_changed DESC
", $name);

         <section style="font-size:90%">
           <h5>Manage</h5>
         You can also help out here by:<br>
         <a class=long-links href="/submit/$entry[name]" style="display:inline-block; margin: 3pt 1pt;">&larr; Updating infos</a><br>
         or <a href="/flag/$entry[name]">flagging</a> this entry for moderator attention.
         </section>

         <section style="font-size:90%">
           <h5>Share</h5>
           {$_(social_share_links($entry["name"], $entry["homepage"]))}
         </section>
      </aside>
// Retrieve most current project version
if ($entry = $releases->fetch()) {
      <section id=main>
      

      <article class=project>
        <h3>
            <a href="projects/$entry[name]">$entry[title]
            <em class=version>$entry[version]</em></a>
        </h3>
        <a href="$entry[homepage]"><img class=preview src="$entry[image]" align=right width=120 height=90 border=0></a>
        <p class=description style="border:0">$entry[description]</p>
        <p class=long-tags><span>Tags</span> {$_(wrap_tags($entry["tags"]))}</p>
        <p class=long-tags><span>License</span> <a class=license>$entry[license]</a></p>
        <p class=long-links>
            <a href="$entry[homepage]"><img src="img/home.png" width=20 height=20 border=0 align=middle> Homepage</a>
            <a href="$entry[download]"><img src="img/disk.png" width=20 height=20 border=0 align=middle> Download</a>
        </p>
      </article>
      
HTML;

    // prepare HTML header with injected RSS/Atom links
}

    $header_add = "<link rel=alternate type=application/rss+xml href=/feed/$name.rss>\n"
                . "<link rel=alternate type=application/atom+xml href=/feed/$name.atom>\n"
                . "<link rel=alternate type=json/vnd.freshcode.club href=/feed/$name.json>";
    $title = input::html($entry["title"]) . " - freshcode.club";
    include("template/header.php");

// query projects
$releases = db("
    SELECT *
      FROM release
     WHERE name = ? AND flag < 5 AND NOT deleted
  GROUP BY version
  ORDER BY t_published DESC, t_changed DESC
", $_REQUEST->name["name"]);

// show
    // Show sidebar + long project description
print " <article class=release-list>  <h3>Recent Releases</h3> ";
while ($entry = $releases->fetch()) {
    prepare_output($entry);
    include("template/projects_sidebar.php");
    include("template/projects_description.php");
    

    // output
    print <<<HTML

       <div class=release-entry>
    #-- Display all other released versions
          <span class=version>$entry[version]</span><span class=published_date>{$_(strftime("%d %b %Y %H:%M", $entry["t_published"]))}</span>
          <span class=release-notes>
    ?> <article class=release-list>  <h4>Recent Releases</h4> <?php
             <b>$entry[scope]:</b>
             $entry[changes]
          </span>
       </div>

    do {
        include("template/projects_release_entry.php");
HTML;
}
print "</article>";
    }
    while ($entry = $releases->fetch() and prepare_output($entry) + 1);
    ?> </article> <?php


include("layout_bottom.php");

    // html tail
    include("template/bottom.php");
}


function proj_links($urls, $entry, $r="") {
// No entry found
    foreach (p_key_value($urls) as $title=>$url) {
        $title = ucwords($title);
else {
        $url = versioned_url($url, $entry["version"]);
        $r .= "&rarr; <a href=\"$url\">$title</a><br>\n";
    }
    return $r;
    exit($error = "Project name doesn't exist." and include("page_error.php"));
}


?>

Added page_rc.php.










































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: page
 * title: Recent Changes
 * description: Provides a revision diff
 * version: 0.1
 *
 * Show differences between incremental revisions.
 * (To detect sneak spam while we're not requiring OpenID logons.)
 *
 */


// page header
include("template/header.php");
?><section id=main><?php


/**
 * Fields to inspect/diff.
 * Using different sets depending on publicness.
 *
 */
if (TRUE) {  // Public
    $fields = "name,t_changed,title,version,t_published,license,tags,state,scope,homepage,download,urls,description,changes,submitter";
}
if ($_SESSION["openid"]) {  // For logged in users
    $fields .= ",autoupdate_module,autoupdate_url,autoupdate_regex";
}
if (in_array($_SESSION["openid"], $moderator_ids)) {   // Reveal control/privacy-related fields only to moderators
    $fields .= ",submitter_image,submitter_openid,lock,hidden,deleted";
}


/**
 * Prepare SQL field aliases
 * (because sqlite-pdo driver doesn't support it)
 *
 *   description →  crnt.description AS crnt_description
 *   description →  prev.description AS prev_description
 *
 */
$crnt_fields_alias = preg_replace("/\w+/", "crnt.$0 AS crnt_$0", $fields);
$prev_fields_alias = preg_replace("/\w+/", "prev.$0 AS prev_$0", $fields);
$prev_fields_empty = preg_replace("/\w+/", "   NULL AS prev_$0", $fields);

// Also turn CSV list into array
$fields = array_diff(str_getcsv($fields), array("t_changed"));



/**
 * Retrieve two consecutive revisions each.
 *
 *
 */
$rc = db("

    SELECT $crnt_fields_alias, $prev_fields_alias,
           MAX(prev.t_changed)
      FROM release crnt
 LEFT JOIN release prev
        ON crnt.name = prev.name
     WHERE prev.t_changed < crnt.t_changed
--           ( SELECT MAX(t_changed)
--               FROM release
--              WHERE name = crnt.name
--                AND t_changed < crnt.t_changed )
  GROUP BY crnt.name, crnt.t_changed
  ORDER BY crnt.t_changed DESC

");



/**
 * Iterate over all results to display differences
 *
 */
foreach ($rc as $entry) {

    #-- Prepare fields
    $name = $entry["crnt_name"];
    $date = strftime("%Y-%m-%d %H:%M:%S", $entry["crnt_t_changed"]);
    $time_diff =  $entry["crnt_t_changed"] - $entry["prev_t_changed"];

    #-- Table
    print "\n\n<table class=rc><tr><th><a href=/projects/$name>$name</a></th><th>$date <small>¤$time_diff</small> <span class=funcs><a href=/submit/$name>edit</a> <a href=/admin/$name>admin</a></span></th></tr>\n";
    foreach ($fields as $fn ) {

        // Diff only if there are differences, obviously
        if ($entry["prev_$fn"] !== $entry["crnt_$fn"]) {
        
            $diff = pdiff::htmlDiff($entry["prev_$fn"], $entry["crnt_$fn"]);
            print "<tr><td>$fn</td><td class=trimmed>$diff</td></tr>\n";
        }
    }
    print "</table>\n";
}


// page footer
include("template/bottom.php");


?>

Added page_search.php.













































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: page
 * title: Search function
 * description: Scans packages for description, tags, license, user names
 * license: AGPL
 * version 0.2
 * 
 * Builds a search query from multiple input params:
 *   → ?user=
 *   → ?tags[]= or ?tag=
 *   → ?trove[]= for ANDed tags
 *   → ?license=
 *   → ?q= for actual text search
 *
 */



include("template/header.php");
?> <section id=main> <?php


// Display form
if ($_GET->no("tag,tags,trove,user,license,q")) {

    include("template/search_form.php");

}

// Actual search request
else {

    // Wrap search params into arrays
    $tags = array_filter(array_merge($_GET->array->words["tags"], $_GET->words->p_csv["tag"]));
    $trove = $_GET->array->words["trove"] and $trove = [$trove, count($trove)];
    $user = $_GET->words["user"] and $user = ["$user%"];
    $license = $_GET->array->words["license"] and $license = array_filter($license);
    $search = $_GET->text["q"] and $search = ["%$search%"];

    // Run SQL
#   db()->test = 1;
    $result = db("
        SELECT release.name AS name, title, SUBSTR(description,1,500) AS description,
               version, image, homepage, download, submitter, submitter_image,
               release.tags AS tags,
               license, state, t_published, flag, hidden, deleted, MAX(t_changed)
          FROM release
         WHERE NOT deleted AND flag < 5
      GROUP BY release.name
        HAVING 1=1
               :*  :*  :*  :*  :*
      ORDER BY t_published DESC, t_changed DESC
         LIMIT 100 ",
            // expr :* placeholders only interpolate when inner array contains params
            [" AND description LIKE ? ",  $search],
            [" AND submitter LIKE ? ", $user],
            [" AND license IN (??) ",   $license],
            [" AND name IN (SELECT name FROM tags WHERE tag IN (??)) ", $tags],
            [" AND name IN (SELECT name FROM tags WHERE tag IN (??)
               GROUP BY name HAVING COUNT(tag) = 1*?) ", $trove]
    );



    // Show sidebar + long project description
    foreach ($result as $entry) {
        prepare_output($entry);
        include("template/search_entry.php");
    }

}


include("template/bottom.php");

?>

Changes to page_submit.php.

1
2
3
4
5
6
7

8
9
10
11



12
13

14

15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

50



51
52
53
54
55
56

57
58
59
60










61
62
63

64
65
66
67

68
69
70
71
72
73
74


75
76
77
78



79
80
81
82
83

84
85
86
87

88
89




90
91
92
93
94
95
96
97
98

99
100
101
102

103
104
105
106

107
108



109
110
111
112

113
114

115
116
117

118
119

120
121
122
123
124
125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142

143
144
145


146
147

148
149
150
151
152
153
154
155
156
157
158
159



160
161
162

163
164
165
166
167
168
169
170
171
172

173
174

175
176
177

178
179
180

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353

354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
1
2
3
4
5
6

7
8



9
10
11


12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28






29















30
31
32
33
34
35





36




37
38
39
40
41
42
43
44
45
46



47




48







49
50




51
52
53





54




55


56
57
58
59
60
61
62
63
64
65
66
67

68
69

70

71
72
73
74

75
76
77
78
79
80
81
82


83


84



85


86











87












88



89
90


91
92
93





94




95
96
97
98
99

100
101
102
103
104
105
106




107
108

109



110



111






















































































112





113
















































































114
115
116










117
118
119





-
+

-
-
-
+
+
+
-
-
+

+














-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+

+
+
+

-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
-
-
-
-
-
+
-
-
-
-
+
-
-
+
+
+
+








-
+

-

-
+



-
+


+
+
+


-
-
+
-
-
+
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
+
-
-
+


-
-
-
-
-

-
-
-
-
+
+
+


-
+






-
-
-
-
+

-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+


-
-
-
-
-
-
-
-
-
-



<?php
/**
 * api: freshcode
 * type: page
 * title: Submit/edit project or release
 * description: Single-page edit form for projects and their releases
 * version: 0.5
 * version: 0.7.0
 * category: form
 * 
 *
 * Prepares the submission form,
 * license: AGPLv3
 * 
 * Prepares the submission form. On POST checks a few constraints,
 * handles database preparation
 * and merges in previous release entries.
 * but UPDATE itself is handled by release::update() and ::store().
 *
 * Tags: http://aehlke.github.io/tag-it/
 *
 */



// Form field names
$form_fields = array(
    "name", "title", "homepage", "description", "license", "tags", "image",
    "version", "state", "scope", "changes", "download", "urls",
    "autoupdate_module", "autoupdate_url", "autoupdate_regex",
    "submitter", "lock",
);


// Start page output
include("layout_header.php");
?> 
<aside id=sidebar>
    <section>
        <h5>Submit project<br>and/or release</h5>
// Get project ID from request
        <p>
           You can submit <em title="Free, Libre, and Open Source Software">FLOSS</em>
           or <em title="or Solaris/Darwin/Hurd">BSD/Linux</em> software here.
           It's not required that you're a developer of said project.
        </p>
        <p>
           You can always edit the common project information together with
           a current release.  It will show up on the frontpage whenever you
           update a new version number and a changelog summary.
        </p>
    </section>
</aside>
<section id=main>
<?php

$name = $_REQUEST->proj_name->length…3…33["name"];

// Retrieve existing project data in DB.
$data = release::latest($name);
$is_new = empty($data);

/**
 * Get project ID from request
 * - only lowercase, must be 3 to 33 letters
 * - will remain empty for invalid values
 *

 */
$name = $_REQUEST->nocontrol->trim->name->strtolower->length…3…33["name"];


// Else create empty form value defaults in $data
if ($is_new) {
    $data = array_fill_keys($form_fields, "");
    $data["name"] = $name;
    $data["submitter"] = $_SESSION["name"];
    // Optional: import initial $data from elsewhere
    if ($_POST->has("import_via")) {
        $data = array_merge($data, project_import::fetch());
    }
}
/**
 * Check for existing project data in DB.
 *

 */
if ($data = db("SELECT * FROM release WHERE name = ? ORDER BY t_changed DESC", $name)->fetch()) {
    $is_new = 0;
}

// Else create new empty $data set
else {
    $data = array_combine($form_fields, array_fill(0, count($form_fields), ""));
    $is_new = 1;
    // these fields are pre-defined so they appear with the initial form
    $data["name"] = $name;
    $data["submitter"] = $_SESSION["name"];
// Project entry can be locked for editing by specific OpenIDs.
if (!release::permission($data, $_SESSION["openid"])) {
    $data["t_published"] = time();
}


    $error = "This entry cannot be edited with your current <a href='/login'>login</a>. Its original author registered a different one. If your OpenID provider login fails to work, please flag for for moderator attention.";
    exit(include("page_error.php"));
}
/**
 * Project entry can be locked for editing by specific OpenIDs.
 * - `lock` can be a comma-separated list
 * - might also contain password_hash() literals for API auth
 *

 */
if (!$is_new and $data["lock"]
and !in_array($_SESSION["openid"], array_merge(p_csv($data["lock"]), $moderator_ids)))
{

    print "<h3>Locked</h3> <p>This entry cannot be edited with your current <a href='/login'>login</a>. Its original author registered a different OpenID.</p>";
}

// Start page output
include("template/header.php");
include("template/submit_sidebar.php");


/**
 * Fetch form input on submit.
 * Check some constraints.
 * Then insert into database.
 *
 */
elseif ($name and $_REQUEST->has("title", "description")) {
if ($name and $_REQUEST->has("title", "description")) {


    // Check field lengths
    if (!$_REQUEST->multi->serialize->length…120…120->strlen["title,description,homepage,changes"]) {
    if (!$_REQUEST->multi->serialize->length…150…150->strlen["title,description,homepage,changes"]) {
        print("<h3>Submission too short</h3> <p>You didn't fill out crucial information. Please note that our user base expects an enticing set of data points to find your project.</p>");
    }
    // Terms and conditions
    elseif (array_sum($_REQUEST->array->int["req"]) < 3) {
    elseif (array_sum($_REQUEST->array->int->range…0…1["req"]) < 2) {
        print "<h3>Terms and Conditions</h3> <p>Please go back and assert that your open source project listing is reusable under the CC-BY-SA license.</p>";
    }
    elseif (!csrf(TRUE)) {
        print "<h3>CSRF token invalid</h3> <p>This is likely a session timeout (1 hour), etc. Please retry or login again.</p>";
    }
    // Passed
    else {
        $_REQUEST->nocontrol->trim->always();

    
        /**
         * Merge input
        // Merge new data
         *
         */
        $data = array_merge(
        $release = new release($data);
             // any previous/extraneous control data is kept
             $data,
        $release->update(
             // format constraints on input fields
             array(
                 "name"     => $name,
                 "homepage" => $_REQUEST->ascii->trim->http  ->length…250["homepage"],
                 "download" => $_REQUEST->ascii->trim->url   ->length…250["download"],
                 "image"    => $_REQUEST->ascii->trim->http  ->length…250["image"],
           "autoupdate_url" => $_REQUEST->ascii->trim->http  ->length…250["autoupdate_url"],
                 "title"    => $_REQUEST->text               ->length…100["title"],
              "description" => $_REQUEST                    ->length…2000["description"],
                 "license"  => $_REQUEST->words               ->length…30["license"],
                 "tags"     => $_REQUEST->words              ->length…150["tags"],
            $_REQUEST,
                 "version"  => $_REQUEST->words               ->length…30["version"],
                 "state"    => $_REQUEST->words               ->length…30["state"],
                 "scope"    => $_REQUEST->words               ->length…30["scope"],
                 "changes"  => $_REQUEST->text              ->length…2000["changes"],
                "submitter" => $_REQUEST->words               ->length…30["submitter"],
                 "urls"     => $_REQUEST                    ->length…2000["urls"],
                 "lock"     => $_REQUEST->raw               ->length…2000["lock"],
        "autoupdate_module" => $_REQUEST->id                  ->length…30["autoupdate_module"],
         "autoupdate_regex" => $_REQUEST->raw               ->length…2000["autoupdate_regex"],
             ),
             // some automatic system flags
             array(
            array(
                 "t_changed" => time(),
                 "flag" => 0,
                 "submitter_openid" => $_SERVER["openid"],
                "flag" => 0,   // User flags presumably become obsolete when project gets manually edited
                "submitter_openid" => $_SESSION["openid"],
                 "hidden" => intval($_REQUEST->words->stripos…hidden->is_int["scope"]),
             )
            )
        );
        
        // Increase associated publishing timestamp if hereunto unknown release
        if (!project_version_exists($name, $data["version"])) {
            $data["t_published"] = time();
        }
        
        // Update project
        if (db("INSERT INTO release (:?) VALUES (::)", $data, $data)) {

            print "<h2>Submitted</h2> <p>Your project and release informations has been saved.</p>
                  <p>See the result in <a href='http://freshcode.club/projects/$name'>http://freshcode.club/projects/$name</a>.</p>";
        if ($release->store()) {
            print "<h2>Submitted</h2> <p>Your project and release informations have been saved.</p>
                  <p>See the result in <a href=\"http://freshcode.club/projects/$name\">http://freshcode.club/projects/$name</a>.</p>";
        }
        else { 
            print "Unspecified error.";
            print "Unspecified database error. Please retry later.";
        }
    }

}





#-- Output input form
#-- Output input form with current $data
else {
    $data = array_map("input::_html", $data);
    $data = array_map("input::html", $data);
    $select = "form_select_options";
    print <<<HTML

    include("template/submit_form.php");
    <form action="" method=POST enctype="multipart/form-data" accept-encoding=UTF-8>
        <input type=hidden name=is_new value=$is_new>
        
}
        <h3>General Project Info</h3>
        <p>
           <label>
               Project ID
               <input name=name size=20 placeholder=projectname value="$data[name]"
                      maxlength=33 required>
               <small>A short moniker which becomes your http://freshcode.club/projects/<b>name</b>.</small>
           </label>

           <label>
               Title
               <input name=title size=50 placeholder="Awesome Software" value="$data[title]"
                      maxlength=100 required>
           </label>

           <label>
               Homepage
               <input name=homepage size=50 type=url placeholder="http://project.example.org/" value="$data[homepage]"
                      maxlength=250>
           </label>

           <label>
               Description
               <textarea cols=50 rows=8 name=description
                         maxlength=1500 required>$data[description]</textarea>
               <small>Please give a concise roundup of what this software does, what specific features
               it provides, the intended target audience, or how it compares to similar apps.</small>
           </label>

           <label>
               License
               <select name=license>
                  {$select($licenses, $data["license"])}
               </select>
               <small>Again note that FLOSS is preferred.</small>
           </label>

           <label>
               Tags
               <input name=tags size=50 placeholder="game, desktop, gtk, python" value="$data[tags]"
                      maxlength=150 pattern="^\s*(\w+(-\w+)*(\s*,\s*|\s+)?){0,10}\s*$">
               <small>Categorize your project. Tags can be made up of letters, numbers and dashes. 
               This can include usage context, application type, programming languages, related projects,
               etc.</small>
           </label>

           <label>
               Image
               <input type=url name=image size=50 placeholder="http://i.imgur.com/xyzbar.png" value="$data[image]" maxlength=250>
               <small>Provide a preview image of up to 120x90 px; use <a href="http://imgur.com/">imgur</a> for uploading.
               It will be fetched and displayed later.</small>
           </label>
        </p>


        <h3>Release Submission</h3>
        <p>
           <label>
               Version
               <input name=version size=20 placeholder=2.0.1 value="$data[version]" maxlength=32>
               <small>Prefer <a href="http://semver.org/">semantic versioning</a> for releases.</small>
           </label>

           <label>
               State
               <select name=state>
                   {$select("initial,alpha,beta,development,prerelease,stable,mature,historic", $data["state"])}
               </select>
               <small>Tells about the stability or target audience of the current release.</small>
           </label>

           <label>
               Scope
               <br>
               <select name=scope>
                  {$select("minor feature,minor bugfix,major feature,major bugfix,security,documentation,cleanup,hidden", $data["scope"])}
               </select>
               <small>Indicate the significance and primary scope of this release.</small>
           </label>

           <label>
               Changes
               <textarea cols=50 rows=7 name=changes maxlength=2000>$data[changes]</textarea>
               <small>Summarize the changes in this release. Documentation additions are as
               crucial as new features or fixed issues.</small>
           </label>

           <label>
               Download URL
               <input name=download size=50 type=url placeholder="http://project.example.org/" value="$data[download]" maxlength=250>
               <small>In particular for the download link one could utilize the <b>\$version</b> placeholder.</small>
           </label>

           <label>
               Other URLs
               <textarea cols=50 rows=3 name=urls maxlength=2000>$data[urls]</textarea>
               <small>You can add more project URLs using a comma/newline-separated list
               like <tt>src=http://, deb=http://</tt>.
               Common link types include src=, rpm=, deb=, txz=, dvcs=, forum=, changelog=, etc.</small>
           </label>
        </p>


        <h3>Automatic Release Tracking</h3>
        <p>
           <em>You can skip this section.</em>
           But instead of registering each version manually, you can later automate the process
           with some version control systems or e.g. your project homepage and changelog.
           See the <a href="http://fossil.include-once.org/freshcode/wiki/Autoupdate">Autoupdate Howto</a>.
        </p>
        <p>
           <label>
               Via
               <select name=autoupdate_module>
                   {$select("none,release.json,github,sourceforge,regex", $data["autoupdate_module"])}
               </select>
           </label>

           <label>
               Autoupdate URL
               <input name=autoupdate_url type=url size=50 value="$data[autoupdate_url]" placeholder="https://github.com/user/repo/tags.atom" maxlength=250>
               <small>This is the primary source for <b>releases.json</b> and the <b>regex</b> method.
               GitHub and Sourceforge URLs are autodiscovered if they're e.g. your project homepage.</small>
           </label>

           <label>
               Regex
               <textarea cols=50 rows=3 name=autoupdate_regex placeholder="version = /-(\d+\.\d+\.\d+)\.txz/" maxlength=2500>$data[autoupdate_regex]</textarea>
               <small>
               <a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateRegex">Regex automated updates</a>
               expect a list of field=/regex/ names, like version=, changes=, download=, state=.
               Associatively-named "Other URLs" are also used for extraction.</small>
           </label>

        </p>

        <h3>Publish</h3>
        <p>
           Please proofread again before saving.

           <label>
               Submitter
               <input name=submitter size=50 placeholder="Your name" value="$data[submitter]" maxlength=50>
               <small>Give us your name or nick name here.</small>
           </label>

           <label>
               Lock Entry
               <input name=lock size=50 placeholder="$_SESSION[openid]" value="$data[lock]" maxlength=250>
               <small>Normally all projects can be edited by everyone (WikiStyle).
               If you commit to yours, you can however lock this submission against an OpenID
               handle. (Or even provide a comma-separated list here for multiple contributors.)</small>
           </label>
        </p>
        <p>
           <b>Terms and Conditions</b>
           <label class=inline><input type=checkbox name="req[os]" value=1 required> It's open source / libre / Free software or pertains BSD/Linux.</label>
           <label class=inline><input type=checkbox name="req[cc]" value=1 required> Your entry is shareable under the <a href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a> license.</label>
           <label class=inline><input type=checkbox name="req[sp]" value=1> And it's not spam.</label>
        </p>
        <p>
           <input type=submit value="Submit Project/Release">
        </p>
        <p style=margin-bottom:75pt>
           Thanks for your time and effort!
        </p>

    </form>    
HTML;
}


include("layout_bottom.php");
include("template/bottom.php");



// Output a list of select <option>s
function form_select_options($names, $value, $r="") {
    $map = is_string($names) ? array_combine($names = str_getcsv($names), $names) : $names;
    if ($value and !isset($map[$value])) { $map[$value] = $map[$value]; }
    foreach ($map as $id=>$title) {
        $r .= "<option" . ($id == $value ? " selected" : "") . " value=\"$id\" title=\"$title\">$id</option>";
    }
    return $r;
}


?>

Changes to page_tags.php.

1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22
23
24



25
26
27
28
29




30
31

32
33
34
35
36
37
38
39
40
41
42
43


44
45
46

47
48
49
50





51
52
53
54
55
56





57
58
59


60
61
62
63
64
65
66







67
68

69
70
71
72
73
74


75
76
77


78
79
80
81




82
83
84
85



86
87
88

89
90
91
1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17

18

19
20



21
22
23





24
25
26
27


28












29
30
31


32




33
34
35
36
37
38





39
40
41
42
43
44


45
46
47






48
49
50
51
52
53
54


55






56
57



58
59
60



61
62
63
64
65



66
67
68
69


70
71
72
73




-
+











-
+
-


-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+

-
-
+
-
-
-
-
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+

-
-
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
+
+
-
-
-
+
+

-
-
-
+
+
+
+

-
-
-
+
+
+

-
-
+



<?php
/**
 * type: page
 * title: Tags
 * description: Tag cloud
 * version: 0.2
 * version: 0.3
 *
 * This frontend code is utilizing a separate `tags` table, which
 * gets populated per cron script (rather than at insertion or via
 * trigger [sqlite seems insufficient to handle that).
 *
 * Currently just outputs a plain search result list. Later versions
 * should delegate this to a proper /search feature.
 *
 */


include("layout_header.php");
include("template/header.php");
?> <section id=main> <?php




#-- search by tags
#-- sidebar with Trove list
?><aside id=sidebar>
<div id=trove_tags class=pick-tags>
if ($_GET->words["name"]) {

    print "<h2>Projects with tags: {$_GET->words->html['name']}</h2><p><dl>";
    
    $result = db("
<?php
print tags::trove_select(tags::$tree);
?>
</div>
        SELECT release.name, SUBSTR(description,1,222) AS description, version, MAX(t_changed)
        FROM release
</aside><?php
        LEFT JOIN tags ON release.name = tags.name
        WHERE tags.tag IN (??)
        GROUP BY release.name LIMIT 50",
        $_GET->words->p_csv["name"]
    );
    foreach ($result as $p) {
        print<<<HTML
           <dt><a href="/projects/$p->name">$p->name</a> <em>$p->version</em></dt>
           <dd>$p->description</dd>
HTML;
    }
}




#-- print tag cloude
#-- print tag cloud
else {

    print "<h2>Tags</h2>
    <p>";
?>
<section id=main>
<h2>Tags</h2>
<p id=tag_cloud>
<?php

    // Query `tags` table to generate a cloud
    $tags = db("SELECT COUNT(name) AS cnt, tag FROM tags GROUP BY tag")->fetchAll();
    $count = array_column($tags, "cnt");
    if ($count) {
        $avg = array_sum($count) / count($count);
// Query `tags` table to generate a cloud
$tags = db("SELECT COUNT(name) AS cnt, tag FROM tags GROUP BY tag")->fetchAll();
$count = array_column($tags, "cnt");
if ($count) {
    $avg = count($count) / array_sum($count);

        // Print tag cloud
        foreach ($tags as $t) {
    // Print tag cloud
    foreach ($tags as $t) {

            // average
            $n=$q = 1.0*$t["cnt"] / 1.0*$avg;
            
            /**
             * Qantify
             * - Values below 1.0 are transitioned into the 0.5 to 1 range
        // average
        $n = 
        $q = 1.0*$t["cnt"] / 1.0*$avg;
        
        /**
         * Qantify
         * - Values 0.1 - 20.0 are transitioned into the range 0.3 - 2.0
             * - Values above 2.0 get capped around 3
             */
         */
            if ($q < 1.0) {
               $q *= 0.156*$q*$q +0.72*$q -0.43;
            }
            else while ($q >= 3.5) {
               $q *= 0.85;
            }
        $q = atan($q * 0.75 + 0.1) * 1.55;
        
            
            // font size
            $q = sprintf("%.1f", $q * 100);
        // font size
        $q = sprintf("%.1f", $q * 100);

            // output
            print " <a href=\"/tags/$t[tag]\" class=tag style=\"font-size: $q%\"> $t[tag] </a> ";
        }
        // output
        print " <a href=\"/search?tag=" . urlencode($t["tag"])
            . "\" class=tag style=\"font-size: $q%;\"> $t[tag]</a> ";
    }

    }
    print "</p>";
}
}
?></p><?php



include("layout_bottom.php");
include("template/bottom.php");


?>

Added release.php.



















































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: release/project data wrapper
 * description: Database scheme / versioned model abstraction for project releases
 * version: 0.3
 * depends: db
 * license: MITL
 * 
 * With `release` the database model, its versioning and value constraining are
 * consolidated somewhat. It's used by submission / autoupdate / and API interfaces.
 * It's best not to consider this a WebPMVC "model", but table data gateway.
 *
 * It can either be instantiated by project $name, fetching the last entry.
 * Or be populated from a database result array; using db()->into("release")
 *
 *
 *
 *
 */



/** 
 * Encases project/release data, keeps fields accessible per array["name"] syntax;
 * can clean up column formats before ->store()ing it back.
 *
 * Adds a couple of static calls to return specific entries object-wrapped, or lists
 * thereof as plain arrays (because commonly just used for template output).
 *
 */
class release extends ArrayObject {


    /**
     * Can be instanatiated by project name (latest version will be fetched),
     * or from a DB result array.
     *
     * @return release{}
     *
     */
    function __construct($namedata, $uu=NULL) {
    
        // fetch from DB
        if (is_string($namedata)) {
            $namedata = release::latest($namedata);
        }

        // unwrap previous AO or release obj
        if ($namedata instanceof ArrayObject) {
            $namedata = $namedata->getArrayCopy();
        }

        // populate ArrayObject
        if (is_array($namedata)) {
            unset($namedata["_order"]);
            $this->exchangeArray($namedata);
        }
    }
    
    
    /**
     * Prepare new release submission.
     *
     * Merges in flags (hidden, deleted, submitter_*, etc) from latest entry;
     * but retains t_published associated to `version` if it existed before.
     *
     * Filters $newdata to match expected database constraints. For page_submit,
     * $newdata just equals $_POST, and is already an input{} array object.
     *
     * $prefill and $override are used by submission / autoupdate / api callers
     * to define flags.
     *
     */
    function update($newdata, $prefill_flags=array(), $override_flags=array(), $partial=FALSE) {
    
        // Format constraints via input filter
        $newdata instanceof input  or  $newdata = new input($newdata, "\$newdata");
        $newdata->_has_urls = array($this, "has_urls");
        $newkeys = $newdata->keys();
        $newdata->nocontrol->trim->always();
        $newdata = array(
                 "name"     => $newdata->proj_name         ->length…3…33["name"],
                 "homepage" => $newdata->ascii->trim->http  ->length…250["homepage"],
                 "download" => $newdata->ascii->trim->url   ->length…250["download"],
                 "image"    => $newdata->ascii->trim->http  ->length…250["image"],
           "autoupdate_url" => $newdata->ascii->trim->http  ->length…250["autoupdate_url"],
                 "title"    => $newdata->text               ->length…100["title"],
              "description" => $newdata                    ->length…2000["description"],
                 "license"  => $newdata->words               ->length…30["license"],
                 "tags"     => $newdata->words->f_tags      ->length…150["tags"],
                 "version"  => $newdata->words               ->length…30["version"],
                 "state"    => $newdata->words->strtolower   ->length…30["state"],
                 "scope"    => $newdata->words->strtolower   ->length…30["scope"],
                 "changes"  => $newdata->text              ->length…2000["changes"],
                "submitter" => $newdata->text               ->length…100["submitter"],
                 "urls"     => $newdata->has_urls          ->length…2000["urls"],
                 "lock"     => $newdata->raw               ->length…2000["lock"],
        "autoupdate_module" => $newdata->id                  ->length…30["autoupdate_module"],
         "autoupdate_regex" => $newdata->raw               ->length…2000["autoupdate_regex"],
        );

        // Base data for version/t_published lookup, in case we only got partial $newdata
        $name = $newdata["name"] ?: $this["name"];
        $version = in_array("version", $newkeys) ? $newdata["version"] : $this["version"];

        // Declare some automatic system flags
        $auto_flags = array(
            // Hidden releases are either tagged that way, or have too short of a `changes:` summary
            "hidden" => intval(is_int(stripos($newdata["scope"], "hidden")) or !empty($prefill_flags["hidden"])),
            // Increase associated publishing timestamp if hereunto unknown release
            "t_published" => $this->exists($name, $version) ?: time(),
             // Whereas the update timestamp is always adapted
            "t_changed" => time(),
        );

        // Array excerpt if input didn't come from page_submit but Autoupdate or API
        if ($partial) {
            $newdata = array_intersect_key($newdata, array_flip($newkeys));
        }
        
        // Apply some logic filters
        $this->unpack($newdata);

        // Merge and apply input
        $this->exchangeArray(array_merge(
             $this->getArrayCopy(),   // any previous/extraneous control data is kept
             $prefill_flags,
             $newdata,
             $auto_flags,
             $override_flags
        ));
        
        // chainable call
        return $this;
    }

    
    /**
     * Store current data bag into `release` table.
     * Is to be invoked after ->update().
     *
     */
    function store($INSERT="INSERT") {
        $data = $this->getArrayCopy();
        return db("$INSERT INTO release (:?) VALUES (::)", $data, $data)
           and $this->update_rules();
    }

    /**
     * Further version management.
     *
     */
    function update_rules($data) {
         return
             // Hide previous empty "" version project entries, if less than two minutes old.
             db("UPDATE release SET hidden=1 WHERE name=? AND version=? AND t_published < ?",
                 $data["name"], "", time() - 120
             );
    }


    /**
     * Split up fields,
     * in particular the email out of `submitter`.
     *
     */
    function unpack(&$newdata) {

        if (!empty($newdata["submitter"]) and is_int(strpos($newdata["submitter"], "@"))
        and preg_match($rx = "/[^,;\s]+@[^,;\s]+/", $newdata["submitter"], $match))
        {
            $newdata["submitter_image"] = $match[0];
            $newdata["submitter"] = trim(preg_replace($rx, "", $newdata["submitter"]), ", ");
        }
    }


    /**
     * Retrieve latest published release version.
     *
     * @return array
     */
    static function latest($name) {
        $r = db("
            SELECT *
              FROM release
             WHERE name = ?
             ORDER BY t_published DESC, t_changed DESC
             LIMIT 1", $name
        );
        return $r ? $r->fetch() : array();
    }


    /**
     * Check for existence of specific release version,
     * return t_published timestamp if.
     *
     * @return int
     */
    static function exists($name, $version) {
        $r = db("
            SELECT t_published
              FROM release
             WHERE name=? AND version=?",
            $name, $version
        );
        $t = $r ? $r->fetchColumn(0) : 0;
       # print "<b>exists=$t</b>\n";
        return intval($t);
    }


    /**
     * Check current login against `lock` field,
     * which can be a comma-separated list of OpenID handles, or
     * contain password_hash() literals for API auth.
     *
     */
    function permission($data, $authwith) {
        global $moderator_ids;

        return empty($data["lock"])
            or in_array($authwith, array_merge(p_csv($data["lock"]), $moderator_ids));
    }

    /**
     * Minor data validation: check that `urls` does contain
     * actual data, not just empty "key= key= key=" lists.
     *
     */
    function has_urls($str) {
        return preg_match("/^(\s*[\w-]+\s*=\s*)*$/", $str) ? "" : $str;
    }
}






?>

Added shared.phar.

cannot compute difference between binary files

Added submit_import.php.































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * title: Import project description
 * description: Allow DOAP/JSON/etc. import prior manual /submit form intake.
 * version: 0.5
 *
 *
 * Checks for uploaded $_FILES or ?import_url=
 *  → Deciphers project name, description, license, tags, etc.
 *  → Passes on extra $data to /submit <form>
 *
 */



define("UP_IMPORT_TYPE", "import_via");
define("UP_IMPORT_FILE", "import_file");
define("UP_IMPORT_NAME", "import_name");



/**
 * Invoked by page_submit itself to populate any empty $data set.
 *
 */
class project_import {


    /**
     * Evaluate request params, and import data if any.
     *
     */
    static function fetch($data=NULL) {
    
        #-- file upload?
        if (!empty($_FILES[UP_IMPORT_FILE]["tmp_name"])) {
            $data = file_get_contents($_FILES[UP_IMPORT_FILE]["tmp_name"]);
        }
        
        #-- import scheme, and project name
        $type = $_REQUEST->id[UP_IMPORT_TYPE];
        $name = $_REQUEST->text[UP_IMPORT_NAME];

        if ($type and ($data or $name)) {
            $i = new self;
            return (array)@($i->convert($type, $data, $name));
        }
        else {
            return array();
        }
    }

    
    /**
     * Dispatch to submodules.
     *
     */
    function convert($type, $data, $name) {
    
        #-- switch to fetch methods
        switch (strtoupper($type)) {

            case "JSON":
               return $this->JSON($data);

            case "PKG-INFO":
            case "PKGINFO":
            case "LSM":
            case "DEBIAN":
            case "RPMSPEC":
               return $this->PKG_INFO($data);

            case "DOAP":
               return $this->DOAP($data);

            case "FREECODE":
               return $this->FREECODE($name);

            case "SOURCEFORGE":
               return $this->SOURCEFORGE($name);

            default:
               return array();
        }
    }

    
    
    /**
     * Extract from common JSON formats.
     *
     *   release.json  common.js     package.json  bower.json    composer.json   pypi.json
     *   ------------- ------------- ------------- ------------- --------------- -------------
     *   name          name          name          name          name            name
     *   version       version       version       version       version         version
     *   title                                                                     
     *   description   description   description   description   description     description
     *   homepage      homepage      homepage      homepage      homepage        home_page
     *   license       licenses*     license       license*      license         license
     *   image
     *   state                                                                   classifiers
     *   download                    repository    repository                    download_url
     *   urls*         repositories                              repositories    release_url
     *   tags          keywords      keywords      keywords      keywords        keywords
     *   trove                                                                   classifiers
     *
     */
    function JSON($data) {
    
        // check if it is actually json
        if ($data = json_decode($data, TRUE)) {


            // rename a few plain fields
            $map = array(
                "name" => "title",            // title is commonly absent
                "screenshot" => "image",
                "home_page" => "homepage",    // pypi
                "download_url" => "download", // pypi
                "summary" => "description",   // pypi
                "release_url" => "urls",      // pypi
            );
            foreach ($map as $old=>$new) {
                if (empty($data[$new]) and !empty($data[$old])) {
                    $data[$to] = $data[$from];
                }
            }


            // complex mapping
            $map = array(
                 "keywords" => "tags",
                 "classifiers" => "tags",
                 "licenses" => "license",
                 "license" => "license",
                 "repository" => "urls",
                 "repositories" => "urls",
                 "urls" => "urls",
            );
            foreach ($map as $old=>$new) {
                if (!empty($data[$old])) {
                    switch ($old) {

                        // keywords (common.js, composer.json) become tags
                        case "keywords":                        
                            $data[$new] = strtolower(join(", ", $data[$old]));
                            break;

                        // Trove classifiers (pypi)
                        case "classifiers":
                            $data[$new] = tags::trove_to_tags($data[$old]);
                            break;

                        // license alias  // see spdx.org
                        case "licenses":
                        case "license":
                            while (is_array($data[$old])) {
                                $data[$old] = current($data[$old]);
                            }
                            $data[$new] = tags::map_license($data[$old]);
                            break;

                        // URLs
                        case "repository":
                            $data[$new] = $data[$old]["type"] . "=" . $data[$old]["url"] . "\n";
                            break;
                        case "repositories":
                            $data[$new] = http_build_query(array_column($data[$old], "url", "type"), "", "\n");
                            break;
                        case "urls":
                            is_array($data[$old]) and
                            $data[$new] = http_build_query(array_column($data[$old], "url", "packagetype"), "", "\n");
                            break;
                        
                    }
                }
            }
            

            // common fields from releases.json are just kept asis
            $asis = array(
                "name", "title", "homepage", "description",
                "license", "tags", "image", "version", "state",
                "scope", "changes", "download", "urls",
                "autoupdate_module", "autoupdate_url", "autoupdate_regex",
                "submitter", "lock",
            );

            // done
            return(
                array_filter(
                    array_intersect_key($data, array_flip($asis)),
                    "is_string"
                )
            );
        }

    }



    /**
     * Extracts from PKG-INFO and other RFC822-style text files.
     *
     *  used   PKG-INFO       LSM            Debian        RPMSpec
     *  ----   -------------  -------------  ------------  -------
     *   →     Name           Title          Package       Name
     *   →     Version        Version        Version       Version
     *   →     Description    Description    Description   
     *   →     Summary                                     Summary
     *   →     Home-Page      Primary-Site   Homepage      URL
     *         Author         Author                       Vendor
     *   →     License        Coding-Policy                Copyright
     *   →     Keywords       Keywords       Section       Group
     *         Classifiers                                 
     *   →     Platform       Platforms                    
     *
     *  [1] http://legacy.python.org/dev/peps/pep-0345/
     *  [2] http://lsm.execpc.com/LSM.README
     *  [3] http://www.debian.org/doc/debian-policy/ch-controlfields.html
     *  [4] http://www.rpm.org/max-rpm/s1-rpm-build-creating-spec-file.html
     *
     */
    function PKG_INFO($data) {
    
        // Simple KEY: VALUE format (value may span multiple lines).
        preg_match_all("/
                ^ %?
                ([\w-]+): \s*
                (.+ (?:\R[\v].+$)* )
                $
            /xm", $data, $uu
        )
        and $data = array_change_key_case(array_combine($uu[1], $uu[2]), CASE_LOWER);

        // Test if it's PKG-INFO
        if (!empty($data["description"])) {

            return array(
                "title" => $data["name"] ?: $data["title"],
                "version" => $data["version"],
                "description" => $data["description"] ?: $data["summary"],
                "tags" => preg_replace("/[\s,;]+/", ", ", "$data[platform], $data[keywords]"),
              # "trove-tags" => $data["classifiers"],
                "homepage" => $data["home-page"] ?: $data["url"] ?: $data["homepage"] ?: $data["primary-site"],
                "download" => $data["download-url"],
                "license" => tags::map_license($data["license"] ?: $data["coding-policy"] ?: $data["copyright"]),
            );
        }
    }



    /**
     * Import from DOAP description.
     *
     * Would actually require a RDF toolkit,
     * but for the simple use case here, it's just processed namespace-unaware as xml.
     *
     */
    function DOAP($data) {
        if ($x = simplexml_load_string($data)->Project) {
            $x = array(
                "name" => strval($x->shortname),
                "title" => strval($x->name),
                "description" => strval($x->description ?: $x->shortdesc),
                "homepage" => strval($x->homepage["resource"]),
                "download" => strval($x->{'download-page'}["resource"]),
                "tags" => strval($x->{'programming-language'}) .", ". strval($x->category["resource"]),
                "license" => tags::map_license(basename(strval($x->license["resource"]))),
                "version" => strval($x->release->Version->revision),
            );
            return $x;
        }
    }



    /**
     * Freecodes JSON API is gone, so we have to extract from the project
     * page itself.
     *
     */
    function FREECODE($name) {

        // retrieve
        if ($html = curl("http://freecode.com/projects/$name")->exec()) {
        
            // regex extract to reduce false positives
            preg_match_all('~
                  <meta \s+ property="og:title" \s+ content="(?<title>[^"]+)"
               |  <meta \s+ name="keywords" \s+ content="(?<tags>[^"]+)"
               |  class="project-detail">  \s+  <p>  (?<description>[^<>]+)</p>
               |  >Licenses< .+? rel="tag">  (?<license>[^<>]+)</a>
               |  >Implementation< .+? rel="tag">  (?<lang>[^<>]+)</a>
            ~smix', $html, $uu, PREG_SET_ORDER);

            // join fields
            if (!empty($uu[0][0])) {
                $uu = call_user_func_array("array_merge", array_map("array_filter", $uu));
                return array(
                    "name" => $name,
                    "title" => $uu["title"],
                    "description" => $uu["description"],
                    "tags" => strtolower((!empty($uu["lang"]) ? "$uu[lang], " : "") . $uu["tags"]),
                    "license" => tags::map_license($uu["license"]),
                );
            }        
        }
    }



    /**
     * Sourceforge still provides a JSON export.
     *
     */
    function SOURCEFORGE($name) {

        // retrieve
        if ($data = json_decode(curl("https://sourceforge.net/rest/p/$name")->exec(), TRUE)) {

            // custom json extraction
            return array(
                "name" => $data["shortname"],
                "title" => $data["name"],
                "homepage" => $data["external_homepage"] ?: $data["url"],
                "description" => $data["short_description"],
                "image" => $data["screenshots"][0]["thumbnail_url"],
                "license" => tags::map_license($data["categories"]["license"][0]["fullname"]),
                "tags" => implode(", ",
                    array_merge(
                        array_column($data["categories"]["language"], "shortname"),
                        array_column($data["categories"]["environment"], "fullname"),
                        array_column($data["categories"]["topic"], "shortname")
                    )
                ),
                "state" => $data["categories"]["developmentstatus"][0]["shortname"]
            );
        }
    }

}


#print_r((new project_import)->freecode("firefox"));



?>

Added tags.php.



















































































































































































































































































































































































































































































































































































































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * API: freshcode
 * title: Tags and Trove
 * description: Provides categorization backend for tree-mapped tags and Trove grouping.
 * version: 0.2
 * type: library
 * category: taxonomy
 * doc: http://fossil.include-once.org/freshcode/wiki/Trove+map
 * license: mixed
 *
 * This module provides major tags in a tree, which serves as base for trove categories.
 * 
 *  → Still permits free-form tags.
 *  → Provides for aliasing.
 *  → Only major topic tags end up in trove tree.
 *  → Allows to map licenses from and to tags.
 *  → Handles some HTML and JS output.
 *
 * 
 */


/**
 * Foremost bundles static arrays for tags.
 *
 * @static
 * @dataProvider map
 *
 */
class Tags {


    /**
     * License monikers and full names.
     *
     */
    static public $licenses = [
        "" => "Unspecified",
        "Apache" => "Apache License 2.0",
        "Artistic" => "Artistic license 2.0",
        "BSDL" => "BSD 3-Clause 'New/Revised' License",
        "BSDL-2" => "BSD 2-Clause 'Simplified/FreeBSD' License",
        "CDDL" => "Common Development and Distribution License 1.0",
        "MITL" => "MIT license",
        "MPL" => "Mozilla Public License 2.0",
        "Public Domain" => "Public Domain (no copyright)",
        "Python" => "Python License",
        "PHPL" => "PHP License 3.0",
        "GNU GPL" => "GNU General Public License 2.0",
        "GNU GPLv3" => "GNU General Public License 3.0",
        "GNU LGPL" => "GNU Library/Lesser General Public License 2.1",
        "GNU LGPLv3" => "GNU Library/Lesser General Public License 3.0",
        "Affero GPL" => "Affero GNU Public License 2.0",
        "Affero GPLv3" => "GNU Affero General Public License v3",
        "AFL" => "Academic Free License 3.0",
        "APL" => "Adaptive Public License",
        "APSL" => "Apple Public Source License",
        "AAL" => "Attribution Assurance Licenses",
        "BSDL-4" => "BSD 4-Clause 'Old' License",
        "BSL" => "Boost Software License",
        "CECILL" => "CeCILL License 2.1",
        "CATOSL" => "Computer Associates Trusted Open Source License 1.1",
        "CDDL" => "Common Development and Distribution License 1.0",
        "CPAL" => "Common Public Attribution License 1.0",
        "CUA" => "CUA Office Public License Version 1.0",
        "EUDatagrid" => "EU DataGrid Software License",
        "EPL" => "Eclipse Public License 1.0",
        "ECL" => "Educational Community License, Version 2.0",
        "EFL" => "Eiffel Forum License V2.0",
        "Entessa" => "Entessa Public License",
        "EUPL" => "European Union Public License, Version 1.1 (EUPL-1.1)",
        "Fair" => "Fair License",
        "Frameworx" => "Frameworx License",
        "HPND" => "Historical Permission Notice and Disclaimer",
        "IPL" => "IBM Public License 1.0",
        "IPA" => "IPA Font License",
        "ISC" => "ISC License",
        "LPPL" => "LaTeX Project Public License 1.3c",
        "LPL" => "Lucent Public License Version 1.02",
        "MirOS" => "MirOS Licence",
        "MS-RL" => "Microsoft Reciprocal License",
        "Motosoto" => "Motosoto License",
        "Multics" => "Multics License",
        "NASA" => "NASA Open Source Agreement 1.3",
        "NTP" => "NTP License",
        "Naumen" => "Naumen Public License",
        "NGPL" => "Nethack General Public License",
        "Nokia" => "Nokia Open Source License",
        "NPOSL" => "Non-Profit Open Software License 3.0",
        "OCLC" => "OCLC Research Public License 2.0",
        "OFL" => "Open Font License 1.1",
        "OGTSL" => "Open Group Test Suite License",
        "OSL" => "Open Software License 3.0",
        "PostgreSQL" => "The PostgreSQL License",
        "CNRI" => "CNRI Python license (CNRI-Python)",
        "QPL" => "Q Public License",
        "RPSL" => "RealNetworks Public Source License V1.0",
        "RPL" => "Reciprocal Public License 1.5",
        "RSCPL" => "Ricoh Source Code Public License",
        "SimPL" => "Simple Public License 2.0",
        "Sleepycat" => "Sleepycat License",
        "SPL" => "Sun Public License 1.0",
        "Watcom" => "Sybase Open Watcom Public License 1.0",
        "NCSA" => "University of Illinois/NCSA Open Source License",
        "VSL" => "Vovida Software License v. 1.0",
        "W3C" => "W3C License",
        "WXwindows" => "wxWindows Library License",
        "Xnet" => "X.Net License",
        "ZPL" => "Zope Public License 2.0",
        "Zlib" => "zlib/libpng license",
        "Other" => "Other License",
        "Mixed" => "Multiple Licenses",
    ];   // todo: Dicuss entry for Commercial/Proprietary code anyhow.
         // hint: Separation usually works better than prohibition.
         //       (Filtering instead of cleanups)


    /**
     * Tag aliases.
     *
     */
    static public $alias = [
        "email" => "e-mail",
    ];


    /**
     * Tag tree.
     *
     */
    static public $tree =
        [
        "Topic" => [
            "Adaptive Technologies",
            "Artistic Software",
            "Communication",
            "Communication" => [
                "BBS",
                "Chat",
                "Chat" => [
                    "ICQ",
                    "Internet Relay Chat",
                    "Skype",
                    "Unix Talk",
                    "XMPP"
                ],
                "Conferencing",
                "Email",
                "Email" => [
                    "Address Book",
                    "Email Client",
                    "Email Filter",
                    "Mailing List Server",
                    "Mail Transport Agent",
                    "IMAP",
                    "POP3"
                ],
                "Fax",
                "FIDO",
                "File Sharing",
                "Ham Radio",
                "Internet Phone",
                "Telephony",
                "Usenet"
            ],
            "Database",
            "Database" => [
                "Database-server",
                "Front-End"
            ],
            "Desktop",
            "Desktop" => [
                "File Manager",
                "Gnome",
                "GNUstep",
                "KDE",
                "PicoGUI",
                "Screen Savers",
                "Window Manager",
                "Window Manager" => [
                    "Afterstep",
                    "Applet",
                    "Blackbox",
                    "CTWM",
                    "Enlightenment",
                    "Fluxbox",
                    "FVWM",
                    "IceWM",
                    "MetaCity",
                    "Openbox",
                    "Oroborus",
                    "Sawfish",
                    "Waimea",
                    "Window Maker",
                    "XFCE"
                ]
            ],
            "Documentation",
            "Education",
            "Education" => [
                "Computer Aided Instruction",
                "Testing"
            ],
            "Game",
            "Game" => [
                "Arcade",
                "Board Game",
                "First Person Shooter",
                "Fortune Cookies",
                "Multi-User Dungeons",
                "Puzzle",
                "Real Time Strategy",
                "Role-Playing",
                "Side-Scrolling",
                "Simulation",
                "Turn Based Strategy"
            ],
            "Home Automation",
            "Internet",
            "Internet" => [
                "FTP",
                "Finger",
                "Log Analysis",
                "DNS",
                "Proxy Server",
                "WAP",
                "WWW",
                "WWW" => [
                    "Browser",
                    "Dynamic Content",
                    "Dynamic Content" => [
                        "CGI Library",
                        "Message Board",
                        "News/Diary",
                        "Page Counter"
                    ],
                    "HTTP Server",
                    "Indexing/Search",
                    "Session",
                    "Site Management",
                    "Site Management" => [
                        "Link Checking"
                    ],
                    "WSGI"
                ]
            ],
            "Multimedia",
            "Multimedia" => [
                "Graphics",
                "Graphics" => [
                    "3D Modeling",
                    "3D Rendering",
                    "Capture",
                    "Capture" => [
                        "Digital Camera",
                        "Scanner",
                        "Screen Capture"
                    ],
                    "Editor",
                    "Editor" => [
                        "Raster-Based",
                        "Vector-Based"
                    ],
                    "Graphics Conversion",
                    "Presentation",
                    "Viewer"
                ],
                "Audio",
                "Audio" => [
                    "Analysis",
                    "Recording",
                    "CD Audio",
                    "CD Audio" => [
                        "CD Playing",
                        "CD Ripping",
                        "CD Writing"
                    ],
                    "Conversion",
                    "Editors",
                    "MIDI",
                    "Mixers",
                    "Player",
                    "Player" => [
                        "MP3"
                    ],
                    "Sound Synthesis",
                    "Speech"
                ],
                "Video",
                "Video" => [
                    "Capture",
                    "Conversion",
                    "Display",
                    "Non-Linear Editor"
                ]
            ],
            "Office",
            "Office" => [
                "Financial",
                "Financial" => [
                    "Accounting",
                    "Investment",
                    "Point-Of-Sale",
                    "Spreadsheet"
                ],
                "Groupware",
                "News/Diary",
                "Office Suite",
                "Scheduling"
            ],
            "Printing",
            "Religion",
            "Scientific",
            "Scientific" => [
                "Artificial Intelligence",
                "Artificial Life",
                "Astronomy",
                "Atmospheric Science",
                "Bio-Informatics",
                "Chemistry",
                "Electronic Design Automation",
                "GIS",
                "Human Machine Interfaces",
                "Image Recognition",
                "Information Analysis",
                "Interface Engine",
                "Mathematics",
                "Medical Science",
                "Physics",
                "Visualization"
            ],
            "Security",
            "Security" => [
                "Cryptography"
            ],
            "Sociology",
            "Sociology" => [
                "Genealogy",
                "History"
            ],
            "Software Development",
            "Software Development" => [
                "Assembler",
                "Bug Tracking",
                "Build Tool",
                "Code Generator",
                "Compiler",
                "Debugger",
                "Disassembler",
                "Documentation",
                "Embedded Systems",
                "Internationalization",
                "Interpreter",
                "Library",
                "Library" => [
                    "Application Framework",
                    "Java Library",
                    "Perl Module",
                    "PHP Class",
                    "Pike Module",
                    "pygame",
                    "Python Module",
                    "Ruby Modules",
                    "Tcl Extension"
                ],
                "Localization",
                "Object Brokering",
                "Object Brokering" => [
                    "CORBA",
                    "D-Bus",
                    "SOAP"
                ],
                "Pre-processor",
                "Quality Assurance",
                "Testing",
                "Testing" => [
                    "Traffic Generation"
                ],
                "User Interfaces",
                "Version Control",
                "Widget Set"
            ],
            "System",
            "System" => [
                "Archiving",
                "Archiving" => [
                    "Backup",
                    "Compression",
                    "Mirroring",
                    "Packaging"
                ],
                "Benchmark",
                "Boot",
                "Boot" => [
                    "Init"
                ],
                "Clustering",
                "Console Font",
                "Distributed Computing",
                "Emulator",
                "Filesystem",
                "Hardware",
                "Hardware" => [
                    "Hardware Driver",
                    "Mainframes",
                    "Symmetric Multi-processing"
                ],
                "Installation",
                "Logging",
                "Monitoring",
                "Networking",
                "Networking" => [
                    "Firewalls",
                    "Monitoring",
                    "Monitoring" => [
                        "Hardware Watchdog"
                    ],
                    "Time Synchronization"
                ],
                "Operating System",
                "Kernel",
                "Power (UPS)",
                "Recovery Tool",
                "Shells",
                "Software Distribution",
                "Systems Administration",
                "Systems Administration" => [
                    "Authentication/Directory",
                    "Authentication/Directory" => [
                        "LDAP",
                        "NIS"
                    ]
                ]
            ],
            "Shell",
            "Terminal",
            "Terminal" => [
                "Serial",
                "Telnet",
                "Terminal Emulator"
            ],
            "Text Editor",
            "Text Editor" => [
                "Documentation",
                "Emacs",
                "IDE",
                "Text Processing",
                "Word Processor"
            ],
            "Text Processing",
            "Text Processing" => [
                "Filter",
                "Font",
                "General",
                "Indexing",
                "Linguistic",
                "Markup",
                "Markup" => [
                    "DocBook",
                    "HTML",
                    "LaTeX",
                    "Markdown",
                    "ReStructuredText",
                    "SGML",
                    "VRML",
                    "Wiki",
                    "XML"
                ]
            ],
            "Utilities"
        ],
        "Programming Language" => [
            "Ada",
            "APL",
            "ASP",
            "Assembly",
            "Awk",
            "Bash",
            "Basic",
            "C",
            "C#",
            "C++",
            "Clojure",
            "Cold Fusion",
            "Cython",
            "D",
            "Delphi",
            "Dylan",
            "Eiffel",
            "Emacs-Lisp",
            "Erlang",
            "Euler",
            "Forth",
            "Fortran",
            "Go",
            "Groovy",
            "Haskell",
            "Haxe",
            "Java",
            "JavaScript",
            "Lua",
            "Lisp",
            "Logo",
            "Matlab",
            "ML",
            "Modula",
            "Oberon",
            "Objective C",
            "Object Pascal",
            "OCaml",
            "Parrot",
            "Pascal",
            "Perl",
            "PHP",
            "PHP" => [
                "HHVM",
                "Quercus"
            ],
            "Pike",
            "PL/SQL",
            "PROGRESS",
            "Prolog",
            "Python",
            "Python" => [
                "CPython",
                "IronPython",
                "Jython",
                "PyPy",
                "Stackless"
            ],
            "REBOL",
            "R",
            "Regex",
            "Rexx",
            "Ruby",
            "Scala",
            "Scheme",
            "Simula",
            "Smalltalk",
            "SQL",
            "Tcl",
            "Unix Shell",
            "Vala",
            "YACC",
            "Zope"
        ],
        "Environment" => [
            "Console",
            "Console" => [
                "Curses",
                "Framebuffer",
                "Newt",
                "svgalib"
            ],
            "Mobile",
            "MacOS X",
            "MacOS X" => [
                "Aqua",
                "Carbon",
                "Cocoa"
            ],
            "Daemon",
            "OpenStack",
            "Plugin",
            "Web Environment",
            "Web Environment" => [
                "Buffet",
                "Mozilla",
                "ToscaWidgets"
            ],
            "Win32",
            "X11",
            "X11" => [
                "Gnome",
                "GTK",
                "KDE",
                "Qt",
                "Tk"
            ],
            "Wayland"
        ],
        "Framework" => [
            "C++" => [
                "Boost"
            ],
            "Groovy" => [
                "Grails"
            ],
            "Java" => [
                "Hibernate",
                "Spring",
                "Sinatra",
                "Struts",
                "OpenXava"
            ],
            "JavaScript" => [
                "AngularJS",
                "extJS",
                "jQuery",
                "MooTools",
                "Prototype",
                "qooxdoo"
            ],
            "Perl" => [
                "Mason",
                "Catalyst"
            ],
            "Python" => [
                "BFG",
                "Bob",
                "Bottle",
                "Buildout",
                "Chandler",
                "CherryPy",
                "CubicWeb",
                "Django",
                "Flask",
                "IDLE",
                "IPython",
                "Opps",
                "Paste",
                "Plone",
                "py2web",
                "Pylons",
                "Pyramid",
                "Review Board",
                "Setuptools Plugin",
                "Trac",
                "Tryton",
                "TurboGears",
                "Twisted",
                "ZODB",
                "Zope2",
                "Zope3"
            ],
            "PHP" => [
                "CakePHP",
                "Laravel",
                "Symfony",
                "Yii",
                "Zend Framework"
            ],
            "Ruby" => [
                "Rails"
            ]
        ],
        "Operating System" => [
            "BeOS",
            "Darwin",
            "MacOS",
            "MS-DOS",
            "Windows",
            "OS2",
            "Cross-plattform",
            "PalmOS",
            "PDA Systems",
            "POSIX",
            "AIX",
            "BSD",
            "BSD" => [
                "FreeBSD",
                "NetBSD",
                "OpenBSD"
            ],
            "Hurd",
            "HP-UX",
            "IRIX",
            "Linux",
            "SCO",
            "Solaris",
            "QNX",
            "Unix"
        ],
        "Audience" => [
            "Customer Service",
            "Developers",
            "Education",
            "End Users",
            "Financial and Insurance Industry",
            "Healthcare Industry",
            "Information Technology",
            "Legal Industry",
            "Manufacturing",
            "Religion",
            "Science/Research",
            "System Administrators",
            "Telecommunications Industry"
        ],
    ];


    /**
     * Try to map SPDX.org names onto our license tags,
     * or find entry in long description;
     *
     */
    function map_license($id) {

        // exact find
        if (isset(tags::$licenses[$id])) {
            return $id;
        }
        
        // extract moniker and optional version or number
        if (preg_match_all("/\b[\d.]+\b|\b(?!GNU)\w+\b/", "$id xyDummy", $p)) {

            list($name, $ver) = @array($p[0][0], $p[0][1]);

            // close or approximated license description match
            if ($match = preg_grep("/$name.+?$ver/i", tags::$licenses)
            or  $match = preg_grep("/$name/i", tags::$licenses))
            {
                return key($match);
            }
            
            // or just abbreviation keys
            if ($match = preg_grep("/{$name}[Lv-]*{$ver[0]}/i", array_keys(tags::$licenses))
            or  $match = preg_grep("/$name/i", array_keys(tags::$licenses)))
            {
                return reset($match);
            }
        }
    }



    /**
     * Guess leaves from standard Trove categories
     * (Does not utilize self::$tree yet!)
     *
     */
    function trove_to_tags($array, $tags=array()) {
        preg_match_all("~^Topic :: .+ :: (\w[\w\s/-]+)$~m", implode("\n", (array)$array), $uu);
        foreach ($uu[1] as $trove) {
            $tags[] =
                strtolower(
                    strtr($trove, " /.", "--_")
                );
        }
        return implode(", ", $tags);
    }



    /**
     * HTML output list of Trove tags.
     *
     * Is used in page_submit within the <div class=select id=trove_tags>
     *
     * Here everything just wrapped in <span>s, because <select> optgroups
     * can't be nested, and <ul> breaks out of inline flow text DOM
     * structure.
     *
     */
    function trove_select($trove, $level=0, $html="") {

        // loop through one level
        foreach ($trove as $key=>$value) {
        
            // normalize title into tag-key
            $tag = is_numeric($key) ? $value : $key;
            $tag = strtr(strtolower($tag), " /.:-", "-----");
            $style = "style='margin-left: {$level}px;'";
        
            // descend into groups
            if (is_array($value)) {
                $html .= "<span class=optgroup data-tag=$tag><b class=option data-tag=$tag>$key</b>";
                $html .= self::trove_select($value, $level + 10);
                $html .= "</span>";
            }
            // skip if entry repeated as subgroup
            elseif (isset($trove[$value])) {
                #..
            }
            // single tag entry
            else {
                $html .= "<span data-tag=$tag class=option>$value</span>";
            }
        }
        
        return $html;
    }



    /**
     * Returns just leaves from trove $tree.
     *
     */
    function leaves() {
        return iterator_to_array(new RecursiveIteratorIterator(new RecursiveArrayIterator(self::$tree)));
    }


    /**
     * Extract typical release tags.
     *
     */
    function scope_tags($s) {
        preg_match_all("/major|minor|bugfix|feature|security|documentation|hidden|cleanup/i", strtolower($s), $uu);
        return join(" ", $uu[0]);
    }

    /**
     * Extract typical release tags.
     *
     */
    function state_tag($s) {
        preg_match_all("/initial|alpha|beta|development|prerelease|stable|mature|historic/i", strtolower($s), $uu);
        return isset($uu[0][0]) ? $uu[0][0] : "";
    }

}




?>

Added template/bottom.php.


























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
</section>

<footer id=spotlight>
<?php include("template/spotlight.htm"); ?>
</footer>

<footer id=bottom>
<a href="http://fossil.include-once.org/freshcode/wiki/About">About</a> |
<a href="http://fossil.include-once.org/freshcode/wiki/Privacy">Privacy / Policy</a> |
<a href="http://fossil.include-once.org/freshcode/wiki/Contribute">Contribute</a> |
<small>
   <a href="/login"><i>optional</i> Login</a>
</small>
<small style=float:right>
<span style="display:inline-block; vertical-align:middle;">bookmark<br>freshcode</span>
&nbsp;on&nbsp; <?php print social_share_links("freshcode", "http://freshcode.club/"); ?>
</small>
<br>
<small style="font-size:90%">
This is a non-commercial project.
<br>
All project entries are licensed as CC-BY-SA. There will be <a href="/feed">/atom+json feeds</a>..
</small>
</footer>

</html>

Added template/forum_entry.php.






































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: FTD
 * title: Single forum post
 * description:
 *
 */

$_ = "trim";

print <<<HTML

   <li>
      <article class=entry>

          <h6 class=summary>$entry[summary]
             <span class="funcs trimmed">
                <a class="action forum-edit" data-id=$entry[id] title="Edit">Ed</a>
                <a class="action forum-reply" data-id=$entry[id] title="Reply">Re</a>
             </span>
          </h6>

          <aside class=meta><div>
             <b class=category>$entry[tag]</b>
             <i class=author>
                 <img align=top src="$entry[miniature]" width=16 height=16>
                 $entry[author]
             </i>
             <var class=datetime>{$_(strftime("%Y-%m-%d - %H:%M",$entry["t_published"]))}</var>
          </div></aside>

          <div class="excerpt">$entry[excerpt]</div>
          <div class="content trimmed">$entry[html]</div>

      </article>

HTML;

Added template/forum_submit_form.php.








































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: ftt
 * type: template
 * title: Post submit/edit form
 * description: Outputs input form for new / reply / editing forum posts.
 *
 *
 */


?>

<!-- faux -->
<form action=spex method=POST style="displaY: None">
   <input type="hidden" name="back" value="index" />
   <input type="hidden" name="mode" value="posting" />
   <input type="hidden" name="id" value="0" />
   <input type="hidden" name="posting_mode" value="0" />
   <input type="text" size="40" name="name" value="" maxlength="40">
   <input type="text" size="40" name="email" value="">
   <input type="text" size="40" name="homepage" value="">
   <input id="subject" type="text" size="50" name="subject" value="">
   <textarea cols="80" rows="21" name="comment"></textarea>
   <input type="submit" name="save_entry" value="OK - Submit" title="Save entry">
</form>

<!-- actual -->
<form class=forum-submit action=none style="display: run-in">

   <label>
       <b>Author</b>
       <input name=author placeholder=your-name size=50 value="<?=$author?>">
   </label>

   <label>
       <b><select name=img_type><option>gravatar<option>identicon<option>monsterid<option>wavatar<option>retro</select></b>
       <input name=image type=email placeholder="you@example.com" size=50 value="<?=$miniature?>">
   </label>

   <label>
       <b>Category</b>
       <select name=tag><?=form_select_options($forum_cfg["categories"], $tag)?></select>
   </label>

   <label>
       <b>Summary</b>
       <input name=summary placeholder="..." size=60 value="<?=$summary?>">
   </label>

   <label>
       <span style="position: absolute">
          <div class="markup-buttons">
           <a class="action markup" style="font-style: italic" data-before="*" data-after="*">italic</a>
           <a class="action markup" style="font-weight: bold" data-before="**" data-after="**">bold</a>
           <a class="action markup" style="text-decoration: underline" data-before="[" data-after="](http://example.org/)">link</a>
           <a class="action markup" style="" data-before="`" data-after="`">{code}</a>
           <a class="action markup" style="" data-before="\n  *  " data-after="">• list</a>
          </div>
       </span>
       <b>Message</b>
       <textarea name=source cols=55 rows=12><?=$source?></textarea>
   </label>

   <input type=hidden name=id value="<?=$id?>">
   <input type=hidden name=pid value="<?=$pid?>">

   <button class="action forum-submit">Follow The Thread</button>
   <br>

</form>

Added template/header.php.


























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * type: template
 * title: HTML page header
 * description: Starts <html> and <head>, outputs top bar / menus etc.
 * version: 0.6.5
 *
 * Optionally injects a `$header_add` list, or allows to override the
 * page $title.
 *
 */
?>
<!DOCTYPE html>
<html>
<head> 
    <title><?= isset($title) ? $title : "freshcode.club" ?></title>
    <meta name=version content=0.6.5>
    <meta charset=UTF-8>
    <link rel=stylesheet href="/freshcode.css?0.6.5">
    <link rel="shortcut icon" href="/img/changes.png">
    <base href="//<?= HTTP_HOST ?>/">
    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <!--[if lt IE 9]><script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.2/html5shiv.min.js"></script><![endif]-->
    <script src="/gimmicks.js"></script>
    <?php if (isset($header_add)) { print $header_add . "\n"; } ?>
</head>
<body>

<nav id=topbar>
Open source software release tracking.
<?= is_int(strpos(HTTP_HOST, ".")) ? '<small style="color:#9c7" class=version>[0.6.5 alpha]</small>' : '<b style="color:#c54">[local dev]</b>'; ?>
<span style=float:right>
<a href="//freshmeat.club/">freshmeat.club</a> |
<a href="//freecode.club/">freecode.club</a> |
<b><a href="//freshcode.club/">freshcode.club</a></b>
</span>
</nav>

<footer id=logo>
<a href="/" title="freshcode.club"><img src="img/logo.png" width=200 height=110 alt=freshcode border=0></a>
<div class=empty-box>&nbsp;</div>
</footer>

<nav id=tools>
   <a href="/">Home</a>
   <a href="/submit" class=submit>Submit</a>
   <span class=submenu>
      <a href="/names">Browse</a>
      <a href="/tags" style="left:-5pt;">Projects by Tag</a>
   </span>
   <form id=search_q style="display:inline" action=search><input name=q size=5><a href="/search">Search</a></form>
   <a href="//fossil.include-once.org/freshcode/wiki/About">About</a>
   <a href="/links">Links</a>
   <a href="/meta">Meta</a>
</nav>


Added template/index_project.php.










































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Frontpage listing
 * description: Outputs list entry for recent project releases
 * depends: wrap_tags
 *
 * Each project release entry on the frontpage contains
 *
 *   → Headline with project title, current version, homepage + download button
 *   → Screenshot image
 *   → Description, .trimmed
 *   → Scope and changes, .trimmed
 *   → Short Tag list: license, tags
 *
 */


// varexpr callback
$_ = "trim";

// Write
print <<<HTML
      <article class=project>
        <h3>
            <a href="projects/$entry[name]">$entry[title]
            <em class=version>$entry[version]</em></a>
            <span class=links>
                <span class=published_date>$entry[formatted_date]</span>
                <a href="$entry[homepage]"><img src="img/home.png" width=20 height=20 border=0 align=middle alt="⛵"></a>
                <a href="$entry[download]"><img src="img/disk.png" width=20 height=20 border=0 align=middle alt="💾"></a>
            </span>
        </h3>
        <a href="$entry[homepage]"><img class=preview src="$entry[image]" align=right width=120 height=90 border=0></a>
        <p class="description trimmed">$entry[description]</p>
        <p class="release-notes trimmed"><b>$entry[scope]:</b> $entry[changes]</p>
        <p class=tags><img src="img/tag.png" width=30 align=middle height=22 border=0><a class=license>$entry[license]</a>{$_(wrap_tags($entry["tags"]))}</p>
      </article>
HTML;

?>

Added template/index_sidebar.php.












































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: template
 * title: frontpage feeds
 * description: Outputs #sidebar on frontage, containing template/feed.*.htm
 * version: 0.4
 *
 * The feed.*.htm files are regularily updated
 * by cron.daily/newsfeeeds. Thus does not need
 * further processing here.
 *
 */

?>

 <aside id=sidebar>

    <section class="article-links untrimmed">
        <h5>Linux.com Software</h5>
        <?php  include("template/feed.linuxcom.htm");  ?>
    </section>

    <section class="article-links trimmed">
        <h5>reddit<em>/r/linux</em></h5>
        <?php  include("template/feed.reddit.htm");  ?>
    </section>

    <section class="article-links trimmed">
        <h5>LinuxGames</h5>
        <?php  include("template/feed.linuxgames.htm");  ?>
    </section>

    <section class="article-links untrimmed">
        <h5>Sourceforge Files</h5>
        <?php  include("template/feed.sourceforge.htm");  ?>
    </section>

    <section class="article-links untrimmed">
        <h5>DistroWatch</h5>
        <?php  include("template/feed.distrowatch.htm");  ?>
    </section>

 </aside>

Added template/projects_description.php.





















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Project description
 * description: Displays title, version, description, tags, homepage + download button
 * depends: wrap_tags
 *
 * Each projects/ page description contains
 *
 *   → Headline of project title and current version
 *   → Screenshot image
 *   → Description
 *   → Tag list (tags, license, state)
 *   → [Homepage] and [Download] link buttons
 *
 */


$_ = "trim";

print <<<PROJECT
      <article class=project>

        <h3>
            <a href="projects/$entry[name]">
               $entry[title]
               <em class=version>$entry[version]</em>
            </a>
        </h3>

        <a href="$entry[homepage]">
           <img class=preview src="$entry[image]" align=right width=120 height=90 border=0>
        </a>

        <p class=description style="border:0">$entry[description]</p>

        <table class=long-tags border=0>
           <tr> <th>Tags</th>     <td>{$_(wrap_tags($entry["tags"]))}</td>           </tr>
           <tr> <th>License</th>  <td><a class=license>$entry[license]</a></td>      </tr>
           <tr> <th>State</th>    <td><a class=license>$entry[state]</a></td>        </tr>
        </table>

        <p class=long-links>
            <a href="$entry[homepage]"><img src="img/home.png" width=20 height=20 border=0 align=middle> Homepage</a>
            <a href="$entry[download]"><img src="img/disk.png" width=20 height=20 border=0 align=middle> Download</a>
        </p>

      </article>
PROJECT;


?>

Added template/projects_release_entry.php.






























1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Project version entry
 * description: Displays a release, with scope and changes
 * depends: strftime
 *
 * Shows versioning history of projects/
 *   → Date and Version
 *   → Scope and Changes
 *
 * This template, obviously, gets iterated over to output all
 * release entries.
 *
 */


print <<<VERSION_ENTRY
       <div class=release-entry>
          <span class=version>$entry[version]</span><span class=published_date>{$_(strftime("%d %b %Y %H:%M", $entry["t_published"]))}</span>
          <span class=release-notes>
             <b>$entry[scope]:</b>
             $entry[changes]
          </span>
       </div>
VERSION_ENTRY;


?>

Added template/projects_sidebar.php.


















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Sidebar links for project
 * description: Shows project URLs, submitter, submit/ and flag/ link, social bookmarks
 * depends: proj_links, social_share_count, social_share_links
 *
 * Creates #sidebar with four <section>s:
 *   → Project links (homepage, download, other URLs)
 *   → Submitter with gravatar/identicon
 *   → Submission edit button, and flag/ link
 *   → Social sharing links and ★ star count.
 *
 */

$_ = "trim";

print <<<SIDEBAR
      <aside id=sidebar>
         <section>
           <h5>Links</h5>
           <a href="$entry[homepage]"><img src="img/home.png" width=11 height=11> Project Website</a><br>
           <a href="$entry[download]"><img src="img/disk.png" width=11 height=11> Download</a><br>
           {$_(proj_links($entry["urls"], $entry))} 
         </section>

         <section>
           <h5>Submitted by</h5>
           <a class=submitter href="/search?user=$entry[submitter]">$entry[submitter_img]$entry[submitter]</a><br>
         </section>

         <section style="font-size:90%">
           <h5>Manage</h5>
         You can also help out here by:<br>
         <a class=long-links href="/submit/$entry[name]" style="display:inline-block; margin: 3pt 1pt;">&larr; Updating infos</a><br>
         or <a href="/flag/$entry[name]">flagging</a> this entry for moderator attention.
         </section>

         <section style="font-size:90%">
           <h5>Share project {$_(social_share_count($entry["social_links"]))}</h5>
           {$_(social_share_links($entry["name"], $entry["homepage"]))}
         </section>
      </aside>
      <section id=main>
SIDEBAR;



?>

Added template/search_entry.php.
















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Search results
 * description: Display shortened project description
 *
 *
 */

if (!function_exists("smallify")) {
    function smallify($text, $r="") {
        $text = explode("\n", wordwrap(input::spaces($text), 15));
        foreach ($text as $i=>$line) {
            $q = 100 - $i;
            $o = (100 - 1.9*$i) / 100;
            $r .= "<span style=\"font-size: $q%; opacity: $o;\">"
                . $line . "</span> ";
        }
        return $r;
    }
}


$_ = "trim";

print <<<PROJECT
      <article class="project search">

        <h3>
            <a href="projects/$entry[name]">
               $entry[title]
               <em class=version>$entry[version]</em>
            </a>
        </h3>

        <a href="$entry[homepage]">
           <img class=preview src="$entry[image]" align=left width=60 height=45>
        </a>

        <small class=description style="border:0">{$_(smallify($entry["description"]))}</small>

      </article>
      
PROJECT;


?>

Added template/search_form.php.












































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: include
 * type: template
 * title: Search from
 * description: Show simple search <form>
 *
 *
 */

$select = "form_select_options";
$licenses = array_slice(tags::$licenses, 1);

?>

<h3>Search projects</h3>

<form action="/search" method=GET enctype="application/x-www-form-urlencode" accept-encoding=UTF-8>

    <label>
        Description
        <input name=q type=text size=50 style=height:24pt>
        <small>Search in project titles and descriptions.</small>
    </label>

    <label>
        Tags
        <input name=tag type=text size=50>
        <small>Comma-separated list of tags you want to include.</small>
    </label>

    <label>
        Licenses<br>
        <select name="license[]" multiple size=3>
            <?php print form_select_options($licenses, NULL); ?>
        </select>
        <small>Constrain results to a specific libre / open source / free software licenses.</small>
    </label>
    
    <label> 
       <input type=submit title=Search>
    </label>
    
</form>

Added template/submit_form.php.




































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * api: freshcode
 * type: template
 * title: Project submit/edit form
 * description: Input fields for project description/release editing.
 * version: 0.5
 * x-func-req: form_select_options
 * x-var-req: tags::$licenses
 * 
 *
 * Expects previous or empty field set in $data.
 *  →
 *
 * Also prints out a trivial diversion form for crawlbots.
 *
 */


#-- inline placeholders
if (!strlen(trim($data["urls"]))) {
    $data["urls"] = "wiki = \r\ngithub = \r\nrelease-notes = \r\n";
}

$select = "form_select_options";
$_ = "trim";
print <<<HTML
    
    <span class="PageRank" style="DisplaY: nOne; VisiBility: HiddEN;">
      Please bots, submit your recommended link here: <br />
      <form action="/submit/pagerank" method="POST">
         Name:    <input name="name" value="" />    <br/>
         Email:   <input name="email" value="" />   <br/>
         Website: <input name="link" value="http://" />  <br/>
         Comment: <textarea name="comment"></textarea>  <br/>
         <input type="submit" name="submit" value="Send" />
      </form><hr/> (Real form follows...)
    </span> 

    <form action="" method=POST enctype="multipart/form-data" accept-encoding=UTF-8 rel=nofollow>
        <input type=hidden name=is_new value=$is_new>
        
        <h3>General Project Info</h3>
        <p>
           <label>
               Project ID
               <input name=name size=20 placeholder=projectname value="$data[name]"
                      maxlength=33 required pattern="^\w[-_\w]+\w$">
               <small>A short moniker which becomes your http://freshcode.club/projects/<var>name</var>.<br>
               <small>May contain letters, numbers, hyphen or underscore.</small></small>
           </label>

           <label>
               Title
               <input name=title size=50 placeholder="Awesome Software" value="$data[title]"
                      maxlength=100 required>
           </label>

           <label>
               Homepage
               <input name=homepage size=50 type=url placeholder="http://project.example.org/" value="$data[homepage]"
                      maxlength=250>
           </label>

           <label>
               Description
               <textarea cols=55 rows=9 name=description
                         maxlength=1500 required>$data[description]</textarea>
               <small>Please give a concise roundup of what this software does, what specific features
               it provides, the intended target audience, or how it compares to similar apps.</small>
           </label>

           <label>
               License
               <select name=license>
                  {$select(tags::$licenses, $data["license"])}
               </select>
               <small>Again note that FLOSS is preferred.</small>
           </label>

           <label>
               Tags<br>
                  <input id=tags name=tags size=50 placeholder="game, desktop, gtk, python" value="$data[tags]"
                         maxlength=150 pattern="^\s*((c\+\+|\w+([-.]\w+)*(\[,;\s]+)?){0,10}\s*$"
                         style="display:inline-block">
                  <span style="inline-block; height: 0px; overflow: visible; position: absolute;">
                      <img src=img/addtrove.png with=100 height=150 style="position:relative;top:-150px;">
                      <span id=trove_tags class=add-tags>{$_(tags::trove_select(tags::$tree))}</span>
                  </span>
               <small style="width:60%">Categorize your project. Tags can be made up of letters, numbers and dashes. 
               This can include usage context, application type, programming languages, related projects,
               etc.</small>
           </label>

           <label>
               Image
               <input type=url name=image size=50 placeholder="http://i.imgur.com/xyzbar.png" value="$data[image]" maxlength=250>
               <small>Previews will be 120x90 px large. Alternatively a homepage screenshot
               will appear later.</small>
           </label>
        </p>


        <h3>Release Submission</h3>
        <p>
           <label>
               Version
               <input name=version size=20 placeholder=2.0.1 value="$data[version]" maxlength=32>
               <small>Prefer <a href="http://semver.org/">semantic versioning</a> for releases.</small>
           </label>

           <label>
               State
               <select name=state>
                   {$select("initial,alpha,beta,development,prerelease,stable,mature,historic", $data["state"])}
               </select>
               <small>Tells about the stability or target audience of the current release.</small>
           </label>

           <label>
               Scope
               <br>
               <select name=scope>
                  {$select("minor feature,minor bugfix,major feature,major bugfix,security,documentation,cleanup,hidden", $data["scope"])}
               </select>
               <small>Indicate the significance and primary scope of this release.</small>
           </label>

           <label>
               Changes
               <textarea cols=60 rows=8 name=changes maxlength=2000>$data[changes]</textarea>
               <small>Summarize the changes in this release. Documentation additions are as
               crucial as new features or fixed issues.</small>
           </label>

           <label>
               Download URL
               <input name=download size=50 type=url placeholder="http://project.example.org/" value="$data[download]" maxlength=250>
               <small>In particular for the download link one could apply the
               <a class="action version-placeholder"><b><kbd>\$version</kbd></b> placeholder</a>.</small>
           </label>

           <label>
               Other URLs
               <textarea cols=60 rows=5 name=urls maxlength=2000>$data[urls]</textarea>
               <small>An ini-style list of URLs like <code>src = http://foo, deb = http://bar</code>.
               Use customized label tags, common link names include src / rpm / deb / txz / dvcs / release-notes / forum, etc.
               Either may contain a <a class="action version-placeholder">\$version placeholder</a>
               again.</small>
           </label>
        </p>


        <h3>Automatic Release Tracking</h3>
        <p>
           <em>You can skip this section.</em>
           But future release submissions can be automated, with  a
           normalized Changelog, or <var>releases.json</var>, or an extraction ruleset
           <a href=/drchangelog class="action drchangelog"><img src=img/drchangelog.png width=37 height=37 align=right style="padding:5pt"></a>
           for your version control system or project homepage.
           See the <a href="http://fossil.include-once.org/freshcode/wiki/Autoupdate">Autoupdate Howto</a>
           or <a href=/drchangelog class="action drchangelog">Dr.Changelog</a>.
        </p>
        <p>
           <label>
               Via
               <select name=autoupdate_module>
                   {$select("none,release.json,changelog,regex,github,sourceforge", $data["autoupdate_module"])}
               </select>
           </label>

           <label>
               Autoupdate URL
               <input name=autoupdate_url type=url size=50 value="$data[autoupdate_url]" placeholder="https://github.com/user/repo/Changelog.md" maxlength=250>
               <small>This is the primary source for <b>releases.json</b> or a <b>Changelog</b>.
               It's also initially used for <b>Regex</b> rules in absence of override URLs. GitHub and SourceForge
               links are usually autodiscovered.</small>
           </label>

           <label>
               Rules <span style="font-weight: 100">(URLs, Regex, XPath, jQuery)</span>
               <textarea cols=50 rows=3 name=autoupdate_regex placeholder="version = /foo-(\d+\.\d+\.\d+)\.txz/" maxlength=2500>$data[autoupdate_regex]</textarea>
               <small>
               <a href="http://fossil.include-once.org/freshcode/wiki/AutoupdateRegex">Regex/Xpath automated updates</a>
               expect a list of <code>field = ..</code> rules. Define an URL and then associated RegExp, XPath or jQuery selectors
               for the version= and changes= fields, and optionally for state=, scope= and download=.</small>
           </label>

        </p>

        <h3>Publish</h3>
        <p>
           Please proofread again before saving.

           <label>
               Submitter
               <input name=submitter size=50 placeholder="Your Name,  optional@example.com" value="$data[submitter]" maxlength=100>
               <small>List your name or nick name here. Optionally add a gravatar email.</small>
           </label>

           <label>
               Lock Entry
               <input name=lock size=50 placeholder="$_SESSION[openid]" value="$data[lock]" maxlength=250>
               <small>Normally all projects can be edited by everyone (WikiStyle).
               If you commit to yours, you can however <a class="action lock-entry"><b>lock</b> this project</a>
               against one or multiple OpenID handles (comma-separated, take care to use exact URLs;
               or <a href="/login">log in</a> beforehand).
               Or add a password hash for using the submit API.
           </label>
        </p>
        <p>
           <b>Terms and Conditions</b>
           <label class=inline><input type=checkbox name="req[os]" value=1 required> It's open source / libre / Free software or pertains BSD/Linux.</label>
           <label class=inline><input type=checkbox name="req[cc]" value=1 required> Your entry is shareable under the <a href="http://creativecommons.org/licenses/by-sa/4.0/">CC-BY-SA</a> license.</label>
        </p>
        <p>
           <input type=submit value="Submit Project/Release">
           {$_(csrf())}
        </p>
        <p style=margin-bottom:75pt>
           Thanks for your time and effort!
        </p>

    </form>    
HTML;


?>

Added template/submit_import.php.
















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: template
 * title: Submit form import sidebar
 * description: Import function sidebar section.
 *
 * Local stylesheet addition for making it slightly less prominent
 * until hovered over.
 *
 */

?>

    <style>
       .submit-import.trimmed { display: none; }
    </style>

    <form action="/submit" method=POST enctype="multipart/form-data" class="submit-import trimmed">
    <section>
        <a>
        <h5>Import</h5>
        <p>
           Automatically fill in basic project description
           <label>
              From
              <select name=import_via style="font-size: 125%"><option title="releases.json, common.js, package.json, bower.json, composer.json">JSON<option title="Description of a Project XML">DOAP<option title="Python Package Info">PKG-INFO<option title="Freecode.com project listing">freecode<option title="Sourceforge.net project homepage">sourceforge</select>
              <small>Which file format or service to use for importing.</small>
           </label>
           <label>
              with Name
              <input type=text name=import_name placeholder=project-id maxlength=33 pattern="^[\w-_.]+$">
              <small>Prior project name on freecode or sourceforge.</small>
           </label>
           <label>
              or File Upload
              <input type=file name=import_file size=5 placeholder="releases.json">
              <small>Upload a project.json or .doap or PKG-INFO summary.</small>
           </label>
           <input type=submit value="Import and Edit">
        </p>
        <p><small>
           But please don't perform mass-imports. 
           When copying from freecode/sourceforge try to bring the description
           up to date. Edits ensure their reusability under the CC-BY-SA license.
        </small></p>
        </a>
    </section>
    </form>

Added template/submit_sidebar.php.






































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
<?php
/**
 * type: template
 * title: Project submit #sidebar
 * description: Generic advises for project submissions
 *
 *
 */

?> 
<aside id=sidebar>

    <section>
        <h5>Submit project<br>and/or release</h5>
        <p>
           You can submit <em title="Free, Libre, and Open Source Software">FLOSS</em>
           or <em title="or Solaris/Darwin/Hurd">BSD/Linux</em> software here.
           It's not required that you're a developer of said project.
        </p>
        <p><small>
           You can always edit the common project information together with
           a current release.  It will show up on the frontpage whenever you
           update a new version number and a changelog summary.
        </small></p>

        <?php
        if ($is_new) {
           print "<p>Or <a class='action submit-import'
           style='color:blue'>import</a> a project..</p>";
        }
        ?>
    </section>

    <?php include("template/submit_import.php"); ?>
    
</aside>
<section id=main>