Skip to content

Commit 1177006

Browse files
committed
Make URL validation more flexible and normalize URLs to add http protocol when missing
1 parent 6446e50 commit 1177006

4 files changed

Lines changed: 37 additions & 16 deletions

File tree

.github/scripts/event-issue-helpers.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ export function isValidEmail(value) {
7272
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
7373
}
7474

75+
const HTTP_PROTOCOL_RE = /^https?:\/\//i;
76+
77+
export function normalizeUrl(value) {
78+
if (!value) return value;
79+
if (HTTP_PROTOCOL_RE.test(value)) return value;
80+
return `http://${value}`;
81+
}
82+
7583
export function isValidHttpUrl(value) {
7684
try {
7785
const url = new URL(value);

.github/scripts/event-issue-helpers.test.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isValidTime,
77
isValidEmail,
88
isValidHttpUrl,
9+
normalizeUrl,
910
slugify,
1011
parseActivities,
1112
parseOrganizers,
@@ -87,6 +88,16 @@ describe('isValidHttpUrl', () => {
8788
test('empty string', () => assert.equal(isValidHttpUrl(''), false));
8889
});
8990

91+
// ── normalizeUrl ──────────────────────────────────────────────────────────────
92+
93+
describe('normalizeUrl', () => {
94+
test('http URL unchanged', () => assert.equal(normalizeUrl('http://example.com'), 'http://example.com'));
95+
test('https URL unchanged', () => assert.equal(normalizeUrl('https://example.com/path'), 'https://example.com/path'));
96+
test('www without protocol', () => assert.equal(normalizeUrl('www.example.com'), 'http://www.example.com'));
97+
test('bare domain', () => assert.equal(normalizeUrl('example.com'), 'http://example.com'));
98+
test('empty string unchanged', () => assert.equal(normalizeUrl(''), ''));
99+
});
100+
90101
// ── slugify ───────────────────────────────────────────────────────────────────
91102

92103
describe('slugify', () => {

.github/scripts/process-edit-event-issue.mjs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isValidTime,
1616
isValidEmail,
1717
isValidHttpUrl,
18+
normalizeUrl,
1819
parseActivities,
1920
parseOrganizers,
2021
buildValidationComment,
@@ -68,25 +69,25 @@ async function main() {
6869
message: 'This field is required. Choose one of: `In person` or `Online`.',
6970
});
7071
const isOnlineEvent = eventFormat === 'Online';
71-
const eventUrl = fields.get('Event URL (only for online events)')?.trim() ?? '';
72+
const eventUrl = normalizeUrl(fields.get('Event URL (only for online events)')?.trim() ?? '');
7273
const primaryContactName = required(fields, 'Primary contact name', errors);
7374
const contactEmail = required(fields, 'Primary contact email', errors);
7475
const city = fields.get('City or locality')?.trim() ?? '';
7576
const country = fields.get('Country')?.trim() ?? '';
7677
const organizationName = fields.get('Organization name')?.trim() ?? '';
77-
const organizationUrl = fields.get('Organization website')?.trim() ?? '';
78+
const organizationUrl = normalizeUrl(fields.get('Organization website')?.trim() ?? '');
7879
const organizationType = (fields.get('Organization type')?.trim() ?? '').replace(/^None$/i, '');
7980
const address = fields.get('Street address of the event venue')?.trim() ?? '';
8081
const eventDate = fields.get('Date of the event')?.trim() ?? '';
8182
const eventEndDate = fields.get('End date (for multi-day events)')?.trim() ?? '';
8283
const startTime = fields.get('Start time')?.trim() ?? '';
8384
const endTime = fields.get('End time')?.trim() ?? '';
84-
const eventPageUrl = fields.get('Event page URL')?.trim() ?? '';
85+
const eventPageUrl = normalizeUrl(fields.get('Event page URL')?.trim() ?? '');
8586
const organizers = parseOrganizers(fields.get('Organizers')?.trim() ?? '');
8687
const shortDescription = fields.get('Short description')?.trim() ?? '';
8788
const fullDescription = fields.get('Full event description')?.trim() ?? '';
8889
const submittedActivities = parseActivities(fields.get('Event activities')?.trim() ?? '');
89-
const forumThreadUrl = fields.get('Forum discussion URL')?.trim() ?? '';
90+
const forumThreadUrl = normalizeUrl(fields.get('Forum discussion URL')?.trim() ?? '');
9091
const maintainerNotes = fields.get('Additional notes')?.trim() ?? '';
9192

9293
// Parse uid from canonical ID (format: <slug>-<7hexchars>)
@@ -159,12 +160,12 @@ async function main() {
159160
) {
160161
errors.push({ field: 'End time', found: endTime, message: 'End time must be later than start time for single-day events.' });
161162
}
162-
if (eventPageUrl && !isValidHttpUrl(eventPageUrl)) errors.push({ field: 'Event page URL', found: eventPageUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
163-
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push({ field: 'Forum discussion URL', found: forumThreadUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
163+
if (eventPageUrl && !isValidHttpUrl(eventPageUrl)) errors.push({ field: 'Event page URL', found: eventPageUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com/pcd-my-city-2026`.' });
164+
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push({ field: 'Forum discussion URL', found: forumThreadUrl, message: 'Not a valid URL. Please enter a web address like `https://discourse.processing.org/t/pcd-my-city-2026`.' });
164165
if (contactEmail && !isValidEmail(contactEmail)) errors.push({ field: 'Primary contact email', found: contactEmail, message: 'Not a valid email address. Please provide a valid email like `you@example.com`.' });
165166
if (rawPlusCode && !resolvedPlusCode) errors.push({ field: 'Map placement (Plus Code)', found: rawPlusCode.replace(/\s+/g, '').toUpperCase(), message: 'Not a valid full global Plus Code. It should look like `8FW4V75V+8Q`. [Find your Plus Code →](https://plus.codes/)' });
166-
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push({ field: 'Event URL', found: eventUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
167-
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push({ field: 'Organization website', found: organizationUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
167+
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push({ field: 'Event URL', found: eventUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com/pcd-my-city-2026`.' });
168+
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push({ field: 'Organization website', found: organizationUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com`.' });
168169
if (eventFormat && !VALID_EVENT_FORMATS.has(eventFormat)) errors.push({ field: 'Event format', found: eventFormat, message: 'Not a recognized option. Please choose one of: `In person` or `Online`.' });
169170
if (organizationType && !VALID_ORG_TYPES.has(organizationType)) errors.push({ field: 'Organization type', found: organizationType, message: 'Not a recognized option. Please choose one of the valid options from the form.' });
170171
if (isOnlineEvent && !eventUrl) errors.push({ field: 'Event URL', message: 'An event URL is required for online events. Please provide the URL where people can join.' });

.github/scripts/process-new-event-issue.mjs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
isValidTime,
1717
isValidEmail,
1818
isValidHttpUrl,
19+
normalizeUrl,
1920
slugify,
2021
parseActivities,
2122
parseOrganizers,
@@ -79,26 +80,26 @@ async function main() {
7980
message: 'This field is required. Choose one of: `In person` or `Online`.',
8081
});
8182
const isOnlineEvent = eventFormat === 'Online';
82-
const eventUrl = fields.get('Event URL (only for online events)')?.trim() ?? '';
83+
const eventUrl = normalizeUrl(fields.get('Event URL (only for online events)')?.trim() ?? '');
8384
const primaryContactName = required(fields, 'Primary contact name', errors);
8485
const contactEmail = required(fields, 'Primary contact email', errors);
8586
// Parse city/country before plus_code resolution — needed for short-code recovery
8687
const city = fields.get('City or locality')?.trim() ?? '';
8788
const country = fields.get('Country')?.trim() ?? '';
8889
const organizationName = fields.get('Organization name')?.trim() ?? '';
89-
const organizationUrl = fields.get('Organization website')?.trim() ?? '';
90+
const organizationUrl = normalizeUrl(fields.get('Organization website')?.trim() ?? '');
9091
const organizationType = (fields.get('Organization type')?.trim() ?? '').replace(/^None$/i, '');
9192
const address = fields.get('Street address of the event venue')?.trim() ?? '';
9293
const eventDate = fields.get('Date of the event')?.trim() ?? '';
9394
const eventEndDate = fields.get('End date (for multi-day events)')?.trim() ?? '';
9495
const startTime = fields.get('Start time')?.trim() ?? '';
9596
const endTime = fields.get('End time')?.trim() ?? '';
96-
const eventPageUrl = fields.get('Event page URL')?.trim() ?? '';
97+
const eventPageUrl = normalizeUrl(fields.get('Event page URL')?.trim() ?? '');
9798
const organizers = parseOrganizers(fields.get('Organizers')?.trim() ?? '');
9899
const shortDescription = fields.get('Short description')?.trim() ?? '';
99100
const fullDescription = fields.get('Full event description')?.trim() ?? '';
100101
const activities = parseActivities(fields.get('Event activities')?.trim() ?? '');
101-
const forumThreadUrl = fields.get('Forum discussion URL')?.trim() ?? '';
102+
const forumThreadUrl = normalizeUrl(fields.get('Forum discussion URL')?.trim() ?? '');
102103
const maintainerNotes = fields.get('Additional notes')?.trim() ?? '';
103104

104105
// Resolve plus_code with smart recovery before validation
@@ -123,12 +124,12 @@ async function main() {
123124
) {
124125
errors.push({ field: 'End time', found: endTime, message: 'End time must be later than start time for single-day events.' });
125126
}
126-
if (eventPageUrl && !isValidHttpUrl(eventPageUrl)) errors.push({ field: 'Event page URL', found: eventPageUrl, message: 'Must be a valid URL starting with `http://` or `https://`, e.g. `https://example.com/pcd`.' });
127-
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push({ field: 'Forum discussion URL', found: forumThreadUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
127+
if (eventPageUrl && !isValidHttpUrl(eventPageUrl)) errors.push({ field: 'Event page URL', found: eventPageUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com/pcd`.' });
128+
if (forumThreadUrl && !isValidHttpUrl(forumThreadUrl)) errors.push({ field: 'Forum discussion URL', found: forumThreadUrl, message: 'Not a valid URL. Please enter a web address like `https://forum.example.com/thread`.' });
128129
if (contactEmail && !isValidEmail(contactEmail)) errors.push({ field: 'Primary contact email', found: contactEmail, message: 'Not a valid email address. Please provide a valid email like `you@example.com`.' });
129130
if (rawPlusCode && !resolvedPlusCode) errors.push({ field: 'Map placement (Plus Code)', found: rawPlusCode.replace(/\s+/g, '').toUpperCase(), message: 'Not a valid full global Plus Code. It should look like `8FW4V75V+8Q`. [Find your Plus Code →](https://plus.codes/)' });
130-
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push({ field: 'Event URL', found: eventUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
131-
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push({ field: 'Organization website', found: organizationUrl, message: 'Must be a valid URL starting with `http://` or `https://`.' });
131+
if (eventUrl && !isValidHttpUrl(eventUrl)) errors.push({ field: 'Event URL', found: eventUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com`.' });
132+
if (organizationUrl && !isValidHttpUrl(organizationUrl)) errors.push({ field: 'Organization website', found: organizationUrl, message: 'Not a valid URL. Please enter a web address like `https://example.com`.' });
132133
if (eventFormat && !VALID_EVENT_FORMATS.has(eventFormat)) errors.push({ field: 'Event format', found: eventFormat, message: 'Not a recognized option. Please choose one of: `In person` or `Online`.' });
133134
if (organizationType && !VALID_ORG_TYPES.has(organizationType)) errors.push({ field: 'Organization type', found: organizationType, message: 'Not a recognized option. Please choose one of the valid options from the form.' });
134135
if (isOnlineEvent && !eventUrl) errors.push({ field: 'Event URL', message: 'An event URL is required for online events. Please provide the URL where people can join.' });

0 commit comments

Comments
 (0)