url-validator.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. // GrabZilla 2.1 - URL Validation Utilities
  2. // Comprehensive URL validation for video platforms
  3. class URLValidator {
  4. // Check if URL is a valid video URL from supported platforms
  5. static isValidVideoUrl(url) {
  6. if (!url || typeof url !== 'string') {
  7. return false;
  8. }
  9. const trimmedUrl = url.trim();
  10. if (trimmedUrl.length === 0) {
  11. return false;
  12. }
  13. // Check against supported platforms
  14. return this.isYouTubeUrl(trimmedUrl) ||
  15. this.isVimeoUrl(trimmedUrl) ||
  16. this.isGenericVideoUrl(trimmedUrl);
  17. }
  18. // Validate YouTube URLs (including Shorts)
  19. static isYouTubeUrl(url) {
  20. // Match YouTube URLs with any query parameters (including Shorts)
  21. const videoPattern = /^(https?:\/\/)?(www\.)?(youtube\.com\/(watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)[\w\-_]{11}([?&].*)?$/i;
  22. const playlistPattern = /^(https?:\/\/)?(www\.)?youtube\.com\/playlist\?list=[\w\-]+/i;
  23. return videoPattern.test(url) || playlistPattern.test(url);
  24. }
  25. // Validate Vimeo URLs
  26. static isVimeoUrl(url) {
  27. const patterns = window.AppConfig?.VALIDATION_PATTERNS || {
  28. VIMEO_URL: /^(https?:\/\/)?(www\.)?(vimeo\.com\/\d+|player\.vimeo\.com\/video\/\d+)/i
  29. };
  30. return patterns.VIMEO_URL.test(url);
  31. }
  32. // Check if URL is a YouTube playlist
  33. static isYouTubePlaylist(url) {
  34. if (!url || typeof url !== 'string') {
  35. return false;
  36. }
  37. return /[?&]list=[\w\-]+/.test(url);
  38. }
  39. // Check if URL is a YouTube Shorts video
  40. static isYouTubeShorts(url) {
  41. if (!url || typeof url !== 'string') {
  42. return false;
  43. }
  44. return /youtube\.com\/shorts\/[\w\-_]{11}/i.test(url);
  45. }
  46. // Validate generic video URLs
  47. static isGenericVideoUrl(url) {
  48. // Disable generic video URL validation to be more strict
  49. // Only allow explicitly supported platforms (YouTube, Vimeo)
  50. return false;
  51. }
  52. // Extract video ID from YouTube URL (including Shorts)
  53. static extractYouTubeId(url) {
  54. if (!this.isYouTubeUrl(url)) {
  55. return null;
  56. }
  57. const patterns = [
  58. /[?&]v=([^&#]*)/, // youtube.com/watch?v=ID
  59. /\/embed\/([^\/\?]*)/, // youtube.com/embed/ID
  60. /\/v\/([^\/\?]*)/, // youtube.com/v/ID
  61. /\/shorts\/([^\/\?]*)/, // youtube.com/shorts/ID
  62. /youtu\.be\/([^\/\?]*)/ // youtu.be/ID
  63. ];
  64. for (const pattern of patterns) {
  65. const match = url.match(pattern);
  66. if (match && match[1]) {
  67. return match[1];
  68. }
  69. }
  70. return null;
  71. }
  72. // Extract video ID from Vimeo URL
  73. static extractVimeoId(url) {
  74. if (!this.isVimeoUrl(url)) {
  75. return null;
  76. }
  77. const match = url.match(/vimeo\.com\/(\d+)/);
  78. return match ? match[1] : null;
  79. }
  80. // Normalize URL to standard format
  81. static normalizeUrl(url) {
  82. if (!url || typeof url !== 'string') {
  83. return url;
  84. }
  85. let normalizedUrl = url.trim();
  86. // Add protocol if missing
  87. if (!/^https?:\/\//i.test(normalizedUrl)) {
  88. normalizedUrl = 'https://' + normalizedUrl;
  89. }
  90. // Normalize YouTube URLs
  91. if (this.isYouTubeUrl(normalizedUrl)) {
  92. const videoId = this.extractYouTubeId(normalizedUrl);
  93. if (videoId) {
  94. return `https://www.youtube.com/watch?v=${videoId}`;
  95. }
  96. }
  97. // Normalize Vimeo URLs
  98. if (this.isVimeoUrl(normalizedUrl)) {
  99. const videoId = this.extractVimeoId(normalizedUrl);
  100. if (videoId) {
  101. return `https://vimeo.com/${videoId}`;
  102. }
  103. }
  104. return normalizedUrl;
  105. }
  106. // Get platform name from URL
  107. static getPlatform(url) {
  108. if (this.isYouTubeUrl(url)) {
  109. return 'YouTube';
  110. }
  111. if (this.isVimeoUrl(url)) {
  112. return 'Vimeo';
  113. }
  114. return 'Unknown';
  115. }
  116. // Validate multiple URLs (one per line)
  117. static validateMultipleUrls(urlText) {
  118. if (!urlText || typeof urlText !== 'string') {
  119. return { valid: [], invalid: [] };
  120. }
  121. // Extract all URLs from text using regex patterns
  122. // Match entire YouTube URLs including all query parameters (including Shorts)
  123. const youtubePattern = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/)|youtu\.be\/)[\w\-_]{11}(?:[?&][^\s]*)*/gi;
  124. const vimeoPattern = /(?:https?:\/\/)?(?:www\.)?(?:vimeo\.com\/|player\.vimeo\.com\/video\/)\d+/gi;
  125. const youtubeMatches = urlText.match(youtubePattern) || [];
  126. const vimeoMatches = urlText.match(vimeoPattern) || [];
  127. const allUrls = [...youtubeMatches, ...vimeoMatches];
  128. const valid = [];
  129. const invalid = [];
  130. const seen = new Set();
  131. allUrls.forEach(url => {
  132. // Fully normalize URLs to canonical format for deduplication
  133. const normalizedUrl = this.normalizeUrl(url);
  134. // Deduplicate based on normalized canonical URL
  135. if (!seen.has(normalizedUrl)) {
  136. seen.add(normalizedUrl);
  137. if (this.isValidVideoUrl(normalizedUrl)) {
  138. valid.push(normalizedUrl);
  139. } else {
  140. invalid.push(url);
  141. }
  142. }
  143. });
  144. return { valid, invalid };
  145. }
  146. // Check for duplicate URLs in a list
  147. static findDuplicates(urls) {
  148. const normalized = urls.map(url => this.normalizeUrl(url));
  149. const duplicates = [];
  150. const seen = new Set();
  151. normalized.forEach((url, index) => {
  152. if (seen.has(url)) {
  153. duplicates.push({ url: urls[index], index });
  154. } else {
  155. seen.add(url);
  156. }
  157. });
  158. return duplicates;
  159. }
  160. // Get validation error message
  161. static getValidationError(url) {
  162. if (url === null || url === undefined) {
  163. return 'URL is required';
  164. }
  165. if (typeof url !== 'string' || url.trim().length === 0) {
  166. return 'URL cannot be empty';
  167. }
  168. const trimmedUrl = url.trim();
  169. if (!/^https?:\/\//i.test(trimmedUrl) && !/^www\./i.test(trimmedUrl) && !trimmedUrl.includes('.')) {
  170. return 'Invalid URL format - must include domain';
  171. }
  172. if (!this.isValidVideoUrl(trimmedUrl)) {
  173. return 'Unsupported video platform - currently supports YouTube and Vimeo';
  174. }
  175. return null; // Valid URL
  176. }
  177. }
  178. // Export for use in other modules
  179. if (typeof module !== 'undefined' && module.exports) {
  180. // Node.js environment
  181. module.exports = URLValidator;
  182. } else {
  183. // Browser environment - attach to window
  184. window.URLValidator = URLValidator;
  185. }