Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: CI

on: [push, pull_request, workflow_dispatch]

permissions: {}

jobs:
migrations:
if: github.event_name != 'push' || github.event.repository.fork == true || github.ref == 'refs/heads/main'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Check collectstatic

on: [push, pull_request, workflow_dispatch]

permissions: {}

jobs:
collectstatic:
if: github.event_name != 'push' || github.event.repository.fork == true || github.ref == 'refs/heads/main'
Expand Down
5 changes: 4 additions & 1 deletion apps/events/templates/events/event_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ <h3 class="widget-title">More events in <!-- The current category--><a href="{{
<script>
$(function(){
$('#dataRangeSelect').change(function(e) {
window.location = $(this).val();
let url = $(this).val();
if (url && url.charAt(0) === '/' && url.charAt(1) !== '/') {
window.location = url;
Comment thread Dismissed
}
});
});
</script>
Expand Down
4 changes: 2 additions & 2 deletions apps/mailing/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Forms for the mailing app."""

from django import forms
from django.template import Context, Template, TemplateSyntaxError
from django.template import Context, TemplateSyntaxError

from apps.mailing.models import BaseEmailTemplate

Expand All @@ -13,7 +13,7 @@ def clean_content(self):
"""Validate that the content field contains valid Django template syntax."""
content = self.cleaned_data["content"]
try:
template = Template(content)
template = BaseEmailTemplate.template_engine.from_string(content)
template.render(Context({}))
except TemplateSyntaxError as e:
raise forms.ValidationError(e) from e
Expand Down
15 changes: 10 additions & 5 deletions apps/mailing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.core.mail import EmailMessage
from django.db import models
from django.template import Context, Template
from django.template import Context, Engine
from django.urls import reverse


Expand Down Expand Up @@ -33,15 +33,20 @@ def preview_content_url(self):
url_name = f"admin:{prefix}_preview"
return reverse(url_name, args=[self.pk])

template_engine = Engine(
builtins=["django.template.defaultfilters"],
autoescape=True,
Comment thread
JacobCoffee marked this conversation as resolved.
)

def render_content(self, context):
"""Render the email body using the Django template engine."""
template = Template(self.content)
"""Render the email body using a sandboxed Django template engine."""
template = self.template_engine.from_string(self.content)
ctx = Context(context)
return template.render(ctx)

def render_subject(self, context):
"""Render the email subject using the Django template engine."""
template = Template(self.subject)
"""Render the email subject using a sandboxed Django template engine."""
template = self.template_engine.from_string(self.subject)
ctx = Context(context)
return template.render(ctx)

Expand Down
42 changes: 12 additions & 30 deletions apps/pages/management/commands/fix_success_story_images.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import re
from pathlib import Path
from pathlib import PurePosixPath
from urllib.parse import urlparse

import requests
from django.conf import settings
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand

from apps.pages.models import Image, Page, page_image_path
from apps.pages.models import Image, Page

HTTP_OK = 200

Expand All @@ -18,14 +17,6 @@ class Command(BaseCommand):
def get_success_pages(self):
return Page.objects.filter(path__startswith="about/success/")

def image_url(self, path):
"""
Given a full filesystem path to an image, return the proper media
url for it
"""
new_url = path.replace(settings.MEDIA_ROOT, settings.MEDIA_URL)
return new_url.replace("//", "/")

def fix_image(self, path, page):
url = f"http://legacy.python.org{path}"
# Retrieve the image
Expand All @@ -34,27 +25,18 @@ def fix_image(self, path, page):
if r.status_code != HTTP_OK:
return None

# Create new associated image and generate ultimate path
# Extract and validate filename (alphanumeric, hyphens, dots only)
raw_name = PurePosixPath(urlparse(url).path).name
filename = re.sub(r"[^\w.\-]", "_", raw_name)
if not filename or filename.startswith("."):
return None

# Use Django's storage API to safely write the file
img = Image()
img.page = page
img.image.save(filename, ContentFile(r.content), save=True)

filename = Path(urlparse(url).path).name
output_path = page_image_path(img, filename)

# Make sure our directories exist
directory = Path(output_path).parent
directory.mkdir(parents=True, exist_ok=True)

# Write image data to our location
with Path(output_path).open("wb") as f:
f.write(r.content)

# Re-open the image as a Django File object
with Path(output_path).open("rb") as reopen:
new_file = File(reopen)
img.image.save(filename, new_file, save=True)

return self.image_url(output_path)
return img.image.url

def find_image_paths(self, page):
content = page.content.raw
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def api_call(uri, query):

headers = {
"X-API-Key": str(settings.PYCON_API_KEY),
"X-API-Signature": str(sha1(base_string.encode("utf-8")).hexdigest()), # noqa: S324 - API signature, not for security storage
"X-API-Signature": str(sha1(base_string.encode("utf-8")).hexdigest()), # noqa: S324 - required by PyCon API
Comment thread Dismissed
"X-API-Timestamp": str(timestamp),
}
scheme = "http" if settings.DEBUG else "https"
Expand Down
8 changes: 6 additions & 2 deletions pydotorg/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import re
from collections import defaultdict
from pathlib import Path
from pathlib import Path, PurePosixPath

from django.conf import settings
from django.http import HttpResponse, JsonResponse
Expand Down Expand Up @@ -84,7 +84,11 @@ class MediaMigrationView(RedirectView):

def get_redirect_url(self, *args, **kwargs):
"""Build the S3 redirect URL from the media path."""
image_path = kwargs["url"]
# Normalize path to prevent traversal (../, ....//, %2e%2e/, etc.)
image_path = PurePosixPath(kwargs["url"]).as_posix().lstrip("/")
# Collapse any remaining parent references after normalization
parts = [p for p in image_path.split("/") if p not in (".", "..")]
image_path = "/".join(parts)
if self.prefix:
image_path = f"{self.prefix}/{image_path}"
return f"{settings.AWS_S3_ENDPOINT_URL}/{settings.AWS_STORAGE_BUCKET_NAME}/{image_path}"
Expand Down
2 changes: 1 addition & 1 deletion static/fonts/demo/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ document.body.addEventListener("click", function(e) {
testDrive = document.getElementById('testDrive'),
testText = document.getElementById('testText');
function updateTest() {
testDrive.innerHTML = testText.value || String.fromCharCode(160);
testDrive.textContent = testText.value || String.fromCharCode(160);
if (window.icomoonLiga) {
window.icomoonLiga(testDrive);
}
Expand Down
24 changes: 21 additions & 3 deletions static/js/sponsors/applicationForm.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
const DESKTOP_WIDTH_LIMIT = 1200;

function isSafeImageSrc(url) {
if (!url) return false;
// Allow relative URLs starting with / (but not protocol-relative //)
if (url.charAt(0) === '/' && url.charAt(1) !== '/') return true;
try {
let parsed = new URL(url, window.location.origin);
return parsed.origin === window.location.origin;
} catch (e) {
return false;
}
}
Comment thread
JacobCoffee marked this conversation as resolved.

$(document).ready(function(){
const SELECTORS = {
packageInput: function() { return $("input[name=package]"); },
Expand Down Expand Up @@ -66,7 +78,9 @@ $(document).ready(function(){
img.setAttribute('data-next-state', src);
}

img.setAttribute('src', initImg);
if (isSafeImageSrc(initImg)) {
img.setAttribute('src', initImg);
Comment thread Dismissed
}
});
$(".selected").removeClass("selected");
$('.custom-fee').hide();
Expand All @@ -89,7 +103,9 @@ $(document).ready(function(){
img.setAttribute('data-next-state', src);
}

img.setAttribute('src', initImg);
if (isSafeImageSrc(initImg)) {
img.setAttribute('src', initImg);
Comment thread Dismissed
}
});
$(".selected").removeClass("selected");

Expand Down Expand Up @@ -166,7 +182,9 @@ function benefitUpdate(benefitId, packageId) {
const benefitsInputs = Array(...document.querySelectorAll('[data-benefit-id]'));
const hiddenInput = benefitsInputs.filter((b) => b.getAttribute('data-benefit-id') == benefitId)[0];
hiddenInput.checked = !hiddenInput.checked;
clickedImg.src = newSrc;
if (isSafeImageSrc(newSrc)) {
clickedImg.src = newSrc;
Comment thread Dismissed
}

// Check if there are any type of customization. If so, display custom-fee label.
let pkgBenefits = Array(...document.getElementById(`package_benefits_${packageId}`).children).map(div => div.textContent).sort();
Expand Down