Changeset 3ceaef6 in Klonkt for src/routes/search.js


Ignore:
Timestamp:
09/30/2026 06:16:32 AM (3 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
9eb6e1b
Parents:
407353e
Message:

Zoeken op een adres: het object erachter, met de knoppen erbij

Plak je een link naar een fediverse-post of -profiel in de zoekbalk, dan is dat
geen zoekterm maar een aanwijzing. Klonkt haalt hem op en zet hem bovenaan de
resultaten, met dezelfde knoppen als /authorize_interaction: antwoorden,
waarderen, boosten, stemmen, volgen en melden.

De weg erheen was vooral opruimen. De render en de formulieren stonden als 200
regels in authorize-interaction.ejs, en er een tweede kopie van maken voor de
zoekpagina zou een tweede plek zijn waar een poort, een sanitizer of een knop
vergeten kan worden. Ze staan nu in partials/remote-object.ejs en
partials/remote-actor.ejs, met de opmaak in een derde. De pagina die ze had
gebruikt ze nu ook, dus er is één render en de formulieren posten in beide
gevallen naar /authorize_interaction/..., waar het werk en de rechtencontrole
al zaten.

DRIE DINGEN DIE VASTLIGGEN, en elk om een eigen reden:

Het schema is verplicht. "soundfabrics.nl" is een geldige zoekterm -- iemand
zoekt naar de naam -- en een kale domeinnaam als adres opvatten maakt van elke
zoekopdracht met een punt erin een netwerkverzoek. Met ​https:// ervoor is er
geen twijfel over de bedoeling.

Ophalen mag alleen een beheerder van de site laten doen. Dat is dezelfde grens
als /authorize_interaction, en met opzet: ophalen is een uitgaand verzoek dat
deze server namens de bezoeker doet. Zonder die grens is de zoekbalk een
haalservice waarmee een vreemde deze server laat aankloppen op adressen naar
keuze. Een bezoeker die toch een link plakt krijgt de gewone zoekresultaten en
een zin die zegt waarom er verder niets staat.

De ophaling gaat ONDERTEKEND als de site. Een post die alleen voor volgers
zichtbaar is weigert een anonieme GET, en dan lijkt een bestaande post te
ontbreken -- dezelfde val als bij het antwoorden op een post van een vriend.

Bijgekomen zonder dat erom gevraagd is, en het is het vermelden waard: het
profiel toont nu zijn avatar. Die hadden we al in handen (actor_icon uit
resolveRemoteActor) en hij stond op geen van beide schermen.

Getoetst: de herkenning zelf, dat een zoekterm GEEN verzoek wordt, dat de post
met zijn vier formulieren binnenkomt, dat een profiel de volgknop geeft, dat
een bezoeker niets laat ophalen, en dat /authorize_interaction na de verhuizing
nog steeds hetzelfde toont -- die pagina had zelf geen enkele toets, dus dat
was de bodem die er onder moest. Tegenproef per zaak.

Nog open: een @naam@server in de zoekbalk (webfinger kan het al, dus klein
werk) en een hint in de live-suggesties terwijl je plakt.

shaer-utpi

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/search.js

    r407353e r3ceaef6  
    99 *   4. Pages (Agenda / Downloads / Links / Press kit / Archive) by name — only
    1010 *      the available ones.
     11 *   5. Een ADRES van een andere server (shaer-utpi): plak je een link naar een
     12 *      fediverse-post of -profiel, dan is dat geen zoekterm maar een aanwijzing.
     13 *      Die halen we op en tonen we, met dezelfde knoppen als
     14 *      /authorize_interaction: antwoorden, waarderen, boosten, stemmen,
     15 *      volgen, melden.
    1116 *
    1217 * FTS5: user input is tokenised on non-letter/digit chars and each token is wrapped
    … …  
    2126import { premiumUnlocked } from '../services/PatreonService.js';
    2227import { t as i18nT, resolveLang } from '../services/i18n.js';
     28import ActivityPubService from '../services/ActivityPubService.js';
     29import PermissionsService from '../services/PermissionsService.js';
    2330
    2431const router = express.Router();
    … …  
    4552  if (!s || /^[…\s]*$/.test(s)) return esc((excerpt || '').slice(0, 160));
    4653  return s;
     54}
     55
     56/**
     57 * Is dit een ADRES en geen zoekterm? Dan het genormaliseerde adres, anders null.
     58 *
     59 * HET SCHEMA IS VERPLICHT, en dat is een keuze. "soundfabrics.nl" is een
     60 * geldige zoekterm -- iemand zoekt naar de naam -- en een bare domeinnaam als
     61 * adres opvatten maakt van elke zoekopdracht met een punt erin een
     62 * netwerkverzoek. Met https:// ervoor is er geen twijfel over de bedoeling.
     63 *
     64 * De fragmentverwijzing gaat eraf: die hoort bij de browser, niet bij het
     65 * object, en met #comment eraan is het een andere sleutel voor hetzelfde ding.
     66 */
     67export function lookupUri(q) {
     68  const s = String(q || '').trim();
     69  if (!/^https?:\/\//i.test(s) || /\s/.test(s)) return null;
     70  try {
     71    const u = new URL(s);
     72    if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
     73    // Een naam zonder punt is geen adres op het open net (localhost, een
     74    // intern hostname). De SSRF-bewaking in de transportlaag houdt de rest
     75    // tegen; dit scheelt het verzoek.
     76    if (!u.hostname.includes('.')) return null;
     77    u.hash = '';
     78    return u.toString();
     79  } catch {
     80    return null;
     81  }
     82}
     83
     84/**
     85 * Mag deze bezoeker een adres laten OPHALEN?
     86 *
     87 * Dezelfde grens als /authorize_interaction (requireSiteManager), en met opzet:
     88 * ophalen is een uitgaand verzoek dat deze server namens iemand anders doet.
     89 * Voor een willekeurige bezoeker zou de zoekbalk daarmee een haalservice zijn
     90 * waarmee je andermans server kunt laten aankloppen op adressen naar keuze.
     91 *
     92 * Dit spiegelt requireSiteManager in plaats van hem aan te roepen: die stuurt
     93 * je naar het inlogscherm, en een zoekopdracht hoort niemand weg te sturen.
     94 */
     95function mayLookUp(req, res) {
     96  const u = req.session && req.session.user;
     97  if (!u) return false;
     98  if (u.role === 'god' || u.role === 'kijker') return true;
     99  const site = res.locals.site;
     100  return !!(site && PermissionsService.canAdminSite(u, site));
    47101}
    48102
    … …  
    143197
    144198// ── Full results page ────────────────────────────────────────────────────────
    145 router.get('/', (req, res) => {
     199router.get('/', async (req, res) => {
    146200  const site = res.locals.site;
    147201  if (!site) return res.status(404).send('No site');
    … …  
    153207      results: [], tracks: [], events: [], pages: [], total: 0,
    154208    });
     209  }
     210
     211  // Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post,
     212  // dan als profiel: een URL die geen van beide oplevert is gewoon een
     213  // zoekterm die toevallig op een link lijkt, en dan blijft de rest staan.
     214  const remoteUri = lookupUri(rawQ);
     215  const mayLookup = remoteUri ? mayLookUp(req, res) : false;
     216  let remote = null;
     217  let remoteKind = null;
     218  if (remoteUri && mayLookup) {
     219    // Ondertekend als deze site: een post die alleen voor volgers zichtbaar is
     220    // weigert een anonieme GET, en dan lijkt een bestaande post te ontbreken.
     221    try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ }
     222    if (remote) remoteKind = 'note';
     223    if (!remote) {
     224      try { remote = await ActivityPubService.resolveRemoteActor(remoteUri); } catch { /* onbereikbaar */ }
     225      if (remote) remoteKind = 'actor';
     226    }
    155227  }
    156228
    … …  
    161233    results: r.results, tracks: r.tracks, events: r.events, pages: r.pages,
    162234    total, queryError: r.queryError,
     235    remoteUri, remote, remoteKind, mayLookup,
     236    remoteReacted: (remoteKind === 'note') ? ActivityPubService.getReaction(site.slug, remote.object_uri || remoteUri) : { liked: false, boosted: false },
     237    siteTitle: site.title || '',
    163238  });
    164239});
Note: See TracChangeset for help on using the changeset viewer.