Skip to content

Commit 82e3c85

Browse files
committed
Add staticfile finder to limit exposure
1 parent 1aeeeaf commit 82e3c85

File tree

8 files changed

+175
-46
lines changed

8 files changed

+175
-46
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,16 @@ INSTALLED_APPS = [
3030
]
3131
```
3232

33-
Next, add the `node_modules` directory to your staticfiles directories:
33+
Next, add a new staticfiles finder to your `STATICFILES_FINDERS` setting:
3434

3535
```python
3636
# settings.py
37-
STATICFILES_DIRS = [
38-
BASE_DIR / "node_modules",
37+
STATICFILES_FINDERS = [
38+
# Django's default finders
39+
"django.contrib.staticfiles.finders.FileSystemFinder",
40+
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
41+
# django-esm finder
42+
"django_esm.finders.ESMFinder",
3943
]
4044
```
4145

django_esm/finders.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import functools
2+
import json
3+
4+
from django.conf import settings
5+
from django.contrib.staticfiles.finders import BaseFinder
6+
from django.contrib.staticfiles.utils import matches_patterns
7+
from django.core.checks import Error
8+
9+
from . import storages, utils
10+
11+
12+
class ESMFinder(BaseFinder):
13+
def __init__(self, apps=None, *args, **kwargs):
14+
self.apps = apps or []
15+
super().__init__(*args, **kwargs)
16+
17+
def check(self, **kwargs):
18+
return [*self._check_package_json()]
19+
20+
def _check_package_json(self):
21+
if not (settings.BASE_DIR / "package.json").exists():
22+
return [
23+
Error(
24+
"package.json not found",
25+
hint="Run `npm init` to create a package.json file.",
26+
obj=self,
27+
id="django_esm.E001",
28+
)
29+
]
30+
return []
31+
32+
def find(self, path, all=False):
33+
if path in self.all:
34+
return [path] if all else path
35+
return [] # this method has a strange return type
36+
37+
def list(self, ignore_patterns):
38+
return self._list(*ignore_patterns)
39+
40+
@staticmethod
41+
@functools.lru_cache()
42+
def _list(*ignore_patterns):
43+
with (settings.BASE_DIR / "package.json").open() as f:
44+
package_json = json.load(f)
45+
46+
return [
47+
(path, storages.root_storage)
48+
for mod, path in utils.parse_root_package(package_json)
49+
if not matches_patterns(path, ignore_patterns)
50+
] + [
51+
(path, storages.node_modules_storage)
52+
for mod, path in utils.parse_dependencies(package_json)
53+
if not matches_patterns(path, ignore_patterns)
54+
]
55+
56+
@functools.cached_property
57+
def all(self):
58+
return [path for path, storage in self.list([])]

django_esm/storages.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.conf import settings
2+
from django.core.files.storage import FileSystemStorage
3+
4+
root_storage = FileSystemStorage(
5+
location=settings.BASE_DIR, base_url=settings.STATIC_URL
6+
)
7+
node_modules_storage = FileSystemStorage(
8+
location=settings.BASE_DIR / "node_modules", base_url=settings.STATIC_URL
9+
)

django_esm/templatetags/esm.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django import template
55
from django.conf import settings
6+
from django.contrib.staticfiles.storage import staticfiles_storage
67
from django.utils.safestring import mark_safe
78

89
from .. import utils
@@ -11,24 +12,18 @@
1112

1213

1314
@register.simple_tag
15+
@functools.lru_cache()
1416
def importmap():
15-
fn = _importmap if settings.DEBUG else _cached_importmap
16-
return fn()
17-
18-
19-
functools.lru_cache()
20-
21-
22-
def _cached_importmap():
23-
return _importmap()
24-
25-
26-
def _importmap():
2717
with (settings.BASE_DIR / "package.json").open() as f:
2818
package_json = json.load(f)
19+
20+
imports = dict(utils.parse_root_package(package_json)) | dict(
21+
utils.parse_dependencies(package_json)
22+
)
23+
2924
return mark_safe( # nosec
3025
json.dumps(
31-
{"imports": dict(utils.parse_root_package(package_json))},
26+
{"imports": {k: staticfiles_storage.url(v) for k, v in imports.items()}},
3227
indent=2 if settings.DEBUG else None,
3328
separators=None if settings.DEBUG else (",", ":"),
3429
)

django_esm/utils.py

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
from __future__ import annotations
2+
13
import itertools
24
import json
35
import re
46
from pathlib import Path
57

68
from django.conf import settings
7-
from django.contrib.staticfiles.finders import get_finders
8-
from django.contrib.staticfiles.storage import staticfiles_storage
99

1010

1111
def parse_root_package(package_json):
@@ -26,32 +26,32 @@ def parse_root_package(package_json):
2626
url = mod
2727
if mod[0] in [".", "/"]:
2828
# local file
29-
yield from get_static_from_abs_path(module_name, settings.BASE_DIR / mod)
29+
yield from get_static_from_abs_path(
30+
module_name, settings.BASE_DIR / mod, settings.BASE_DIR
31+
)
3032
else:
3133
yield module_name, url
3234

35+
36+
def parse_dependencies(package_json):
3337
for dep_name, dep_version in package_json.get("dependencies", {}).items():
3438
yield from parse_package_json(settings.BASE_DIR / "node_modules" / dep_name)
3539

3640

37-
def get_static_from_abs_path(mod: str, path: Path):
38-
for finder in get_finders():
39-
for storage in finder.storages.values():
40-
try:
41-
rel_path = path.relative_to(Path(storage.location).resolve())
42-
except ValueError:
43-
pass
44-
else:
45-
if "*" in mod:
46-
for match in Path(storage.location).rglob(
47-
str(rel_path).replace("*", "**/*")
48-
):
49-
sp = str(match.relative_to(Path(storage.location).resolve()))
50-
pattern = re.escape(str(rel_path)).replace(r"\*", r"(.*)")
51-
bit = re.match(pattern, sp).group(1)
52-
yield mod.replace("*", bit), staticfiles_storage.url(sp)
53-
else:
54-
yield mod, staticfiles_storage.url(str(rel_path))
41+
def get_static_from_abs_path(mod: str, path: Path, location: Path):
42+
try:
43+
rel_path = path.relative_to(location.resolve())
44+
except ValueError:
45+
pass
46+
else:
47+
if "*" in mod:
48+
for match in location.rglob(str(rel_path).replace("*", "**/*")):
49+
sp = str(match.relative_to(location.resolve()))
50+
pattern = re.escape(str(rel_path)).replace(r"\*", r"(.*)")
51+
bit = re.match(pattern, sp).group(1)
52+
yield mod.replace("*", bit), sp
53+
else:
54+
yield mod, str(rel_path)
5555

5656

5757
# There is a long history how ESM is supported in Node.js
@@ -101,7 +101,9 @@ def parse_package_json(path: Path = None):
101101
module = next(find_default_key(module))
102102

103103
yield from get_static_from_abs_path(
104-
str(Path(name) / module_name), path / module
104+
str(Path(name) / module_name),
105+
path / module,
106+
settings.BASE_DIR / "node_modules",
105107
)
106108

107109
for dep_name, dep_version in dependencies.items():

tests/test_finders.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from django.contrib.staticfiles.finders import get_finder
2+
from django.core.checks import Error
3+
4+
from django_esm import storages
5+
6+
7+
class TestESMFinder:
8+
finder = get_finder("django_esm.finders.ESMFinder")
9+
10+
def test_find(self):
11+
assert self.finder.find("foo") == []
12+
assert (
13+
self.finder.find("testapp/static/js/index.js")
14+
== "testapp/static/js/index.js"
15+
)
16+
assert (
17+
self.finder.find("testapp/static/js/components/index.js")
18+
== "testapp/static/js/components/index.js"
19+
)
20+
assert self.finder.find("lit-html/lit-html.js", all=True) == [
21+
"lit-html/lit-html.js"
22+
]
23+
assert self.finder.find("foo/bar.js") == []
24+
25+
def test_list(self):
26+
all_files = self.finder.list([])
27+
assert ("testapp/static/js/index.js", storages.root_storage) in all_files
28+
assert (
29+
"testapp/static/js/components/index.js",
30+
storages.root_storage,
31+
) in all_files
32+
assert ("lit-html/lit-html.js", storages.node_modules_storage) in all_files
33+
assert ("htmx.org/dist/htmx.min.js", storages.node_modules_storage) in all_files
34+
35+
def test_check(self, settings):
36+
assert not self.finder.check()
37+
38+
settings.BASE_DIR = settings.BASE_DIR / "foo"
39+
40+
assert self.finder.check() == [
41+
Error(
42+
"package.json not found",
43+
hint="Run `npm init` to create a package.json file.",
44+
obj=self.finder,
45+
id="django_esm.E001",
46+
)
47+
]

tests/test_utils.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22

33
import pytest
4+
from django.conf import settings
45

56
from django_esm import utils
67

@@ -9,16 +10,20 @@
910

1011
def test_parse_root_package(package_json):
1112
import_map = dict(utils.parse_root_package(package_json))
12-
assert import_map["htmx.org"] == "/static/htmx.org/dist/htmx.min.js"
13-
assert import_map["lit"] == "/static/lit/index.js"
13+
assert import_map["#index"] == "testapp/static/js/index.js"
14+
assert import_map["#components/index.js"] == "testapp/static/js/components/index.js"
15+
assert import_map["#htmx"] == "https://unpkg.com/htmx.org@1.9.10"
16+
17+
18+
def test_parse_dependencies(package_json):
19+
import_map = dict(utils.parse_dependencies(package_json))
20+
assert import_map["lit-html"] == "lit-html/lit-html.js"
21+
assert import_map["htmx.org"] == "htmx.org/dist/htmx.min.js"
22+
assert import_map["lit"] == "lit/index.js"
1423
assert (
1524
import_map["@lit/reactive-element"]
16-
== "/static/%40lit/reactive-element/reactive-element.js"
25+
== "@lit/reactive-element/reactive-element.js"
1726
)
18-
assert import_map["lit-html"] == "/static/lit-html/lit-html.js"
19-
assert import_map["#index"] == "/static/js/index.js"
20-
assert import_map["#components/index.js"] == "/static/js/components/index.js"
21-
assert import_map["#htmx"] == "https://unpkg.com/htmx.org@1.9.10"
2227

2328

2429
def test_parse_root_package__bad_imports(package_json):
@@ -47,4 +52,8 @@ def test_cast_exports():
4752

4853

4954
def test_get_static_from_abs_path():
50-
assert not list(utils.get_static_from_abs_path("#some-module", Path("/foo/bar")))
55+
assert not list(
56+
utils.get_static_from_abs_path(
57+
"#some-module", Path("/foo/bar"), settings.BASE_DIR
58+
)
59+
)

tests/testapp/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,8 @@
8080
STATICFILES_DIRS = [
8181
BASE_DIR / "node_modules",
8282
]
83+
STATICFILES_FINDERS = [
84+
"django.contrib.staticfiles.finders.FileSystemFinder",
85+
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
86+
"django_esm.finders.ESMFinder",
87+
]

0 commit comments

Comments
 (0)