/c/<slug> renders one of the eight categories with every active listing
in it, newest first, as a grid of cards. Empty category shows
a "be the first" prompt rather than a dead page.
/app/<pub>/<slug> renders a listing: icon + name + publisher + version
chip, a prominent Download card with size, hash chip,
signed scan badge (green/clean | red/infected |
neutral/unknown), description, homepage link, who
signed, and an endorsements placeholder for when APS1
lands.
The home-page and /browse grids now read real counts from the DB — the
hardcoded "0 listings" placeholders are gone. A new helper
`renderCategoriesGrid()` is the one place category cards are rendered,
so the two surfaces always agree.
404s for unknown categories (`/c/nope`) and unknown listings
(`/app/foo/bar`) go through the same notFound page and now actually
return HTTP 404 instead of 200 — pages opt in by returning the
notFound object, server detects it by title and sets the status.
CATEGORIES is the single source of truth for slug/icon/title/blurb;
adding a new category now means one row here plus one entry in the
listings allow-list.
Verified end-to-end with three listings across three categories: counts
update, cards render, detail page shows the signed scan badge, 404s
return 404.
534 lines
22 KiB
JavaScript
534 lines
22 KiB
JavaScript
/**
|
|
* The pages Prometheus.X renders.
|
|
*
|
|
* Each export is `(req?) => {title, description, body}`. The server is the one
|
|
* that wraps them in the layout — these just return the inner content.
|
|
*
|
|
* The copy was lifted from site/prometheus/index.html (the ICANN teaser) and
|
|
* split by concern so each page is one topic a reader opened on purpose. The
|
|
* single-scroll version lives at silentmode.st/prometheus and will stay there
|
|
* as the SEO-friendly mirror.
|
|
*/
|
|
|
|
import { renderCategoriesGrid, countsByCategory } from './pages-browse.js';
|
|
import { countActive } from './db.js';
|
|
|
|
/**
|
|
* The "Browse by category" section used on the home page. Lives here so the
|
|
* home page's inline template reads cleanly, but delegates the actual grid to
|
|
* pages-browse.js so a single row of counts drives both surfaces.
|
|
*/
|
|
function renderHomeBrowseSection() {
|
|
const total = countActive();
|
|
return `<section class="plain">
|
|
<h2>Browse by category</h2>
|
|
<p class="lede">On a conventional store, one company decides which shelf an app
|
|
belongs on. Here the publisher labels their own listing, and your trust list
|
|
decides whether to show it. The categories below are the initial shape; a new
|
|
one gets proposed by publishing a listing against it.</p>
|
|
|
|
${renderCategoriesGrid({ verbose: true })}
|
|
|
|
${total === 0 ? `
|
|
<div class="note warn" style="margin-top:1.6rem"><b>No live listings yet.</b>
|
|
The on-chain publication protocol (<code class="inline">APS1</code>) is in
|
|
progress; in the meantime the site, the scanner, the publisher-signed
|
|
listing endpoint and the publishers' walkthrough are up. First real
|
|
listings appear in this grid as they publish.</div>
|
|
` : ''}
|
|
</section>`;
|
|
}
|
|
|
|
/* ------------------------------- home ------------------------------- */
|
|
|
|
export function home() {
|
|
return {
|
|
title: 'Apps and programs on the decentralized web',
|
|
description:
|
|
'Prometheus is a home for apps, programs, and dApps published on the Bitcoin Cash chain. Listings live on chain, binaries live on Sia, endorsements are signed. Anyone can publish; nobody can gatekeep.',
|
|
here: '/',
|
|
body: `
|
|
<header class="hero">
|
|
<div class="mark" style="font-size:52px">🔥</div>
|
|
<h1>Prometheus
|
|
<span class="pill p-designed" style="vertical-align:middle;font-size:12px;margin-left:.6rem">designed · not yet built</span>
|
|
</h1>
|
|
<p class="tag">A home for apps, programs, dApps, and libraries — <b>listed on the
|
|
Bitcoin Cash chain, hosted on Sia, endorsed by signature</b>. Anyone can publish.
|
|
Nobody can gatekeep. Named for the one who gave fire to humans and did not ask
|
|
the gods first.</p>
|
|
<div class="cta">
|
|
<a class="btn acid" href="/browse">Browse apps</a>
|
|
<a class="btn ghost" href="/submit">Submit an app</a>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
${renderHomeBrowseSection()}
|
|
|
|
<section>
|
|
<h2>How Prometheus works, in a nutshell</h2>
|
|
<ol class="steps">
|
|
<li><b>A listing is a signed on-chain record.</b>
|
|
<span>Publishing an app writes a small <code class="inline">APS1</code>
|
|
record to Bitcoin Cash — same protocol shape as a BNS name registration,
|
|
different prefix. The payload is a hash pointing at a full manifest stored
|
|
on Sia. Nobody approves the record; the transaction is the publish.</span></li>
|
|
<li><b>The binary lives on Sia.</b>
|
|
<span>App files are uploaded to the Sia storage network — encrypted on
|
|
the publisher's machine, sharded across roughly 30 independent hosts,
|
|
held by nobody in particular. The listing carries the SHA-256 the file
|
|
must match.</span></li>
|
|
<li><b>Scan reports are signed and attached.</b>
|
|
<span>Prometheus runs an automated malware scan on every binary it hosts
|
|
and publishes the signed report alongside the listing. The chain still
|
|
goes up; the report tells your client whether to warn.</span></li>
|
|
<li><b>Install verifies against the chain.</b>
|
|
<span>Clients pull the file, check the SHA-256 against the on-chain
|
|
record, check the signing certificate against the fingerprint, and only
|
|
then install. The chain is the truth, not the mirror.</span></li>
|
|
</ol>
|
|
<p style="margin-top:1.2rem"><a href="/publish">The full publishers' walkthrough,
|
|
step by step →</a></p>
|
|
</section>
|
|
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- publish ---------------------------- */
|
|
|
|
export function publish() {
|
|
return {
|
|
title: "Publishers' walkthrough",
|
|
description:
|
|
'The end-to-end flow of publishing an app on Prometheus: name, hosting, manifest, listing, verification, updates, withdrawal, and endorsements.',
|
|
here: '/publish',
|
|
body: `
|
|
<header class="hero compact">
|
|
<h1>Publishers' walkthrough</h1>
|
|
<p class="tag">Pick a name, host the binary, write the manifest, sign the listing,
|
|
verify it resolves, ship an update when it's time. Here's the whole shape.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<section class="plain">
|
|
<p class="lede">Publishing an app on Prometheus is a sequence of signed records
|
|
pointing at files stored where you keep them. The whole thing — identity,
|
|
hosting, the listing itself — is yours; nothing here grants anyone else
|
|
permission to pull your app down, raise your price, or take a cut of what
|
|
people pay you for it.</p>
|
|
|
|
<p>Steps are tagged <span class="pill p-live">available today</span> or
|
|
<span class="pill p-designed">designed</span> so it is clear what you can do
|
|
now and what is waiting on the APS1 record format landing.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>What you need before you start</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<span class="pill p-live">available today</span>
|
|
<h3>A BNS name</h3>
|
|
<p>Any name registered through <a href="//sirius.x/">Sirius</a> works. The
|
|
name is what signs the listing, so it is also what readers trust the
|
|
listing by. Fractions of a cent to register, no monthly bill.</p>
|
|
</div>
|
|
<div class="card">
|
|
<span class="pill p-live">available today</span>
|
|
<h3>Somewhere to put the binary</h3>
|
|
<p>Prometheus can host for you — upload here and we put it on Sia, signed
|
|
and scanned. If you'd rather self-host on your own URL, the listing just
|
|
records the URL and the hash and will fetch from wherever you say.</p>
|
|
</div>
|
|
<div class="card">
|
|
<span class="pill p-live">available today</span>
|
|
<h3>A signing key</h3>
|
|
<p>The private key for the BNS name. If the name is in a wallet you already
|
|
use for Bitcoin Cash, that key already exists; publishing re-uses it.</p>
|
|
</div>
|
|
<div class="card">
|
|
<span class="pill p-live">available today</span>
|
|
<h3>A pinch of BCH</h3>
|
|
<p>One listing is one transaction. The transaction fee is cents, denominated
|
|
in sats. During the free wave, that is the only cost. Later, a flat
|
|
listing fee covers the Sia storage.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>The walkthrough</h2>
|
|
<ol class="steps">
|
|
<li><b>1. Register a name — or use one you already own.</b>
|
|
<span>The listing is signed by the name's key. A reader who follows your
|
|
name will see your listing with the publisher chip set to that name; a
|
|
reader who does not will see it marked unsigned by anyone they trust.
|
|
One name can carry any number of listings, so a studio with three games
|
|
publishes under one identity.</span></li>
|
|
|
|
<li><b>2. Upload the binary (or point us at your URL).</b>
|
|
<span>Prometheus runs a malware scan on upload. If it passes, the file
|
|
goes into a Sia bucket Silent Mode controls, the signed scan report is
|
|
prepared, and the manifest fields are prefilled. If you'd rather host
|
|
the file yourself, give us the URL and the expected SHA-256; the manifest
|
|
records both, the scan still runs against whatever the URL serves, and
|
|
the client refuses to install a byte-mismatched download.</span></li>
|
|
|
|
<li><b>3. Fill in the manifest.</b>
|
|
<span>Name, description, category, version, icon, download URL,
|
|
<code class="inline">sha256</code>, and the signing-cert fingerprint when
|
|
the binary itself is signed (it should be, if the platform supports it).
|
|
Icons are referenced by URL, not embedded. A 2 KB manifest with a 500 KB
|
|
icon beside it is the right shape; a 500 KB manifest is not.</span></li>
|
|
|
|
<li><b>4. Sign the APS1 record with your BNS key.</b>
|
|
<span>One transaction. The on-chain record carries the manifest hash,
|
|
the category label, and the signature from your name's key. From the
|
|
moment the transaction confirms, every client that follows the chain can
|
|
see your listing — no approval queue, no editorial review, no publisher
|
|
dashboard waiting for a human on the other end.</span></li>
|
|
|
|
<li><b>5. Verify it resolves.</b>
|
|
<span>Open one of the Ariadne clients and look for your listing in the
|
|
category you labelled it under. The client should pull the manifest from
|
|
Sia, show the fields you wrote, surface the scan report, and offer the
|
|
download. Clicking install should fetch the binary, check the SHA-256,
|
|
check the signing certificate, and only then run.</span></li>
|
|
|
|
<li><b>6. Ship updates with another signed record.</b>
|
|
<span>A new version is another APS1 transaction referencing the same app
|
|
id, signed by the same key, pointing at a new manifest hash. Clients
|
|
already following the app see the update offered the next time they
|
|
check. Nobody can push a hostile update unless they have your key.</span></li>
|
|
|
|
<li><b>7. Withdraw a listing when it is time.</b>
|
|
<span>A withdrawal is a signed record that marks the app id as retired.
|
|
Clients hide the listing from browse and refuse to install it, though
|
|
anyone with the manifest hash and the binary can still side-load it —
|
|
the chain is a signal, not a lock.</span></li>
|
|
</ol>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>A note on endorsements</h2>
|
|
<p>A listing is one signed record. An <em>endorsement</em> is another — a short
|
|
on-chain statement from any name saying "I vouch for this app." Readers whose
|
|
trust list includes the endorser's name will see your listing marked as
|
|
endorsed by them; readers who do not follow that name will not. Endorsements
|
|
are not reviews, and the client does not aggregate them into a star rating.
|
|
They are statements: who said what, when, signed.</p>
|
|
<div class="note"><b>You can endorse too.</b> Any name can publish an endorsement
|
|
of any listing, and most publishers end up endorsing a handful of apps they
|
|
use themselves. The trust graph that emerges is the directory's real curation
|
|
layer; the categories are just filing.</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>What this costs</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Per listing</h3>
|
|
<p>One Bitcoin Cash transaction. Fractions of a cent at current rates. Paid
|
|
once, lives on chain.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Per update</h3>
|
|
<p>Another transaction. Same shape, same cost. There is no "update bundle"
|
|
discount because there is no central party to collect one.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Per month of hosting</h3>
|
|
<p>Free during the opening wave while Prometheus underwrites the storage.
|
|
Afterwards, a flat per-listing fee (planned around $1 at current rates)
|
|
covers Sia storage for the typical binary. Your own HTTPS host costs
|
|
what it costs you.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Per download</h3>
|
|
<p>Zero to the directory. The reader pulls the binary from wherever the
|
|
manifest points; Prometheus never meters the traffic.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="note warn"><b>Where this stands today.</b> Names, Sia hosting and
|
|
BCH signing exist and are in use. The <code class="inline">APS1</code> record
|
|
format and the client-side browse/install flow are being wired. This page
|
|
describes the end state; the first live listings will go up as soon as the
|
|
submit path lands on this site and the Ariadne clients learn to read it.</div>
|
|
</section>
|
|
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- browse ----------------------------- */
|
|
|
|
export function browse() {
|
|
const total = countActive();
|
|
const totalLabel = total === 0
|
|
? `No listings yet — the eight categories are set and ready to receive publications.`
|
|
: `${total} ${total === 1 ? 'listing' : 'listings'} across ${Object.values(countsByCategory()).filter((n) => n > 0).length} ${Object.values(countsByCategory()).filter((n) => n > 0).length === 1 ? 'category' : 'categories'}.`;
|
|
return {
|
|
title: 'Browse',
|
|
description:
|
|
'Browse apps and programs published on Prometheus by category. Signed listings on the Bitcoin Cash chain; scan reports attached; your trust list filters.',
|
|
here: '/browse',
|
|
body: `
|
|
<header class="hero compact">
|
|
<h1>Browse</h1>
|
|
<p class="tag">Signed listings on chain, scan reports attached, your trust list
|
|
decides what to render.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<section class="plain">
|
|
<p class="lede">${totalLabel}</p>
|
|
|
|
${renderCategoriesGrid({ verbose: false })}
|
|
</section>
|
|
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- submit ----------------------------- */
|
|
|
|
export function submit() {
|
|
return {
|
|
title: 'Submit an app',
|
|
description:
|
|
'Upload an app to Prometheus. We scan it, generate the manifest and the APS1 record, you sign with your BNS key and the listing goes on chain.',
|
|
here: '/submit',
|
|
body: `
|
|
<header class="hero compact">
|
|
<h1>Submit an app</h1>
|
|
<p class="tag">Upload, scan, sign, publish. Four screens; your key stays with you.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<section class="plain">
|
|
<div class="note warn"><b>Milestone 1 of 4: upload + scan.</b> This step is
|
|
live and testable. The next milestone wires the manifest and APS1 record
|
|
(see <a href="/publish">the publishers' walkthrough</a>); payment and the
|
|
on-chain publish come after that.</div>
|
|
|
|
<ol class="steps" style="margin-top:1.4rem">
|
|
<li><b>1. Upload the binary.</b>
|
|
<span>Pick a file (any file — we don't care what the format is, only
|
|
what ClamAV says about the bytes). The server scans it, hashes it,
|
|
and hands back a signed scan report.</span></li>
|
|
<li><b>2. Fill the manifest.</b>
|
|
<span>Name, description, category, version, icon URL. Scan verdict is
|
|
attached automatically. <em>Not wired yet.</em></span></li>
|
|
<li><b>3. Sign with your BNS key.</b>
|
|
<span>The service shows the APS1 payload. Sign in Theseus, paste a
|
|
BIP-137 signature from any BCH wallet, or import your name's mnemonic
|
|
once in the page (wiped on close). <em>Not wired yet.</em></span></li>
|
|
<li><b>4. Pay the listing fee (eventually).</b>
|
|
<span>Free during the opening wave; a BCH invoice for the per-listing
|
|
fee once storage costs cross a threshold. <em>Not wired yet.</em></span></li>
|
|
</ol>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Upload a file to try it</h2>
|
|
<p class="lede">Pick any file to send through the scanner. The upload is
|
|
accepted, SHA-256'd, and either kept (clean) or discarded (infected or
|
|
error). Nothing is published; this is just the scan step.</p>
|
|
|
|
<form id="up" style="display:grid;gap:12px;max-width:520px;margin-top:1rem">
|
|
<input type="file" name="file" required
|
|
style="padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;color:var(--ink)">
|
|
<button class="btn acid" type="submit">Upload & scan</button>
|
|
<div id="status" style="color:var(--mut);font-size:14px;min-height:1.4em"></div>
|
|
</form>
|
|
|
|
<pre id="result" style="display:none;max-height:480px;overflow:auto;margin-top:1.2rem"></pre>
|
|
</section>
|
|
|
|
<script>
|
|
(() => {
|
|
const form = document.getElementById('up');
|
|
const status = document.getElementById('status');
|
|
const result = document.getElementById('result');
|
|
|
|
form.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault();
|
|
const file = form.file.files[0];
|
|
if (!file) return;
|
|
|
|
status.textContent = \`Uploading \${file.name} (\${fmtBytes(file.size)})…\`;
|
|
result.style.display = 'none';
|
|
result.textContent = '';
|
|
|
|
const body = new FormData();
|
|
body.append('file', file);
|
|
|
|
const started = performance.now();
|
|
let json;
|
|
try {
|
|
const r = await fetch('/api/upload', { method: 'POST', body });
|
|
json = await r.json();
|
|
const elapsed = Math.round(performance.now() - started);
|
|
const v = json?.scan?.verdict;
|
|
const emoji = v === 'clean' ? '✅' : v === 'infected' ? '🚨' : '⚠️';
|
|
status.innerHTML = \`\${emoji} <b>\${v || 'done'}</b> — \${elapsed} ms round trip.\`;
|
|
} catch (err) {
|
|
status.innerHTML = '⚠️ <b>network error</b> — ' + String(err?.message || err);
|
|
return;
|
|
}
|
|
result.style.display = 'block';
|
|
result.textContent = JSON.stringify(json, null, 2);
|
|
});
|
|
|
|
function fmtBytes(n) {
|
|
if (n < 1024) return n + ' B';
|
|
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KiB';
|
|
if (n < 1024 * 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MiB';
|
|
return (n / 1024 / 1024 / 1024).toFixed(2) + ' GiB';
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- boundaries ------------------------- */
|
|
|
|
export function boundaries() {
|
|
return {
|
|
title: 'Boundaries',
|
|
description:
|
|
'What Prometheus does and does not do. Not a curator. Not a moderator. Storage costs money. Readers decide; the chain carries signed records.',
|
|
here: '/boundaries',
|
|
body: `
|
|
<header class="hero compact">
|
|
<h1>Boundaries</h1>
|
|
<p class="tag">What this directory is not, and why.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<section class="plain">
|
|
<p class="lede">A permissionless app directory earns the right to call itself
|
|
permissionless by being honest about what that costs. Here is the shape of
|
|
the trade.</p>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>What Prometheus is not</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>Not a curator</h3>
|
|
<p>Prometheus does not decide what is on it. Malware can be published — as
|
|
it can on any store. The scan report tells your client to warn; the
|
|
trust lists you subscribe to filter the view. If nobody you trust
|
|
endorses it, your client says so.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Not a moderator</h3>
|
|
<p>Content that is illegal in your jurisdiction is your responsibility to
|
|
avoid, same as any other permissionless network. Silent Mode operates
|
|
infrastructure; it does not decide what runs on it.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Not a payment processor</h3>
|
|
<p>Payments in an app go publisher-to-reader. The listing may carry a BCH
|
|
receive address or a wallet-connect URL; Prometheus never routes a
|
|
transaction. There is no refund desk because there is no middleman to
|
|
issue a refund.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Not an identity provider</h3>
|
|
<p>The only identity on Prometheus is the BNS name a listing is signed by.
|
|
Readers decide which names count via their own trust lists. There is no
|
|
Prometheus account, no "verified publisher" badge, no sign-in flow for
|
|
readers.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>What Silent Mode does control</h2>
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h3>The scan report</h3>
|
|
<p>Our ClamAV + VirusTotal pipeline runs on every binary we host. The
|
|
signed report is attached to the listing. We cannot stop a listing from
|
|
being on chain; we can tell readers what we found.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>Which binaries we host</h3>
|
|
<p>Silent Mode's Sia bucket. Operators can refuse to host something and
|
|
require the publisher to self-host at their own URL. The listing still
|
|
exists; the scan report says we did not host it.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>The default sort</h3>
|
|
<p>What the Browse page shows first is a choice. We publish the sort order
|
|
and let clients override it. The chain holds the data; the directory is
|
|
one lens.</p>
|
|
</div>
|
|
<div class="card">
|
|
<h3>The publish mode</h3>
|
|
<p>Three modes exist (append-only, scan-first-blocking, human-review).
|
|
Operators pick which one is active per category. The current mode is
|
|
always visible on this page; a change is announced before it takes
|
|
effect.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Storage costs money</h2>
|
|
<p>Sia hosting is inexpensive but not free — publishers pay for the bytes
|
|
their apps take. There is no advertising model, no data harvesting, no
|
|
subscription. Storage is the only cost, and it is small. During the
|
|
opening wave Silent Mode absorbs it; afterwards it is a flat per-listing
|
|
fee paid in BCH.</p>
|
|
</section>
|
|
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- 404 -------------------------------- */
|
|
|
|
export function notFound(req) {
|
|
return {
|
|
title: 'Not here',
|
|
description: 'The requested page does not exist on Prometheus.',
|
|
here: '',
|
|
body: `
|
|
<header class="hero compact">
|
|
<h1>Not here</h1>
|
|
<p class="tag">Nothing lives at <code class="inline">${escapeHtml(req?.url || '')}</code>.</p>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
<section class="plain">
|
|
<p>Try the <a href="/">home page</a>, the <a href="/browse">browse</a> or the
|
|
<a href="/publish">publishers' walkthrough</a>.</p>
|
|
</section>
|
|
</div>`,
|
|
};
|
|
}
|
|
|
|
/* ------------------------------- helpers ---------------------------- */
|
|
|
|
function escapeHtml(s) {
|
|
return String(s)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|