mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Make work in macOS using docker instead of podman
This commit is contained in:
parent
8a84c115a1
commit
dd295271f5
8 changed files with 275 additions and 66 deletions
52
BUILD.md
Normal file
52
BUILD.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
# Development environment
|
||||
|
||||
After cloning this git repo, make sure to checkout the git submodules.
|
||||
|
||||
```
|
||||
git submodule init
|
||||
git submodule update
|
||||
```
|
||||
|
||||
## Debian/Ubuntu
|
||||
|
||||
You need [podman](https://podman.io/getting-started/installation) ([these instructions](https://kushaldas.in/posts/podman-on-debian-buster.html) are useful for installing in Debian or Ubuntu).
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```sh
|
||||
sudo apt install -y python3 python3-pyqt5 python3-appdirs python3-click python3-xdg
|
||||
```
|
||||
|
||||
Run from source tree:
|
||||
|
||||
```sh
|
||||
./dev_script/dangerzone
|
||||
```
|
||||
|
||||
Create a .deb:
|
||||
|
||||
```sh
|
||||
./install/linux/build_deb.py
|
||||
```
|
||||
|
||||
## macOS
|
||||
|
||||
## macOS
|
||||
|
||||
Install Xcode from the Mac App Store. Once it's installed, run it for the first time to set it up. Also, run this to make sure command line tools are installed: `xcode-select --install`. And finally, open Xcode, go to Preferences > Locations, and make sure under Command Line Tools you select an installed version from the dropdown. (This is required for installing Qt5.)
|
||||
|
||||
Download and install Python 3.7.4 from https://www.python.org/downloads/release/python-374/. I downloaded `python-3.7.4-macosx10.9.pkg`.
|
||||
|
||||
Install Qt 5.14.0 for macOS from https://www.qt.io/offline-installers. I downloaded `qt-opensource-mac-x64-5.14.0.dmg`. In the installer, you can skip making an account, and all you need is `Qt` > `Qt 5.14.0` > `macOS`.
|
||||
|
||||
If you don't have it already, install pipenv (`pip3 install --user pipenv`). Then install dependencies:
|
||||
|
||||
```sh
|
||||
pipenv install --dev --pre
|
||||
```
|
||||
|
||||
Run from source tree:
|
||||
|
||||
```
|
||||
pipenv run ./dev_scripts/dangerzone
|
||||
```
|
4
Pipfile
4
Pipfile
|
@ -7,10 +7,12 @@ name = "pypi"
|
|||
PyQt5 = "*"
|
||||
click = "*"
|
||||
appdirs = "*"
|
||||
pyxdg = "*"
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
|
167
Pipfile.lock
generated
Normal file
167
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,167 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0141117b8b77eba5269d570a53da1ab9d2029bc11b6a4862111e04b8b72b9ea7"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.7"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.python.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"pyqt5": {
|
||||
"hashes": [
|
||||
"sha256:2b79209aa6e4688f6ac46e6d2694236dcf91db5f3a87270150d0f82082e3d360",
|
||||
"sha256:2f230f2dbd767099de7a0cb915abdf0cbc3256a0b5bb910eb09b99117db7a65b",
|
||||
"sha256:3d6e315e6e2d6489a2e1e0148d00e784e277c6590c189227d6060f15b9be690a",
|
||||
"sha256:812233bd155735377e2e9c7eea7a28815f357440334db51788d941e2a8b62f64",
|
||||
"sha256:be10fa95e6bdc9cad616ebf368c51b3f5748138b2b3a600cf7c4f80b78cb9852"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.14.1"
|
||||
},
|
||||
"pyqt5-sip": {
|
||||
"hashes": [
|
||||
"sha256:02d94786bada670ab17a2b62ce95b3cf8e3b40c99d36007593a6334d551840bb",
|
||||
"sha256:06bc66b50556fb949f14875a4c224423dbf03f972497ccb883fb19b7b7c3b346",
|
||||
"sha256:091fbbe10a7aebadc0e8897a9449cda08d3c3f663460d812eca3001ca1ed3526",
|
||||
"sha256:0a067ade558befe4d46335b13d8b602b5044363bfd601419b556d4ec659bca18",
|
||||
"sha256:1910c1cb5a388d4e59ebb2895d7015f360f3f6eeb1700e7e33e866c53137eb9e",
|
||||
"sha256:1c7ad791ec86247f35243bbbdd29cd59989afbe0ab678e0a41211f4407f21dd8",
|
||||
"sha256:3c330ff1f70b3eaa6f63dce9274df996dffea82ad9726aa8e3d6cbe38e986b2f",
|
||||
"sha256:482a910fa73ee0e36c258d7646ef38f8061774bbc1765a7da68c65056b573341",
|
||||
"sha256:7695dfafb4f5549ce1290ae643d6508dfc2646a9003c989218be3ce42a1aa422",
|
||||
"sha256:8274ed50f4ffbe91d0f4cc5454394631edfecd75dc327aa01be8bc5818a57e88",
|
||||
"sha256:9047d887d97663790d811ac4e0d2e895f1bf2ecac4041691487de40c30239480",
|
||||
"sha256:9f6ab1417ecfa6c1ce6ce941e0cebc03e3ec9cd9925058043229a5f003ae5e40",
|
||||
"sha256:b43ba2f18999d41c3df72f590348152e14cd4f6dcea2058c734d688dfb1ec61f",
|
||||
"sha256:c3ab9ea1bc3f4ce8c57ebc66fb25cd044ef92ed1ca2afa3729854ecc59658905",
|
||||
"sha256:da69ba17f6ece9a85617743cb19de689f2d63025bf8001e2facee2ec9bcff18f",
|
||||
"sha256:ef3c7a0bf78674b0dda86ff5809d8495019903a096c128e1f160984b37848f73",
|
||||
"sha256:fabff832046643cdb93920ddaa8f77344df90768930fbe6bb33d211c4dcd0b5e"
|
||||
],
|
||||
"version": "==12.7.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
|
||||
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
|
||||
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
|
||||
],
|
||||
"version": "==19.3.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
|
||||
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==19.10b0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
|
||||
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
|
||||
],
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525",
|
||||
"sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b",
|
||||
"sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576",
|
||||
"sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5",
|
||||
"sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0",
|
||||
"sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35",
|
||||
"sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003",
|
||||
"sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d",
|
||||
"sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161",
|
||||
"sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26",
|
||||
"sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9",
|
||||
"sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1",
|
||||
"sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146",
|
||||
"sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f",
|
||||
"sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149",
|
||||
"sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351",
|
||||
"sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461",
|
||||
"sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b",
|
||||
"sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242",
|
||||
"sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c",
|
||||
"sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77"
|
||||
],
|
||||
"version": "==2020.1.8"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
|
||||
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
|
||||
],
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161",
|
||||
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
|
||||
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
|
||||
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
|
||||
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
|
||||
"sha256:48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47",
|
||||
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
|
||||
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
|
||||
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
|
||||
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
|
||||
"sha256:7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2",
|
||||
"sha256:838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e",
|
||||
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
|
||||
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
|
||||
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
|
||||
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
|
||||
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
|
||||
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
|
||||
"sha256:fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66",
|
||||
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
34
README.md
34
README.md
|
@ -15,36 +15,6 @@ Some features:
|
|||
- Dangerzone compresses the safe PDF to reduce file size
|
||||
- After converting, dangerzone lets you open the safe PDF in the PDF viewer of your choice, which allows you to open PDFs and office docs in dangerzone by default so you never accidentally open a dangerous document
|
||||
|
||||
Dangerzone was inspired by [Qubes trusted PDF](https://blog.invisiblethings.org/2013/02/21/converting-untrusted-pdfs-into-trusted.html), but it works in non-Qubes operating systems and sandboxes the document conversion in [podman](https://podman.io/) containers instead of virtual machines. Podman is like docker but more secure -- it doesn't require a privileged daemon, and containers can be launched without root.
|
||||
Dangerzone was inspired by [Qubes trusted PDF](https://blog.invisiblethings.org/2013/02/21/converting-untrusted-pdfs-into-trusted.html), but it works in non-Qubes operating systems and sandboxes the document conversion in containers instead of virtual machines (using [podman](https://podman.io/) for Linux, and Docker for macOS, for now). Podman is like docker but more secure -- it doesn't require a privileged daemon, and containers can be launched without root.
|
||||
|
||||
Right now, dangerzone only works in Linux, but the goal is to [get it working in macOS](https://github.com/firstlookmedia/dangerzone/issues/1) so it can be more useful to journalists (who tend to <3 using Macs).
|
||||
|
||||
## Development environment
|
||||
|
||||
After cloning this git repo, make sure to checkout the git submodules.
|
||||
|
||||
```
|
||||
git submodule init
|
||||
```
|
||||
|
||||
### Debian/Ubuntu
|
||||
|
||||
You need [podman](https://podman.io/getting-started/installation) ([these instructions](https://kushaldas.in/posts/podman-on-debian-buster.html) are useful for installing in Debian or Ubuntu).
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```
|
||||
sudo apt install -y python3 python3-pyqt5 python3-appdirs python3-click python3-xdg
|
||||
```
|
||||
|
||||
Run locally:
|
||||
|
||||
```
|
||||
./dev_script/dangerzone
|
||||
```
|
||||
|
||||
Create a .deb:
|
||||
|
||||
```
|
||||
./install/linux/build_deb.py
|
||||
```
|
||||
Set up a development environment by following [these instructions](/BUILD.md).
|
||||
|
|
|
@ -3,8 +3,11 @@ import os
|
|||
import inspect
|
||||
import tempfile
|
||||
import appdirs
|
||||
import platform
|
||||
from PyQt5 import QtGui
|
||||
from xdg.DesktopEntry import DesktopEntry
|
||||
|
||||
if platform.system() == "Linux":
|
||||
from xdg.DesktopEntry import DesktopEntry
|
||||
|
||||
from .settings import Settings
|
||||
|
||||
|
@ -19,8 +22,9 @@ class Common(object):
|
|||
self.app = app
|
||||
|
||||
# Temporary directory to store pixel data
|
||||
self.pixel_dir = tempfile.TemporaryDirectory()
|
||||
self.safe_dir = tempfile.TemporaryDirectory()
|
||||
# Note in macOS, temp dirs must be in /tmp (or a few other paths) for Docker to mount them
|
||||
self.pixel_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-pixel-")
|
||||
self.safe_dir = tempfile.TemporaryDirectory(prefix="/tmp/dangerzone-safe-")
|
||||
print(
|
||||
f"Temporary directories created, dangerous={self.pixel_dir.name}, safe={self.safe_dir.name}"
|
||||
)
|
||||
|
@ -37,6 +41,12 @@ class Common(object):
|
|||
# App data folder
|
||||
self.appdata_path = appdirs.user_config_dir("dangerzone")
|
||||
|
||||
# Container runtime
|
||||
if platform.system() == "Darwin":
|
||||
self.container_runtime = "docker"
|
||||
else:
|
||||
self.container_runtime = "podman"
|
||||
|
||||
# Preload list of PDF viewers on computer
|
||||
self.pdf_viewers = self._find_pdf_viewers()
|
||||
|
||||
|
@ -231,6 +241,7 @@ class Common(object):
|
|||
def _find_pdf_viewers(self):
|
||||
pdf_viewers = {}
|
||||
|
||||
if platform.system == "Linux":
|
||||
for search_path in [
|
||||
"/usr/share/applications",
|
||||
"/usr/local/share/applications",
|
||||
|
|
|
@ -8,12 +8,17 @@ class Settings:
|
|||
self.common = common
|
||||
self.settings_filename = os.path.join(self.common.appdata_path, "settings.json")
|
||||
|
||||
if len(self.common.pdf_viewers) == 0:
|
||||
default_pdf_viewer = None
|
||||
else:
|
||||
default_pdf_viewer = list(self.common.pdf_viewers)[0]
|
||||
|
||||
self.default_settings = {
|
||||
"save": True,
|
||||
"ocr": True,
|
||||
"ocr_language": "English",
|
||||
"open": True,
|
||||
"open_app": list(self.common.pdf_viewers)[0],
|
||||
"open_app": default_pdf_viewer,
|
||||
"update_container": True,
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,9 @@ class SettingsWidget(QtWidgets.QWidget):
|
|||
self.update_checkbox.setCheckState(QtCore.Qt.Unchecked)
|
||||
|
||||
# Is update containers required?
|
||||
output = subprocess.check_output(["podman", "image", "ls", "dangerzone"])
|
||||
output = subprocess.check_output(
|
||||
[self.common.container_runtime, "image", "ls", "dangerzone"]
|
||||
)
|
||||
if b"localhost/dangerzone" not in output:
|
||||
self.update_checkbox.setCheckState(QtCore.Qt.Checked)
|
||||
self.update_checkbox.setEnabled(False)
|
||||
|
|
|
@ -3,6 +3,7 @@ import time
|
|||
import tempfile
|
||||
import os
|
||||
import pipes
|
||||
import platform
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
|
||||
|
@ -15,7 +16,8 @@ class TaskBase(QtCore.QThread):
|
|||
def __init__(self):
|
||||
super(TaskBase, self).__init__()
|
||||
|
||||
def execute_podman(self, args, watch="stdout"):
|
||||
def exec_container(self, args, watch="stdout"):
|
||||
args = [self.common.container_runtime] + args
|
||||
args_str = " ".join(pipes.quote(s) for s in args)
|
||||
print(f"Executing: {args_str}")
|
||||
output = f"Executing: {args_str}\n\n"
|
||||
|
@ -55,8 +57,8 @@ class PullImageTask(TaskBase):
|
|||
def run(self):
|
||||
self.update_label.emit("Pulling container image")
|
||||
self.update_details.emit("")
|
||||
args = ["podman", "pull", "ubuntu:18.04"]
|
||||
self.execute_podman(args, watch="stderr")
|
||||
args = ["pull", "ubuntu:18.04"]
|
||||
self.exec_container(args, watch="stderr")
|
||||
self.task_finished.emit()
|
||||
|
||||
|
||||
|
@ -69,8 +71,8 @@ class BuildContainerTask(TaskBase):
|
|||
container_path = self.common.get_resource_path("container")
|
||||
self.update_label.emit("Building container")
|
||||
self.update_details.emit("")
|
||||
args = ["podman", "build", "-t", "dangerzone", container_path]
|
||||
self.execute_podman(args)
|
||||
args = ["build", "-t", "dangerzone", container_path]
|
||||
self.exec_container(args)
|
||||
self.task_finished.emit()
|
||||
|
||||
|
||||
|
@ -86,7 +88,6 @@ class ConvertToPixels(TaskBase):
|
|||
def run(self):
|
||||
self.update_label.emit("Converting document to pixels")
|
||||
args = [
|
||||
"podman",
|
||||
"run",
|
||||
"--network",
|
||||
"none",
|
||||
|
@ -97,7 +98,7 @@ class ConvertToPixels(TaskBase):
|
|||
"dangerzone",
|
||||
"document-to-pixels",
|
||||
]
|
||||
output = self.execute_podman(args)
|
||||
output = self.exec_container(args)
|
||||
|
||||
# Did we hit an error?
|
||||
for line in output.split("\n"):
|
||||
|
@ -185,7 +186,6 @@ class ConvertToPDF(TaskBase):
|
|||
|
||||
args = (
|
||||
[
|
||||
"podman",
|
||||
"run",
|
||||
"--network",
|
||||
"none",
|
||||
|
@ -197,5 +197,5 @@ class ConvertToPDF(TaskBase):
|
|||
+ envs
|
||||
+ ["dangerzone", "pixels-to-pdf",]
|
||||
)
|
||||
self.execute_podman(args)
|
||||
self.exec_container(args)
|
||||
self.task_finished.emit()
|
||||
|
|
Loading…
Reference in a new issue