views.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. __package__ = 'archivebox.core'
  2. from typing import Callable
  3. from io import StringIO
  4. from pathlib import Path
  5. from contextlib import redirect_stdout
  6. from django.shortcuts import render, redirect
  7. from django.http import HttpRequest, HttpResponse, Http404
  8. from django.utils.html import format_html, mark_safe
  9. from django.views import View, static
  10. from django.views.generic.list import ListView
  11. from django.views.generic import FormView
  12. from django.db.models import Q
  13. from django.contrib.auth.mixins import UserPassesTestMixin
  14. from django.views.decorators.csrf import csrf_exempt
  15. from django.utils.decorators import method_decorator
  16. from admin_data_views.typing import TableContext, ItemContext
  17. from admin_data_views.utils import render_with_table_view, render_with_item_view, ItemLink
  18. from core.models import Snapshot
  19. from core.forms import AddLinkForm
  20. from ..config import (
  21. OUTPUT_DIR,
  22. PUBLIC_INDEX,
  23. PUBLIC_SNAPSHOTS,
  24. PUBLIC_ADD_VIEW,
  25. VERSION,
  26. COMMIT_HASH,
  27. FOOTER_INFO,
  28. SNAPSHOTS_PER_PAGE,
  29. CONFIG,
  30. CONFIG_SCHEMA,
  31. DYNAMIC_CONFIG_SCHEMA,
  32. USER_CONFIG,
  33. SAVE_ARCHIVE_DOT_ORG,
  34. PREVIEW_ORIGINALS,
  35. CONSTANTS,
  36. )
  37. from ..logging_util import printable_filesize
  38. from ..main import add
  39. from ..util import base_url, ansi_to_html, htmlencode, urldecode, urlencode, ts_to_date_str
  40. from ..search import query_search_index
  41. from ..extractors.wget import wget_output_path
  42. class HomepageView(View):
  43. def get(self, request):
  44. if request.user.is_authenticated:
  45. return redirect('/admin/core/snapshot/')
  46. if PUBLIC_INDEX:
  47. return redirect('/public')
  48. return redirect(f'/admin/login/?next={request.path}')
  49. class SnapshotView(View):
  50. # render static html index from filesystem archive/<timestamp>/index.html
  51. @staticmethod
  52. def render_live_index(request, snapshot):
  53. TITLE_LOADING_MSG = 'Not yet archived...'
  54. HIDDEN_RESULTS = ('favicon', 'headers', 'title', 'htmltotext', 'warc', 'archive_org')
  55. archiveresults = {}
  56. results = snapshot.archiveresult_set.all()
  57. for result in results:
  58. embed_path = result.embed_path()
  59. abs_path = result.snapshot_dir / (embed_path or 'None')
  60. if (result.status == 'succeeded'
  61. and (result.extractor not in HIDDEN_RESULTS)
  62. and embed_path
  63. and abs_path.exists()):
  64. if abs_path.is_dir() and not any(abs_path.glob('*.*')):
  65. continue
  66. result_info = {
  67. 'name': result.extractor,
  68. 'path': embed_path,
  69. 'ts': ts_to_date_str(result.end_ts),
  70. 'size': abs_path.stat().st_size or '?',
  71. }
  72. archiveresults[result.extractor] = result_info
  73. existing_files = {result['path'] for result in archiveresults.values()}
  74. min_size_threshold = 10_000 # bytes
  75. allowed_extensions = {
  76. 'txt',
  77. 'html',
  78. 'htm',
  79. 'png',
  80. 'jpg',
  81. 'jpeg',
  82. 'gif',
  83. 'webp'
  84. 'svg',
  85. 'webm',
  86. 'mp4',
  87. 'mp3',
  88. 'opus',
  89. 'pdf',
  90. 'md',
  91. }
  92. # iterate through all the files in the snapshot dir and add the biggest ones to1 the result list
  93. snap_dir = Path(snapshot.link_dir)
  94. for result_file in (*snap_dir.glob('*'), *snap_dir.glob('*/*')):
  95. extension = result_file.suffix.lstrip('.').lower()
  96. if result_file.is_dir() or result_file.name.startswith('.') or extension not in allowed_extensions:
  97. continue
  98. if result_file.name in existing_files or result_file.name == 'index.html':
  99. continue
  100. file_size = result_file.stat().st_size or 0
  101. if file_size > min_size_threshold:
  102. archiveresults[result_file.name] = {
  103. 'name': result_file.stem,
  104. 'path': result_file.relative_to(snap_dir),
  105. 'ts': ts_to_date_str(result_file.stat().st_mtime or 0),
  106. 'size': file_size,
  107. }
  108. preferred_types = ('singlefile', 'screenshot', 'wget', 'dom', 'media', 'pdf', 'readability', 'mercury')
  109. all_types = preferred_types + tuple(result_type for result_type in archiveresults.keys() if result_type not in preferred_types)
  110. best_result = {'path': 'None'}
  111. for result_type in preferred_types:
  112. if result_type in archiveresults:
  113. best_result = archiveresults[result_type]
  114. break
  115. link = snapshot.as_link()
  116. link_info = link._asdict(extended=True)
  117. try:
  118. warc_path = 'warc/' + list(Path(snap_dir).glob('warc/*.warc.*'))[0].name
  119. except IndexError:
  120. warc_path = 'warc/'
  121. context = {
  122. **link_info,
  123. **link_info['canonical'],
  124. 'title': htmlencode(
  125. link.title
  126. or (link.base_url if link.is_archived else TITLE_LOADING_MSG)
  127. ),
  128. 'extension': link.extension or 'html',
  129. 'tags': link.tags or 'untagged',
  130. 'size': printable_filesize(link.archive_size) if link.archive_size else 'pending',
  131. 'status': 'archived' if link.is_archived else 'not yet archived',
  132. 'status_color': 'success' if link.is_archived else 'danger',
  133. 'oldest_archive_date': ts_to_date_str(link.oldest_archive_date),
  134. 'warc_path': warc_path,
  135. 'SAVE_ARCHIVE_DOT_ORG': SAVE_ARCHIVE_DOT_ORG,
  136. 'PREVIEW_ORIGINALS': PREVIEW_ORIGINALS,
  137. 'archiveresults': sorted(archiveresults.values(), key=lambda r: all_types.index(r['name']) if r['name'] in all_types else -r['size']),
  138. 'best_result': best_result,
  139. # 'tags_str': 'somealskejrewlkrjwer,werlmwrwlekrjewlkrjwer324m532l,4m32,23m324234',
  140. }
  141. return render(template_name='core/snapshot_live.html', request=request, context=context)
  142. def get(self, request, path):
  143. if not request.user.is_authenticated and not PUBLIC_SNAPSHOTS:
  144. return redirect(f'/admin/login/?next={request.path}')
  145. snapshot = None
  146. try:
  147. slug, archivefile = path.split('/', 1)
  148. except (IndexError, ValueError):
  149. slug, archivefile = path.split('/', 1)[0], 'index.html'
  150. # slug is a timestamp
  151. if slug.replace('.','').isdigit():
  152. # missing trailing slash -> redirect to index
  153. if '/' not in path:
  154. return redirect(f'{path}/index.html')
  155. try:
  156. try:
  157. snapshot = Snapshot.objects.get(Q(timestamp=slug) | Q(id__startswith=slug))
  158. if archivefile == 'index.html':
  159. # if they requested snapshot index, serve live rendered template instead of static html
  160. response = self.render_live_index(request, snapshot)
  161. else:
  162. response = static.serve(request, archivefile, document_root=snapshot.link_dir, show_indexes=True)
  163. response["Link"] = f'<{snapshot.url}>; rel="canonical"'
  164. return response
  165. except Snapshot.DoesNotExist:
  166. if Snapshot.objects.filter(timestamp__startswith=slug).exists():
  167. raise Snapshot.MultipleObjectsReturned
  168. else:
  169. raise
  170. except Snapshot.DoesNotExist:
  171. # Snapshot does not exist
  172. return HttpResponse(
  173. format_html(
  174. (
  175. '<center><br/><br/><br/>'
  176. 'No Snapshot directories match the given timestamp or UUID: <code>{}</code><br/><br/>'
  177. 'You can <a href="/add/" target="_top">add a new Snapshot</a>, or return to the <a href="/" target="_top">Main Index</a>'
  178. '</center>'
  179. ),
  180. slug,
  181. path,
  182. ),
  183. content_type="text/html",
  184. status=404,
  185. )
  186. except Snapshot.MultipleObjectsReturned:
  187. snapshot_hrefs = mark_safe('<br/>').join(
  188. format_html(
  189. '{} <a href="/archive/{}/index.html"><b><code>{}</code></b></a> {} <b>{}</b>',
  190. snap.added.strftime('%Y-%m-%d %H:%M:%S'),
  191. snap.timestamp,
  192. snap.timestamp,
  193. snap.url,
  194. snap.title_stripped[:64] or '',
  195. )
  196. for snap in Snapshot.objects.filter(timestamp__startswith=slug).only('url', 'timestamp', 'title', 'added').order_by('-added')
  197. )
  198. return HttpResponse(
  199. format_html(
  200. (
  201. 'Multiple Snapshots match the given timestamp/UUID <code>{}</code><br/><pre>'
  202. ),
  203. slug,
  204. ) + snapshot_hrefs + format_html(
  205. (
  206. '</pre><br/>'
  207. 'Choose a Snapshot to proceed or go back to the <a href="/" target="_top">Main Index</a>'
  208. )
  209. ),
  210. content_type="text/html",
  211. status=404,
  212. )
  213. except Http404:
  214. assert snapshot # (Snapshot.DoesNotExist is already handled above)
  215. # Snapshot dir exists but file within does not e.g. 124235.324234/screenshot.png
  216. return HttpResponse(
  217. format_html(
  218. (
  219. '<center><br/><br/><br/>'
  220. f'Snapshot <a href="/archive/{snapshot.timestamp}/index.html" target="_top"><b><code>[{snapshot.timestamp}]</code></b></a>: <a href="{snapshot.url}" target="_blank" rel="noreferrer">{snapshot.url}</a><br/>'
  221. f'was queued on {str(snapshot.added).split(".")[0]}, '
  222. f'but no files have been saved yet in:<br/><b><a href="/archive/{snapshot.timestamp}/" target="_top"><code>{snapshot.timestamp}</code></a><code>/'
  223. '{}'
  224. f'</code></b><br/><br/>'
  225. 'It\'s possible {} '
  226. f'during the last capture on {str(snapshot.added).split(".")[0]},<br/>or that the archiving process has not completed yet.<br/>'
  227. f'<pre><code># run this cmd to finish/retry archiving this Snapshot</code><br/>'
  228. f'<code style="user-select: all; color: #333">archivebox update -t timestamp {snapshot.timestamp}</code></pre><br/><br/>'
  229. '<div class="text-align: left; width: 100%; max-width: 400px">'
  230. '<i><b>Next steps:</i></b><br/>'
  231. f'- list all the <a href="/archive/{snapshot.timestamp}/" target="_top">Snapshot files <code>.*</code></a><br/>'
  232. f'- view the <a href="/archive/{snapshot.timestamp}/index.html" target="_top">Snapshot <code>./index.html</code></a><br/>'
  233. f'- go to the <a href="/admin/core/snapshot/{snapshot.pk}/change/" target="_top">Snapshot admin</a> to edit<br/>'
  234. f'- go to the <a href="/admin/core/snapshot/?uuid__startswith={snapshot.uuid}" target="_top">Snapshot actions</a> to re-archive<br/>'
  235. '- or return to <a href="/" target="_top">the main index...</a></div>'
  236. '</center>'
  237. ),
  238. archivefile if str(archivefile) != 'None' else '',
  239. f'the {archivefile} resource could not be fetched' if str(archivefile) != 'None' else 'the original site was not available',
  240. ),
  241. content_type="text/html",
  242. status=404,
  243. )
  244. # # slud is an ID
  245. # ulid = slug.split('_', 1)[-1]
  246. # try:
  247. # try:
  248. # snapshot = snapshot or Snapshot.objects.get(Q(abid=ulid) | Q(id=ulid) | Q(old_id=ulid))
  249. # except Snapshot.DoesNotExist:
  250. # pass
  251. # try:
  252. # snapshot = Snapshot.objects.get(Q(abid__startswith=slug) | Q(abid__startswith=Snapshot.abid_prefix + slug) | Q(id__startswith=slug) | Q(old_id__startswith=slug))
  253. # except (Snapshot.DoesNotExist, Snapshot.MultipleObjectsReturned):
  254. # pass
  255. # try:
  256. # snapshot = snapshot or Snapshot.objects.get(Q(abid__icontains=snapshot_id) | Q(id__icontains=snapshot_id) | Q(old_id__icontains=snapshot_id))
  257. # except Snapshot.DoesNotExist:
  258. # pass
  259. # return redirect(f'/archive/{snapshot.timestamp}/index.html')
  260. # except Snapshot.DoesNotExist:
  261. # pass
  262. # slug is a URL
  263. try:
  264. try:
  265. # try exact match on full url / ABID first
  266. snapshot = Snapshot.objects.get(
  267. Q(url='http://' + path) | Q(url='https://' + path) | Q(id__startswith=path)
  268. | Q(abid__icontains=path) | Q(id__icontains=path) | Q(old_id__icontains=path)
  269. )
  270. except Snapshot.DoesNotExist:
  271. # fall back to match on exact base_url
  272. try:
  273. snapshot = Snapshot.objects.get(
  274. Q(url='http://' + base_url(path)) | Q(url='https://' + base_url(path))
  275. )
  276. except Snapshot.DoesNotExist:
  277. # fall back to matching base_url as prefix
  278. snapshot = Snapshot.objects.get(
  279. Q(url__startswith='http://' + base_url(path)) | Q(url__startswith='https://' + base_url(path))
  280. )
  281. return redirect(f'/archive/{snapshot.timestamp}/index.html')
  282. except Snapshot.DoesNotExist:
  283. return HttpResponse(
  284. format_html(
  285. (
  286. '<center><br/><br/><br/>'
  287. 'No Snapshots match the given url: <code>{}</code><br/><br/><br/>'
  288. 'Return to the <a href="/" target="_top">Main Index</a>, or:<br/><br/>'
  289. '+ <i><a href="/add/?url={}" target="_top">Add a new Snapshot for <code>{}</code></a><br/><br/></i>'
  290. '</center>'
  291. ),
  292. base_url(path),
  293. path if '://' in path else f'https://{path}',
  294. path,
  295. ),
  296. content_type="text/html",
  297. status=404,
  298. )
  299. except Snapshot.MultipleObjectsReturned:
  300. snapshot_hrefs = mark_safe('<br/>').join(
  301. format_html(
  302. '{} <code style="font-size: 0.8em">{}</code> <a href="/archive/{}/index.html"><b><code>{}</code></b></a> {} <b>{}</b>',
  303. snap.added.strftime('%Y-%m-%d %H:%M:%S'),
  304. snap.abid,
  305. snap.timestamp,
  306. snap.timestamp,
  307. snap.url,
  308. snap.title_stripped[:64] or '',
  309. )
  310. for snap in Snapshot.objects.filter(
  311. Q(url__startswith='http://' + base_url(path)) | Q(url__startswith='https://' + base_url(path))
  312. | Q(abid__icontains=path) | Q(id__icontains=path) | Q(old_id__icontains=path)
  313. ).only('url', 'timestamp', 'title', 'added').order_by('-added')
  314. )
  315. return HttpResponse(
  316. format_html(
  317. (
  318. 'Multiple Snapshots match the given URL <code>{}</code><br/><pre>'
  319. ),
  320. base_url(path),
  321. ) + snapshot_hrefs + format_html(
  322. (
  323. '</pre><br/>'
  324. 'Choose a Snapshot to proceed or go back to the <a href="/" target="_top">Main Index</a>'
  325. )
  326. ),
  327. content_type="text/html",
  328. status=404,
  329. )
  330. class PublicIndexView(ListView):
  331. template_name = 'public_index.html'
  332. model = Snapshot
  333. paginate_by = SNAPSHOTS_PER_PAGE
  334. ordering = ['-added']
  335. def get_context_data(self, **kwargs):
  336. return {
  337. **super().get_context_data(**kwargs),
  338. 'VERSION': VERSION,
  339. 'COMMIT_HASH': COMMIT_HASH,
  340. 'FOOTER_INFO': FOOTER_INFO,
  341. }
  342. def get_queryset(self, **kwargs):
  343. qs = super().get_queryset(**kwargs)
  344. query = self.request.GET.get('q', default = '').strip()
  345. if not query:
  346. return qs.distinct()
  347. query_type = self.request.GET.get('query_type')
  348. if not query_type or query_type == 'all':
  349. qs = qs.filter(Q(title__icontains=query) | Q(url__icontains=query) | Q(timestamp__icontains=query) | Q(tags__name__icontains=query))
  350. try:
  351. qs = qs | query_search_index(query)
  352. except Exception as err:
  353. print(f'[!] Error while using search backend: {err.__class__.__name__} {err}')
  354. elif query_type == 'fulltext':
  355. try:
  356. qs = qs | query_search_index(query)
  357. except Exception as err:
  358. print(f'[!] Error while using search backend: {err.__class__.__name__} {err}')
  359. elif query_type == 'meta':
  360. qs = qs.filter(Q(title__icontains=query) | Q(url__icontains=query) | Q(timestamp__icontains=query) | Q(tags__name__icontains=query))
  361. elif query_type == 'url':
  362. qs = qs.filter(Q(url__icontains=query))
  363. elif query_type == 'title':
  364. qs = qs.filter(Q(title__icontains=query))
  365. elif query_type == 'timestamp':
  366. qs = qs.filter(Q(timestamp__icontains=query))
  367. elif query_type == 'tags':
  368. qs = qs.filter(Q(tags__name__icontains=query))
  369. else:
  370. print(f'[!] Unknown value for query_type: "{query_type}"')
  371. return qs.distinct()
  372. def get(self, *args, **kwargs):
  373. if PUBLIC_INDEX or self.request.user.is_authenticated:
  374. response = super().get(*args, **kwargs)
  375. return response
  376. else:
  377. return redirect(f'/admin/login/?next={self.request.path}')
  378. @method_decorator(csrf_exempt, name='dispatch')
  379. class AddView(UserPassesTestMixin, FormView):
  380. template_name = "add.html"
  381. form_class = AddLinkForm
  382. def get_initial(self):
  383. """Prefill the AddLinkForm with the 'url' GET parameter"""
  384. if self.request.method == 'GET':
  385. url = self.request.GET.get('url', None)
  386. if url:
  387. return {'url': url if '://' in url else f'https://{url}'}
  388. return super().get_initial()
  389. def test_func(self):
  390. return PUBLIC_ADD_VIEW or self.request.user.is_authenticated
  391. def get_context_data(self, **kwargs):
  392. return {
  393. **super().get_context_data(**kwargs),
  394. 'title': "Add URLs",
  395. # We can't just call request.build_absolute_uri in the template, because it would include query parameters
  396. 'absolute_add_path': self.request.build_absolute_uri(self.request.path),
  397. 'VERSION': VERSION,
  398. 'FOOTER_INFO': FOOTER_INFO,
  399. 'stdout': '',
  400. }
  401. def form_valid(self, form):
  402. url = form.cleaned_data["url"]
  403. print(f'[+] Adding URL: {url}')
  404. parser = form.cleaned_data["parser"]
  405. tag = form.cleaned_data["tag"]
  406. depth = 0 if form.cleaned_data["depth"] == "0" else 1
  407. extractors = ','.join(form.cleaned_data["archive_methods"])
  408. input_kwargs = {
  409. "urls": url,
  410. "tag": tag,
  411. "depth": depth,
  412. "parser": parser,
  413. "update_all": False,
  414. "out_dir": OUTPUT_DIR,
  415. "created_by_id": self.request.user.pk,
  416. }
  417. if extractors:
  418. input_kwargs.update({"extractors": extractors})
  419. add_stdout = StringIO()
  420. with redirect_stdout(add_stdout):
  421. add(**input_kwargs)
  422. print(add_stdout.getvalue())
  423. context = self.get_context_data()
  424. context.update({
  425. "stdout": ansi_to_html(add_stdout.getvalue().strip()),
  426. "form": AddLinkForm()
  427. })
  428. return render(template_name=self.template_name, request=self.request, context=context)
  429. class HealthCheckView(View):
  430. """
  431. A Django view that renders plain text "OK" for service discovery tools
  432. """
  433. def get(self, request):
  434. """
  435. Handle a GET request
  436. """
  437. return HttpResponse(
  438. 'OK',
  439. content_type='text/plain',
  440. status=200
  441. )
  442. def find_config_section(key: str) -> str:
  443. if key in CONSTANTS:
  444. return 'CONSTANT'
  445. matching_sections = [
  446. name for name, opts in CONFIG_SCHEMA.items() if key in opts
  447. ]
  448. section = matching_sections[0] if matching_sections else 'DYNAMIC'
  449. return section
  450. def find_config_default(key: str) -> str:
  451. default_val = USER_CONFIG.get(key, {}).get('default', lambda: None)
  452. if isinstance(default_val, Callable):
  453. return None
  454. else:
  455. default_val = repr(default_val)
  456. return default_val
  457. def find_config_type(key: str) -> str:
  458. if key in USER_CONFIG:
  459. return USER_CONFIG[key]['type'].__name__
  460. elif key in DYNAMIC_CONFIG_SCHEMA:
  461. return type(CONFIG[key]).__name__
  462. return 'str'
  463. def key_is_safe(key: str) -> bool:
  464. for term in ('key', 'password', 'secret', 'token'):
  465. if term in key.lower():
  466. return False
  467. return True
  468. @render_with_table_view
  469. def live_config_list_view(request: HttpRequest, **kwargs) -> TableContext:
  470. assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  471. rows = {
  472. "Section": [],
  473. "Key": [],
  474. "Type": [],
  475. "Value": [],
  476. "Default": [],
  477. # "Documentation": [],
  478. "Aliases": [],
  479. }
  480. for section in CONFIG_SCHEMA.keys():
  481. for key in CONFIG_SCHEMA[section].keys():
  482. rows['Section'].append(section) # section.replace('_', ' ').title().replace(' Config', '')
  483. rows['Key'].append(ItemLink(key, key=key))
  484. rows['Type'].append(mark_safe(f'<code>{find_config_type(key)}</code>'))
  485. rows['Value'].append(mark_safe(f'<code>{CONFIG[key]}</code>') if key_is_safe(key) else '******** (redacted)')
  486. rows['Default'].append(mark_safe(f'<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+{key}&type=code"><code style="text-decoration: underline">{find_config_default(key) or "See here..."}</code></a>'))
  487. # rows['Documentation'].append(mark_safe(f'Wiki: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">{key}</a>'))
  488. rows['Aliases'].append(', '.join(CONFIG_SCHEMA[section][key].get('aliases', [])))
  489. section = 'DYNAMIC'
  490. for key in DYNAMIC_CONFIG_SCHEMA.keys():
  491. if key in CONSTANTS:
  492. continue
  493. rows['Section'].append(section) # section.replace('_', ' ').title().replace(' Config', '')
  494. rows['Key'].append(ItemLink(key, key=key))
  495. rows['Type'].append(mark_safe(f'<code>{find_config_type(key)}</code>'))
  496. rows['Value'].append(mark_safe(f'<code>{CONFIG[key]}</code>') if key_is_safe(key) else '******** (redacted)')
  497. rows['Default'].append(mark_safe(f'<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+{key}&type=code"><code style="text-decoration: underline">{find_config_default(key) or "See here..."}</code></a>'))
  498. # rows['Documentation'].append(mark_safe(f'Wiki: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">{key}</a>'))
  499. rows['Aliases'].append(ItemLink(key, key=key) if key in USER_CONFIG else '')
  500. section = 'CONSTANT'
  501. for key in CONSTANTS.keys():
  502. rows['Section'].append(section) # section.replace('_', ' ').title().replace(' Config', '')
  503. rows['Key'].append(ItemLink(key, key=key))
  504. rows['Type'].append(mark_safe(f'<code>{find_config_type(key)}</code>'))
  505. rows['Value'].append(mark_safe(f'<code>{CONFIG[key]}</code>') if key_is_safe(key) else '******** (redacted)')
  506. rows['Default'].append(mark_safe(f'<a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+{key}&type=code"><code style="text-decoration: underline">{find_config_default(key) or "See here..."}</code></a>'))
  507. # rows['Documentation'].append(mark_safe(f'Wiki: <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">{key}</a>'))
  508. rows['Aliases'].append(ItemLink(key, key=key) if key in USER_CONFIG else '')
  509. return TableContext(
  510. title="Computed Configuration Values",
  511. table=rows,
  512. )
  513. @render_with_item_view
  514. def live_config_value_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
  515. assert request.user.is_superuser, 'Must be a superuser to view configuration settings.'
  516. aliases = USER_CONFIG.get(key, {}).get("aliases", [])
  517. if key in CONSTANTS:
  518. section_header = mark_safe(f'[CONSTANTS] &nbsp; <b><code style="color: lightgray">{key}</code></b> &nbsp; <small>(read-only, hardcoded by ArchiveBox)</small>')
  519. elif key in USER_CONFIG:
  520. section_header = mark_safe(f'data / ArchiveBox.conf &nbsp; [{find_config_section(key)}] &nbsp; <b><code style="color: lightgray">{key}</code></b>')
  521. else:
  522. section_header = mark_safe(f'[DYNAMIC CONFIG] &nbsp; <b><code style="color: lightgray">{key}</code></b> &nbsp; <small>(read-only, calculated at runtime)</small>')
  523. return ItemContext(
  524. slug=key,
  525. title=key,
  526. data=[
  527. {
  528. "name": section_header,
  529. "description": None,
  530. "fields": {
  531. 'Key': key,
  532. 'Type': find_config_type(key),
  533. 'Value': CONFIG[key] if key_is_safe(key) else '********',
  534. },
  535. "help_texts": {
  536. 'Key': mark_safe(f'''
  537. <a href="https://github.com/ArchiveBox/ArchiveBox/wiki/Configuration#{key.lower()}">Documentation</a> &nbsp;
  538. <span style="display: {"inline" if aliases else "none"}">
  539. Aliases: {", ".join(aliases)}
  540. </span>
  541. '''),
  542. 'Type': mark_safe(f'''
  543. <a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+{key}&type=code">
  544. See full definition in <code>archivebox/config.py</code>...
  545. </a>
  546. '''),
  547. 'Value': mark_safe(f'''
  548. {'<b style="color: red">Value is redacted for your security. (Passwords, secrets, API tokens, etc. cannot be viewed in the Web UI)</b><br/><br/>' if not key_is_safe(key) else ''}
  549. <br/><hr/><br/>
  550. Default: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
  551. <a href="https://github.com/search?q=repo%3AArchiveBox%2FArchiveBox+path%3Aconfig.py+{key}&type=code">
  552. <code>{find_config_default(key) or '↗️ See in ArchiveBox source code...'}</code>
  553. </a>
  554. <br/><br/>
  555. <p style="display: {"block" if key in USER_CONFIG else "none"}">
  556. <i>To change this value, edit <code>data/ArchiveBox.conf</code> or run:</i>
  557. <br/><br/>
  558. <code>archivebox config --set {key}="{
  559. val.strip("'")
  560. if (val := find_config_default(key)) else
  561. (repr(CONFIG[key] if key_is_safe(key) else '********')).strip("'")
  562. }"</code>
  563. </p>
  564. '''),
  565. },
  566. },
  567. ],
  568. )