mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-14 11:06:13 +05:00
fix: harden reflected metadata rendering
This commit is contained in:
parent
6ae56b7366
commit
18e3580dcd
@ -142,18 +142,22 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
|
||||
process = result.process_record
|
||||
process_display = "-"
|
||||
if process:
|
||||
process_url = html.escape(reverse("admin:machine_process_change", args=[process.id]), quote=True)
|
||||
process_label = html.escape(get_process_link_label(process), quote=True)
|
||||
process_display = f'''
|
||||
<a href="{reverse("admin:machine_process_change", args=[process.id])}"
|
||||
<a href="{process_url}"
|
||||
style="color: #2563eb; text-decoration: none; font-family: ui-monospace, monospace; font-size: 12px;"
|
||||
title="View process">{get_process_link_label(process)}</a>
|
||||
title="View process">{process_label}</a>
|
||||
'''
|
||||
|
||||
machine_display = "-"
|
||||
if process and process.machine_id:
|
||||
machine_url = html.escape(reverse("admin:machine_machine_change", args=[process.machine_id]), quote=True)
|
||||
machine_label = html.escape(str(process.machine.hostname or ""), quote=True)
|
||||
machine_display = f'''
|
||||
<a href="{reverse("admin:machine_machine_change", args=[process.machine_id])}"
|
||||
<a href="{machine_url}"
|
||||
style="color: #2563eb; text-decoration: none; font-size: 12px;"
|
||||
title="View machine">{process.machine.hostname}</a>
|
||||
title="View machine">{machine_label}</a>
|
||||
'''
|
||||
|
||||
# Truncate output for display
|
||||
@ -176,9 +180,13 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
|
||||
output_link = build_snapshot_url(snapshot_id, embed_path, config=config)
|
||||
else:
|
||||
output_link = build_snapshot_url(snapshot_id, "", config=config)
|
||||
output_link_attr = html.escape(output_link, quote=True)
|
||||
|
||||
# Get version - try cmd_version field
|
||||
version = result.cmd_version if result.cmd_version else "-"
|
||||
version = html.escape(str(result.cmd_version if result.cmd_version else "-"), quote=True)
|
||||
plugin_text = html.escape(str(result.plugin or ""), quote=True)
|
||||
status_text = html.escape(str(status), quote=True)
|
||||
pwd_text = html.escape(str(result.pwd or "-"), quote=True)
|
||||
|
||||
# Unique ID for this row's expandable output
|
||||
row_id = f"output_{idx}_{str(result.id)[:8]}"
|
||||
@ -195,18 +203,18 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
|
||||
<td style="padding: 10px 12px; white-space: nowrap;">
|
||||
<span style="display: inline-block; padding: 3px 10px; border-radius: 12px;
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
color: {color}; background: {bg};">{status}</span>
|
||||
color: {color}; background: {bg};">{status_text}</span>
|
||||
</td>
|
||||
<td style="padding: 10px 12px; white-space: nowrap; font-size: 20px;" title="{result.plugin}">
|
||||
<td style="padding: 10px 12px; white-space: nowrap; font-size: 20px;" title="{plugin_text}">
|
||||
{icon}
|
||||
</td>
|
||||
<td style="padding: 10px 12px; font-weight: 500; color: #334155;">
|
||||
<a href="{output_link}" target="_blank"
|
||||
<a href="{output_link_attr}" target="_blank"
|
||||
style="color: #334155; text-decoration: none;"
|
||||
title="View output fullscreen"
|
||||
onmouseover="this.style.color='#2563eb'; this.style.textDecoration='underline';"
|
||||
onmouseout="this.style.color='#334155'; this.style.textDecoration='none';">
|
||||
{result.plugin}
|
||||
{plugin_text}
|
||||
</a>
|
||||
</td>
|
||||
<td style="padding: 10px 12px; max-width: 280px;">
|
||||
@ -233,7 +241,7 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
|
||||
</td>
|
||||
<td style="padding: 10px 8px; white-space: nowrap;">
|
||||
<div style="display: flex; gap: 4px;">
|
||||
<a href="{output_link}" target="_blank"
|
||||
<a href="{output_link_attr}" target="_blank"
|
||||
style="padding: 4px 8px; background: #f1f5f9; border-radius: 4px; color: #475569; text-decoration: none; font-size: 11px;"
|
||||
title="View output">📄</a>
|
||||
<a href="{reverse("admin:core_archiveresult_change", args=[result.id])}"
|
||||
@ -252,7 +260,7 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None):
|
||||
<div style="font-size: 11px; color: #64748b; margin-bottom: 8px;">
|
||||
<span style="margin-right: 16px;"><b>ID:</b> <code>{str(result.id)}</code></span>
|
||||
<span style="margin-right: 16px;"><b>Version:</b> <code>{version}</code></span>
|
||||
<span style="margin-right: 16px;"><b>PWD:</b> <code>{result.pwd or "-"}</code></span>
|
||||
<span style="margin-right: 16px;"><b>PWD:</b> <code>{pwd_text}</code></span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #64748b; margin-bottom: 8px;">
|
||||
<b>Output:</b>
|
||||
|
||||
@ -36,7 +36,7 @@ from archivebox.misc.util import (
|
||||
to_json,
|
||||
ts_to_date_str,
|
||||
urlencode,
|
||||
htmlencode,
|
||||
htmldecode,
|
||||
sanitize_html_text,
|
||||
urldecode,
|
||||
validate_url,
|
||||
@ -3434,8 +3434,8 @@ class Snapshot(ModelWithDeleteAfter, ModelWithOutputDir, ModelWithConfig, ModelW
|
||||
context = {
|
||||
**self.to_dict(extended=True),
|
||||
"snapshot": self,
|
||||
"title": htmlencode(self.resolved_title or (self.base_url if is_archived else TITLE_LOADING_MSG)),
|
||||
"url_str": htmlencode(urldecode(self.base_url)),
|
||||
"title": htmldecode(self.resolved_title or (self.base_url if is_archived else TITLE_LOADING_MSG)),
|
||||
"url_str": htmldecode(urldecode(self.base_url)),
|
||||
"archive_url": urlencode(f"warc/{self.timestamp}" or (self.domain if is_archived else "")) or "about:blank",
|
||||
"extension": self.extension or "html",
|
||||
"tags": self.tags_str() or "untagged",
|
||||
|
||||
@ -560,6 +560,17 @@ def snapshot_index_row(context, link) -> str:
|
||||
f'<img src="{escape(static("spinner.gif"))}" alt="" decoding="async" loading="lazy">'
|
||||
"</span>"
|
||||
)
|
||||
elif "_public_preview_paths" in link.__dict__:
|
||||
preview_paths = list(getattr(link, "_public_preview_paths", []) or [])
|
||||
if preview_paths:
|
||||
preview_urls = [build_snapshot_url(snapshot_id, path, request=request, config=config) for path in preview_paths]
|
||||
preview_html = (
|
||||
f'<img src="{escape(preview_urls[0])}" '
|
||||
f'data-fallbacks="{escape(",".join(preview_urls[1:]))}" '
|
||||
'onerror="nextPublicSnapshotPreview(this)" class="snapshot-preview screenshot" alt="" decoding="async" loading="lazy">'
|
||||
)
|
||||
else:
|
||||
preview_html = '<span class="snapshot-preview snapshot-preview-empty" aria-label="No preview available"></span>'
|
||||
else:
|
||||
preview_html = (
|
||||
f'<img src="{escape(screenshot_plugin_url)}" '
|
||||
|
||||
@ -51,7 +51,7 @@ from archivebox.misc.paginators import AcceleratedPaginator
|
||||
from archivebox.misc.util import (
|
||||
base_url,
|
||||
filter_queryset_by_uuid_substring,
|
||||
htmlencode,
|
||||
htmldecode,
|
||||
sanitize_html_text,
|
||||
ts_to_date_str,
|
||||
urldecode,
|
||||
@ -441,7 +441,7 @@ class SnapshotView(View):
|
||||
"progress_endpoint": progress_endpoint("snapshot", snapshot.id),
|
||||
"url": snapshot.url,
|
||||
"archive_path": snapshot.archive_path_from_db,
|
||||
"title": htmlencode(snapshot.resolved_title or (snapshot.base_url if is_archived else TITLE_LOADING_MSG)),
|
||||
"title": htmldecode(snapshot.resolved_title or (snapshot.base_url if is_archived else TITLE_LOADING_MSG)),
|
||||
"extension": snapshot.extension or "html",
|
||||
"tags": snapshot.tags_str() or "untagged",
|
||||
"size": printable_filesize(output_size) if output_size else "—",
|
||||
@ -1226,6 +1226,7 @@ class PublicIndexView(ListView):
|
||||
snapshots = list(context.get("object_list") or ())
|
||||
icons_by_snapshot: dict[str, set[str]] = {str(snapshot.id): set() for snapshot in snapshots}
|
||||
tag_names_by_snapshot: dict[str, list[str]] = {str(snapshot.id): [] for snapshot in snapshots}
|
||||
preview_paths_by_snapshot: dict[str, list[str]] = {str(snapshot.id): [] for snapshot in snapshots}
|
||||
progress_by_snapshot: dict[str, dict[str, int]] = {
|
||||
str(snapshot.id): {
|
||||
"total": 0,
|
||||
@ -1246,12 +1247,18 @@ class PublicIndexView(ListView):
|
||||
):
|
||||
tag_names_by_snapshot[str(snapshot_id)].append(tag_name)
|
||||
|
||||
for snapshot_id, plugin, status in (
|
||||
preview_candidates = {
|
||||
"screenshot": ("screenshot.png",),
|
||||
"chrome_extension_screenshot": ("screenshot-1.png", "screenshot.png"),
|
||||
"favicon": ("favicon.ico",),
|
||||
}
|
||||
|
||||
for snapshot_id, plugin, status, output_files in (
|
||||
ArchiveResult.objects.filter(
|
||||
snapshot_id__in=icons_by_snapshot.keys(),
|
||||
)
|
||||
.exclude(plugin="")
|
||||
.values_list("snapshot_id", "plugin", "status")
|
||||
.values_list("snapshot_id", "plugin", "status", "output_files")
|
||||
.iterator(chunk_size=1000)
|
||||
):
|
||||
snapshot_key = str(snapshot_id)
|
||||
@ -1260,6 +1267,11 @@ class PublicIndexView(ListView):
|
||||
if status == ArchiveResult.StatusChoices.SUCCEEDED:
|
||||
icons_by_snapshot[snapshot_key].add(plugin)
|
||||
progress["succeeded"] += 1
|
||||
if plugin in preview_candidates and isinstance(output_files, dict):
|
||||
for filename in preview_candidates[plugin]:
|
||||
file_info = output_files.get(filename)
|
||||
if file_info and int((file_info or {}).get("size") or 0) > 0:
|
||||
preview_paths_by_snapshot[snapshot_key].append(f"{plugin}/{filename}")
|
||||
elif status == ArchiveResult.StatusChoices.FAILED:
|
||||
progress["failed"] += 1
|
||||
elif status == ArchiveResult.StatusChoices.STARTED:
|
||||
@ -1275,6 +1287,7 @@ class PublicIndexView(ListView):
|
||||
snapshot._icons_progress_stats = progress_by_snapshot.get(str(snapshot.id), {})
|
||||
snapshot.num_outputs_cached = snapshot._icons_progress_stats.get("succeeded", 0)
|
||||
snapshot._tags_str_cached = ",".join(tag_names_by_snapshot.get(str(snapshot.id), []))
|
||||
snapshot._public_preview_paths = preview_paths_by_snapshot.get(str(snapshot.id), [])
|
||||
snapshot._is_archived_cached = bool(snapshot.downloaded_at or snapshot.status == Snapshot.StatusChoices.SEALED)
|
||||
context["object_list"] = snapshots
|
||||
return context
|
||||
|
||||
@ -656,18 +656,22 @@
|
||||
return url
|
||||
}
|
||||
|
||||
function replaceRegionFromDocument(doc, selector) {
|
||||
const next = doc.querySelector(selector)
|
||||
const current = document.querySelector(selector)
|
||||
if (next && current) current.innerHTML = next.innerHTML
|
||||
}
|
||||
|
||||
async function renderNow() {
|
||||
const url = buildListUrl()
|
||||
const response = await fetch(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}, signal: controller.signal})
|
||||
const html = await response.text()
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html')
|
||||
const nextList = doc.querySelector('.public-snapshot-list')
|
||||
const currentList = document.querySelector('.public-snapshot-list')
|
||||
if (nextList && currentList) {
|
||||
currentList.replaceWith(nextList)
|
||||
initPublicSearchMode()
|
||||
setSearchLoading(controller, resultVersion ? 'Searching... ' + resultVersion + ' found' : 'Searching...')
|
||||
}
|
||||
replaceRegionFromDocument(doc, '.public-snapshot-count')
|
||||
replaceRegionFromDocument(doc, '#table-bookmarks thead th:nth-child(3)')
|
||||
replaceRegionFromDocument(doc, '#table-bookmarks tbody')
|
||||
replaceRegionFromDocument(doc, '.public-pagination')
|
||||
setSearchLoading(controller, resultVersion ? 'Searching... ' + resultVersion + ' found' : 'Searching...')
|
||||
}
|
||||
|
||||
const listUrl = buildListUrl()
|
||||
|
||||
@ -1151,7 +1151,7 @@
|
||||
</div>
|
||||
<div class="header-title-line header-toggle-trigger">
|
||||
<img src="{% snapshot_url snapshot 'favicon/favicon.ico' %}" onerror="this.onerror=null;this.src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='rgba(255,255,255,0.65)' stroke-width='1.4'><circle cx='10' cy='10' r='7.25'/><ellipse cx='10' cy='10' rx='3.25' ry='7.25'/><line x1='2.75' y1='10' x2='17.25' y2='10'/></svg>"" alt="Favicon" class="favicon"/>
|
||||
<span class="header-title-text">{{title|truncatechars:120|safe}}</span>
|
||||
<span class="header-title-text">{{title|truncatechars:120}}</span>
|
||||
{% if title_tags %}
|
||||
<span class="header-tags">
|
||||
<span class="tag-pill permission-pill">{{ snapshot_permissions_icon }} {{ snapshot_permissions }}</span>
|
||||
|
||||
@ -479,6 +479,15 @@ class TestPublicIndexSearch:
|
||||
assert response.status_code == 200
|
||||
assert b"archivebox-search-stream-status" in response.content
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_search_stream_preserves_search_form_dom(self, client, public_snapshot):
|
||||
response = client.get("/public/", {"q": "Public Example"}, HTTP_HOST=WEB_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"replaceRegionFromDocument(doc, '.public-snapshot-count')" in response.content
|
||||
assert b"replaceRegionFromDocument(doc, '#table-bookmarks tbody')" in response.content
|
||||
assert b"currentList.replaceWith(nextList)" not in response.content
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_search_stream_populates_public_results_cache(self, client, public_snapshot):
|
||||
search_params = {"q": "Public Example", "search_mode": "meta"}
|
||||
@ -581,12 +590,21 @@ class TestPublicIndexSearch:
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
assert "screenshot/screenshot.png" in content
|
||||
first = content.index("chrome_extension_screenshot/screenshot-1.png")
|
||||
second = content.index("chrome_extension_screenshot/screenshot.png")
|
||||
assert first < second
|
||||
assert "screenshot/screenshot.png" not in content
|
||||
assert "chrome_extension_screenshot/screenshot-2.png" not in content
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_index_snapshot_without_preview_renders_placeholder(self, client, public_snapshot):
|
||||
response = client.get("/public/", HTTP_HOST=WEB_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
assert "snapshot-preview-empty" in content
|
||||
assert "screenshot/screenshot.png" not in content
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_index_pending_snapshot_uses_small_preview_spinner(self, client, crawl):
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
@ -71,6 +71,72 @@ def test_snapshot_admin_archive_results_escape_extractor_output(admin_client, sn
|
||||
assert b"<img src=x onerror="window.__archivebox_archiveresult_xss__=1">" in body
|
||||
|
||||
|
||||
def test_snapshot_admin_archive_result_table_escapes_legacy_string_fields(admin_client, snapshot):
|
||||
from uuid import uuid4
|
||||
|
||||
from archivebox.core.models import ArchiveResult
|
||||
from archivebox.machine.models import Binary, Machine, Process
|
||||
|
||||
machine = Machine.objects.create(
|
||||
guid=f"xss-machine-{uuid4()}",
|
||||
hostname='<script id="machine-xss">x</script>',
|
||||
hw_in_docker=False,
|
||||
hw_in_vm=False,
|
||||
hw_manufacturer="Test",
|
||||
hw_product="Test Product",
|
||||
hw_uuid=f"xss-hw-{uuid4()}",
|
||||
os_arch="arm64",
|
||||
os_family="darwin",
|
||||
os_platform="macOS",
|
||||
os_release="14.0",
|
||||
os_kernel="Darwin",
|
||||
stats={},
|
||||
config={},
|
||||
)
|
||||
binary = Binary.objects.create(
|
||||
machine=machine,
|
||||
name="staticfile",
|
||||
abspath="/usr/bin/staticfile",
|
||||
version='<b id="version-xss">v</b>',
|
||||
binprovider="env",
|
||||
binproviders="env",
|
||||
status=Binary.StatusChoices.INSTALLED,
|
||||
)
|
||||
process = Process.objects.create(
|
||||
machine=machine,
|
||||
binary=binary,
|
||||
process_type=Process.TypeChoices.HOOK,
|
||||
pwd='/tmp/archivebox"><script id="pwd-xss">x</script>',
|
||||
cmd=["staticfile"],
|
||||
status=Process.StatusChoices.EXITED,
|
||||
)
|
||||
result = ArchiveResult.objects.create(
|
||||
snapshot=snapshot,
|
||||
plugin="staticfile",
|
||||
hook_name="on_Snapshot__00_staticfile.py",
|
||||
process=process,
|
||||
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
||||
output_files={'evil"><script id="file-xss">x</script>.txt': {"size": 12, "mimetype": "text/plain"}},
|
||||
output_str='staticfile/evil"><script id="file-xss">x</script>.txt',
|
||||
)
|
||||
ArchiveResult.objects.filter(pk=result.pk).update(plugin='<script id="plugin-xss">x</script>')
|
||||
|
||||
response = admin_client.get(reverse("admin:core_snapshot_change", args=[snapshot.pk]), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
body = response.content
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b'<script id="machine-xss">' not in body
|
||||
assert b'<script id="version-xss">' not in body
|
||||
assert b'<script id="pwd-xss">' not in body
|
||||
assert b'<script id="file-xss">' not in body
|
||||
assert b'<script id="plugin-xss">' not in body
|
||||
assert b"<script id="machine-xss">" in body
|
||||
assert b"<b id="version-xss">v</b>" in body
|
||||
assert b"<script id="pwd-xss">" in body
|
||||
assert b"<script id="file-xss">" in body
|
||||
assert b"<script id="plugin-xss">" in body
|
||||
|
||||
|
||||
def test_snapshot_changelist_bulk_permissions_action_updates_selected_snapshots(client, admin_user, crawl, snapshot):
|
||||
client.force_login(admin_user)
|
||||
url = reverse("admin:core_snapshot_changelist")
|
||||
|
||||
@ -282,7 +282,7 @@ class TestPublicIndex:
|
||||
|
||||
@override_settings(PUBLIC_INDEX=True)
|
||||
def test_public_snapshot_surfaces_escape_legacy_raw_title_and_tag_values(self, client, admin_user):
|
||||
from archivebox.core.models import Snapshot, Tag
|
||||
from archivebox.core.models import ArchiveResult, Snapshot, Tag
|
||||
from archivebox.crawls.models import Crawl
|
||||
|
||||
crawl = Crawl.objects.create(urls="https://public-xss.example", created_by=admin_user, config={"PERMISSIONS": "public"})
|
||||
@ -296,11 +296,19 @@ class TestPublicIndex:
|
||||
snapshot.tags.add(tag)
|
||||
snapshot_archive_path = snapshot.archive_path
|
||||
|
||||
title_payload = "</script><script id=public-title-xss>window.__archivebox_public_title_xss__=1</script>"
|
||||
title_payload = "Legacy title 1 < 2 & 3 </script><script id=public-title-xss>window.__archivebox_public_title_xss__=1</script>"
|
||||
tag_payload = "</script><script id=public-tag-xss>window.__archivebox_public_tag_xss__=1</script>"
|
||||
url_payload = "https://public-xss.example/</script><script id=public-url-xss>window.__archivebox_public_url_xss__=1</script>"
|
||||
filename_payload = 'evil"><script id=public-file-xss>window.__archivebox_public_file_xss__=1</script>.txt'
|
||||
Snapshot.objects.filter(pk=snapshot.pk).update(title=title_payload, url=url_payload)
|
||||
Tag.objects.filter(pk=tag.pk).update(name=tag_payload)
|
||||
ArchiveResult.objects.create(
|
||||
snapshot=snapshot,
|
||||
plugin="staticfile",
|
||||
hook_name="on_Snapshot__00_staticfile.py",
|
||||
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
||||
output_files={filename_payload: {"size": 12, "mimetype": "text/plain"}},
|
||||
)
|
||||
|
||||
public_index = client.get("/public/", HTTP_HOST=WEB_TEST_HOST)
|
||||
snapshot_detail = client.get(f"/{snapshot_archive_path}/index.html", HTTP_HOST=WEB_TEST_HOST)
|
||||
@ -311,9 +319,13 @@ class TestPublicIndex:
|
||||
assert b"<script id=public-title-xss>" not in response.content
|
||||
assert b"<script id=public-tag-xss>" not in response.content
|
||||
assert b"<script id=public-url-xss>" not in response.content
|
||||
assert b"<script id=public-file-xss>" not in response.content
|
||||
assert b"</script><script id=public-tag-xss>" in response.content
|
||||
assert b"</script><script id=public-title-xss>" in public_index.content
|
||||
assert b"window.__archivebox_public_title_xss__=1" in snapshot_detail.content
|
||||
assert b"Legacy title 1 < 2 & 3" in snapshot_detail.content
|
||||
assert b"Legacy title 1 &lt; 2 &amp; 3" not in snapshot_detail.content
|
||||
assert b"<script id=public-file-xss>" in snapshot_detail.content
|
||||
|
||||
def test_direct_snapshot_urls_allow_unlisted_but_not_private_for_guests(self, client, admin_user):
|
||||
from archivebox.core.models import Snapshot
|
||||
|
||||
@ -7,6 +7,7 @@ ARCHIVEBOX_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORKSPACE_DIR="$(cd "${ARCHIVEBOX_REPO}/.." && pwd)"
|
||||
PYPI_USERNAME="${PYPI_USERNAME:-__token__}"
|
||||
PYPI_WAIT_ATTEMPTS="${PYPI_WAIT_ATTEMPTS:-90}"
|
||||
DOCKER_IMAGE_REPOS="${DOCKER_IMAGE_REPOS:-archivebox/archivebox ghcr.io/archivebox/archivebox}"
|
||||
|
||||
cd "${WORKSPACE_DIR}"
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.35rc28",
|
||||
"version": "0.9.35rc29",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc28"
|
||||
version = "0.9.35rc29"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
@ -79,9 +79,9 @@ dependencies = [
|
||||
### Extractor dependencies (optional binaries detected at runtime via shutil.which)
|
||||
### Binary/Package Management
|
||||
"abxbus==2.5.10", # EventBus API
|
||||
"abxpkg>=1.11.210", # for: detecting, versioning, and installing binaries via apt/brew/pip/npm
|
||||
"abx-plugins>=1.11.213", # shared ArchiveBox plugin package with Chrome/Puppeteer dependency wiring
|
||||
"abx-dl>=1.11.213", # shared ArchiveBox downloader package with blocking install preflight
|
||||
"abxpkg>=1.11.211", # for: detecting, versioning, and installing binaries via apt/brew/pip/npm
|
||||
"abx-plugins>=1.11.214", # shared ArchiveBox plugin package with Chrome/Puppeteer dependency wiring
|
||||
"abx-dl>=1.11.214", # shared ArchiveBox downloader package with blocking install preflight
|
||||
### UUID7 backport for Python <3.14
|
||||
"uuid7>=0.1.0; python_version < '3.14'", # provides the uuid_extensions module on Python 3.13
|
||||
]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user