⌈⌋ ⎇ branch:  freshcode


Check-in [df89a65d10]

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

Overview
Comment:Remove uneeded pages, cron-jobs, doc/*, templates for FC-mirrors.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | mirror
Files: files | file ages | folders
SHA1: df89a65d102136b21b24e130025dd3307cfc729d
User & Date: mario 2014-11-16 15:53:45
Context
2014-11-16
16:02
Allow direct invocations, remove [END] and API mapping. check-in: 72d74c8cca user: mario tags: mirror
15:53
Remove uneeded pages, cron-jobs, doc/*, templates for FC-mirrors. check-in: df89a65d10 user: mario tags: mirror
15:50
Empty default SQlite store (for mirroring) check-in: 80739648ae user: mario tags: mirror
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Deleted 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
31
32
33
34
<?php
/**
 * api: freshcode
 * title: Autoupdate runner
 * description: Cron job for invoking autoupdates on per-project basis
 * version: 0.6.0
 * depends: curl
 * author: mario
 * license: AGPL
 * x-cron: 15 03 * * *
 * 
 *
 * 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
$_SESSION["submitter"] = "";
$run = new Autoupdate();
$run->debug = 1;
$run->msg_html = 0;
$run->all();
#print_r($run->test("regex", "linux"));
#print_r($run->test("regex", "php"));
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































Deleted cron.daily/news_github.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
<?php
/**
 * title: github releases news feed
 * description: fetch from cache database, build feed and releases page
 * version: 0.3
 * category: template
 * api: cli
 * type: cron
 * x-cron: 15 * * * *
 *
 * Short summary
 *     → ./template/feed.github.htm
 * Long table
 *     → ./template/github-releases.htm
 *
 */


chdir(dirname(__DIR__));
include("./config.php");
db(new PDO("sqlite:github.db"));


// query
$recent = db("
       SELECT t_published, created_at, repo_name, author_login, author_avatar,
              release_url, release_tag, release_title, release_body,
              repo_url, repo_description, repo_homepage,
              COALESCE(NULLIF(repo_language, ?), ?) AS repo_language
         FROM releases
        WHERE LENGTH(repo_description) > 0
     GROUP BY repo_name
     ORDER BY t_published DESC
        LIMIT 500
", "", "no-lang");

// prepare output
$lang = "";
$out = [];
$full = "";

// printout
foreach ($recent as $r) {
    $r = array_map("htmlspecialchars", $r);

    
    #-- filter some
    if (preg_match("~/(test|main)$~", $r["repo_name"])
    or  preg_match("~Contribute to .+? by creating an account on GitHub~", $r["repo_description"]))
    {
       continue;
    }


    #-- sidebar feed
    if (count($out) < 25) {
       $name = trim(strstr($r["repo_name"], "/"), "/");
       $out[] = "   <a href=\"$r[release_url]\" title=\"$r[repo_description]\">$name "
              . "<em class=version title=\"$r[release_title]\">$r[release_tag]</em></a>";
    }


    #-- complete list
    $verdesc = input::spaces(substr($r["release_body"], 0, 200));
    $name = explode("/", $r["repo_name"]);

    // project blob    
    $full .= <<<HTML
 <tr class="github release">
    <td class=author-avatar><img src="$r[author_avatar]&s=40" alt="$r[author_login]" height=40 width=40></td>
    <td class=repo-name>
       <a href="$r[repo_url]" title="$r[repo_name]">
          <small class=repo-org>$name[0] /</small>
          <strong class=repo-localname>$name[1]</strong>
       </a>
       <span class=repo-language>$r[repo_language]<span>
    </td>
    <td class=repo-description>
       $r[repo_description]
       <a class=repo-homepage href="$r[repo_homepage]">$r[repo_homepage]</a>
    </td>
    <td class=release>
        <a class=release-tag href="$r[release_url]"><em class=version title="$r[release_title]">$r[release_tag]</em></a>
        <span class=release-body>$verdesc</span>
    </td>
 </tr>
 
HTML;
}

// write
file_put_contents("./template/feed.github.htm", implode("\n", $out));
file_put_contents("./template/github-releases.htm", $full);


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































Deleted cron.daily/poll_githubarchive.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
<?php
/**
 * title: GitHubArchive releases poll
 * description: Fetch GitHub releases via BigQuery githubarchive:github.timeline
 * version: 0.4
 * category: rpc
 * api: cli
 * depends: config.local
 * doc: http://www.githubarchive.org/
 * github-url: https://github.com/igrigorik/githubarchive.org
 * type: cron
 * x-cron: 05 * * * *
 *
 * Queries githubarchive.org event blobs.
 * (Fetching via Google BigQuery too easily exceeded the quotas.)
 *
 * JSON blobs are stored under:
 *    http://data.githubarchive.org/2014-10-30-{0..23}.json.gz
 * which contain newline-separated JSON objects.
 *   → fetched via SplFileObject and gzip-decoding stream prefix
 *   → pre-filtered for "type":"ReleaseEvent" by RegexIterator
 *   → merged with repo meta data (desc, urls, lang) per GitHub API
 *
 * Stores everything into github.db cache table.
 *
 *    CREATE TABLE releases ( 
 *        t_published      INT,
 *        created_at       VARCHAR,
 *        repo_name        VARCHAR,
 *        author_login     VARCHAR,
 *        author_avatar    VARCHAR,
 *        release_url      VARCHAR UNIQUE,
 *        release_tag      VARCHAR,
 *        release_title    VARCHAR,
 *        release_body     TEXT,
 *        repo_url         VARCHAR,
 *        repo_description TEXT,
 *        repo_homepage    VARCHAR,
 *        repo_language    VARCHAR,
 *        UNIQUE ( repo_name, release_tag )  ON CONFLICT FAIL 
 *    );
 *    CREATE INDEX idx_releases ON releases ( 
 *        t_published ASC 
 *    );
 *    CREATE INDEX unique_releases ON releases ( 
 *        repo_name,
 *        release_tag 
 *    );
 *
 * Which allows easier display/feed generation for news_github.php.
 *
 */


// Common settings
chdir(dirname(__DIR__));
include("./config.php");
// Separate github.releases database
db(new PDO("sqlite:github.db"));




/**
 * GitHubArchive via Google BigQuery
 * (unused now)
 *
 */
class GHA_BQ {

    /**
     * Google API OAuth connection
     *
     */
    function Google_API_Client() {
        $client = new Google_Client();
        $client->setApplicationName("freshcode-github");
        $client->setDeveloperKey(GOOGLEAPI_DEV_KEY);
        $client->setClientId(GOOGLEAPI_CLIENT_ID);
        $cred = new Google_Auth_AssertionCredentials(
             GOOGLEAPI_EMAIL,
             array('https://www.googleapis.com/auth/bigquery'),
             file_get_contents(GOOGLEAPI_KEYFILE)
        );
        $client->setAssertionCredentials($cred);
        $client->getAuth()->isAccessTokenExpired() and $client->getAuth()->refreshTokenWithAssertion($cred);
        return $client;
    }


    /**
     * Populates BigQuery configuration and Job,
     * executes it right away, and waits for response.
     *
     */
    function BigQuery_execute($sql) {

        // new BigQuery + Google_Client instances
        $client = new Google_Service_Bigquery( self::Google_API_Client() );
        
        // create query job
        $job = new Google_Service_Bigquery_Job();
        $config = new Google_Service_Bigquery_JobConfiguration();
        $queryConfig = new Google_Service_Bigquery_JobConfigurationQuery();
        $queryConfig->setQuery($sql);
        $queryConfig->setPriority("INTERACTIVE");  // speedier results
        $config->setQuery($queryConfig);
        $job->setId(md5(microtime()));
        $job->setConfiguration($config);

        // run job and pack results
        $res = $client->jobs->getQueryResults(
            GOOGLEAPI_PROFILE,
            $client->jobs->insert(GOOGLEAPI_PROFILE, $job)->getJobReference()["jobId"]
        );
        return $res;
    }


    // run query for recent project releases
    function githubarchive_releases() {
       return self::BigQuery_execute("
           SELECT
                TIMESTAMP(created_at) as t_published,
                created_at, url, type,
                repository_url, repository_owner, repository_name,
                repository_description, repository_language, repository_homepage
                -- , payload_name, payload_url, payload_desc, payload_commit, payload_member_avatar_url
                -- , payload_release_tag_name, payload_release_name, payload_release_body
             FROM
                [githubarchive:github.timeline]
            WHERE
                type='ReleaseEvent'
                -- AND LENGTH(repository_description) > 0
         ORDER BY
                created_at DESC
            LIMIT
                500
        ");
    }
}




/**
 * Query GitHub projects (max 5000/hour)
 *
 */
class GitHub_API {

    /**
     * Generic wrapper for simple GitHub API
     *
     */
    function call($api = "events", $data = []) {
        return json_decode(
            curl("https://api.github.com/$api")
            ->userpwd(GITHUB_API_PW)
            //->writeheader(fopen("gh-curl-header.txt", "a+"))
            ->timeout(5)
            ->exec()
        );
    }


    /**
     * Retrieve Github global /events (push, pull, commit, comment, release, ...)
     *
     */
    function events() {
        return self::call("events");
    }


    /**
     * Fetch meta data for repository
     *
     */
    function repo_meta($fullname) {
        return self::call("repos/$fullname");
    }


    /**
     * HTML extract repo title
     * @obsolete, see `repo_meta`
     *
     */
    function repo_title($gh_url) {
        preg_match('~<meta\s+content="([^<>"]+?)"\s+property="og:description"\s+/?>~s', curl($gh_url)->exec(), $m);
        $gh_title = htmlentities(html_entity_decode($m[1]));
        if (preg_match("/Contribute to .+? development by creating an account on GitHub./", $gh_title)) {
            continue;
        }
    }


    /**
     * Normalize Github.com URL, split full, owner and repo name
     *
     */
    function repo_name($url) {
        if (preg_match("~^https?://github\.com/([\w.-]+)/([\w.-]+)(?:/|$)~", $url, $m)
        or  preg_match("~^https?://(?:api|uploads)\.github\.com/(?:repos|users)/([\w.-]+)/([\w.-]+)(?:/|$)~", $url, $m))
        {
            return array("https://github.com/$m[1]/$m[2]/", "$m[1]/$m[2]", $m[1], $m[2]);
        }
    }

}




/**
 * Retrieve event archives from //data.githubarchive.org/YYYY-MM-DD-HH.json.gz
 *
 */
class GitHubArchive {
    
    /**
     * date/timespec for GHA
     *
     */
    function last_hour($hour=1) {
        // return 2014-05-31-hH (minus 8 hours, because GHA uses filenames according to its own timezone)
        return gmdate("Y-m-d-G", time() - 5*60 - (7 + $hour) * 3600);
    }


    /**
     * Fetch GHA day+hour .json.gz resource line-wise filtered by ReleaseEvent type
     *
     */
    function json_lines($day_hour) {
        // open remote resource and decompress implicitly
        $gz = new SplFileObject("compress.zlib://http://data.githubarchive.org/$day_hour.json.gz");
        // filter JSON blob lines on type=ReleaseEvents
        return new RegexIterator($gz, '/ "type" : "ReleaseEvent" /smux');
    }

    
    /**
     * Iterator over last GitHubArchive data files
     *
     */
    function fetch_json_last_hours($count = 2) {
        $it = new AppendIterator;
        foreach (range(1, 24) as $prev) {
            // Keep iterating for successful URL accesses
            try {
                $it->append(self::json_lines($day_hour = self::last_hour($prev)));
                print "fetching .../$day_hour.json.gz\n";
            }
            catch (Exception $http_fail) {  // SplFileObject just gives RuntimeException for HTTP 404 results
                print "not present .../$day_hour.json.gz\n";
                continue;
            }
            // Retrieve at least two blob files
            if (--$count <= 0) {
                break;
            }
        }
        return $it;
    }

}


/**
 * Apply callback over iterator values.
 *
 */
class CallbackIterator extends IteratorIterator
{
    public $mapfn;

    function __construct($iter, $mapfn) {
        $this->mapfn = $mapfn;
        parent::__construct($iter);
    }

    function current() {
        return call_user_func($this->mapfn, parent::current());
    }
}




#-- Select input source,
#   JSON lines/blobs/files, pre-converted to objects

// from GHA files
$in_json = new CallbackIterator(GitHubArchive::fetch_json_last_hours(), "json_decode");

// direct polling of GitHub API (priorly polled in a loop)
#$in_json = GitHub_API::events();

// previously collected event blobs
#$in_json = new CallbackIterator(new CallbackIterator(new GlobIterator("./@/github/ev*"), "file_get_contents"), "json_decode");



#-- Load into cache database
insert_github_releases($in_json);




/**
 * Traverse github /events JSON objects and update DB.
 *
 */
function insert_github_releases($in_json) {
    foreach ($in_json as $event) {

        // Properties from events payload
        $p = array(
            "t_published" => strtotime($event->payload->release->published_at),
            "created_at" => $event->payload->release->created_at,
            "repo_name" => GitHub_API::repo_name($event->payload->release->html_url)[1],
            "author_login" => $event->payload->release->author->login,
            "author_avatar" => $event->payload->release->author->avatar_url,
            "release_url" => $event->payload->release->html_url,
            "release_tag" => $event->payload->release->tag_name,
            "release_title" => $event->payload->release->name,
            "release_body" => $event->payload->release->body,
        );

        // Skip existing entries    
        $exists = db("
             SELECT t_published
               FROM releases
              WHERE release_url = ?
                 OR (repo_name = ? AND release_tag = ?)",
             $p["release_url"], $p["repo_name"], $p["release_tag"]
        );
        if ($exists->t_published) {
            print "already have $p[release_url]\n";
            continue;
        }
        
        // Add additional repository infos
        if ($meta = GitHub_API::repo_meta($p["repo_name"])) {
            $p = $p + array(
                "repo_url" => $meta->html_url,
                "repo_description" => $meta->description,
                "repo_homepage" => $meta->homepage,
                "repo_language" => $meta->language,
            );
        }
        else {
            continue;
        }

        // Store
        print_r($p);
        db("INSERT INTO releases (:?) VALUES (::)", $p, $p);
    }
}



?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































































































































































































































































































































































































































































































































































































Deleted 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
55
56
57
<?php
/**
 * api: cli
 * title: Social links count
 * description: Queries api.i-o/links for project homepages
 * version: 0.1
 * category: rpc
 * type: cron
 * x-cron: 20 03 * * *
 *
 * 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")->fetchAll() 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"],
        )
    );

}


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































Deleted cron.daily/twitter.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
<?php
/**
 * api: cli
 * title: Twitter bridge
 * description: Posts recent releases on twitter
 * version: 0.1
 * category: rpc
 * type: cron
 * x-cron: 50 * * * *
 *
 * Summarize new releases for Twitter feed.
 * Currently using `twidge` (pre-configured in $HOME),
 * which doesn't support //t.co/ inline images yet.
 *
 */


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



/**
 * Releases within the last hour
 *
 */
$rel = db("
    SELECT *
      FROM release_versions
     WHERE t_published >= ?
    ", time()-7300
)->fetchAll();



// query recent/previous status messages
$prev = twit("lsarchive");

// condense individual tweets
foreach ($rel as $row) {

    // skip existing
    if (is_int(strpos($prev, $row["title"]))) {
    print "skip($row[name]) ";
        continue;
    }
    
    // homepage
    if (empty($row["homepage"]) or strlen($row["homepage"] > 80)) {
        $row["homepage"] = "http://freshcode.club/projects/$row[name]";
    }

    // prepare post
    $msg = "$row[title] $row[version] released. $row[homepage]";
    $msg = preg_replace("/\s+/", " ", $msg);
    
    // add tags
    $tags = p_csv($row["tags"]);
    shuffle($tags);
    foreach ($tags as $tag) {
        $tag = preg_replace("/^(\w)\+\+/", "\\1pp", $tag);
        $tag = preg_replace("/-/", "_", $tag);
        $msg .= " #$tag";
    }
    
    // cut to limit
    while (strlen($msg) > 140) {
        $msg = preg_replace("/\s\S+$/s", "", $msg);
    }
    
    // send
    print_r("$msg\n");
    twit("update", $msg);
}




/**
 * Invoke cmdline twitter client
 *
 */
function twit() {
    $cmd = "twidge";
    foreach (func_get_args() as $param) {
        $cmd .= " " . escapeshellarg($param);
    }
    return ` $cmd `;
}


?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
























































































































































































Changes to db.sql.

1

2
3
4

5
6
7
8
9
10
11
#

# title: freshcode database schema
# version: 0.7
#


CREATE TABLE [release] ( 
    name              VARCHAR( 100 )     NOT NULL,
    title             TEXT               NOT NULL,
    homepage          TEXT,
    description       TEXT               NOT NULL,
    license           VARCHAR( 100 ),
<
>
|
|
<
>








1
2
3

4
5
6
7
8
9
10
11

--#
--# title: freshcode database schema
--# version: 0.7

--#

CREATE TABLE [release] ( 
    name              VARCHAR( 100 )     NOT NULL,
    title             TEXT               NOT NULL,
    homepage          TEXT,
    description       TEXT               NOT NULL,
    license           VARCHAR( 100 ),

Deleted 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-----
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































Deleted 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

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































































































































































































































































































































































































































































































Deleted doc/freecode2releases.py.

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
#!/usr/bin/env python
# encoding: utf-8
# api: cli
# type: main
# title: freecode-to-releases     
# description: Extracts project descriptions+history from Freecode.com into releases.JSON
# category: scraping
# version: 0.7
# config:
#   <env-unused name=XDG_CONFIG_HOME value=~/.config description="user config base dir"> 
# license: MITL
# doc: http://fossil.include-once.org/freshcode/wiki/freecode2releases
# 
#
# Fetches prior freshmeat/freecode.com project listings, and extracts   
# the version history. Packages data up as `releases.json` format, which  
# makes a nice backup format and exchange format. And also suits importing
# into freshcode.club listings.
#
# Notably this should only be done by package maintainers to retain their
# original authorship and thus reusability.
#

import sys
import errno
import collections
from datetime import datetime
import re
import requests
try:
    from bs4 import BeautifulSoup as bs
except:
    print("f2r: BeatifulSoup missing,\nuse `apt-get install python-bs4` || `pip install beautifulsoup4`\n")
    exit(errno.ENOPKG)
try:
    import simplejson as json
except:
    import json



# scrape from freecode.com
def freecode_fetch(name):
    try:
        url = "http://freecode.com/projects/%s" % name
        html = bs(requests.get(url).text)
    except:
        print("project not found, %s" % url)
        return None
    # le basics
    r = collections.OrderedDict([
        ("$feed-license", "author/editor"),
        ("$feed-origin", url),
        ("name", name),
        ("title", html.find("meta", {"property": "og:title"})["content"]),
        ("oneliner", html.find("meta", {"property": "og:description"})["content"]),
        #("image", "http://freshcode.com" + html.find("meta", {"property": "og:image"})["content"]),
        ("keywords", html.find("meta", {"name": "keywords"})["content"]),
        ("description", html.select("div.project-detail p")[0].string),
        ("tags", freecode_tags(html.select("#project-tag-cloud a"))),
        ("submitter", html.select("li.avatar a.avatar")[0]["title"]),
        ("urls", freecode_urls(html.select(".sub-navigation li.url a"))),
        ("releases", freecode_releases(name)),
    ])
    return r


# extract tag basename from <a> link list
def freecode_tags(li):
    return ", ".join([  a["href"][6:] for a in li  ])


# convert url list <li> <a> into dict
def freecode_urls(li):
    r = [  (a.string, "http://freecode.com" + a["href"]) for a in li  ]
    return collections.OrderedDict(r)


# fetch releases pages
def freecode_releases(name):
    last_page = 1
    page = 1
    r = []
    while page <= last_page:
        # iterate through /releases pages
        url = "http://freecode.com/projects/%s/releases%s" % (name, ("?page=%s" % page if page else ""))
        html = bs(requests.get(url).text)
        for ver in html.select("div.release.clearfix"):
            # remove changelog gimmicks
            for rm in ("strong", ".truncate_ellipsis", ".truncate_more_link"):
                for e in ver.select(rm):
                    e.replace_with("");
            # collect
            r.append({
                "version": ver.select("li.release a")[0].string,
                "changes": "".join(ver.select("p.truncate.changes")[0].contents).strip(),
                "scope": "incremental",
                "state": "historic",
                "published": strftime(ver.select("li.rdate")[0].string.strip()).isoformat(),
                "submitter": ver.select(".author a")[0]["title"]
            })
        # next page
        try:
            last_page = int(html.select(".pagination a")[-2].string)
        except:
            last_page = 1
        page = page + 1
    return r


# try to deduce time from different formats
def strftime(s):
    for fmt in [
        "%d %b %Y %H:%M",
        "%Y-%m-%d %H:%M"
    ]:
        try:
            return datetime.strptime(s, fmt)
        except:
            None
    pass


# process CLI arguments, invoke retrieval methods
def freecode_cli(argv0="f2r", name="", output=""):
    if name:
        output = output or "%s-releases.json" % name
        json.dump(freecode_fetch(name), open(output, "wt"), indent=4)
    else:
        print("synopsis: freecode2releases.py [projectname [output.json]]");
        print("Please only download and resubmit project data you initially wrote yourself.");
    return


# argv to main
if __name__ == "__main__":
    freecode_cli(*sys.argv)


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































































































































Deleted doc/logo.svgz.

cannot compute difference between binary files

Deleted 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);
    }
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































































Deleted doc/trove.ods.

cannot compute difference between binary files

Deleted 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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/**
 * 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);
   font-size: 92pt;
   font-size: 12.5vh;
   position: relative;
   top: 500pt;
   top: 67vh;
   white-space: nowrap;
   letter-spacing: -0.025em;
}
h1 * {
   color: #fff;
   text-decoration: none;
}
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 10pt 30pt;
   list-style: none;
}
ul.forum > li {
   padding-bottom: 10pt;
}
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;
   z-index: 7;
   width: 600pt;
}
.forum .entry .excerpt:hover {
   color: #5a5a5a !important;
   cursor: nw-resize;
}
.forum .entry .trimmed {
   /* hidden divs */
   display: none;
}
.forum .entry {
   color: #444;
}
.forum .entry .content p {
   padding-top: 0; margin-top: 0;
}
.forum .entry .content ul {
   margin: 10pt !important;
   padding: 0 !important;
}
.forum .entry .content ul li:before {
   content: "→";
   margin-right: 5pt;
   color: #522;
}
.forum .entry .content code {
   background: #eee;
   font-size: 90%;
   padding: 1pt;
   border-radius: 3pt;
}


.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
 *
 */
label {
   display: block;
   padding: 5pt;
}
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;
}
input, textarea, select {
   font-size: 105%;
   padding: 1.5pt;
   border: 1px solid #bbb;
   border-left: 5pt solid #ccc;
   border-radius: 5pt;
}
.error, .warning {
   background: #eeddaa;
   border: 2px solid #ddcc99;
   border-radius: 5pt;
   padding: 5pt;
}
.error {
   background: #eebbaa;
}


<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































































































































































































































































































































































Deleted 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
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
<?php
/**
 * api: freshcode
 * title: Submit API
 * description: Implements the Freecode JSON Rest API for release updates
 * version: 0.3
 * 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 request 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 {


    // HTTP method
    var $method;

    // API function
    var $api;

    // Project name
    var $name;

    // Optional revision ID (just used for releases/; either "pending" or t_published timestamp) 
    var $rev;
    
    // inner @array from JSON request body
    var $body;
    
    // Optional auth_code (from URL or JSON body)
    var $auth_code;
    
    
    // Logging
    var $log = TRUE;
    var $timestamp = 0;




    /**
     * 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->rev = $_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->stripos…json->is_int["CONTENT_TYPE"]) {
            $this->body = json_decode(file_get_contents("php://input"), TRUE);
            $this->auth_code = $this->body["auth_code"];
        }
        
        // Logging
        $this->timestamp = sprintf("%s (%s)", time(), gmdate(DATE_ISO8601, time()));
        $this->log($this, "\n\n\n/* << REQUEST */");
    }
    
    
    /**
     * Log incoming request, outgoing response, or data set as prepared prior updating DB.
     *
     */
    function log($what, $prefix) {
        if ($this->log) {
            file_put_contents("api-log.txt", "$prefix\n" . json_encode($what, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n", FILE_APPEND);
        }
        return $what;
    }


    
    /**
     * Invoke API target function.
     * After retrieving current project data.
     *
     */
    function dispatch() {
    
        // Fetch latest revision
        $project = new release($this->name);
        if (!$project["name"]) {
            $this->error_exit(NULL, "404 No such project", "Invalid Project ID");
        }
        
        // Run dialed method, then output JSON response.
        $this->json_exit(
            $this->{ $this->api ?: "error_exit" }($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" => f_tags(join(" ", $core->text["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"],
            "deleted" => 0,
        );
        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->rev === "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 marked "deleted"
     * (formerly just "hidden" and/or flagged for moderator attention.)
     * This somewhat still may terminate a project lifeline (due to VIEW
     * revision grouping), but can be undone by submitting the release
     * anew.
     *
     * 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
        $this->requires_permission($project);
        assert(is_numeric($this->rev));

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

        return $r ? array("success" => TRUE) : $this->error_exit(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") {
            $urls = p_key_value($project["urls"], NULL);
            $urls["homepage"] = $project["homepage"];
            $urls["download"] = $project["download"];
            return array("urls" => $urls);
        }
        
        /**
         * Updates may come as PUT, POST, PUSH request
         *
         * @auth-required
         *
         */
        else {

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

            $new["urls"] = "";
            foreach ($urls as $label => $url) {

                // Remove non-alphanumeric characters
                $label = trim(preg_replace("/\W+/", "-", $label), "-");
                $lower = strtolower($label);
                in_array($lower, ["home-page", "website"]) and $lower = "homepage";

                // Split homepage, download URL into separate fields,
                if ($lower == "homepage" or $lower == "download") {
                    $new[$lower] = $url;
                }
                // While remaining go into `urls` key=value block, retain case-sensitivity here
                else {
                    $new["urls"] .= "$label = $url\r\n";
                }
            }

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


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

        // Write permissions required obviously.
        $this->requires_permission($project);
 
        // Log data
        $this->log($new, "/* ++ STORE DATA */");

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

        // Store or return JSON API error.
        return ($project->store() and (header("Status: 201 Created") + 1))
             ? array("success" => TRUE)
             : $this->error_exit(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["submitter_image"],
                $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.
     *
     * @exit if unauthorized
     *
     */
    function requires_permission($data) {
        return $this->is_authorized($data)
             ? $data
             : $this->error_exit(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");
        $this->log($data, "/* >> RESPONSE */");
        exit(
            json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
        );
    }


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

}



?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































Deleted 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,sourceforge"),
         "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,sourceforge", $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");

?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































































































































































































































































































































































































Deleted 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?019"></script>
    <meta charset=UTF-8>
    <?= "<style>\n"
      . file_get_contents("forum.css")
      . "</style>";
    ?>
</head>
<body>
<div id=title>
   <h1><a href="/"><b>fresh</b>(code)<b class=red>.</b><span class=grey>club</span></a></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>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































































































Deleted page_githubreleases.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
<?php
/**
 * api: freshcode
 * title: github-releases
 * description: dump releases feed
 * version: 0.3
 *
 * Shows the summarized GitHub releases from the stored template dump
 * (updated by cron.daily/news_github.php from GHA and cache DB.)
 *
 */

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

  <style>
    .github-releases {
       width: 100%;
    }
    #githubreleases {
       table-layout: fixed;
       width: 100%;
    }
    .github.release td {
       padding: 4pt 1pt;
       font-size: 95%;
       overflow: hidden;
       text-overflow: ellipsis;
       box-shadow: none;
    }
    .github.release .author-avatar img {
       border-radius: 4pt;
    }
    .github.release .repo-name {
    }
    .github.release .repo-name small {
       display: block;
       font-size: 85%;
       color: #555;
    }
    .github.release .repo-name strong {
       font-weight: 400;
       display: block;
    }
    .github.release .repo-description {
       font-size: 90%;
    }
    .github.release .repo-homepage {
       font-size: 70%;
       display: block;
       color: #b0b0f0;
    }
    .github.release .release-tag {
       font-weight: 700;
    }
    .github.release .release-body {
       font: 70%/80% normal;
       max-height: 25pt;
       color: #999;
    }
    .github-releases .repo-language {
       font-size: 60%;
       padding: 0.5pt 1pt;
       border: dotted 1px #eef;
       background: #f1f3ff;
       color: #aae;
    }
  </style>

  <section id=main style="width:70%">
  <h2>GitHub Releases</h2>
  <article class=github-releases>

     <table id=githubreleases>
     <colgroup>
        <col width="5%">
        <col width="25%">
        <col width="35%">
        <col width="35%">
     </colgroup>
<?php include("template/github-releases.htm"); ?>
     </table>

  </article>

  <p style="break: both; clear: all; background: #f3f5f7; padding: 20pt;;">
    Project information courtesy of
    <a href="http://githubarchive.org/">http://githubarchive.org/</a>
    and the GitHub API.
  </p>

<?php
include("template/bottom.php");

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




























































































































































































Deleted 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
<?php
/**
 * api: freshcode
 * type: page
 * title: Submit/edit project or release
 * description: Single-page edit form for projects and their releases
 * version: 0.7.0
 * category: form
 * license: AGPLv3
 * 
 * Prepares the submission form. On POST checks a few constraints,
 * 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",
);


// Get project ID from request
$name = $_REQUEST->proj_name->length…3…33["name"];

// Retrieve existing project data in DB.
$data = release::latest($name);
$is_new = empty($data);


// 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());
    }
}


// Project entry can be locked for editing by specific OpenIDs.
if (!release::permission($data, $_SESSION["openid"])) {
    $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"));
}



// Start page output
include("template/header.php");
include("template/submit_sidebar.php");


/**
 * Fetch form input on submit.
 * Check some constraints.
 * Then insert into database.
 *
 */
if ($name and $_REQUEST->has("title", "description")) {

    // Check field lengths
    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->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 {
    
        // Merge new data
        $release = new release($data);
        $release->update(
            $_REQUEST,
            array(
                "flag" => 0,   // User flags presumably become obsolete when project gets manually edited
                "submitter_openid" => $_SESSION["openid"],
                "via" => "form",
            )
        );
        
        // Update project
        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 database error. Please retry later.";
        }
    }

}


#-- Output input form with current $data
else {
    $data = array_map("input::html", $data);
    include("template/submit_form.php");
}


include("template/bottom.php");




?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































































































































Deleted 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
352
<?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"],
                "urls" => "SourceForge = https://sourceforge.net/projects/$name\nGitHub = \n",
            );
        }
    }

}


#print_r((new project_import)->freecode("firefox"));



?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































































































































































































































































































































































































































































































































































































































































Changes to 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>








<
<

|



|



<
<





1
2
3
4
5
6
7
8


9
10
11
12
13
14
15
16
17


18
19
20
21
22
</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> |


<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://opensourcestore.org/"); ?>
</small>
<br>
<small style="font-size:90%">


All project entries are licensed as CC-BY-SA. There will be <a href="/feed">/atom+json feeds</a>..
</small>
</footer>

</html>

Deleted 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><b>$entry[summary]</b>
             <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;

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































Deleted 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="<?=$image?>">
   </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>

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
















































































































































Changes to template/header.php.

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
 * page $title.
 *
 */
?>
<!DOCTYPE html>
<html>
<head> 
    <title><?= isset($title) ? $title : "freshcode.club" ?></title>
    <meta name=version content=0.7.4>
    <meta charset=UTF-8>
    <link rel=stylesheet href="/freshcode.css?0.7.3">
    <link rel="shortcut icon" href="/img/changes.png">
    <base href="/index">
    <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?2"></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.7.4 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>
<?=file_get_contents("template/stats.htm");?>
</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" class=meta>Meta</a>
</nav>









|














|

|
<
|
>










|





<

<



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
 * page $title.
 *
 */
?>
<!DOCTYPE html>
<html>
<head> 
    <title><?= isset($title) ? $title : "opensourcestore.org" ?></title>
    <meta name=version content=0.7.4>
    <meta charset=UTF-8>
    <link rel=stylesheet href="/freshcode.css?0.7.3">
    <link rel="shortcut icon" href="/img/changes.png">
    <base href="/index">
    <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?2"></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.7.4]</small>' : '<b style="color:#c54">[local dev]</b>'; ?>
<span style=float:right>
<a href="http://fossies.org/">fossies.org</a> |

<a href="http://freshcode.club/">freshcode.club</a> |
<b><a href="//opensourcestore.org/">opensourcestore.org</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>
<?=file_get_contents("template/stats.htm");?>
</footer>

<nav id=tools>
   <a href="/">Home</a>
   <a href="#" onclick="void(window.location='http://fresh'+'code.club'+'/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="/links">Links</a>

</nav>


Changes to template/projects_sidebar.php.

27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

         <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;



?>







|
|
|
<













27
28
29
30
31
32
33
34
35
36

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

         <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>Update</h5>
         You can update this project information, or new releases<br>
         <a class=long-links href="#" onclick="void(window.location = 'http://freshcode.club/'+'submit/$entry[name]')" style="display:inline-block; margin: 3pt 1pt;">&larr; Updating infos</a>

         </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;



?>

Deleted 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
229
230
231
232
233
234
235
<?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";
}

// editors note
$editor_note = $data["editor_note"] ? "<var class=editor-note>$data[editor_note]</var>" : "";

// varexpressions
$select = "form_select_options";
$_ = "trim";

// output
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>
    {$editor_note}
        <p>
           <label>
               Project ID
               <input name=name size=20 placeholder=projectname value="$data[name]"
                      maxlength=33 required pattern="^\w[-_\w]+\w(\.\w{2,7})?$">
               <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;


?>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































































































































































































































































































































































































































Deleted 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>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































Deleted 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>

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<