← back to Paul Conrad Archive
src/conrad/web/imagehost.py
93 lines
"""Display-image policy (Steve, TK-12199): on the PRIVATE admin viewer, a cartoon may be SHOWN only when the holding
institution itself publishes a public image of it, and only by HOTLINKING that institution's own URL.
Nothing here ever fetches, proxies, caches or stores image bytes — it only rewrites/validates URL strings that the
crawlers already recorded. The browser loads the image straight from the institution, and the Content-Security-Policy
built from the same allowlist makes the browser itself refuse any other image host.
"""
from __future__ import annotations
import re
from urllib.parse import urlparse, urlunparse
# Exact hosts (no wildcards) — also the CSP img-src list. Determined from the stored URLs:
# tile.loc.gov / cdn.loc.gov -> Library of Congress Prints & Photographs image services
# 5008.sydneyplus.com -> History Colorado's collections portal (ViewImage.aspx derivative)
# Web-image hosts (web_images/latimes crawlers, TK-12199 2026-09-25): added ONLY where the page that publishes the image
# was robots-allowed for our UA when crawled, the image path itself is robots-allowed, and a HEAD with our UA returned
# 200 image/* (no bytes kept). image.invaluable.com is deliberately ABSENT: its robots.txt answers 403 (unreadable ->
# treated as disallowed), so it could not be verified; those auction records stay text cards with link-outs.
WEB_IMAGE_HOSTS = (
"ca-times.brightspotcdn.com", # Los Angeles Times gallery CDN (Conrad's paper)
"library.syracuse.edu", # Syracuse University Libraries cartoonists exhibit
"www.original-political-cartoon.com", # dealer gallery (original art)
"pophistorydig.com", # Pop History Dig article illustrations
"www.truthdig.com", # Truthdig (Conrad's post-2006 syndication outlet) article images
)
_WEB_IMAGE_PATH = re.compile(r"\.(jpe?g|gif|png|webp)(__.*)?$|/resize/\d+x\d+", re.I)
ALLOWED_IMAGE_HOSTS = ("tile.loc.gov", "cdn.loc.gov", "5008.sydneyplus.com") + WEB_IMAGE_HOSTS
LOC_HOSTS = ("tile.loc.gov", "cdn.loc.gov")
CSP = "img-src 'self' " + " ".join("https://" + h for h in ALLOWED_IMAGE_HOSTS)
_LOC_THUMB = re.compile(r"_150px\.jpg$", re.I)
_LOC_IMAGE = re.compile(r"/service/pnp/.+\.(jpe?g|gif|png)$", re.I)
def display_url(url: str | None) -> str | None:
"""Return the https URL to show for a stored image/thumbnail URL, or None if it is not on the allowlist.
LOC: the stored value is the 150px thumbnail; LOC's standard medium "reference" derivative is <stem>r.jpg
(present in the cached item JSON for every LOC record we hold). History Colorado: its ViewImage.aspx derivative.
Item PAGES (www.loc.gov/pictures/item/...) are not images and are never returned.
"""
if not url or not isinstance(url, str):
return None
try:
p = urlparse(url.strip())
except ValueError:
return None
host = (p.hostname or "").lower()
if p.scheme not in ("http", "https") or host not in ALLOWED_IMAGE_HOSTS or p.username or p.password or p.port:
return None
if host in LOC_HOSTS:
if not _LOC_IMAGE.search(p.path):
return None
path = _LOC_THUMB.sub("r.jpg", p.path)
return urlunparse(("https", host, path, "", "", ""))
if host == "5008.sydneyplus.com":
if not p.path.lower().endswith("/viewimage.aspx"):
return None
return urlunparse(("https", host, p.path, "", p.query, ""))
if host in WEB_IMAGE_HOSTS:
if not _WEB_IMAGE_PATH.search(p.path):
return None # an article/gallery PAGE on the same host is not an image
return urlunparse(("https", host, p.path, "", p.query, ""))
return None # pragma: no cover — every allowlisted host is handled above
def is_allowed(url: str | None) -> bool:
"""True iff url is an https URL on an allowlisted image host (used by tests + defence in depth)."""
if not url:
return False
p = urlparse(url)
return p.scheme == "https" and (p.hostname or "").lower() in ALLOWED_IMAGE_HOSTS
def pick(sources) -> dict | None:
"""First source (in the given order) that yields an allowlisted display URL -> display_image dict."""
for s in sources:
for key in ("image_url", "thumbnail_url"):
u = display_url(s[key])
if u and is_allowed(u):
repo = s["repository"] or "holding institution"
coll = s["collection_name"]
return {
"url": u,
"repository": repo,
"credit": repo + ((" — " + coll) if coll and coll != "via DPLA" else ""),
"record_url": s["record_url"],
"rights_url": s["rights_url"],
}
return None