@bebranded/bb-contents 1.0.135 → 1.0.137

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 +192 -32
  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.137
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.137');
36
36
 
37
37
  // Configuration
38
38
  const config = {
39
- version: '1.0.135',
39
+ version: '1.0.137',
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 {
@@ -362,10 +394,50 @@
362
394
  const repeatBlock1 = mainBlock.cloneNode(true);
363
395
  const repeatBlock2 = mainBlock.cloneNode(true);
364
396
 
365
- scrollContainer.appendChild(mainBlock);
366
- scrollContainer.appendChild(repeatBlock1);
367
- scrollContainer.appendChild(repeatBlock2);
368
- mainContainer.appendChild(scrollContainer);
397
+ // Pour les marquees horizontaux, calculer la hauteur avant de mettre en absolute
398
+ if (!isVertical) {
399
+ // Temporairement mettre scrollContainer en relative pour calculer la hauteur
400
+ scrollContainer.style.position = 'relative';
401
+ scrollContainer.appendChild(mainBlock);
402
+ scrollContainer.appendChild(repeatBlock1);
403
+ scrollContainer.appendChild(repeatBlock2);
404
+ mainContainer.appendChild(scrollContainer);
405
+
406
+ // Forcer un reflow pour calculer les dimensions
407
+ void scrollContainer.offsetHeight;
408
+
409
+ // Calculer la hauteur maximale des items
410
+ const items = mainBlock.querySelectorAll('.bb-marquee_item, [role="listitem"], > *');
411
+ let maxHeight = 0;
412
+ items.forEach(function(item) {
413
+ const itemHeight = item.offsetHeight;
414
+ if (itemHeight > maxHeight) {
415
+ maxHeight = itemHeight;
416
+ }
417
+ });
418
+
419
+ // Si aucun item trouvé, essayer de prendre la hauteur du scrollContainer
420
+ if (maxHeight === 0) {
421
+ maxHeight = scrollContainer.offsetHeight;
422
+ }
423
+
424
+ // Appliquer la hauteur calculée au mainContainer si elle est valide
425
+ if (maxHeight > 0) {
426
+ mainContainer.style.height = maxHeight + 'px';
427
+ }
428
+
429
+ // Maintenant mettre scrollContainer en absolute
430
+ scrollContainer.style.position = 'absolute';
431
+ scrollContainer.style.height = '100%';
432
+ scrollContainer.style.top = '0px';
433
+ scrollContainer.style.left = '0px';
434
+ } else {
435
+ // Pour vertical, garder le comportement actuel
436
+ scrollContainer.appendChild(mainBlock);
437
+ scrollContainer.appendChild(repeatBlock1);
438
+ scrollContainer.appendChild(repeatBlock2);
439
+ mainContainer.appendChild(scrollContainer);
440
+ }
369
441
 
370
442
  element.innerHTML = '';
371
443
  element.appendChild(mainContainer);
@@ -1046,9 +1118,11 @@
1046
1118
  return response.text();
1047
1119
  })
1048
1120
  .then(function(html) {
1121
+ // Nettoyer le HTML avant parsing (supprimer scripts et événements)
1122
+ const cleanedHtml = bbContents.utils.cleanHtml(html);
1049
1123
  // Parser le HTML pour extraire le contenu principal
1050
1124
  const parser = new DOMParser();
1051
- const doc = parser.parseFromString(html, 'text/html');
1125
+ const doc = parser.parseFromString(cleanedHtml, 'text/html');
1052
1126
 
1053
1127
  let contentNode = null;
1054
1128
 
@@ -1151,7 +1225,30 @@
1151
1225
  articleUrl = urlAttr;
1152
1226
  // Si l'URL est relative, la transformer en absolue
1153
1227
  if (articleUrl && !bbContents.utils.isValidUrl(articleUrl)) {
1154
- articleUrl = new URL(articleUrl, window.location.origin).href;
1228
+ try {
1229
+ const url = new URL(articleUrl, window.location.origin);
1230
+ // Vérifier que c'est bien le même domaine (sécurité)
1231
+ if (url.origin !== window.location.origin) {
1232
+ // URL externe non autorisée, ignorer
1233
+ articleUrl = null;
1234
+ } else {
1235
+ articleUrl = url.href;
1236
+ }
1237
+ } catch (e) {
1238
+ // URL invalide, ignorer
1239
+ articleUrl = null;
1240
+ }
1241
+ } else if (articleUrl && bbContents.utils.isValidUrl(articleUrl)) {
1242
+ // Vérifier que l'URL absolue est du même domaine
1243
+ try {
1244
+ const url = new URL(articleUrl);
1245
+ if (url.origin !== window.location.origin) {
1246
+ // URL externe non autorisée, ignorer
1247
+ articleUrl = null;
1248
+ }
1249
+ } catch (e) {
1250
+ articleUrl = null;
1251
+ }
1155
1252
  }
1156
1253
  }
1157
1254
 
@@ -1225,11 +1322,20 @@
1225
1322
 
1226
1323
  // Détecter la langue
1227
1324
  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';
1325
+ let lang = element.getAttribute('lang');
1326
+ if (!lang && element.closest) {
1327
+ const langElement = element.closest('[lang]');
1328
+ if (langElement) {
1329
+ lang = langElement.getAttribute('lang');
1330
+ }
1331
+ }
1332
+ if (!lang) {
1333
+ lang = document.documentElement.getAttribute('lang');
1334
+ }
1335
+ if (!lang) {
1336
+ lang = 'fr';
1337
+ }
1338
+ return lang && lang.startsWith('en') ? 'en' : 'fr';
1233
1339
  },
1234
1340
 
1235
1341
  // Trouver un pays par code ou nom
@@ -1351,13 +1457,29 @@
1351
1457
 
1352
1458
  // Récupérer les styles visuels du select pour les appliquer au dropdown custom
1353
1459
  const selectBgColor = selectComputedStyle.backgroundColor;
1354
- const selectBorder = selectComputedStyle.border || selectComputedStyle.borderWidth + ' ' + selectComputedStyle.borderStyle + ' ' + selectComputedStyle.borderColor;
1460
+ // Construire selectBorder de manière sécurisée
1461
+ let selectBorder = selectComputedStyle.border;
1462
+ if (!selectBorder || selectBorder === 'none' || selectBorder === '0px none rgb(0, 0, 0)') {
1463
+ if (selectComputedStyle.borderWidth && selectComputedStyle.borderStyle && selectComputedStyle.borderColor) {
1464
+ selectBorder = selectComputedStyle.borderWidth + ' ' + selectComputedStyle.borderStyle + ' ' + selectComputedStyle.borderColor;
1465
+ } else {
1466
+ selectBorder = null;
1467
+ }
1468
+ }
1355
1469
  const selectBorderColor = selectComputedStyle.borderColor;
1356
1470
  const selectBorderRadius = selectComputedStyle.borderRadius;
1357
1471
  const selectColor = selectComputedStyle.color;
1358
1472
  const selectFontSize = selectComputedStyle.fontSize;
1359
1473
  const selectFontFamily = selectComputedStyle.fontFamily;
1360
- const selectPadding = selectComputedStyle.padding || (selectComputedStyle.paddingTop + ' ' + selectComputedStyle.paddingRight + ' ' + selectComputedStyle.paddingBottom + ' ' + selectComputedStyle.paddingLeft);
1474
+ // Construire selectPadding de manière sécurisée
1475
+ let selectPadding = selectComputedStyle.padding;
1476
+ if (!selectPadding || selectPadding === '0px') {
1477
+ if (selectComputedStyle.paddingTop && selectComputedStyle.paddingRight && selectComputedStyle.paddingBottom && selectComputedStyle.paddingLeft) {
1478
+ selectPadding = selectComputedStyle.paddingTop + ' ' + selectComputedStyle.paddingRight + ' ' + selectComputedStyle.paddingBottom + ' ' + selectComputedStyle.paddingLeft;
1479
+ } else {
1480
+ selectPadding = null;
1481
+ }
1482
+ }
1361
1483
 
1362
1484
  // Créer le wrapper avec les dimensions du select
1363
1485
  const wrapper = document.createElement('div');
@@ -1389,7 +1511,8 @@
1389
1511
 
1390
1512
  const selectedCountry = defaultCountry;
1391
1513
  const selectedName = selectedCountry ? selectedCountry.name[language] : placeholder;
1392
- const selectedFlag = selectedCountry ?
1514
+ // Valider le code pays avant utilisation
1515
+ const selectedFlag = selectedCountry && bbContents.utils.isValidCountryCode(selectedCountry.alpha2) ?
1393
1516
  '<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
1517
  '';
1395
1518
 
@@ -1502,19 +1625,24 @@
1502
1625
  }
1503
1626
 
1504
1627
  list.innerHTML = countries.map(function(country) {
1628
+ // Valider le code pays avant utilisation
1629
+ if (!bbContents.utils.isValidCountryCode(country.alpha2)) {
1630
+ return ''; // Ignorer les pays avec codes invalides
1631
+ }
1505
1632
  const isSelected = currentSelectedCountry && currentSelectedCountry.alpha2 === country.alpha2;
1506
1633
  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
1634
  // Appliquer uniquement font-size et font-family du select natif (pas la couleur)
1635
+ // Échapper les valeurs CSS pour éviter l'injection
1508
1636
  if (selectFontSize) {
1509
- itemStyle += ' font-size: ' + selectFontSize + ';';
1637
+ itemStyle += ' font-size: ' + bbContents.utils.escapeCss(selectFontSize) + ';';
1510
1638
  }
1511
1639
  if (selectFontFamily) {
1512
- itemStyle += ' font-family: ' + selectFontFamily + ';';
1640
+ itemStyle += ' font-family: ' + bbContents.utils.escapeCss(selectFontFamily) + ';';
1513
1641
  }
1514
1642
  if (isSelected) {
1515
1643
  itemStyle += ' background-color: #f3f4f6;';
1516
1644
  }
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>';
1645
+ 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
1646
  }).join('');
1519
1647
 
1520
1648
  // Ajouter hover effect
@@ -1557,11 +1685,13 @@
1557
1685
  document.querySelectorAll('.bb-country-select-popover').forEach(function(otherPopover) {
1558
1686
  if (otherPopover !== popover && otherPopover.style.display === 'block') {
1559
1687
  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)';
1688
+ if (otherPopover.parentElement) {
1689
+ const otherTrigger = otherPopover.parentElement.querySelector('.bb-country-select-trigger');
1690
+ if (otherTrigger) {
1691
+ otherTrigger.setAttribute('aria-expanded', 'false');
1692
+ const otherChevron = otherTrigger.querySelector('svg');
1693
+ if (otherChevron) otherChevron.style.transform = 'rotate(0deg)';
1694
+ }
1565
1695
  }
1566
1696
  }
1567
1697
  });
@@ -1613,17 +1743,23 @@
1613
1743
  if (!item) return;
1614
1744
 
1615
1745
  const countryCode = item.dataset.country;
1746
+ // Valider le code pays avant utilisation
1747
+ if (!bbContents.utils.isValidCountryCode(countryCode)) {
1748
+ return; // Code invalide, ignorer
1749
+ }
1616
1750
  const country = self.countries.find(function(c) {
1617
- return c.alpha2 === countryCode;
1751
+ return c.alpha2.toLowerCase() === countryCode.toLowerCase();
1618
1752
  });
1619
1753
  if (!country) return;
1620
1754
 
1621
1755
  // Mettre à jour le pays sélectionné
1622
1756
  currentSelectedCountry = country;
1623
1757
 
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];
1758
+ // Mettre à jour l'affichage (country.alpha2 déjà validé par la recherche)
1759
+ if (bbContents.utils.isValidCountryCode(country.alpha2)) {
1760
+ 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;">';
1761
+ nameSpan.textContent = country.name[language];
1762
+ }
1627
1763
 
1628
1764
  // Mettre à jour le select natif avec le nom du pays (pas le code ISO)
1629
1765
  const countryName = country.name[language];
@@ -1637,7 +1773,15 @@
1637
1773
  const newOption = document.createElement('option');
1638
1774
  newOption.value = countryName;
1639
1775
  newOption.textContent = countryName;
1640
- element.innerHTML = '';
1776
+ // Vérifier s'il y a d'autres options avant de tout supprimer
1777
+ if (element.options.length > 0) {
1778
+ // Supprimer seulement les options vides ou placeholder
1779
+ Array.from(element.options).forEach(function(opt) {
1780
+ if (!opt.value || opt.value === '') {
1781
+ opt.remove();
1782
+ }
1783
+ });
1784
+ }
1641
1785
  element.appendChild(newOption);
1642
1786
  }
1643
1787
  const changeEvent = new Event('change', { bubbles: true });
@@ -1969,7 +2113,23 @@
1969
2113
  container.innerHTML = '<div style="padding: 20px; text-align: center; color: #6b7280;">Chargement des vidéos YouTube...</div>';
1970
2114
 
1971
2115
  // Appeler l'API via le Worker
1972
- fetch(`${endpoint}?channelId=${channelId}&maxResults=${videoCount}&allowShorts=${allowShorts}`)
2116
+ // Valider l'endpoint et le channelId avant fetch
2117
+ if (!endpoint || typeof endpoint !== 'string') {
2118
+ throw new Error('Endpoint YouTube invalide');
2119
+ }
2120
+ // Vérifier que l'endpoint correspond à la configuration
2121
+ if (bbContents.config.youtubeEndpoint && !endpoint.startsWith(bbContents.config.youtubeEndpoint)) {
2122
+ throw new Error('Endpoint YouTube non autorisé');
2123
+ }
2124
+ // Valider le format de channelId (alphanumérique, tirets, underscores)
2125
+ if (!channelId || !/^[a-zA-Z0-9_-]+$/.test(channelId)) {
2126
+ throw new Error('Channel ID invalide');
2127
+ }
2128
+ // Valider videoCount et allowShorts
2129
+ const safeVideoCount = parseInt(videoCount, 10);
2130
+ const safeAllowShorts = allowShorts === true || allowShorts === 'true';
2131
+
2132
+ fetch(`${endpoint}?channelId=${encodeURIComponent(channelId)}&maxResults=${safeVideoCount}&allowShorts=${safeAllowShorts}`)
1973
2133
  .then(response => {
1974
2134
  if (!response.ok) {
1975
2135
  throw new Error(`HTTP ${response.status}`);
@@ -2009,7 +2169,7 @@
2009
2169
  }
2010
2170
  }
2011
2171
 
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>`;
2172
+ 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
2173
  });
2014
2174
  },
2015
2175
 
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.137",
4
4
  "description": "Contenus additionnels français pour Webflow",
5
5
  "main": "bb-contents.js",
6
6
  "scripts": {