@bebranded/bb-contents 1.0.135 → 1.0.136

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.
Files changed (2) hide show
  1. package/bb-contents.js +148 -28
  2. package/package.json +1 -1
package/bb-contents.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * BeBranded Contents
3
3
  * Contenus additionnels français pour Webflow
4
- * @version 1.0.135
4
+ * @version 1.0.136
5
5
  * @author BeBranded
6
6
  * @license MIT
7
7
  * @website https://www.bebranded.xyz
@@ -32,11 +32,11 @@
32
32
  window._bbContentsInitialized = true;
33
33
 
34
34
  // Log de démarrage simple (une seule fois)
35
- console.log('bb-contents | v1.0.135');
35
+ console.log('bb-contents | v1.0.136');
36
36
 
37
37
  // Configuration
38
38
  const config = {
39
- version: '1.0.135',
39
+ version: '1.0.136',
40
40
  debug: false, // Debug désactivé pour rendu propre
41
41
  prefix: 'bb-', // utilisé pour générer les sélecteurs (data-bb-*)
42
42
  youtubeEndpoint: null, // URL du worker YouTube (à définir par l'utilisateur)
@@ -84,6 +84,38 @@
84
84
  return div.innerHTML;
85
85
  },
86
86
 
87
+ // Valider un code pays ISO 3166-1 alpha-2 (2 lettres)
88
+ isValidCountryCode: function(code) {
89
+ if (!code || typeof code !== 'string') return false;
90
+ return /^[a-z]{2}$/i.test(code.trim());
91
+ },
92
+
93
+ // Échapper les valeurs CSS pour éviter l'injection CSS
94
+ escapeCss: function(value) {
95
+ if (!value || typeof value !== 'string') return '';
96
+ // Échapper les guillemets et caractères spéciaux
97
+ return value.replace(/[<>"']/g, function(match) {
98
+ const escapeMap = {
99
+ '<': '\\3C ',
100
+ '>': '\\3E ',
101
+ '"': '\\22 ',
102
+ "'": '\\27 '
103
+ };
104
+ return escapeMap[match] || match;
105
+ });
106
+ },
107
+
108
+ // Nettoyer le HTML en supprimant les scripts et événements
109
+ cleanHtml: function(html) {
110
+ if (!html || typeof html !== 'string') return '';
111
+ // Supprimer les scripts
112
+ let cleaned = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
113
+ // Supprimer les attributs d'événements (onclick, onerror, etc.)
114
+ cleaned = cleaned.replace(/\s*on\w+\s*=\s*["'][^"']*["']/gi, '');
115
+ cleaned = cleaned.replace(/\s*on\w+\s*=\s*[^\s>]*/gi, '');
116
+ return cleaned;
117
+ },
118
+
87
119
  // Validation des URLs
88
120
  isValidUrl: function(string) {
89
121
  try {
@@ -1046,9 +1078,11 @@
1046
1078
  return response.text();
1047
1079
  })
1048
1080
  .then(function(html) {
1081
+ // Nettoyer le HTML avant parsing (supprimer scripts et événements)
1082
+ const cleanedHtml = bbContents.utils.cleanHtml(html);
1049
1083
  // Parser le HTML pour extraire le contenu principal
1050
1084
  const parser = new DOMParser();
1051
- const doc = parser.parseFromString(html, 'text/html');
1085
+ const doc = parser.parseFromString(cleanedHtml, 'text/html');
1052
1086
 
1053
1087
  let contentNode = null;
1054
1088
 
@@ -1151,7 +1185,30 @@
1151
1185
  articleUrl = urlAttr;
1152
1186
  // Si l'URL est relative, la transformer en absolue
1153
1187
  if (articleUrl && !bbContents.utils.isValidUrl(articleUrl)) {
1154
- articleUrl = new URL(articleUrl, window.location.origin).href;
1188
+ try {
1189
+ const url = new URL(articleUrl, window.location.origin);
1190
+ // Vérifier que c'est bien le même domaine (sécurité)
1191
+ if (url.origin !== window.location.origin) {
1192
+ // URL externe non autorisée, ignorer
1193
+ articleUrl = null;
1194
+ } else {
1195
+ articleUrl = url.href;
1196
+ }
1197
+ } catch (e) {
1198
+ // URL invalide, ignorer
1199
+ articleUrl = null;
1200
+ }
1201
+ } else if (articleUrl && bbContents.utils.isValidUrl(articleUrl)) {
1202
+ // Vérifier que l'URL absolue est du même domaine
1203
+ try {
1204
+ const url = new URL(articleUrl);
1205
+ if (url.origin !== window.location.origin) {
1206
+ // URL externe non autorisée, ignorer
1207
+ articleUrl = null;
1208
+ }
1209
+ } catch (e) {
1210
+ articleUrl = null;
1211
+ }
1155
1212
  }
1156
1213
  }
1157
1214
 
@@ -1225,11 +1282,20 @@
1225
1282
 
1226
1283
  // Détecter la langue
1227
1284
  getLanguage: function(element) {
1228
- const lang = element.getAttribute('lang') ||
1229
- (element.closest && element.closest('[lang]') ? element.closest('[lang]').getAttribute('lang') : null) ||
1230
- document.documentElement.getAttribute('lang') ||
1231
- 'fr';
1232
- return lang.startsWith('en') ? 'en' : 'fr';
1285
+ let lang = element.getAttribute('lang');
1286
+ if (!lang && element.closest) {
1287
+ const langElement = element.closest('[lang]');
1288
+ if (langElement) {
1289
+ lang = langElement.getAttribute('lang');
1290
+ }
1291
+ }
1292
+ if (!lang) {
1293
+ lang = document.documentElement.getAttribute('lang');
1294
+ }
1295
+ if (!lang) {
1296
+ lang = 'fr';
1297
+ }
1298
+ return lang && lang.startsWith('en') ? 'en' : 'fr';
1233
1299
  },
1234
1300
 
1235
1301
  // Trouver un pays par code ou nom
@@ -1351,13 +1417,29 @@
1351
1417
 
1352
1418
  // Récupérer les styles visuels du select pour les appliquer au dropdown custom
1353
1419
  const selectBgColor = selectComputedStyle.backgroundColor;
1354
- const selectBorder = selectComputedStyle.border || selectComputedStyle.borderWidth + ' ' + selectComputedStyle.borderStyle + ' ' + selectComputedStyle.borderColor;
1420
+ // Construire selectBorder de manière sécurisée
1421
+ let selectBorder = selectComputedStyle.border;
1422
+ if (!selectBorder || selectBorder === 'none' || selectBorder === '0px none rgb(0, 0, 0)') {
1423
+ if (selectComputedStyle.borderWidth && selectComputedStyle.borderStyle && selectComputedStyle.borderColor) {
1424
+ selectBorder = selectComputedStyle.borderWidth + ' ' + selectComputedStyle.borderStyle + ' ' + selectComputedStyle.borderColor;
1425
+ } else {
1426
+ selectBorder = null;
1427
+ }
1428
+ }
1355
1429
  const selectBorderColor = selectComputedStyle.borderColor;
1356
1430
  const selectBorderRadius = selectComputedStyle.borderRadius;
1357
1431
  const selectColor = selectComputedStyle.color;
1358
1432
  const selectFontSize = selectComputedStyle.fontSize;
1359
1433
  const selectFontFamily = selectComputedStyle.fontFamily;
1360
- const selectPadding = selectComputedStyle.padding || (selectComputedStyle.paddingTop + ' ' + selectComputedStyle.paddingRight + ' ' + selectComputedStyle.paddingBottom + ' ' + selectComputedStyle.paddingLeft);
1434
+ // Construire selectPadding de manière sécurisée
1435
+ let selectPadding = selectComputedStyle.padding;
1436
+ if (!selectPadding || selectPadding === '0px') {
1437
+ if (selectComputedStyle.paddingTop && selectComputedStyle.paddingRight && selectComputedStyle.paddingBottom && selectComputedStyle.paddingLeft) {
1438
+ selectPadding = selectComputedStyle.paddingTop + ' ' + selectComputedStyle.paddingRight + ' ' + selectComputedStyle.paddingBottom + ' ' + selectComputedStyle.paddingLeft;
1439
+ } else {
1440
+ selectPadding = null;
1441
+ }
1442
+ }
1361
1443
 
1362
1444
  // Créer le wrapper avec les dimensions du select
1363
1445
  const wrapper = document.createElement('div');
@@ -1389,7 +1471,8 @@
1389
1471
 
1390
1472
  const selectedCountry = defaultCountry;
1391
1473
  const selectedName = selectedCountry ? selectedCountry.name[language] : placeholder;
1392
- const selectedFlag = selectedCountry ?
1474
+ // Valider le code pays avant utilisation
1475
+ const selectedFlag = selectedCountry && bbContents.utils.isValidCountryCode(selectedCountry.alpha2) ?
1393
1476
  '<img src="https://hatscripts.github.io/circle-flags/flags/' + selectedCountry.alpha2.toLowerCase() + '.svg" alt="' + bbContents.utils.sanitize(selectedCountry.name[language]) + '" style="width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;">' :
1394
1477
  '';
1395
1478
 
@@ -1502,19 +1585,24 @@
1502
1585
  }
1503
1586
 
1504
1587
  list.innerHTML = countries.map(function(country) {
1588
+ // Valider le code pays avant utilisation
1589
+ if (!bbContents.utils.isValidCountryCode(country.alpha2)) {
1590
+ return ''; // Ignorer les pays avec codes invalides
1591
+ }
1505
1592
  const isSelected = currentSelectedCountry && currentSelectedCountry.alpha2 === country.alpha2;
1506
1593
  let itemStyle = 'display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; transition: background-color 0.15s; min-height: 36px; box-sizing: border-box;';
1507
1594
  // Appliquer uniquement font-size et font-family du select natif (pas la couleur)
1595
+ // Échapper les valeurs CSS pour éviter l'injection
1508
1596
  if (selectFontSize) {
1509
- itemStyle += ' font-size: ' + selectFontSize + ';';
1597
+ itemStyle += ' font-size: ' + bbContents.utils.escapeCss(selectFontSize) + ';';
1510
1598
  }
1511
1599
  if (selectFontFamily) {
1512
- itemStyle += ' font-family: ' + selectFontFamily + ';';
1600
+ itemStyle += ' font-family: ' + bbContents.utils.escapeCss(selectFontFamily) + ';';
1513
1601
  }
1514
1602
  if (isSelected) {
1515
1603
  itemStyle += ' background-color: #f3f4f6;';
1516
1604
  }
1517
- return '<div class="bb-country-item" data-country="' + country.alpha2 + '" role="option" aria-selected="' + (isSelected ? 'true' : 'false') + '" style="' + itemStyle + '"><img src="https://hatscripts.github.io/circle-flags/flags/' + country.alpha2.toLowerCase() + '.svg" alt="' + bbContents.utils.sanitize(country.name[language]) + '" style="width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;"><span style="line-height: 1.2;">' + bbContents.utils.sanitize(country.name[language]) + '</span></div>';
1605
+ return '<div class="bb-country-item" data-country="' + country.alpha2.toLowerCase() + '" role="option" aria-selected="' + (isSelected ? 'true' : 'false') + '" style="' + itemStyle + '"><img src="https://hatscripts.github.io/circle-flags/flags/' + country.alpha2.toLowerCase() + '.svg" alt="' + bbContents.utils.sanitize(country.name[language]) + '" style="width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;"><span style="line-height: 1.2;">' + bbContents.utils.sanitize(country.name[language]) + '</span></div>';
1518
1606
  }).join('');
1519
1607
 
1520
1608
  // Ajouter hover effect
@@ -1557,11 +1645,13 @@
1557
1645
  document.querySelectorAll('.bb-country-select-popover').forEach(function(otherPopover) {
1558
1646
  if (otherPopover !== popover && otherPopover.style.display === 'block') {
1559
1647
  otherPopover.style.display = 'none';
1560
- const otherTrigger = otherPopover.parentElement.querySelector('.bb-country-select-trigger');
1561
- if (otherTrigger) {
1562
- otherTrigger.setAttribute('aria-expanded', 'false');
1563
- const otherChevron = otherTrigger.querySelector('svg');
1564
- if (otherChevron) otherChevron.style.transform = 'rotate(0deg)';
1648
+ if (otherPopover.parentElement) {
1649
+ const otherTrigger = otherPopover.parentElement.querySelector('.bb-country-select-trigger');
1650
+ if (otherTrigger) {
1651
+ otherTrigger.setAttribute('aria-expanded', 'false');
1652
+ const otherChevron = otherTrigger.querySelector('svg');
1653
+ if (otherChevron) otherChevron.style.transform = 'rotate(0deg)';
1654
+ }
1565
1655
  }
1566
1656
  }
1567
1657
  });
@@ -1613,17 +1703,23 @@
1613
1703
  if (!item) return;
1614
1704
 
1615
1705
  const countryCode = item.dataset.country;
1706
+ // Valider le code pays avant utilisation
1707
+ if (!bbContents.utils.isValidCountryCode(countryCode)) {
1708
+ return; // Code invalide, ignorer
1709
+ }
1616
1710
  const country = self.countries.find(function(c) {
1617
- return c.alpha2 === countryCode;
1711
+ return c.alpha2.toLowerCase() === countryCode.toLowerCase();
1618
1712
  });
1619
1713
  if (!country) return;
1620
1714
 
1621
1715
  // Mettre à jour le pays sélectionné
1622
1716
  currentSelectedCountry = country;
1623
1717
 
1624
- // Mettre à jour l'affichage
1625
- flagSpan.innerHTML = '<img src="https://hatscripts.github.io/circle-flags/flags/' + country.alpha2.toLowerCase() + '.svg" alt="' + bbContents.utils.sanitize(country.name[language]) + '" style="width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;">';
1626
- nameSpan.textContent = country.name[language];
1718
+ // Mettre à jour l'affichage (country.alpha2 déjà validé par la recherche)
1719
+ if (bbContents.utils.isValidCountryCode(country.alpha2)) {
1720
+ flagSpan.innerHTML = '<img src="https://hatscripts.github.io/circle-flags/flags/' + country.alpha2.toLowerCase() + '.svg" alt="' + bbContents.utils.sanitize(country.name[language]) + '" style="width: 20px; height: 20px; border-radius: 50%; object-fit: cover; flex-shrink: 0;">';
1721
+ nameSpan.textContent = country.name[language];
1722
+ }
1627
1723
 
1628
1724
  // Mettre à jour le select natif avec le nom du pays (pas le code ISO)
1629
1725
  const countryName = country.name[language];
@@ -1637,7 +1733,15 @@
1637
1733
  const newOption = document.createElement('option');
1638
1734
  newOption.value = countryName;
1639
1735
  newOption.textContent = countryName;
1640
- element.innerHTML = '';
1736
+ // Vérifier s'il y a d'autres options avant de tout supprimer
1737
+ if (element.options.length > 0) {
1738
+ // Supprimer seulement les options vides ou placeholder
1739
+ Array.from(element.options).forEach(function(opt) {
1740
+ if (!opt.value || opt.value === '') {
1741
+ opt.remove();
1742
+ }
1743
+ });
1744
+ }
1641
1745
  element.appendChild(newOption);
1642
1746
  }
1643
1747
  const changeEvent = new Event('change', { bubbles: true });
@@ -1969,7 +2073,23 @@
1969
2073
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Chargement des vidéos YouTube...</div>';
1970
2074
 
1971
2075
  // Appeler l'API via le Worker
1972
- fetch(`${endpoint}?channelId=${channelId}&maxResults=${videoCount}&allowShorts=${allowShorts}`)
2076
+ // Valider l'endpoint et le channelId avant fetch
2077
+ if (!endpoint || typeof endpoint !== 'string') {
2078
+ throw new Error('Endpoint YouTube invalide');
2079
+ }
2080
+ // Vérifier que l'endpoint correspond à la configuration
2081
+ if (bbContents.config.youtubeEndpoint && !endpoint.startsWith(bbContents.config.youtubeEndpoint)) {
2082
+ throw new Error('Endpoint YouTube non autorisé');
2083
+ }
2084
+ // Valider le format de channelId (alphanumérique, tirets, underscores)
2085
+ if (!channelId || !/^[a-zA-Z0-9_-]+$/.test(channelId)) {
2086
+ throw new Error('Channel ID invalide');
2087
+ }
2088
+ // Valider videoCount et allowShorts
2089
+ const safeVideoCount = parseInt(videoCount, 10);
2090
+ const safeAllowShorts = allowShorts === true || allowShorts === 'true';
2091
+
2092
+ fetch(`${endpoint}?channelId=${encodeURIComponent(channelId)}&maxResults=${safeVideoCount}&allowShorts=${safeAllowShorts}`)
1973
2093
  .then(response => {
1974
2094
  if (!response.ok) {
1975
2095
  throw new Error(`HTTP ${response.status}`);
@@ -2009,7 +2129,7 @@
2009
2129
  }
2010
2130
  }
2011
2131
 
2012
- container.innerHTML = `<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Erreur de chargement</strong><br>${error.message}</div>`;
2132
+ container.innerHTML = `<div style="padding: 20px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; color: #dc2626;"><strong>Erreur de chargement</strong><br>${bbContents.utils.sanitize(error.message || 'Erreur inconnue')}</div>`;
2013
2133
  });
2014
2134
  },
2015
2135
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bebranded/bb-contents",
3
- "version": "1.0.135",
3
+ "version": "1.0.136",
4
4
  "description": "Contenus additionnels français pour Webflow",
5
5
  "main": "bb-contents.js",
6
6
  "scripts": {