diff --git a/assets/src/bundles/browse/swhid-utils.js b/assets/src/bundles/browse/swhid-utils.js --- a/assets/src/bundles/browse/swhid-utils.js +++ b/assets/src/bundles/browse/swhid-utils.js @@ -112,7 +112,7 @@ $('#swh-identifiers').tabSlideOut(tabSlideOptions); // set the tab visible once the close animation is terminated - $('#swh-identifiers').css('display', 'block'); + $('#swh-identifiers').addClass('d-none d-sm-block'); $('.swhid-context-option').trigger('click'); // highlighted code lines changed diff --git a/assets/src/bundles/webapp/code-highlighting.js b/assets/src/bundles/webapp/code-highlighting.js --- a/assets/src/bundles/webapp/code-highlighting.js +++ b/assets/src/bundles/webapp/code-highlighting.js @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2019 The Software Heritage developers + * Copyright (C) 2018-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information @@ -22,21 +22,35 @@ return lineTd; } +// function to highlight a range of lines +export function highlightLines(first, last) { + if (!first) { + return; + } + if (!last) { + last = first; + } + for (let i = first; i <= last; ++i) { + highlightLine(i); + } +} + // function to reset highlighting export function resetHighlightedLines() { firstHighlightedLine = null; $('.hljs-ln-line[data-line-number]').css('background-color', 'inherit'); } -export function scrollToLine(lineDomElt) { +export function scrollToLine(lineDomElt, offset = 70) { if ($(lineDomElt).closest('.swh-content').length > 0) { $('html, body').animate({ - scrollTop: $(lineDomElt).offset().top - 70 + scrollTop: $(lineDomElt).offset().top - offset }, 500); } } -export async function highlightCode(showLineNumbers = true, selector = 'code') { +export async function highlightCode(showLineNumbers = true, selector = 'code', + enableLinesSelection = true) { await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs'); @@ -60,9 +74,7 @@ } else if (lines[0] < lines[lines.length - 1]) { firstHighlightedLine = parseInt(lines[0]); scrollToLine(highlightLine(lines[0])); - for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) { - highlightLine(i); - } + highlightLines(lines[0] + 1, lines[lines.length - 1]); } } @@ -75,7 +87,7 @@ } }); - if (!showLineNumbers) { + if (!showLineNumbers || !enableLinesSelection) { return; } @@ -88,9 +100,7 @@ if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) { const firstLine = firstHighlightedLine; resetHighlightedLines(); - for (let i = firstLine; i <= line; ++i) { - highlightLine(i); - } + highlightLines(firstLine, line); firstHighlightedLine = firstLine; window.location.hash = `#L${firstLine}-L${line}`; } else { diff --git a/assets/src/bundles/webapp/iframes.js b/assets/src/bundles/webapp/iframes.js new file mode 100644 --- /dev/null +++ b/assets/src/bundles/webapp/iframes.js @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2021 The Software Heritage developers + * See the AUTHORS file at the top-level directory of this distribution + * License: GNU Affero General Public License version 3, or any later version + * See top-level LICENSE file for more information + */ + +export function showIframeInfoModal(objectType, objectSWHID) { + const html = ` +
+ You can embed that ${objectType} view in an external website + through the use of an iframe. Use the following HTML code + to do so. +
+<iframe style="width: 100%; height: 500px; border: 1px solid rgba(0, 0, 0, 0.125);"
+ src="${window.location.origin}${Urls.swhid_iframe(objectSWHID.replaceAll('\n', ''))}">
+</iframe>
+ `;
+ swh.webapp.showModalHtml(`Software Heritage ${objectType} iframe`, html, '1000px');
+ swh.webapp.highlightCode(false, '.swh-iframe-html');
+}
diff --git a/assets/src/bundles/webapp/index.js b/assets/src/bundles/webapp/index.js
--- a/assets/src/bundles/webapp/index.js
+++ b/assets/src/bundles/webapp/index.js
@@ -26,3 +26,4 @@
export * from './sentry';
export * from './math-typesetting';
export * from './status-widget';
+export * from './iframes';
diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js
--- a/assets/src/bundles/webapp/webapp-utils.js
+++ b/assets/src/bundles/webapp/webapp-utils.js
@@ -251,9 +251,11 @@
$('#swh-web-modal-confirm').modal('show');
}
-export function showModalHtml(title, html) {
+export function showModalHtml(title, html, width = '500px') {
$('#swh-web-modal-html .modal-title').text(title);
$('#swh-web-modal-html .modal-body').html(html);
+ $('#swh-web-modal-html .modal-dialog').css('max-width', width);
+ $('#swh-web-modal-html .modal-dialog').css('width', width);
$('#swh-web-modal-html').modal('show');
}
diff --git a/assets/src/bundles/webapp/webapp.css b/assets/src/bundles/webapp/webapp.css
--- a/assets/src/bundles/webapp/webapp.css
+++ b/assets/src/bundles/webapp/webapp.css
@@ -662,6 +662,7 @@
}
.swh-badge-html,
+.swh-iframe-html,
.swh-badge-md,
.swh-badge-rst {
white-space: pre-wrap !important;
diff --git a/docs/index.rst b/docs/index.rst
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,6 +15,7 @@
uri-scheme-api
uri-scheme-browse
uri-scheme-identifiers
+ uri-scheme-misc
diff --git a/docs/uri-scheme-misc.rst b/docs/uri-scheme-misc.rst
new file mode 100644
--- /dev/null
+++ b/docs/uri-scheme-misc.rst
@@ -0,0 +1,41 @@
+Miscellaneous URLs
+^^^^^^^^^^^^^^^^^^
+
+Iframe view for contents and directories
+----------------------------------------
+
+A subset of Software Heritage objects (contents and directories) can be embedded
+in external websites through the use of iframes. A dedicated endpoint serving
+a minimalist Web UI is available for that use case.
+
+.. http:get:: /embed/(swhid)/
+
+ Endpoint to embed Software Heritage objects in external websites using
+ an iframe.
+
+ :param string swhid: a SoftWare Heritage persistent IDentifier
+ object, or SWHID (see :ref:`persistent-identifiers` to learn more about its syntax)
+
+ :statuscode 200: no error
+ :statuscode 400: the provided identifier is malformed or
+ the object type is not supported by the view
+ :statuscode 404: requested object cannot be found in the archive
+
+ **Example:**
+
+ By adding HTML code similar to the one below in a web page,
+
+ .. code-block:: html
+
+
+
+ you will obtain the following rendering.
+
+ .. raw:: html
+
+
+
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -285,6 +285,7 @@
),
"swh_web_version": get_distribution("swh.web").version,
"visit_types": ORIGIN_VISIT_TYPES,
+ "iframe_mode": False,
}
diff --git a/swh/web/misc/iframe.py b/swh/web/misc/iframe.py
new file mode 100644
--- /dev/null
+++ b/swh/web/misc/iframe.py
@@ -0,0 +1,310 @@
+# Copyright (C) 2021 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU Affero General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from typing import Any, Dict, List, Optional
+
+from django.conf.urls import url
+from django.shortcuts import render
+from django.views.decorators.clickjacking import xframe_options_exempt
+
+from swh.model.hashutil import hash_to_bytes
+from swh.model.identifiers import (
+ CONTENT,
+ DIRECTORY,
+ REVISION,
+ SNAPSHOT,
+ ObjectType,
+ QualifiedSWHID,
+)
+from swh.web.browse.snapshot_context import get_snapshot_context
+from swh.web.browse.utils import (
+ content_display_max_size,
+ get_directory_entries,
+ prepare_content_for_display,
+ request_content,
+)
+from swh.web.common import archive
+from swh.web.common.exc import BadInputExc, NotFoundExc, http_status_code_message
+from swh.web.common.identifiers import get_swhid, get_swhids_info
+from swh.web.common.typing import SnapshotContext, SWHObjectInfo
+from swh.web.common.utils import gen_path_info, reverse
+
+
+def _get_content_rendering_data(cnt_swhid: QualifiedSWHID, path: str) -> Dict[str, Any]:
+ content_data = request_content(f"sha1_git:{cnt_swhid.object_id.hex()}")
+ content = None
+ language = None
+ mimetype = None
+ if content_data.get("raw_data") is not None:
+ content_display_data = prepare_content_for_display(
+ content_data["raw_data"], content_data["mimetype"], path
+ )
+ content = content_display_data["content_data"]
+ language = content_display_data["language"]
+ mimetype = content_display_data["mimetype"]
+
+ return {
+ "content": content,
+ "content_size": content_data.get("length"),
+ "max_content_size": content_display_max_size,
+ "filename": path.split("/")[-1],
+ "encoding": content_data.get("encoding"),
+ "mimetype": mimetype,
+ "language": language,
+ }
+
+
+def _get_directory_rendering_data(
+ dir_swhid: QualifiedSWHID, focus_swhid: QualifiedSWHID, path: str,
+) -> Dict[str, Any]:
+ dirs, files = get_directory_entries(dir_swhid.object_id.hex())
+ for d in dirs:
+ if d["type"] == "rev":
+ d["url"] = None
+ else:
+ dir_swhid = QualifiedSWHID(
+ object_type=ObjectType.DIRECTORY,
+ object_id=hash_to_bytes(d["target"]),
+ origin=dir_swhid.origin,
+ visit=dir_swhid.visit,
+ anchor=dir_swhid.anchor,
+ path=(path or "/") + d["name"] + "/",
+ )
+ d["url"] = reverse(
+ "swhid-iframe",
+ url_args={"swhid": str(dir_swhid)},
+ query_params={"focus_swhid": str(focus_swhid)},
+ )
+
+ for f in files:
+ object_id = hash_to_bytes(f["target"])
+ cnt_swhid = QualifiedSWHID(
+ object_type=ObjectType.CONTENT,
+ object_id=object_id,
+ origin=dir_swhid.origin,
+ visit=dir_swhid.visit,
+ anchor=dir_swhid.anchor,
+ path=(path or "/") + f["name"],
+ lines=(focus_swhid.lines if object_id == focus_swhid.object_id else None),
+ )
+ f["url"] = reverse(
+ "swhid-iframe",
+ url_args={"swhid": str(cnt_swhid)},
+ query_params={"focus_swhid": str(focus_swhid)},
+ )
+
+ return {"dirs": dirs, "files": files}
+
+
+def _get_breacrumbs_data(
+ swhid: QualifiedSWHID,
+ focus_swhid: QualifiedSWHID,
+ path: str,
+ snapshot_context: Optional[SnapshotContext] = None,
+) -> List[Dict[str, Any]]:
+ breadcrumbs = []
+ filename = None
+ # strip any leading or trailing slash from path qualifier of SWHID
+ if path and path[0] == "/":
+ path = path[1:]
+ if path and path[-1] == "/":
+ path = path[:-1]
+ if swhid.object_type == ObjectType.CONTENT:
+ split_path = path.split("/")
+ filename = split_path[-1]
+ path = path[: -len(filename)]
+
+ path_info = gen_path_info(path) if path != "/" else []
+
+ root_dir = None
+ if snapshot_context and snapshot_context["root_directory"]:
+ root_dir = snapshot_context["root_directory"]
+ elif focus_swhid.object_type == ObjectType.DIRECTORY:
+ root_dir = focus_swhid.object_id.hex()
+
+ if root_dir:
+ root_dir_swhid = QualifiedSWHID(
+ object_type=ObjectType.DIRECTORY,
+ object_id=hash_to_bytes(root_dir),
+ origin=swhid.origin,
+ visit=swhid.visit,
+ anchor=swhid.anchor,
+ )
+ breadcrumbs.append(
+ {
+ "name": root_dir[:7],
+ "object_id": root_dir_swhid.object_id.hex(),
+ "path": "/",
+ "url": reverse(
+ "swhid-iframe",
+ url_args={"swhid": str(root_dir_swhid)},
+ query_params={"focus_swhid": focus_swhid},
+ ),
+ }
+ )
+
+ for pi in path_info:
+ dir_info = archive.lookup_directory_with_path(root_dir, pi["path"])
+ dir_swhid = QualifiedSWHID(
+ object_type=ObjectType.DIRECTORY,
+ object_id=hash_to_bytes(dir_info["target"]),
+ origin=swhid.origin,
+ visit=swhid.visit,
+ anchor=swhid.anchor,
+ path="/" + pi["path"] + "/",
+ )
+ breadcrumbs.append(
+ {
+ "name": pi["name"],
+ "object_id": dir_swhid.object_id.hex(),
+ "path": dir_swhid.path.decode("utf-8") if dir_swhid.path else "",
+ "url": reverse(
+ "swhid-iframe",
+ url_args={"swhid": str(dir_swhid)},
+ query_params={"focus_swhid": focus_swhid},
+ ),
+ }
+ )
+ if filename:
+ breadcrumbs.append(
+ {
+ "name": filename,
+ "object_id": swhid.object_id.hex(),
+ "path": path,
+ "url": "",
+ }
+ )
+
+ return breadcrumbs
+
+
+@xframe_options_exempt
+def swhid_iframe(request, swhid: str):
+ """Django view that can be embedded in an iframe to display objects archived
+ by Software Heritage (currently contents and directories) in a minimalist
+ Web UI.
+ """
+ focus_swhid = request.GET.get("focus_swhid", swhid)
+ parsed_swhid = None
+ view_data = {}
+ breadcrumbs = []
+ swh_objects = []
+ snapshot_context = None
+ swhids_info_extra_context = {}
+ try:
+ parsed_swhid = get_swhid(swhid)
+ parsed_focus_swhid = get_swhid(focus_swhid)
+ path = parsed_swhid.path.decode("utf-8") if parsed_swhid.path else ""
+
+ snapshot_context = None
+ revision_id = None
+ if (
+ parsed_swhid.anchor
+ and parsed_swhid.anchor.object_type == ObjectType.REVISION
+ ):
+ revision_id = parsed_swhid.anchor.object_id.hex()
+ if parsed_swhid.origin or parsed_swhid.visit:
+ snapshot_context = get_snapshot_context(
+ origin_url=parsed_swhid.origin,
+ snapshot_id=parsed_swhid.visit.object_id.hex()
+ if parsed_swhid.visit
+ else None,
+ revision_id=revision_id,
+ )
+
+ error_info: Dict[str, Any] = {"status_code": 200, "description": ""}
+
+ if parsed_swhid and parsed_swhid.object_type == ObjectType.CONTENT:
+ view_data = _get_content_rendering_data(parsed_swhid, path)
+ swh_objects.append(
+ SWHObjectInfo(
+ object_type=CONTENT, object_id=parsed_swhid.object_id.hex()
+ )
+ )
+
+ elif parsed_swhid and parsed_swhid.object_type == ObjectType.DIRECTORY:
+ view_data = _get_directory_rendering_data(
+ parsed_swhid, parsed_focus_swhid, path
+ )
+ swh_objects.append(
+ SWHObjectInfo(
+ object_type=DIRECTORY, object_id=parsed_swhid.object_id.hex()
+ )
+ )
+
+ elif parsed_swhid:
+ error_info = {
+ "status_code": 400,
+ "description": (
+ f"Objects of type {parsed_swhid.object_type} are not supported"
+ ),
+ }
+
+ swhids_info_extra_context["path"] = path
+ if parsed_swhid and view_data:
+ breadcrumbs = _get_breacrumbs_data(
+ parsed_swhid, parsed_focus_swhid, path, snapshot_context
+ )
+
+ if parsed_swhid.object_type == ObjectType.CONTENT and len(breadcrumbs) > 1:
+ swh_objects.append(
+ SWHObjectInfo(
+ object_type=DIRECTORY, object_id=breadcrumbs[-2]["object_id"]
+ )
+ )
+ swhids_info_extra_context["path"] = breadcrumbs[-2]["path"]
+ swhids_info_extra_context["filename"] = breadcrumbs[-1]["name"]
+
+ if snapshot_context:
+ swh_objects.append(
+ SWHObjectInfo(
+ object_type=REVISION,
+ object_id=snapshot_context["revision_id"] or "",
+ )
+ )
+ swh_objects.append(
+ SWHObjectInfo(
+ object_type=SNAPSHOT,
+ object_id=snapshot_context["snapshot_id"] or "",
+ )
+ )
+
+ except BadInputExc as e:
+ error_info = {"status_code": 400, "description": f"BadInputExc: {str(e)}"}
+ except NotFoundExc as e:
+ error_info = {"status_code": 404, "description": f"NotFoundExc: {str(e)}"}
+ except Exception as e:
+ error_info = {"status_code": 500, "description": str(e)}
+
+ return render(
+ request,
+ "misc/iframe.html",
+ {
+ **view_data,
+ "iframe_mode": True,
+ "object_type": parsed_swhid.object_type.value if parsed_swhid else None,
+ "lines": parsed_swhid.lines if parsed_swhid else None,
+ "breadcrumbs": breadcrumbs,
+ "swhid": swhid,
+ "focus_swhid": focus_swhid,
+ "error_code": error_info["status_code"],
+ "error_message": http_status_code_message.get(error_info["status_code"]),
+ "error_description": error_info["description"],
+ "snapshot_context": None,
+ "swhids_info": get_swhids_info(
+ swh_objects, snapshot_context, swhids_info_extra_context
+ ),
+ },
+ status=error_info["status_code"],
+ )
+
+
+urlpatterns = [
+ url(
+ r"^embed/(?P