Commit

Author:

Hash:

Timestamp:

+271 -2 +/-5 browse

Kevin Schoon [me@kevinschoon.com]

3548bc75b1749d43cfaef9538fb427448f6232fe

Tue, 02 Dec 2025 12:47:57 +0000 (4 days ago)

hack up a basic semi-functioning ajax based validator
hack up a basic semi-functioning ajax based validator

More to do:

* Most of the specification is still broken on Ayllu
* Property Validation
* URL Validation
* Feed Validation
1diff --git a/content/_index.md b/content/_index.md
2index 64029d0..677a170 100644
3--- a/content/_index.md
4+++ b/content/_index.md
5 @@ -9,7 +9,8 @@ supported by any particular organizational entity.**
6 ForgeFeed is a collection of specifications and recommendations which when
7 implemented can enhance interoperability and content discovery of different
8 [code forges](https://en.wikipedia.org/wiki/Forge_(software))
9- across the internet.
10+ across the internet. You can validate an existing ForgeFeed implementation
11+ by navigating to the [validator](/validator) page.
12
13 NOTE that all specifications provided below MAY be implemented indepedenantly
14 at the descrescion of the developer however it is a recommended that a server
15 diff --git a/content/validator.md b/content/validator.md
16new file mode 100644
17index 0000000..d47dbc5
18--- /dev/null
19+++ b/content/validator.md
20 @@ -0,0 +1,4 @@
21+ ---
22+ title: Validator
23+ template: "validator.html"
24+ ---
25 diff --git a/sass/_custom.scss b/sass/_custom.scss
26index cce5ce9..bc93021 100644
27--- a/sass/_custom.scss
28+++ b/sass/_custom.scss
29 @@ -31,4 +31,26 @@
30
31 footer>a>img {
32 max-height: 20px;
33- }
34\ No newline at end of file
35+ }
36+
37+ div#validator > form {
38+ display: flex;
39+ flex-direction: column;
40+ background-color: grey;
41+ border: solid;
42+ padding: 10px;
43+ }
44+
45+ div#validator > form > input {
46+ margin: 2px;
47+ }
48+
49+ div#validator > form > button {
50+ margin-top: 20px;
51+ }
52+
53+ .computed {
54+ padding: 8px 15px;
55+ border-radius: 5px;
56+ border: 1px solid #e5e5e5;
57+ }
58 diff --git a/static/verifier.js b/static/verifier.js
59new file mode 100644
60index 0000000..9bcd9f2
61--- /dev/null
62+++ b/static/verifier.js
63 @@ -0,0 +1,198 @@
64+ const Kind = {
65+ HREF: "href",
66+ PROPERTIES: "properties",
67+ TITLES: "titles",
68+ };
69+
70+ function newRecorder() {
71+ const Recorder = {
72+ state: [],
73+ ok(kind) {
74+ this.state.push([kind])
75+ },
76+ error(kind, message, object) {
77+ this.state.push([kind, message, object])
78+ }
79+ };
80+ return Recorder
81+ }
82+
83+ // TODO
84+ function constraint(kind, values = []) {
85+ if (kind == Kind.HREF) {
86+ function check_href() {
87+ // TODO
88+ };
89+ return { kind: Kind.HREF, check: check_href }
90+ } else if (kind == Kind.PROPERTIES) {
91+ function check_properties() {
92+ // TODO
93+ };
94+ return { kind: Kind.PROPERTIES, check: check_properties }
95+ } else if (kind == Kind.TITLES) {
96+ function check_titles() { };
97+ return { kind: Kind.TITLES, check: check_titles }
98+ }
99+ }
100+
101+ function validateLink(object, recorder, constraints = []) {
102+ for (let i = 0; i < constraints.length; i++) {
103+ let constraint = constraints[i];
104+ if (constraint.kind == Kind.HREF) {
105+ if (!('href' in object)) {
106+ recorder.error(Kind.HREF, "href not in link", object)
107+ continue;
108+ }
109+ recorder.ok(Kind.HREF);
110+ if ('check' in constraint) {
111+ constraint.check(object["properties"], recorder);
112+ };
113+ } else if (constraint.kind == Kind.PROPERTIES) {
114+ if (!('properties' in object)) {
115+ recorder.error(Kind.PROPERTIES, "properties not present in link", object);
116+ continue;
117+ };
118+ recorder.ok(Kind.PROPERTIES);
119+ if ('check' in constraint) {
120+ constraint.check(object["properties"], recorder);
121+ };
122+ } else if (constraint.kind == Kind.TITLES) {
123+ if (!('titles' in object)) {
124+ recorder.error(Kind.TITLES, "titles not present in object", object);
125+ continue;
126+ };
127+ recorder.ok(Kind.TITLES);
128+ if ('check' in constraint) {
129+ constraint.check(object["titles"], recorder);
130+ };
131+ }
132+ }
133+ }
134+
135+ const _project = {
136+ "http://forge-feed.org/rel/avatar": [constraint(Kind.HREF)],
137+ "http://forge-feed.org/rel/chatroom": [
138+ constraint(Kind.HREF),
139+ constraint(Kind.PROPERTIES, {
140+ "http://forge-feed.org/ns/chatroom": ["irc", "xmpp"]
141+ })
142+ ],
143+ "http://forge-feed.org/rel/description": [constraint(Kind.TITLES)],
144+ "http://forge-feed.org/rel/label": [
145+ constraint(Kind.PROPERTIES, { "http://forge-feed.org/ns/label": null }),
146+ ],
147+ "http://forge-feed.org/rel/homepage": [constraint(Kind.HREF)],
148+ "http://forge-feed.org/rel/repository": [
149+ constraint(Kind.HREF),
150+ constraint(Kind.PROPERTIES, {
151+ "http://forge-feed.org/ns/repository-uri": null,
152+ "http://forge-feed.org/ns/vcs-type": [
153+ "bzr", "darcs", "fossil", "git", "hg", "pijul", "svn"],
154+ }),
155+ constraint(Kind.TITLES),
156+ ],
157+ "http://forge-feed.org/rel/mailing-list": [
158+ constraint(Kind.HREF),
159+ constraint(Kind.PROPERTIES, {
160+ "http://feed-forge.org/ns/mailing-list-subscribe": null,
161+ "http://feed-forge.org/ns/mailing-list-unsubscribe": null,
162+ })
163+ ],
164+ "http://forge-feed.org/rel/ticketing-system": [constraint(Kind.HREF)],
165+ };
166+
167+ const _repository = {
168+ "http://forge-feed.org/rel/avatar": [constraint(Kind.HREF)],
169+ "http://feed-forge.org/rel/clone": [
170+ constraint(Kind.HREF),
171+ constraint(Kind.PROPERTIES, {
172+ "http://feed-forge.org/ns/vcs-type": [
173+ "bzr", "darcs", "fossil", "git", "hg", "pijul", "svn",
174+ ]
175+ })],
176+ "http://forge-feed.org/rel/description": [constraint(Kind.TITLES)],
177+ "http://forge-feed.org/rel/license": [constraint(Kind.PROPERTIES, {
178+ "http://forge-feed.org/ns/spdx-identifier": null
179+ })],
180+ "http://forge-feed.org/rel/label": [
181+ constraint(Kind.PROPERTIES, { "http://forge-feed.org/ns/label": null }),
182+ ],
183+ "http://forge-feed.org/rel/homepage": [constraint(Kind.HREF)],
184+ "http://forge-feed.org/rel/mailing-list": [
185+ constraint(Kind.HREF),
186+ constraint(Kind.PROPERTIES, {
187+ "http://feed-forge.org/ns/mailing-list-subscribe": null,
188+ "http://feed-forge.org/ns/mailing-list-unsubscribe": null,
189+ })
190+ ],
191+ "http://forge-feed.org/rel/ticketing-system": [constraint(Kind.HREF)],
192+ };
193+
194+
195+ function doQuery(url, cb) {
196+ let xhr = new XMLHttpRequest();
197+ xhr.open('GET', url, true);
198+ xhr.onload = function() {
199+ if (xhr.status === 200) {
200+ cb(xhr.responseText)
201+ }
202+ };
203+ xhr.send();
204+ }
205+
206+ function validateProject() {
207+ document.getElementById("results").innerText = "";
208+ const fqdn = document.getElementById("fqdn")[0].value;
209+ const project = document.getElementById("project")[0].value;
210+ let queryUrl = "https://" + fqdn + "/.well-known/webfinger?resource=" + project;
211+ document.getElementById("query-url").innerText = queryUrl;
212+ doQuery(queryUrl, function(data) {
213+ let parsed = JSON.parse(data);
214+ let recorder = newRecorder();
215+ for (let i = 0; i < parsed.links.length; i++) {
216+ let link = parsed.links[i];
217+ if (link["rel"] in _project) {
218+ let constraints = _project[link["rel"]];
219+ validateLink(link, recorder, constraints);
220+ }
221+ };
222+ var textResult = "";
223+ for (let i = 0; i < recorder.state.length; i++) {
224+ console.log(recorder.state[i]);
225+ if (recorder.state[i][1]) {
226+ textResult += "Fail: " + recorder.state[i][1] + "\r\n";
227+ let asJson = JSON.stringify(recorder.state[i][2]);
228+ textResult += "\t\t" + asJson + "\r\n";
229+ }
230+ document.getElementById("results").innerText = textResult;
231+ }
232+ });
233+ }
234+
235+ function validateRepository() {
236+ document.getElementById("results").innerText = "";
237+ const fqdn = document.getElementById("fqdn")[0].value;
238+ const repository = document.getElementById("repository")[0].value;
239+ let queryUrl = "https://" + fqdn + "/.well-known/webfinger?resource=" + repository;
240+ document.getElementById("query-url").innerText = queryUrl;
241+ doQuery(queryUrl, function(data) {
242+ let parsed = JSON.parse(data);
243+ let recorder = newRecorder();
244+ for (let i = 0; i < parsed.links.length; i++) {
245+ let link = parsed.links[i];
246+ if (link["rel"] in _repository) {
247+ let constraints = _project[link["rel"]];
248+ validateLink(link, recorder, constraints);
249+ }
250+ };
251+ var textResult = "";
252+ for (let i = 0; i < recorder.state.length; i++) {
253+ if (recorder.state[i][1]) {
254+ textResult += "Fail: " + recorder.state[i][1] + "\r\n";
255+ let asJson = JSON.stringify(recorder.state[i][2]);
256+ textResult += "\t\t" + asJson + "\r\n";
257+ }
258+ document.getElementById("results").innerText = textResult;
259+ }
260+ });
261+ }
262 diff --git a/templates/validator.html b/templates/validator.html
263new file mode 100644
264index 0000000..ab8d2fe
265--- /dev/null
266+++ b/templates/validator.html
267 @@ -0,0 +1,44 @@
268+ {% extends "base.html" %}
269+
270+ {% block content %}
271+ <h1>{{ page.title }}</h1>
272+ <p>
273+ Fill out the form below to validate a ForgeFeed enabled website. Note that
274+ your server must have the appropriate CORS headers:
275+ <div>
276+ <code>Access-Control-Allow-Origin: https://forge-feed.org</code>.
277+ </div>
278+ </p>
279+ <div id="validator">
280+ <form id="fqdn">
281+ <label for="domain">
282+ FQDN
283+ </label>
284+ <input
285+ type="text"
286+ id="domain"
287+ name="domain"
288+ value="ayllu-forge.org"
289+ required>
290+ </form>
291+ <form id="project" onsubmit="return false">
292+ <label for="project-uri">Project URI</label>
293+ <input type="text" id="project-uri" name="project-uri" value="project://ayllu" required>
294+ <button type="submit" onclick="validateProject()">Validate</button>
295+ </form>
296+ <form id="repository" onsubmit="return false">
297+ <label for="repository-uri">Repository URI</label>
298+ <input type="text" id="repository-uri" name="repository-uri" value="repository://ayllu/ayllu" required>
299+ <button type="submit" onclick="validateRepository()">Validate</button>
300+ </form>
301+ <!-- <form id="feed" onsubmit="return false"> -->
302+ <!-- <label for="feed">Feed URL</label> -->
303+ <!-- <input type="text" id="feed-uri" name="feed-uri" value="/firehose.xml" required> -->
304+ <!-- <button type="submit" onclick="verify()">Validate</button> -->
305+ <!-- </form> -->
306+ <div id="query-url" class="computed"></div>
307+ <div id="results" class="computed"></div>
308+
309+ </div>
310+ <script src="/verifier.js"></script>
311+ {% endblock content %}