ultimate-jekyll-manager 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/CLAUDE.md +110 -1
- package/dist/assets/css/core/_utilities.scss +2 -2
- package/dist/assets/js/core/auth.js +2 -1
- package/dist/assets/js/libs/form-manager.js +1 -1
- package/dist/assets/js/libs/prerendered-icons.js +2 -1
- package/dist/assets/js/modules/redirect.js +15 -7
- package/dist/assets/js/modules/vert.js +5 -1
- package/dist/assets/js/pages/account/sections/billing.js +2 -2
- package/dist/assets/js/pages/account/sections/connections.js +1 -1
- package/dist/assets/js/pages/account/sections/referrals.js +10 -18
- package/dist/assets/js/pages/account/sections/refund.js +2 -2
- package/dist/assets/js/pages/account/sections/security.js +1 -1
- package/dist/assets/js/pages/account/sections/team.js +1 -1
- package/dist/assets/js/pages/admin/calendar/campaign-preview.js +7 -1
- package/dist/defaults/dist/redirects/misc/search-cse.html +1 -9
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
|
+
## [1.1.2] - 2026-04-08
|
|
19
|
+
### Fixed
|
|
20
|
+
- Fix AdSense minimum width error in dashboard sidebar by increasing sidebar width from 280px to 282px (content area now meets 250px minimum)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Update dependencies: fast-xml-parser, postcss, webpack, wonderful-fetch, prepare-package
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
## [1.1.1] - 2026-04-06
|
|
27
|
+
### Security
|
|
28
|
+
- Fix open redirect via `authReturnUrl` URL parameter in core/auth.js — now validated with `isValidRedirectUrl()`
|
|
29
|
+
- Fix cross-origin redirect via unvalidated postMessage in vert.js — added origin allowlist
|
|
30
|
+
- Replace `new Function()` code execution in redirect.js with safe named modifier lookup
|
|
31
|
+
- Sanitize markdown-it output with DOMPurify in campaign-preview.js (newsletter-safe tag allowlist)
|
|
32
|
+
- Validate OAuth redirect URL scheme in connections.js
|
|
33
|
+
- Escape `classes` parameter in prerendered-icons.js to prevent attribute breakout
|
|
34
|
+
- Defense-in-depth: escape `formatDate()` outputs in security.js, team.js, referrals.js
|
|
35
|
+
- Defense-in-depth: escape cancel/refund reason strings in billing.js, refund.js
|
|
36
|
+
- Defense-in-depth: escape `submittingText` in form-manager.js spinner
|
|
37
|
+
- Document redirect validation, postMessage origin checks, eval prohibition, and DOMPurify rules in CLAUDE.md
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `dompurify` dependency for HTML sanitization
|
|
41
|
+
|
|
18
42
|
## [1.1.0] - 2026-04-06
|
|
19
43
|
### Added
|
|
20
44
|
- `payment-config.js` shared library for reading payment data from build-time config
|
package/CLAUDE.md
CHANGED
|
@@ -429,6 +429,69 @@ $el.textContent = data.message; // Safe — no escaping needed
|
|
|
429
429
|
|
|
430
430
|
Only use `innerHTML` when you need actual HTML structure (tags, classes, etc.), and escape every dynamic value in it.
|
|
431
431
|
|
|
432
|
+
### Even "Safe" Values Must Be Escaped
|
|
433
|
+
Even values that *seem* safe (like `Date.toLocaleDateString()` output, numeric calculations, or hardcoded config strings) MUST be escaped when inserted via `innerHTML`. This is defense-in-depth — if the data source ever changes, the escaping is already in place.
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// ✅ CORRECT — escape even "safe" values in innerHTML
|
|
437
|
+
$el.innerHTML = `<small>${webManager.utilities().escapeHTML(formatDate(timestamp))}</small>`;
|
|
438
|
+
$el.innerHTML = `<span>${webManager.utilities().escapeHTML(reason)}</span>`;
|
|
439
|
+
|
|
440
|
+
// ❌ WRONG — assuming the value is safe because it's from a date formatter
|
|
441
|
+
$el.innerHTML = `<small>${formatDate(timestamp)}</small>`;
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Redirects Must Be Validated
|
|
445
|
+
Never redirect to a URL from untrusted sources without validation:
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
// ✅ CORRECT — validate before redirect
|
|
449
|
+
const url = urlParams.get('returnUrl');
|
|
450
|
+
if (url && webManager.isValidRedirectUrl(url)) {
|
|
451
|
+
window.location.href = url;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ✅ CORRECT — validate API response URLs have safe scheme
|
|
455
|
+
if (response.url && /^https?:\/\//i.test(response.url)) {
|
|
456
|
+
window.location.href = response.url;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ❌ WRONG — redirect to unvalidated input
|
|
460
|
+
window.location.href = urlParams.get('returnUrl');
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### postMessage Handlers Must Check Origin
|
|
464
|
+
Always validate `event.origin` when handling `window.addEventListener('message', ...)`:
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
// ✅ CORRECT
|
|
468
|
+
window.addEventListener('message', (event) => {
|
|
469
|
+
if (event.origin !== window.location.origin && event.origin !== 'https://trusted-domain.com') {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// handle message
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ❌ WRONG — any origin can send messages
|
|
476
|
+
window.addEventListener('message', (event) => {
|
|
477
|
+
window.location.href = event.data.url; // attacker-controlled redirect
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Never Use eval() or new Function()
|
|
482
|
+
Do not use `eval()`, `new Function()`, `setTimeout(string)`, or `setInterval(string)`. These execute arbitrary code and violate CSP policies.
|
|
483
|
+
|
|
484
|
+
### Sanitize Markdown/Rich Text Output
|
|
485
|
+
When rendering user-authored markdown or rich text, use DOMPurify to sanitize the output:
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
import DOMPurify from 'dompurify';
|
|
489
|
+
const safeHTML = DOMPurify.sanitize(md.render(userContent), {
|
|
490
|
+
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'br', 'a', 'b', 'strong', 'i', 'em', 'ul', 'ol', 'li', 'img', 'code', 'pre'],
|
|
491
|
+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'target', 'rel'],
|
|
492
|
+
});
|
|
493
|
+
```
|
|
494
|
+
|
|
432
495
|
### Do NOT Escape Values Passed to textContent-Based APIs
|
|
433
496
|
`showNotification()`, `formManager.showSuccess()`, `formManager.showError()`, and `textContent` assignments use safe text insertion internally. Pre-escaping these causes double-encoding (e.g., `We'll` displays as `We'll`).
|
|
434
497
|
|
|
@@ -1108,12 +1171,24 @@ const response = await authorizedFetch(url, options);
|
|
|
1108
1171
|
- No need to manually call `webManager.auth().getIdToken()`
|
|
1109
1172
|
- Automatic token injection as Authorization Bearer header
|
|
1110
1173
|
- Centralized authentication handling
|
|
1174
|
+
- Automatic usage sync: extracts `bm-properties` header from every response and updates `webManager.bindings()` with fresh usage data under the `usage` key
|
|
1175
|
+
|
|
1176
|
+
**Options pass-through:** All `wonderful-fetch` options (`response`, `output`, `body`, `timeout`, etc.) are passed through untouched. Internally, `authorizedFetch` uses `output: 'complete'` to read response headers, then returns only the body by default. If the caller passes `output: 'complete'`, they get the full `{ status, headers, body }` response.
|
|
1177
|
+
|
|
1178
|
+
**Automatic Usage Binding Sync:**
|
|
1179
|
+
|
|
1180
|
+
After every successful response, `authorizedFetch` reads the `bm-properties` header and updates the `usage` bindings key:
|
|
1181
|
+
```javascript
|
|
1182
|
+
// After an API call, bindings are automatically updated:
|
|
1183
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
1184
|
+
```
|
|
1185
|
+
This means any `data-wm-bind` elements bound to `usage.*` paths are automatically kept in sync without any manual work. See "Usage Bindings" below.
|
|
1111
1186
|
|
|
1112
1187
|
**⚠️ IMPORTANT: Auth State Requirement**
|
|
1113
1188
|
|
|
1114
1189
|
`authorizedFetch` requires Firebase Auth to have determined the current user's authentication state before being called. On fresh page loads (e.g., OAuth callback pages, deep links), Firebase Auth needs time to restore the session from IndexedDB/localStorage.
|
|
1115
1190
|
|
|
1116
|
-
**If called before auth state is determined, it will
|
|
1191
|
+
**If called before auth state is determined, it will warn: `"No authenticated user found"`**
|
|
1117
1192
|
|
|
1118
1193
|
**Solution:** Wait for auth state before calling `authorizedFetch`:
|
|
1119
1194
|
|
|
@@ -1136,6 +1211,40 @@ webManager.auth().listen({ once: true }, async () => {
|
|
|
1136
1211
|
|
|
1137
1212
|
**Reference:** `src/assets/js/libs/authorized-fetch.js`
|
|
1138
1213
|
|
|
1214
|
+
#### Usage Bindings
|
|
1215
|
+
|
|
1216
|
+
Usage data is available in the `usage` bindings key. It is populated from two sources:
|
|
1217
|
+
|
|
1218
|
+
1. **On page load (auth settle):** `web-manager` reads `account.usage` from Firestore and resolves plan limits from `config.payment.plans`, then sets `usage` bindings with the merged data.
|
|
1219
|
+
2. **After API calls:** `authorizedFetch` reads the `bm-properties` response header and merges fresh usage counters + limits into the existing `usage` bindings.
|
|
1220
|
+
|
|
1221
|
+
**Bindings structure:**
|
|
1222
|
+
```javascript
|
|
1223
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
1224
|
+
// usage.requests = { monthly: 20, limit: 500 }
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
**HTML usage:**
|
|
1228
|
+
```html
|
|
1229
|
+
<!-- Show usage counter: "5/100" -->
|
|
1230
|
+
<span data-wm-bind="@show usage.credits">
|
|
1231
|
+
<span data-wm-bind="usage.credits.monthly">–</span>/<span data-wm-bind="usage.credits.limit">–</span>
|
|
1232
|
+
</span>
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
**Config requirement:** Plan limits must be defined in `_config.yml` under `web_manager.payment.plans`:
|
|
1236
|
+
```yaml
|
|
1237
|
+
web_manager:
|
|
1238
|
+
payment:
|
|
1239
|
+
plans:
|
|
1240
|
+
- id: basic
|
|
1241
|
+
limits:
|
|
1242
|
+
credits: 100
|
|
1243
|
+
- id: premium
|
|
1244
|
+
limits:
|
|
1245
|
+
credits: 500
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1139
1248
|
#### Payment Config Library
|
|
1140
1249
|
|
|
1141
1250
|
Reads payment configuration (products, processors, prices, limits) from `webManager.config.payment` — populated from `_config.yml` at build time. **Do NOT fetch `/backend-manager/brand` to get payment data.** It's already available instantly via this library.
|
|
@@ -38,7 +38,8 @@ export default function () {
|
|
|
38
38
|
webManager.auth().listen({}, async (state) => {
|
|
39
39
|
const user = state.user;
|
|
40
40
|
const url = new URL(window.location.href);
|
|
41
|
-
const
|
|
41
|
+
const authReturnUrlRaw = url.searchParams.get('authReturnUrl');
|
|
42
|
+
const authReturnUrl = authReturnUrlRaw && webManager.isValidRedirectUrl(authReturnUrlRaw) ? authReturnUrlRaw : null;
|
|
42
43
|
const authSignout = url.searchParams.get('authSignout');
|
|
43
44
|
|
|
44
45
|
// Log
|
|
@@ -803,7 +803,7 @@ export class FormManager {
|
|
|
803
803
|
if (show) {
|
|
804
804
|
// Store original content
|
|
805
805
|
$btn._originalHTML = $btn.innerHTML;
|
|
806
|
-
$btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${this.config.submittingText}`;
|
|
806
|
+
$btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${webManager.utilities().escapeHTML(this.config.submittingText)}`;
|
|
807
807
|
} else if ($btn._originalHTML) {
|
|
808
808
|
$btn.innerHTML = $btn._originalHTML;
|
|
809
809
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Prerendered Icons Library
|
|
3
3
|
* Retrieves pre-rendered icon HTML from the frontmatter icon system
|
|
4
4
|
*/
|
|
5
|
+
import webManager from 'web-manager';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Get pre-rendered icon by name from frontmatter icon system.
|
|
@@ -31,5 +32,5 @@ export function getPrerenderedIcon(iconName, classes) {
|
|
|
31
32
|
return $iconTemplate.innerHTML;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
return $iconTemplate.innerHTML.replace('class="fa"', `class="fa ${classes}"`);
|
|
35
|
+
return $iconTemplate.innerHTML.replace('class="fa"', `class="fa ${webManager.utilities().escapeHTML(classes)}"`);
|
|
35
36
|
}
|
|
@@ -34,15 +34,23 @@ const performRedirect = () => {
|
|
|
34
34
|
const currentUrl = new URL(window.location.href);
|
|
35
35
|
const siteUrl = new URL(config.siteUrl);
|
|
36
36
|
|
|
37
|
-
//
|
|
37
|
+
// Named modifier lookup (safe alternative to eval/new Function)
|
|
38
|
+
const MODIFIERS = {
|
|
39
|
+
'search-cse': (url) => {
|
|
40
|
+
const q = url.searchParams.get('q');
|
|
41
|
+
url.searchParams.set('q', 'site:' + window.location.origin + ' ' + q);
|
|
42
|
+
return url;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Resolve modifier by name
|
|
38
47
|
let modifierFunction = (url) => url;
|
|
39
48
|
if (config.modifier && config.modifier !== '""' && config.modifier !== '') {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
modifierFunction =
|
|
43
|
-
}
|
|
44
|
-
console.warn('[Redirect]
|
|
45
|
-
console.warn('[Redirect] Modifier string:', config.modifier);
|
|
49
|
+
const modifierName = config.modifier.trim();
|
|
50
|
+
if (MODIFIERS[modifierName]) {
|
|
51
|
+
modifierFunction = MODIFIERS[modifierName];
|
|
52
|
+
} else {
|
|
53
|
+
console.warn('[Redirect] Unknown modifier:', modifierName);
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
|
|
@@ -90,8 +90,12 @@ const setupMessageHandler = () => {
|
|
|
90
90
|
// Flag as set up
|
|
91
91
|
window.__ujVertMessageHandlerSetup = true;
|
|
92
92
|
|
|
93
|
-
// Listen for messages from iframes
|
|
93
|
+
// Listen for messages from vert iframes (validate origin)
|
|
94
94
|
window.addEventListener('message', (event) => {
|
|
95
|
+
if (event.origin !== window.location.origin && event.origin !== 'https://promo-server.itwcreativeworks.com') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
const message = event.data || {};
|
|
96
100
|
const command = message.command || '';
|
|
97
101
|
const payload = message.payload || {};
|
|
@@ -320,8 +320,8 @@ function populateCancelReasons() {
|
|
|
320
320
|
|
|
321
321
|
$container.innerHTML = shuffled.map((reason, i) => `
|
|
322
322
|
<div class="form-check mb-2">
|
|
323
|
-
<input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${reason}">
|
|
324
|
-
<label class="form-check-label" for="cancel-reason-${i}">${reason}</label>
|
|
323
|
+
<input class="form-check-input" type="radio" name="cancel_reason" id="cancel-reason-${i}" value="${webManager.utilities().escapeHTML(reason)}">
|
|
324
|
+
<label class="form-check-label" for="cancel-reason-${i}">${webManager.utilities().escapeHTML(reason)}</label>
|
|
325
325
|
</div>
|
|
326
326
|
`).join('');
|
|
327
327
|
}
|
|
@@ -281,7 +281,7 @@ async function handleConnect(providerId) {
|
|
|
281
281
|
tries: 2,
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
if (response.url) {
|
|
284
|
+
if (response.url && /^https?:\/\//i.test(response.url)) {
|
|
285
285
|
window.location.href = response.url;
|
|
286
286
|
} else {
|
|
287
287
|
throw new Error(response.message || 'Failed to get authorization URL');
|
|
@@ -110,12 +110,12 @@ function updateReferralsList(referrals) {
|
|
|
110
110
|
<span class="badge bg-secondary me-2">#${sortedReferrals.length - index}</span>
|
|
111
111
|
<div>
|
|
112
112
|
<strong class="font-monospace small">${webManager.utilities().escapeHTML(referral.uid || 'Unknown User')}</strong>
|
|
113
|
-
<div class="text-muted small">${dateStr}${timeStr ? ` at ${timeStr}` : ''}</div>
|
|
113
|
+
<div class="text-muted small">${webManager.utilities().escapeHTML(dateStr)}${timeStr ? ` at ${webManager.utilities().escapeHTML(timeStr)}` : ''}</div>
|
|
114
114
|
</div>
|
|
115
115
|
</div>
|
|
116
116
|
</div>
|
|
117
117
|
<div class="text-end">
|
|
118
|
-
|
|
118
|
+
<small class="text-muted">${webManager.utilities().escapeHTML(getTimeSince(timestamp))}</small>
|
|
119
119
|
</div>
|
|
120
120
|
</div>
|
|
121
121
|
</div>
|
|
@@ -146,48 +146,40 @@ function formatTime(date) {
|
|
|
146
146
|
|
|
147
147
|
// Get time since string
|
|
148
148
|
function getTimeSince(timestamp) {
|
|
149
|
-
if (!timestamp) return '
|
|
149
|
+
if (!timestamp) return 'Unknown';
|
|
150
150
|
|
|
151
151
|
const now = Date.now();
|
|
152
152
|
const diff = now - timestamp;
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
if (diff < 60000) {
|
|
156
|
-
return '<small class="text-success">Just now</small>';
|
|
157
|
-
}
|
|
154
|
+
if (diff < 60000) return 'Just now';
|
|
158
155
|
|
|
159
|
-
// Less than 1 hour
|
|
160
156
|
if (diff < 3600000) {
|
|
161
157
|
const minutes = Math.floor(diff / 60000);
|
|
162
|
-
return
|
|
158
|
+
return `${minutes} min${minutes > 1 ? 's' : ''} ago`;
|
|
163
159
|
}
|
|
164
160
|
|
|
165
|
-
// Less than 24 hours
|
|
166
161
|
if (diff < 86400000) {
|
|
167
162
|
const hours = Math.floor(diff / 3600000);
|
|
168
|
-
return
|
|
163
|
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
|
169
164
|
}
|
|
170
165
|
|
|
171
|
-
// Less than 7 days
|
|
172
166
|
if (diff < 604800000) {
|
|
173
167
|
const days = Math.floor(diff / 86400000);
|
|
174
|
-
return
|
|
168
|
+
return `${days} day${days > 1 ? 's' : ''} ago`;
|
|
175
169
|
}
|
|
176
170
|
|
|
177
|
-
// Less than 30 days
|
|
178
171
|
if (diff < 2592000000) {
|
|
179
172
|
const weeks = Math.floor(diff / 604800000);
|
|
180
|
-
return
|
|
173
|
+
return `${weeks} week${weeks > 1 ? 's' : ''} ago`;
|
|
181
174
|
}
|
|
182
175
|
|
|
183
|
-
// More than 30 days
|
|
184
176
|
const months = Math.floor(diff / 2592000000);
|
|
185
177
|
if (months < 12) {
|
|
186
|
-
return
|
|
178
|
+
return `${months} month${months > 1 ? 's' : ''} ago`;
|
|
187
179
|
}
|
|
188
180
|
|
|
189
181
|
const years = Math.floor(months / 12);
|
|
190
|
-
return
|
|
182
|
+
return `${years} year${years > 1 ? 's' : ''} ago`;
|
|
191
183
|
}
|
|
192
184
|
|
|
193
185
|
// Setup button handlers
|
|
@@ -128,8 +128,8 @@ function populateRefundReasons() {
|
|
|
128
128
|
|
|
129
129
|
$container.innerHTML = shuffled.map((reason, i) => `
|
|
130
130
|
<div class="form-check mb-2">
|
|
131
|
-
<input class="form-check-input" type="radio" name="refund_reason" id="refund-reason-${i}" value="${reason}" required>
|
|
132
|
-
<label class="form-check-label" for="refund-reason-${i}">${reason}</label>
|
|
131
|
+
<input class="form-check-input" type="radio" name="refund_reason" id="refund-reason-${i}" value="${webManager.utilities().escapeHTML(reason)}" required>
|
|
132
|
+
<label class="form-check-label" for="refund-reason-${i}">${webManager.utilities().escapeHTML(reason)}</label>
|
|
133
133
|
</div>
|
|
134
134
|
`).join('');
|
|
135
135
|
}
|
|
@@ -301,7 +301,7 @@ async function updateActiveSessions(account) {
|
|
|
301
301
|
</div>
|
|
302
302
|
</div>
|
|
303
303
|
<div class="text-end">
|
|
304
|
-
<small class="text-muted">${formatDate(session.timestamp || (session.timestampUNIX * 1000))}</small>
|
|
304
|
+
<small class="text-muted">${webManager.utilities().escapeHTML(formatDate(session.timestamp || (session.timestampUNIX * 1000)))}</small>
|
|
305
305
|
${session.isCurrent ? '<span class="badge bg-primary ms-2">Current</span>' : ''}
|
|
306
306
|
</div>
|
|
307
307
|
</div>
|
|
@@ -66,7 +66,7 @@ function updateInviteStatus(invites) {
|
|
|
66
66
|
<div class="d-flex justify-content-between align-items-center">
|
|
67
67
|
<div>
|
|
68
68
|
<strong>${webManager.utilities().escapeHTML(invite.email)}</strong>
|
|
69
|
-
<small class="text-muted d-block">Invited ${formatDate(invite.invitedAt)}</small>
|
|
69
|
+
<small class="text-muted d-block">Invited ${webManager.utilities().escapeHTML(formatDate(invite.invitedAt))}</small>
|
|
70
70
|
</div>
|
|
71
71
|
<div>
|
|
72
72
|
<button class="btn btn-sm btn-outline-danger" data-action="cancel-invite" data-invite-id="${webManager.utilities().escapeHTML(invite.id)}">
|
|
@@ -26,7 +26,13 @@ async function renderEmailPreview(formData) {
|
|
|
26
26
|
md = new MarkdownIt({ html: true, breaks: true, linkify: true });
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const DOMPurify = (await import('dompurify')).default;
|
|
30
|
+
const renderedContent = content
|
|
31
|
+
? DOMPurify.sanitize(md.render(content), {
|
|
32
|
+
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'hr', 'ul', 'ol', 'li', 'a', 'b', 'strong', 'i', 'em', 'u', 's', 'del', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'div', 'span', 'sup', 'sub'],
|
|
33
|
+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'width', 'height', 'class', 'style', 'target', 'rel'],
|
|
34
|
+
})
|
|
35
|
+
: '<p class="text-muted">No content yet</p>';
|
|
30
36
|
|
|
31
37
|
return `
|
|
32
38
|
<div class="email-preview">
|
|
@@ -6,13 +6,5 @@ permalink: /search/cse
|
|
|
6
6
|
### REGULAR PAGES ###
|
|
7
7
|
redirect:
|
|
8
8
|
url: "https://cse.google.com/cse?cx={{ page.resolved.advertising.cse.site-id }}&ie=UTF-8"
|
|
9
|
-
modifier: "
|
|
10
|
-
function (url) {
|
|
11
|
-
var q = url.searchParams.get('q');
|
|
12
|
-
|
|
13
|
-
url.searchParams.set('q', 'site:' + window.location.origin + ' ' + q);
|
|
14
|
-
|
|
15
|
-
return url;
|
|
16
|
-
}
|
|
17
|
-
"
|
|
9
|
+
modifier: "search-cse"
|
|
18
10
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-jekyll-manager",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Ultimate Jekyll dependency manager",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -78,8 +78,9 @@
|
|
|
78
78
|
"chart.js": "^4.5.1",
|
|
79
79
|
"cheerio": "^1.2.0",
|
|
80
80
|
"chrome-launcher": "^1.2.1",
|
|
81
|
+
"dompurify": "^3.3.3",
|
|
81
82
|
"dotenv": "^17.4.1",
|
|
82
|
-
"fast-xml-parser": "^5.5.
|
|
83
|
+
"fast-xml-parser": "^5.5.11",
|
|
83
84
|
"fs-jetpack": "^5.1.0",
|
|
84
85
|
"glob": "^13.0.6",
|
|
85
86
|
"gulp-clean-css": "^4.3.0",
|
|
@@ -99,14 +100,14 @@
|
|
|
99
100
|
"minimatch": "^10.2.5",
|
|
100
101
|
"node-powertools": "^3.0.0",
|
|
101
102
|
"npm-api": "^1.0.1",
|
|
102
|
-
"postcss": "^8.5.
|
|
103
|
+
"postcss": "^8.5.9",
|
|
103
104
|
"prettier": "^3.8.1",
|
|
104
105
|
"sass": "^1.99.0",
|
|
105
106
|
"spellchecker": "^3.7.1",
|
|
106
107
|
"through2": "^4.0.2",
|
|
107
108
|
"web-manager": "^4.1.37",
|
|
108
|
-
"webpack": "^5.
|
|
109
|
-
"wonderful-fetch": "^2.0.
|
|
109
|
+
"webpack": "^5.106.0",
|
|
110
|
+
"wonderful-fetch": "^2.0.5",
|
|
110
111
|
"wonderful-version": "^1.3.2",
|
|
111
112
|
"yargs": "^18.0.0"
|
|
112
113
|
},
|
|
@@ -114,6 +115,6 @@
|
|
|
114
115
|
"gulp": "^5.0.1"
|
|
115
116
|
},
|
|
116
117
|
"devDependencies": {
|
|
117
|
-
"prepare-package": "^2.0.
|
|
118
|
+
"prepare-package": "^2.0.8"
|
|
118
119
|
}
|
|
119
120
|
}
|