source: Klonkt/test/search-fediverse.test.js@ fd8faf5

main
Last change on this file since fd8faf5 was fd8faf5, checked in by Robin <roboburr@…>, 9 hours ago

Zoeken is één vlak: de balk bovenaan, de resultaten eronder, en @naam@server

Robins ontwerp van 30-9: de aparte resultatenpagina is niet meer nodig als het
vak onder de zoekbalk blijft staan. De balk en het vak eronder zijn samen het
hele zoeken, en het vak krijgt alle ruimte die er onder de balk is.

EEN VORM, TWEE INGANGEN. partials/search-surface.ejs is de balk plus het vak;
de overlay op elke pagina gebruikt hem, en /search?q=... ook. De resultaten
zelf staan in partials/search-results.ejs, en het live vak haalt precies die
render op (/search/results). De pagina bevat dus teken voor teken wat het vak
toont; een toets bewaakt dat, want twee renders lopen gegarandeerd uit de pas.
/search blijft bestaan zodat een gedeelde link en Enter zonder JavaScript op
hetzelfde beeld uitkomen.

/search/suggest (JSON, dat de browser tot een eigen lijstje bouwde, anders dan
de pagina) en /search/remote (alleen het adres) zijn weg; niets anders
gebruikte ze. remote-preview.ejs ging mee.

HET VLAK. Vaste positie over het hele scherm, de balk bovenaan, het vak vult
de rest en scrolt zelf. Op z-index 1060: de vorige overlay lag op 200, onder de
tabbalk (1050), zodat op een telefoon het onderste stuk resultaten wegviel. Enter
blijft staan en zoekt meteen, want het vak ís de resultaten. Op de pagina beweegt
het adres mee (/search?q=...), zodat wat je ziet te delen blijft; sluiten is daar
teruggaan. Balk en resultaten staan op één lijn: een scrollbalk alleen rechts
schoof de kolom anders zo'n 9 pixels naar links.

WEBFINGER. @naam@server (met of zonder @ vooraan, zoals Mastodon) haalt het
profiel op, met de volgknop. Zonder eerst een post te proberen: een handle is
nooit een post. De server moet een echte domeinnaam zijn; "@robin" blijft een
naam om naar te zoeken. WebFinger zelf is openbaar en loopt via safeFetch;
het profiel erachter gaat ondertekend, en alleen namens een ingelogde
beheerder, dezelfde grens als een geplakte link.

Onderweg gevonden: search-results.ejs verwees kaal naar queryError, en een
aanroeper die hem vergat liet de hele render omvallen. Alles gaat nu via
locals. In de app gaf gather hem altijd mee, dus live ging het goed; het was
één vergeten veld van een kapotte zoekpagina.

Nagelopen in de browser op 375 en 1280 pixels: de balk bovenaan, het vak tot
de onderrand, tabbalk bedekt, geen horizontale overloop, tikdoelen 44, balk en
resultaten op één lijn. Het gedrag met de echte chrome.js: openen vers en met
focus, typen, Enter zonder weg te gaan, de wachtregel bij een link en een
handle, een traag antwoord dat niets overschrijft, een resultaat aantikken en
Escape sluiten, en het adres dat op de pagina meebeweegt.

Getoetst: pagina en vak teken voor teken gelijk, de handle-herkenning, een
handle ondertekend en zonder post-poging, en niet ingelogd geen enkel verzoek.
Tegenproef: pagina laten afwijken, handle niet herkennen, bij een handle toch
een post proberen en de inlogcontrole eruit laten elk hun toetsen omvallen.
Volledige suite 1292 groen.

shaer-utpi

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 18.7 KB
Line 
1// Een geplakt adres in de zoekbalk is geen zoekterm (shaer-utpi).
2//
3// Wat hier bewaakt wordt, en waarom het elk een eigen zaak is:
4//
5// 1. HERKENNING. "soundfabrics.nl" is een zoekterm en https://... is een
6// adres. Het schema is het verschil; zonder die regel wordt elke
7// zoekopdracht met een punt erin een uitgaand netwerkverzoek.
8// 2. WIE HET MAG LATEN OPHALEN. Ophalen is een verzoek dat deze server
9// namens de bezoeker doet. Voor een willekeurige bezoeker zou de zoekbalk
10// daarmee een haalservice zijn waarmee je andermans server laat aankloppen
11// op adressen naar keuze. Dezelfde grens als /authorize_interaction.
12// 3. DAT DE KNOPPEN ER ECHT STAAN. De render is een gedeelde partial; valt
13// die uit de pagina, dan ziet het er nog steeds uit als een gevonden post
14// en kan er alleen niets meer mee.
15import { test } from 'node:test';
16import assert from 'node:assert/strict';
17
18process.env.DATABASE_PATH = ':memory:';
19process.env.PUBLIC_BASE_URL = 'https://test.example';
20
21const dbMod = await import('../src/config/database.js');
22const db = dbMod.default;
23{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
24
25const AP = (await import('../src/services/ActivityPubService.js')).default;
26const searchMod = await import('../src/routes/search.js');
27const router = searchMod.default;
28const { lookupUri, lookupHandle } = searchMod;
29
30db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
31 .run('u1', 'baas', 'b@t.nl', 'x', 'god');
32db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)')
33 .run('s1', 'robo', 'Soundfabrics', 'u1');
34const site = () => db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
35
36// De echte resolvers doen netwerk. Hier staat wat ze zouden vinden, plus een
37// teller: "is er überhaupt opgehaald" is de helft van wat deze toets bewaakt.
38let gevraagd = [];
39let opties = [];
40AP.resolveRemoteNote = async (uri, opts) => {
41 gevraagd.push(['note', uri]);
42 opties.push(opts || {});
43 if (!uri.includes('/notes/')) return null;
44 return {
45 object_uri: uri, url: uri,
46 actor_uri: 'https://elders.test/u/oma', actor_url: 'https://elders.test/@oma',
47 actor_handle: '@oma@elders.test', actor_name: 'Oma', actor_icon: '',
48 content: '<p>Een bericht van ver weg</p>', images: [], media: [], poll: null,
49 preview: 'Een bericht van ver weg', threadInboxes: [], localPostId: '',
50 sensitive: uri.includes('/cw'), cw: uri.includes('/cw') ? 'Spinnen' : '',
51 };
52};
53let actorOpties = [];
54AP.resolveRemoteActor = async (uri, opts) => {
55 gevraagd.push(['actor', uri]);
56 actorOpties.push(opts || {});
57 const actorUri = uri === '@oma@elders.test' ? 'https://elders.test/u/oma' : uri;
58 if (!actorUri.includes('/u/')) return null;
59 return {
60 actor_uri: actorUri, actor_name: 'Oma', actor_handle: '@oma@elders.test',
61 actor_url: actorUri, actor_icon: 'https://elders.test/avatar.png', inbox: actorUri + '/inbox',
62 };
63};
64
65const express = (await import('express')).default;
66let ingelogd = true;
67const app = express();
68app.use((req, res, next) => {
69 res.locals.site = site();
70 res.locals.siteUrlBase = '';
71 req.session = ingelogd ? { user: { id: 'u1', role: 'god', username: 'baas' } } : {};
72 next();
73});
74app.use('/search', router);
75const server = app.listen(0);
76server.unref();
77const poort = server.address().port;
78test.after(() => server.close());
79
80const zoek = async (q) => {
81 gevraagd = [];
82 const r = await fetch(`http://127.0.0.1:${poort}/search?q=${encodeURIComponent(q)}`, { signal: AbortSignal.timeout(10000) });
83 return { status: r.status, html: await r.text() };
84};
85
86test('een adres wordt herkend, een zoekterm niet', () => {
87 assert.equal(lookupUri('https://elders.test/notes/1'), 'https://elders.test/notes/1');
88 assert.equal(lookupUri(' https://elders.test/notes/1 '), 'https://elders.test/notes/1');
89 // De fragmentverwijzing hoort bij de browser, niet bij het object.
90 assert.equal(lookupUri('https://elders.test/notes/1#comment-2'), 'https://elders.test/notes/1');
91 // Geen schema is geen adres: anders wordt zoeken naar een naam een verzoek.
92 assert.equal(lookupUri('soundfabrics.nl'), null);
93 assert.equal(lookupUri('www.soundfabrics.nl/muziek'), null);
94 assert.equal(lookupUri('soundfabrics'), null);
95 // Twee woorden waarvan er een een adres is, is een zoekopdracht.
96 assert.equal(lookupUri('https://elders.test/notes/1 oma'), null);
97 // Andere schema's halen we niet op.
98 assert.equal(lookupUri('javascript:alert(1)'), null);
99 assert.equal(lookupUri('file:///etc/passwd'), null);
100 // Een naam zonder punt is geen adres op het open net.
101 assert.equal(lookupUri('http://localhost:3000/notes/1'), null);
102});
103
104test('een gewone zoekopdracht haalt niets op', async () => {
105 const { status, html } = await zoek('muziek');
106 assert.equal(status, 200);
107 assert.deepEqual(gevraagd, [], 'een zoekterm hoort geen netwerkverzoek te worden');
108 assert.ok(!html.includes('sr-fediverse'), 'en er hoort geen fediverse-blok te staan');
109});
110
111test('een post van een andere server komt met zijn knoppen binnen', async () => {
112 const { html } = await zoek('https://elders.test/notes/1');
113 assert.deepEqual(gevraagd, [['note', 'https://elders.test/notes/1']]);
114 assert.ok(html.includes('Een bericht van ver weg'), 'de inhoud hoort er te staan');
115 assert.ok(html.includes('@oma@elders.test'), 'en van wie het is');
116 // Waarderen en boosten kan ter plekke; antwoorden en melden op de volle
117 // interactiepagina. De zoekpagina ziet er sinds 30-9 precies zo uit als het
118 // live vak, dus ook hier de compacte kaart.
119 for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost']) {
120 assert.ok(html.includes(`action="${actie}"`), `${actie} ontbreekt`);
121 }
122 assert.ok(html.includes('/authorize_interaction?uri=https%3A%2F%2Felders.test%2Fnotes%2F1&amp;reply=1'),
123 'antwoorden wijst naar de volle pagina, met het venster open');
124});
125
126test('een profiel levert de volgknop', async () => {
127 const { html } = await zoek('https://elders.test/u/oma');
128 assert.deepEqual(gevraagd.map((g) => g[0]), ['note', 'actor'], 'eerst post, dan profiel');
129 assert.ok(html.includes('action="/authorize_interaction/follow"'), 'de volgknop ontbreekt');
130 assert.ok(html.includes('https://elders.test/u/oma'), 'het profiel hoort in het formulier te staan');
131});
132
133test('een adres dat niets oplevert laat de rest van de zoekpagina staan', async () => {
134 const { status, html } = await zoek('https://elders.test/iets-anders');
135 assert.equal(status, 200);
136 assert.equal(gevraagd.length, 2, 'beide vormen zijn geprobeerd');
137 assert.ok(/sr-fediverse[\s\S]*class="sr-note"/.test(html), 'het blok staat er, met de melding erin');
138 assert.ok(html.includes('search-bar-form'), 'en de zoekbalk zelf is niet weggevallen');
139});
140
141test('een bezoeker laat deze server niets ophalen', async () => {
142 ingelogd = false;
143 try {
144 const { status, html } = await zoek('https://elders.test/notes/1');
145 assert.equal(status, 200);
146 assert.deepEqual(gevraagd, [], 'zonder rechten hoort er geen enkel verzoek uit te gaan');
147 assert.ok(!html.includes('/authorize_interaction/like'), 'en er staan geen knoppen');
148 } finally {
149 ingelogd = true;
150 }
151});
152
153// ── Dezelfde render, de andere ingang ───────────────────────────────────────
154//
155// De zoekpagina en /authorize_interaction delen sinds deze ronde één partial.
156// Die pagina had zelf geen enkele toets, dus een verhuizing van 200 regels eruit
157// zou stil kunnen mislukken: de suite bleef groen en het scherm was leeg. Deze
158// toets is de bodem daaronder.
159const postsRouter = (await import('../src/routes/posts.js')).default;
160const app2 = express();
161app2.use((req, res, next) => {
162 res.locals.site = site();
163 res.locals.siteUrlBase = '';
164 req.session = { user: { id: 'u1', role: 'god', username: 'baas' } };
165 next();
166});
167app2.use('/', postsRouter);
168const server2 = app2.listen(0);
169server2.unref();
170const poort2 = server2.address().port;
171test.after(() => server2.close());
172
173test('de interactiepagina toont hetzelfde als de zoekpagina', async () => {
174 const r = await fetch(
175 `http://127.0.0.1:${poort2}/authorize_interaction?uri=${encodeURIComponent('https://elders.test/notes/1')}`,
176 { signal: AbortSignal.timeout(10000) },
177 );
178 assert.equal(r.status, 200);
179 const html = await r.text();
180 assert.ok(html.includes('Een bericht van ver weg'), 'de inhoud hoort er te staan');
181 assert.ok(html.includes('@oma@elders.test'), 'en van wie het is');
182 for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost', '/authorize_interaction/report']) {
183 assert.ok(html.includes(`action="${actie}"`), `${actie} ontbreekt op de interactiepagina`);
184 }
185 // En de opmaak reist mee: die staat sinds deze ronde in een eigen partial,
186 // en een render zonder opmaak ziet er kapot uit zonder dat er iets faalt.
187 assert.ok(html.includes('.rp-card'), 'de opmaak van de kaart ontbreekt');
188});
189
190test('een profiel op de interactiepagina levert dezelfde volgknop', async () => {
191 const r = await fetch(
192 `http://127.0.0.1:${poort2}/authorize_interaction?uri=${encodeURIComponent('https://elders.test/u/oma')}`,
193 { signal: AbortSignal.timeout(10000) },
194 );
195 const html = await r.text();
196 assert.ok(html.includes('action="/authorize_interaction/follow"'), 'de volgknop ontbreekt');
197 assert.ok(html.includes('@oma@elders.test'));
198});
199
200// ── De live-preview onder de zoekbalk ───────────────────────────────────────
201//
202// Plak je een link, dan laadt de uitklaplijst de post zelf. Deze route wordt bij
203// elke toetsaanslag geraakt, en dat maakt de rechtencheck hier zwaarder dan op
204// de pagina: zonder hem is dit een haalservice waarmee iedereen deze server op
205// adressen naar keuze laat afsturen.
206
207const preview = async (q) => {
208 gevraagd = []; opties = [];
209 const r = await fetch(`http://127.0.0.1:${poort}/search/results?q=${encodeURIComponent(q)}`, { signal: AbortSignal.timeout(10000) });
210 return { status: r.status, html: await r.text(), cache: r.headers.get('cache-control') };
211};
212
213test('de preview laadt de post zelf, ondertekend als de site', async () => {
214 const { status, html, cache } = await preview('https://elders.test/notes/1');
215 assert.equal(status, 200);
216 assert.ok(html.includes('Een bericht van ver weg'), 'de inhoud hoort in de preview te staan');
217 assert.ok(html.includes('@oma@elders.test'));
218 assert.equal(opties[0].asSlug, 'robo', 'een post voor volgers weigert een anonieme GET');
219 // Wat iemand ziet hangt van zijn rechten en reacties af.
220 assert.match(cache || '', /no-store/);
221});
222
223test('in de preview kun je waarderen en boosten, antwoorden gaat naar de volle pagina', async () => {
224 const { html } = await preview('https://elders.test/notes/1');
225 for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost']) {
226 assert.ok(html.includes(`action="${actie}"`), `${actie} hoort in de preview te staan`);
227 }
228 // Een editor in een uitklaplijst is op een telefoon geen plek om te
229 // schrijven: de antwoordknop wijst naar de volle pagina, met het venster open.
230 assert.ok(!html.includes('action="/authorize_interaction"'), 'geen antwoordvenster in de preview');
231 assert.ok(!html.includes('action="/authorize_interaction/report"'), 'melden gebeurt op de volle pagina');
232 assert.ok(html.includes('/authorize_interaction?uri=https%3A%2F%2Felders.test%2Fnotes%2F1&amp;reply=1'),
233 'de antwoordknop moet naar de volle pagina met het venster al open');
234});
235
236test('een profiel in de preview geeft de volgknop', async () => {
237 const { status, html } = await preview('https://elders.test/u/oma');
238 assert.equal(status, 200);
239 assert.ok(html.includes('action="/authorize_interaction/follow"'));
240});
241
242test('een bezoeker laat via de preview niets ophalen', async () => {
243 ingelogd = false;
244 try {
245 const { status, html } = await preview('https://elders.test/notes/1');
246 // Voor een bezoeker is een geplakte link gewoon een zoekterm: hij krijgt
247 // de gewone resultaten en de zin waarom er verder niets staat.
248 assert.equal(status, 200);
249 assert.deepEqual(gevraagd, [], 'zonder rechten hoort er geen enkel verzoek uit te gaan');
250 assert.ok(!html.includes('/authorize_interaction/like'), 'en er staan geen knoppen');
251 } finally {
252 ingelogd = true;
253 }
254});
255
256test('een zoekterm in de preview haalt niets op', async () => {
257 const { status } = await preview('soundfabrics.nl');
258 assert.equal(status, 200, 'gewoon zoeken');
259 assert.deepEqual(gevraagd, [], 'maar niets ophalen');
260});
261
262test('een inhoudswaarschuwing blijft dicht tot je ervoor kiest', async () => {
263 // De oude render toonde alles open, ook wat de schrijver achter een
264 // waarschuwing had gezet.
265 const { html } = await preview('https://elders.test/notes/cw');
266 const cw = html.indexOf('<details class="rp-cw">');
267 assert.ok(cw >= 0, 'de post hoort achter een waarschuwing te staan');
268 assert.ok(html.indexOf('Spinnen', cw) > cw, 'met de tekst van de waarschuwing erop');
269 assert.ok(html.indexOf('Een bericht van ver weg') > cw, 'en de inhoud erachter, niet ervoor');
270});
271
272test('de interactiepagina opent het antwoordvenster op verzoek, en haalt ondertekend op', async () => {
273 const pagina = async (extra) => {
274 opties = [];
275 const r = await fetch(
276 `http://127.0.0.1:${poort2}/authorize_interaction?uri=${encodeURIComponent('https://elders.test/notes/1')}${extra}`,
277 { signal: AbortSignal.timeout(10000) },
278 );
279 return r.text();
280 };
281 const dicht = await pagina('');
282 assert.match(dicht, /<section class="rp-reply" id="rp-reply" hidden>/, 'zonder verzoek is het venster dicht');
283 assert.equal(opties[0].asSlug, 'robo', 'ook hier ondertekend, anders "niet gevonden" voor een volgerspost');
284 const open = await pagina('&reply=1');
285 assert.match(open, /<section class="rp-reply" id="rp-reply">/, 'met ?reply=1 staat het open, ook zonder JS');
286 assert.ok(open.includes('action="/authorize_interaction"'), 'en het venster heeft zijn formulier');
287});
288
289// ── Een profiel op een instance met authorized fetch ────────────────────────
290//
291// mastodon.social geeft een onbetekende GET van een profiel een 401; arvr.social
292// niet. Het profiel werd onbetekend opgehaald, dus het ene Mastodon toonde een
293// profiel en het andere "niet gevonden". De post-kant was al ondertekend, de
294// profiel-kant niet (gemeten op dev, 30-9: onbetekend niets, als dev gevonden).
295//
296// En de regel eronder: ondertekenen is deze site die voor het verzoek instaat,
297// en dat mag alleen namens een INGELOGDE beheerder. Een bezoeker laat niets
298// ophalen, ondertekend of niet.
299
300test('een profiel wordt ondertekend opgehaald, op de zoekpagina en in de preview', async () => {
301 actorOpties = [];
302 await zoek('https://elders.test/u/oma');
303 assert.equal(actorOpties[0] && actorOpties[0].asSlug, 'robo', 'de zoekpagina haalt het profiel als de site op');
304 actorOpties = [];
305 await preview('https://elders.test/u/oma');
306 assert.equal(actorOpties[0] && actorOpties[0].asSlug, 'robo', 'de preview ook');
307});
308
309test('ook de interactiepagina haalt een profiel ondertekend op', async () => {
310 actorOpties = [];
311 await fetch(`http://127.0.0.1:${poort2}/authorize_interaction?uri=${encodeURIComponent('https://elders.test/u/oma')}`,
312 { signal: AbortSignal.timeout(10000) });
313 assert.equal(actorOpties[0] && actorOpties[0].asSlug, 'robo');
314});
315
316test('niet ingelogd gaat er geen enkel verzoek uit, dus ook geen ondertekend', async () => {
317 ingelogd = false;
318 try {
319 actorOpties = [];
320 await zoek('https://elders.test/u/oma');
321 await preview('https://elders.test/u/oma');
322 assert.deepEqual(gevraagd, [], 'geen post- en geen profielopvraging');
323 assert.deepEqual(actorOpties, [], 'en dus niets dat de sleutel van de site gebruikt');
324 } finally {
325 ingelogd = true;
326 }
327});
328
329// ── Eén zoekvlak: de pagina en het live vak zijn hetzelfde ─────────────────
330//
331// Robins ontwerp van 30-9: de zoekbalk en het vak eronder zijn het hele
332// zoeken, en /search?q=... ziet er hetzelfde uit. Dat is alleen waar zolang
333// beide dezelfde render gebruiken; deze toets vangt het moment dat ze uit
334// elkaar gaan lopen.
335
336test('de pagina en het live vak tonen dezelfde resultaten, teken voor teken', async () => {
337 const pagina = (await zoek('https://elders.test/notes/1')).html;
338 const vak = (await preview('https://elders.test/notes/1')).html.trim();
339 assert.ok(vak.startsWith('<div class="sr">'), 'het vak levert de resultaten-render');
340 assert.ok(pagina.includes(vak), 'de pagina bevat exact dezelfde render als het live vak');
341});
342
343test('een lege vraag in het live vak levert niets en haalt niets op', async () => {
344 const { status } = await preview('x');
345 assert.equal(status, 204);
346 assert.deepEqual(gevraagd, []);
347});
348
349// ── WebFinger: @naam@server in de zoekbalk ───────────────────────────────────
350
351test('een handle wordt herkend, een naam of zin niet', () => {
352 assert.equal(lookupHandle('@oma@elders.test'), '@oma@elders.test');
353 assert.equal(lookupHandle('oma@Elders.Test'), '@oma@elders.test', 'zonder @ vooraan, zoals Mastodon');
354 assert.equal(lookupHandle(' @oma.v_d-berg@sub.elders.test '), '@oma.v_d-berg@sub.elders.test');
355 assert.equal(lookupHandle('@oma'), null, 'een naam om naar te zoeken, geen adres');
356 assert.equal(lookupHandle('@oma@localhost'), null, 'een server zonder domeinnaam');
357 assert.equal(lookupHandle('oma @elders.test'), null, 'een spatie maakt er een zoekopdracht van');
358 assert.equal(lookupHandle('https://elders.test/@oma'), null, 'een adres is een adres, geen handle');
359});
360
361test('een handle haalt het profiel op, ondertekend, zonder eerst een post te proberen', async () => {
362 actorOpties = [];
363 const { html } = await preview('@oma@elders.test');
364 assert.deepEqual(gevraagd, [['actor', '@oma@elders.test']], 'alleen het profiel: een handle is nooit een post');
365 assert.equal(actorOpties[0] && actorOpties[0].asSlug, 'robo', 'het profiel achter de handle gaat ondertekend');
366 assert.ok(html.includes('action="/authorize_interaction/follow"'), 'met de volgknop');
367 assert.ok(html.includes('https://elders.test/u/oma'), 'voor het profiel dat WebFinger opleverde');
368});
369
370test('een handle op de zoekpagina geeft hetzelfde profiel', async () => {
371 const { html } = await zoek('oma@elders.test');
372 assert.ok(html.includes('action="/authorize_interaction/follow"'));
373});
374
375test('niet ingelogd levert een handle geen enkel verzoek op', async () => {
376 ingelogd = false;
377 try {
378 const { status, html } = await preview('@oma@elders.test');
379 assert.equal(status, 200);
380 assert.deepEqual(gevraagd, [], 'geen WebFinger en geen profiel');
381 assert.ok(!html.includes('/authorize_interaction/follow'));
382 } finally {
383 ingelogd = true;
384 }
385});
386
Note: See TracBrowser for help on using the repository browser.