From 74064b0cac02131fbe1119ca5a5feb7c0815f8e6 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sat, 27 Sep 2025 20:20:51 +0700 Subject: [PATCH 01/20] Add featured sponsors in README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 79c1f73..03eb487 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +#### Featured Sponsors + +[![https://ref.wisprflow.ai/Rxj3n8H](https://github.com/user-attachments/assets/e39fc98d-5d1b-4f7b-a355-4648e1f79a94)](https://ref.wisprflow.ai/Rxj3n8H) + +--- + [![https://uaparser.dev](https://raw.githubusercontent.com/faisalman/ua-parser-js/gh-pages/images/uap-header.png)](https://uaparser.dev) [![https://uaparser.dev](https://github.com/user-attachments/assets/9f30f3d4-5cfe-441c-8f86-ead7c955f940)](https://uaparser.dev) [![https://uaparser.dev](https://github.com/user-attachments/assets/50da50fc-7c8a-46e3-a2bc-6a8249914372)](https://uaparser.dev) From 737cdd4d40dc0daaf73bfccc67022d4a0c40c8f1 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Mon, 29 Sep 2025 11:32:04 +0700 Subject: [PATCH 02/20] Improve device detection: iPad --- src/main/ua-parser.js | 5 +- test/data/ua/device/apple.json | 99 ++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index bc0ef93..816c7da 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -568,9 +568,8 @@ // Apple /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone ], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [ - /\((ipad);[-\w\),; ]+apple/i, // iPad - /applecoremedia\/[\w\.]+ \((ipad)/i, - /\b(ipad)\d\d?,\d\d?[;\]].+ios/i + /\b(?:ios|apple\w+)\/.+[\(\/](ipad)/i, // iPad + /\b(ipad)[\d,]*[;\] ].+(mac |i(pad)?)os/i ], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [ /(macintosh);/i ], [MODEL, [VENDOR, APPLE]], [ diff --git a/test/data/ua/device/apple.json b/test/data/ua/device/apple.json index ea68e8a..aca64ee 100644 --- a/test/data/ua/device/apple.json +++ b/test/data/ua/device/apple.json @@ -44,6 +44,105 @@ "type": "tablet" } }, + { + "desc": "iPad using Chrome", + "ua": "Mozilla/5.0 (iPad13,10; CPU OS 16_6_2 like Mac OS X) AppleWebKit/602.3 (KHTML, like Gecko) CriOS/97.0.0.5927.72 Mobile/11S296 Safari/602.3", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using DuckDuckGo", + "ua": "Mozilla/5.0 (iPad; iPad13,6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) DuckDuckGo/130.0.0.0 Mobile/15E148 Safari/604.1", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using Discord", + "ua": "Discord/52.0 (iPad; iOS 14.4; Scale/2.00)", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using Firefox", + "ua": "Mozilla/5.0 (iPad; iPad14,2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Firefox/120.0 Mobile/15E148 Safari/604.1", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using iTunes", + "ua": "itunesstored/1.0 iOS/15.6.1 model/iPad13,8 hwp/t8103 build/19G82 (5; dt:238)", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using MS OneNote", + "ua": "Microsoft Office OneNote/16.81/240108 (iOS/16.5.1; Tablet; AppStore; Apple/iPad8,3)", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using MS Word", + "ua": "Microsoft Office Word/2.44.1211 (iOS/13.7; Tablet; es-MX; AppStore; Apple/iPad11,3)", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using Quora", + "ua": "Quora 8.4.30 rv:3230 env:prod (iPad11,3; iPadOS 17.7; en_GB) AppleWebKit", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using TuneIn Radio", + "ua": "TuneIn Radio/27.1.0; iPad6,3; iPadOS/16.6", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using TuneIn Radio Pro", + "ua": "TuneIn Radio Pro/21.4.1; iPad8,9; iOS/15.0", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, + { + "desc": "iPad using YouTube", + "ua": "com.google.ios.youtube/20.31.6 (iPad13,5; U; CPU iPadOS 18_6 like Mac OS X; en_US)", + "expect": { + "vendor": "Apple", + "model": "iPad", + "type": "tablet" + } + }, { "desc": "iPod", "ua": "Mozilla/5.0 (iPod touch; CPU iPhone OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53", From 95d2b151a38cab06f5dce3fb0569846760560fe3 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Tue, 30 Sep 2025 12:59:21 +0700 Subject: [PATCH 03/20] Improve OS detection: iOS --- src/main/ua-parser.js | 4 +-- test/data/ua/device/apple.json | 9 +++++ test/data/ua/os/ios.json | 63 ++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 816c7da..42b409a 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -566,7 +566,7 @@ ], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [ // Apple - /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone + /(?:\/|\()(ip(?:hone|od)[\w, ]*)[\/\);]/i // iPod/iPhone ], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [ /\b(?:ios|apple\w+)\/.+[\(\/](ipad)/i, // iPad /\b(ipad)[\d,]*[;\] ].+(mac |i(pad)?)os/i @@ -964,7 +964,7 @@ // iOS/macOS /[adehimnop]{4,7}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS - /(?:ios;fbsv\/|iphone.+ios[\/ ])([\d\.]+)/i, + /(?:ios;fbsv|ios(?=.+ip(?:ad|hone))|ip(?:ad|hone)(?: |.+i(?:pad)?)os)[\/ ]([\w\.]+)/i, /cfnetwork\/.+darwin/i ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [ /(mac os x) ?([\w\. ]*)/i, diff --git a/test/data/ua/device/apple.json b/test/data/ua/device/apple.json index aca64ee..0ea909e 100644 --- a/test/data/ua/device/apple.json +++ b/test/data/ua/device/apple.json @@ -161,6 +161,15 @@ "type": "mobile" } }, + { + "desc": "iPhone using Spotify", + "ua": "Spotify/8.7.70 iOS/16.0 (iPhone15,3)", + "expect": { + "vendor": "Apple", + "model": "iPhone15,3", + "type": "mobile" + } + }, { "desc": "iPhone SE", "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBDV/iPhone8,4;FBMD/iPhone;FBSN/iOS;FBSV/13.3.1;FBSS/2;FBID/phone;FBLC/en_US;FBOP/5;FBCR/]", diff --git a/test/data/ua/os/ios.json b/test/data/ua/os/ios.json index 86ba8cb..1b0d636 100644 --- a/test/data/ua/os/ios.json +++ b/test/data/ua/os/ios.json @@ -17,6 +17,15 @@ "version" : "5.1.1" } }, + { + "desc" : "iOS with DuckDuckGo", + "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1 Ddg/26.0", + "expect" : + { + "name" : "iOS", + "version" : "18.7" + } + }, { "desc" : "iOS with Opera Mini", "ua" : "Opera/9.80 (iPhone; Opera Mini/7.1.32694/27.1407; U; en) Presto/2.8.119 Version/11.10", @@ -35,6 +44,33 @@ "version" : "13.6.1" } }, + { + "desc": "iOS with Instagram", + "ua": "Instagram 5.0.2 (iPhone5,1; iPhone OS 7_0_4; en_US; en) AppleWebKit/420+", + "expect": + { + "name" : "iOS", + "version" : "7.0.4" + } + }, + { + "desc": "iOS with MS Word App", + "ua": "Microsoft Office Word/2.44.1211 (iOS/13.7; Tablet; es-MX; AppStore; Apple/iPad11,3)", + "expect": + { + "name" : "iOS", + "version" : "13.7" + } + }, + { + "desc": "iOS with Quora App", + "ua": "Quora 8.4.30 rv:3230 env:prod (iPad11,3; iPadOS 17.7; en_GB) AppleWebKit", + "expect": + { + "name" : "iOS", + "version" : "17.7" + } + }, { "desc": "iOS with Slack App", "ua": "com.tinyspeck.chatlyio/23.04.10 (iPhone; iOS 16.4.1; Scale/3.00)", @@ -44,6 +80,33 @@ "version" : "16.4.1" } }, + { + "desc": "iOS with Snapchat", + "ua": "Snapchat/12.12.1.40 (iPhone15,2; iOS 16.2; gzip)", + "expect": + { + "name" : "iOS", + "version" : "16.2" + } + }, + { + "desc": "iOS with Spotify App", + "ua": "Spotify/8.7.70 iOS/16.0 (iPhone15,3)", + "expect": + { + "name" : "iOS", + "version" : "16.0" + } + }, + { + "desc": "iOS with TuneIn Radio App", + "ua": "TuneIn Radio/27.1.0; iPad6,3; iPadOS/16.6", + "expect": + { + "name" : "iOS", + "version" : "16.6" + } + }, { "desc" : "iOS BE App", "ua" : "APP-BE Test/1.0 (iPad; Apple; CPU iPhone OS 7_0_2 like Mac OS X)", From e7e7aaad4cbe429690e288bfec92bf0a3e68a6c4 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 1 Oct 2025 11:10:43 +0700 Subject: [PATCH 04/20] Add new browser: Qwant --- src/enums/ua-parser-enums.js | 1 + src/main/ua-parser.js | 3 ++- test/data/ua/browser/browser-all.json | 30 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 1dd8d9f..ed88bdc 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -125,6 +125,7 @@ const BrowserName = Object.freeze({ QUARK: 'Quark', QUPZILLA: 'QupZilla', QUTEBROWSER: 'qutebrowser', + QWANT: 'Qwant', REKONQ: 'rekonq', ROCKMELT: 'Rockmelt', SAFARI: 'Safari', diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 42b409a..b1abedb 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -380,7 +380,8 @@ // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser)\/([-\w\.]+)/i, // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio/Dragon - /(heytap|ovi|115|surf)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf + /(heytap|ovi|115|surf|qwant)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf + /(qwant)(?:ios|mobile)\/([\d\.]+)/i, // Qwant /(ecosia|weibo)(?:__| \w+@)([\d\.]+)/i // Ecosia/Weibo ], [NAME, VERSION], [ /quark(?:pc)?\/([-\w\.]+)/i // Quark diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json index a2a6379..5293bc7 100644 --- a/test/data/ua/browser/browser-all.json +++ b/test/data/ua/browser/browser-all.json @@ -1688,6 +1688,36 @@ "major" : "2" } }, + { + "desc" : "Qwant", + "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 16_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) QwantMobile/6.7.6 Mobile/15E148 Safari/605.1.15", + "expect" : + { + "name" : "Qwant", + "version" : "6.7.6", + "major" : "6" + } + }, + { + "desc" : "Qwant", + "ua" : "QwantMobile/2.0 (Android 8.0.0; Mobile; rv:59.0) Gecko/59.0 Firefox/59.0 QwantBrowser/59.0", + "expect" : + { + "name" : "Qwant", + "version" : "59.0", + "major" : "59" + } + }, + { + "desc" : "Qwant", + "ua" : "QwantMobile/2.0 (iPad; CPU OS 15_8_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) QwantiOS/2.7.0b1 Mobile/15E148 Safari/605.1.15", + "expect" : + { + "name" : "Qwant", + "version" : "2.0", + "major" : "2" + } + }, { "desc" : "Rekonq 2", "ua" : "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.21 (KHTML, like Gecko) rekonq/2.2.1 Safari/537.21", From bd6bb216bdff7e823bc2ec5d6ce11f664d0360da Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 1 Oct 2025 11:25:26 +0700 Subject: [PATCH 05/20] [extensions] Add new fetcher: Discordbot, KeybaseBot, Slackbot, Slackbot-LinkExpanding, Slack-ImgProxy, Twitterbot --- src/enums/ua-parser-enums.js | 6 +++ src/extensions/ua-parser-extensions.js | 4 +- test/data/ua/extension/fetcher.json | 62 +++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index ed88bdc..3dea4fb 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -628,6 +628,7 @@ const Extension = Object.freeze({ BLUESKY: 'Bluesky', BUFFER_LINKPREVIEWBOT: 'BufferLinkPreviewBot', COHERE_AI: 'Cohere-AI', + DISCORD_BOT: 'Discordbot', DUCKDUCKGO_ASSISTBOT: 'DuckAssistBot', GOOGLE_CHROME_LIGHTHOUSE: 'Chrome-Lighthouse', GOOGLE_FEEDFETCHER: 'FeedFetcher-Google', @@ -640,6 +641,7 @@ const Extension = Object.freeze({ HUBSPOT_PAGE_FETCHER: 'HubSpot Page Fetcher', IFRAMELY: 'Iframely', KAKAOTALK_SCRAP: 'kakaotalk-scrap', + KEYBASE_BOT: 'KeybaseBot', META_EXTERNALFETCHER: 'meta-externalfetcher', META_WHATSAPP: 'WhatsApp', MICROSOFT_BINGPREVIEW: 'BingPreview', @@ -651,6 +653,9 @@ const Extension = Object.freeze({ PERPLEXITY_USER: 'Perplexity-User', PINTEREST_BOT: 'Pinterestbot', SEMRUSH_SITEAUDITBOT: 'SiteAuditBot', + SLACK_BOT: 'Slackbot', + SLACK_BOT_LINKEXPANDING: 'Slackbot-LinkExpanding', + SLACK_IMGPROXY: 'Slack-ImgProxy', SNAP_URL_PREVIEW: 'Snap URL Preview', SKYPE_URIPREVIEW: 'SkypeUriPreview', TELEGRAM_BOT: 'TelegramBot', @@ -660,6 +665,7 @@ const Extension = Object.freeze({ VERCEL_BOT: 'Vercelbot', VERCEL_FLAGS: 'vercelflags', VERCEL_TRACING: 'verceltracing', + X_TWITTERBOT: 'Twitterbot', YANDEX_CALENDAR: 'YandexCalendar', YANDEX_DIRECT: 'YandexDirect', YANDEX_DIRECTDYN: 'YandexDirectDyn', diff --git a/src/extensions/ua-parser-extensions.js b/src/extensions/ua-parser-extensions.js index 2caf129..8438095 100644 --- a/src/extensions/ua-parser-extensions.js +++ b/src/extensions/ua-parser-extensions.js @@ -283,8 +283,8 @@ const Fetchers = Object.freeze({ [NAME, VERSION, [TYPE, FETCHER]], [ - // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / Snapchat / Vercelbot / Yandex Bots - /((?:better uptime |telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i + // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / KeybaseBot / Snapchat / Vercelbot / Yandex Bots + /((?:better uptime |keybase|telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i ], [NAME, [TYPE, FETCHER]], ], diff --git a/test/data/ua/extension/fetcher.json b/test/data/ua/extension/fetcher.json index 373b402..6ab908c 100644 --- a/test/data/ua/extension/fetcher.json +++ b/test/data/ua/extension/fetcher.json @@ -51,7 +51,7 @@ }, { "desc" : "Blueno", - "ua" : "acebookexternalhit/1.1 (compatible; Blueno/1.0; +http://naver.me/scrap)", + "ua" : "facebookexternalhit/1.1 (compatible; Blueno/1.0; +http://naver.me/scrap)", "expect" : { "name" : "Blueno", @@ -119,6 +119,16 @@ "type" : "fetcher" } }, + { + "desc" : "Discordbot", + "ua" : "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)", + "expect" : + { + "name" : "Discordbot", + "version" : "2.0", + "type" : "fetcher" + } + }, { "desc" : "DuckAssistBot", "ua" : "DuckAssistBot/1.2; (+http://duckduckgo.com/duckassistbot.html)", @@ -239,6 +249,16 @@ "type" : "fetcher" } }, + { + "desc" : "KeybaseBot", + "ua" : "Mozilla/5.0 (compatible; KeybaseBot; +https://keybase.io)", + "expect" : + { + "name" : "KeybaseBot", + "version" : "undefined", + "type" : "fetcher" + } + }, { "desc" : "Meta-ExternalFetcher", "ua" : "meta-externalfetcher/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)", @@ -309,6 +329,36 @@ "type" : "fetcher" } }, + { + "desc" : "Slack-ImgProxy", + "ua" : "Slack-ImgProxy 0.19 (+https://api.slack.com/robots)", + "expect" : + { + "name" : "Slack-ImgProxy", + "version" : "0.19", + "type" : "fetcher" + } + }, + { + "desc" : "Slackbot", + "ua" : "Slackbot 1.0 (+https://api.slack.com/robots)", + "expect" : + { + "name" : "Slackbot", + "version" : "1.0", + "type" : "fetcher" + } + }, + { + "desc" : "Slackbot-LinkExpanding", + "ua" : "Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)", + "expect" : + { + "name" : "Slackbot-LinkExpanding", + "version" : "1.0", + "type" : "fetcher" + } + }, { "desc" : "Snap URL Preview", "ua" : "Snap URL Preview Service; bot; snapchat; https://developers.snap.com/robots ", @@ -339,6 +389,16 @@ "type" : "fetcher" } }, + { + "desc" : "Twitterbot", + "ua" : "Twitterbot/1.0", + "expect" : + { + "name" : "Twitterbot", + "version" : "1.0", + "type" : "fetcher" + } + }, { "desc" : "UptimeRobot", "ua" : "Mozilla/5.0 (compatible; UptimeRobot/2.0; http://www.uptimerobot.com/)", From b3281b7c12867409755eec71278f4a742f3a41f2 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 1 Oct 2025 12:19:46 +0700 Subject: [PATCH 06/20] [extensions] Add new crawler: Qwantbot-news, SurdotlyBot, Swiftbot --- src/enums/ua-parser-enums.js | 3 +++ src/extensions/ua-parser-extensions.js | 6 ++++-- test/data/ua/extension/crawler.json | 22 +++++++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 3dea4fb..945dfaa 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -486,6 +486,7 @@ const Extension = Object.freeze({ DUCKDUCKGO_BOT: 'DuckDuckBot', DUCKDUCKGO_FAVICONS_BOT: 'DuckDuckGo-Favicons-Bot', ELASTIC: 'Elastic', + ELASTIC_SWIFTYPE_BOT: 'Swiftbot', EXALEAD_EXABOT: 'Exabot', FIRECRAWL_AGENT: 'FirecrawlAgent', FREESPOKE: 'Freespoke', @@ -535,6 +536,7 @@ const Extension = Object.freeze({ PERPLEXITY_BOT: 'PerplexityBot', QIHOO_360_SPIDER: '360Spider', QWANT_BOT: 'Qwantbot', + QWANT_BOT_NEWS: 'Qwantbot-news', REPLICATE_BOT: 'Replicate-Bot', RUNPOD_BOT: 'RunPod-Bot', SB_INTUITIONS_BOT: 'SBIntuitionsBot', @@ -548,6 +550,7 @@ const Extension = Object.freeze({ SOGOU_PIC_SPIDER: 'Sogou Pic Spider', SOGOU_WEB_SPIDER: 'Sogou web spider', STARTPAGE: 'Startpage', + SURLY_BOT: 'SurdotlyBot', TIMPI_BOT: 'Timpibot', TOGETHER_BOT: 'Together-Bot', TURNITIN_BOT: 'TurnitinBot', diff --git a/src/extensions/ua-parser-extensions.js b/src/extensions/ua-parser-extensions.js index 8438095..af8ca1d 100644 --- a/src/extensions/ua-parser-extensions.js +++ b/src/extensions/ua-parser-extensions.js @@ -63,8 +63,10 @@ const Crawlers = Object.freeze({ // PerplexityBot - https://perplexity.ai/perplexitybot // SBIntuitionsBot - https://www.sbintuitions.co.jp/bot/ // SeznamBot - http://napoveda.seznam.cz/seznambot-intro + // SurdotlyBot - http://sur.ly/bot.html + // Swiftbot - https://swiftype.com/swiftbot // YepBot - https://yep.com/yepbot/ - /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|yep)bot)\/([\w\.-]+)/i, + /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|surdotly|swift|yep)bot)\/([\w\.-]+)/i, // Algolia Crawler /(algolia crawler(?: renderscript)?)\/?([\w\.]*)/i, @@ -98,7 +100,7 @@ const Crawlers = Object.freeze({ /(oncrawl) mobile\/([\w\.]+)/i, // Qwantbot - https://help.qwant.com/bot - /(qwantbot)[-\w]*\/?([\w\.]*)/i, + /(qwantbot(?:-news)?)[-\w]*\/?([\w\.]*)/i, // SemrushBot - http://www.semrush.com/bot.html /((?:semrush|splitsignal)bot[-abcfimostw]*)\/?([\w\.-]*)/i, diff --git a/test/data/ua/extension/crawler.json b/test/data/ua/extension/crawler.json index d00a108..d9d7996 100644 --- a/test/data/ua/extension/crawler.json +++ b/test/data/ua/extension/crawler.json @@ -965,7 +965,7 @@ "ua" : "Mozilla/5.0 (compatible; Qwantbot-news/2.0; +https://help.qwant.com/bot/)", "expect" : { - "name" : "Qwantbot", + "name" : "Qwantbot-news", "version" : "2.0", "type" : "crawler" } @@ -1100,6 +1100,26 @@ "type" : "crawler" } }, + { + "desc" : "SurdotlyBot", + "ua" : "Mozilla/5.0 (compatible; SurdotlyBot/1.0; +http://sur.ly/bot.html)", + "expect" : + { + "name" : "SurdotlyBot", + "version" : "1.0", + "type" : "crawler" + } + }, + { + "desc" : "Swiftbot", + "ua" : "Swiftbot/1.0 (swiftype.com)", + "expect" : + { + "name" : "Swiftbot", + "version" : "1.0", + "type" : "crawler" + } + }, { "desc" : "Teoma", "ua" : "Mozilla/2.0 (compatible; Ask Jeeves/Teoma; +http://sp.ask.com/docs/about/tech_crawling.html)", From 6565d24567621a37be627cbfc22c2bde4522b053 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sun, 5 Oct 2025 13:27:17 +0700 Subject: [PATCH 07/20] Improve OS detection: iOS 26 --- src/main/ua-parser.js | 15 +++++++++++++-- test/data/ua/os/ios.json | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index b1abedb..e0fdd51 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -1253,8 +1253,19 @@ if (this.itemType != UA_RESULT) { rgxMapper.call(this.data, this.ua, this.rgxMap); } - if (this.itemType == UA_BROWSER) { - this.set(MAJOR, majorize(this.get(VERSION))); + switch (this.itemType) { + case UA_BROWSER: + this.set(MAJOR, majorize(this.get(VERSION))); + break; + case UA_OS: + if (this.get(NAME) == 'iOS' && this.get(VERSION) == '18.6') { + // Based on the assumption that iOS version is tightly coupled with Safari version + var realVersion = /\) Version\/([\d\.]+)/.exec(this.ua); // Get Safari version + if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) { + this.set(VERSION, realVersion[1]); // Set as iOS version + } + } + break; } return this; }; diff --git a/test/data/ua/os/ios.json b/test/data/ua/os/ios.json index 1b0d636..bf10c3e 100644 --- a/test/data/ua/os/ios.json +++ b/test/data/ua/os/ios.json @@ -1,4 +1,22 @@ [ + { + "desc" : "iOS 18.6", + "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Mobile/15E148 Safari/604.1", + "expect" : + { + "name" : "iOS", + "version" : "18.6" + } + }, + { + "desc" : "iOS 26", + "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1", + "expect" : + { + "name" : "iOS", + "version" : "26.0" + } + }, { "desc" : "iOS in App", "ua" : "AppName/version CFNetwork/version Darwin/version", From 5749302c47f60f7fa0bc14b800e55ec00579b3d6 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sun, 5 Oct 2025 13:55:18 +0700 Subject: [PATCH 08/20] [chore] Rename constants --- src/main/ua-parser.js | 218 +++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 109 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index e0fdd51..370116e 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -24,21 +24,21 @@ USER_AGENT = 'user-agent', EMPTY = '', UNKNOWN = '?', - - // typeof - FUNC_TYPE = 'function', - UNDEF_TYPE = 'undefined', - OBJ_TYPE = 'object', - STR_TYPE = 'string', + TYPEOF = { + FUNCTION : 'function', + OBJECT : 'object', + STRING : 'string', + UNDEFINED : 'undefined' + }, // properties - UA_BROWSER = 'browser', - UA_CPU = 'cpu', - UA_DEVICE = 'device', - UA_ENGINE = 'engine', - UA_OS = 'os', - UA_RESULT = 'result', - + BROWSER = 'browser', + CPU = 'cpu', + DEVICE = 'device', + ENGINE = 'engine', + OS = 'os', + RESULT = 'result', + NAME = 'name', TYPE = 'type', VENDOR = 'vendor', @@ -66,16 +66,16 @@ PLATFORM = 'platform', PLATFORMVER = 'platformVersion', BITNESS = 'bitness', - CH_HEADER = 'sec-ch-ua', - CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list', - CH_HEADER_ARCH = CH_HEADER + '-arch', - CH_HEADER_BITNESS = CH_HEADER + '-' + BITNESS, - CH_HEADER_FORM_FACTORS = CH_HEADER + '-form-factors', - CH_HEADER_MOBILE = CH_HEADER + '-' + MOBILE, - CH_HEADER_MODEL = CH_HEADER + '-' + MODEL, - CH_HEADER_PLATFORM = CH_HEADER + '-' + PLATFORM, - CH_HEADER_PLATFORM_VER = CH_HEADER_PLATFORM + '-version', - CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS], + CH = 'sec-ch-ua', + CH_FULL_VER_LIST= CH + '-full-version-list', + CH_ARCH = CH + '-arch', + CH_BITNESS = CH + '-' + BITNESS, + CH_FORM_FACTORS = CH + '-form-factors', + CH_MOBILE = CH + '-' + MOBILE, + CH_MODEL = CH + '-' + MODEL, + CH_PLATFORM = CH + '-' + PLATFORM, + CH_PLATFORM_VER = CH_PLATFORM + '-version', + CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS], // device vendors AMAZON = 'Amazon', @@ -114,7 +114,7 @@ // os WINDOWS = 'Windows'; - var isWindow = typeof window !== UNDEF_TYPE, + var isWindow = typeof window !== TYPEOF.UNDEFINED, NAVIGATOR = (isWindow && window.navigator) ? window.navigator : undefined, @@ -150,7 +150,7 @@ return enums; }, has = function (str1, str2) { - if (typeof str1 === OBJ_TYPE && str1.length > 0) { + if (typeof str1 === TYPEOF.OBJECT && str1.length > 0) { for (var i in str1) { if (lowerize(str2) == lowerize(str1[i])) return true; } @@ -164,7 +164,7 @@ } }, isString = function (val) { - return typeof val === STR_TYPE; + return typeof val === TYPEOF.STRING; }, itemListToArray = function (header) { if (!header) return undefined; @@ -191,7 +191,7 @@ if (!arr.hasOwnProperty(i)) continue; var propName = arr[i]; - if (typeof propName == OBJ_TYPE && propName.length == 2) { + if (typeof propName == TYPEOF.OBJECT && propName.length == 2) { this[propName[0]] = propName[1]; } else { this[propName] = undefined; @@ -208,7 +208,7 @@ trim = function (str, len) { if (isString(str)) { str = strip(/^\s\s*/, str); - return typeof len === UNDEF_TYPE ? str : str.substring(0, UA_MAX_LENGTH); + return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, UA_MAX_LENGTH); } }; @@ -240,9 +240,9 @@ match = matches[++k]; q = props[p]; // check if given property is actually array - if (typeof q === OBJ_TYPE && q.length > 0) { + if (typeof q === TYPEOF.OBJECT && q.length > 0) { if (q.length === 2) { - if (typeof q[1] == FUNC_TYPE) { + if (typeof q[1] == TYPEOF.FUNCTION) { // assign modified match this[q[0]] = q[1].call(this, match); } else { @@ -251,7 +251,7 @@ } } else if (q.length >= 3) { // Check whether q[1] FUNCTION or REGEX - if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { + if (typeof q[1] === TYPEOF.FUNCTION && !(q[1].exec && q[1].test)) { if (q.length > 3) { this[q[0]] = match ? q[1].apply(this, q.slice(2)) : undefined; } else { @@ -283,7 +283,7 @@ for (var i in map) { // check if current value is array - if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { + if (typeof map[i] === TYPEOF.OBJECT && map[i].length > 0) { for (var j = 0; j < map[i].length; j++) { if (has(map[i][j], str)) { return (i === UNKNOWN) ? undefined : i; @@ -1050,27 +1050,27 @@ var defaultProps = (function () { var props = { init : {}, isIgnore : {}, isIgnoreRgx : {}, toString : {}}; setProps.call(props.init, [ - [UA_BROWSER, [NAME, VERSION, MAJOR, TYPE]], - [UA_CPU, [ARCHITECTURE]], - [UA_DEVICE, [TYPE, MODEL, VENDOR]], - [UA_ENGINE, [NAME, VERSION]], - [UA_OS, [NAME, VERSION]] + [BROWSER, [NAME, VERSION, MAJOR, TYPE]], + [CPU, [ARCHITECTURE]], + [DEVICE, [TYPE, MODEL, VENDOR]], + [ENGINE, [NAME, VERSION]], + [OS, [NAME, VERSION]] ]); setProps.call(props.isIgnore, [ - [UA_BROWSER, [VERSION, MAJOR]], - [UA_ENGINE, [VERSION]], - [UA_OS, [VERSION]] + [BROWSER, [VERSION, MAJOR]], + [ENGINE, [VERSION]], + [OS, [VERSION]] ]); setProps.call(props.isIgnoreRgx, [ - [UA_BROWSER, / ?browser$/i], - [UA_OS, / ?os$/i] + [BROWSER, / ?browser$/i], + [OS, / ?os$/i] ]); setProps.call(props.toString, [ - [UA_BROWSER, [NAME, VERSION]], - [UA_CPU, [ARCHITECTURE]], - [UA_DEVICE, [VENDOR, MODEL]], - [UA_ENGINE, [NAME, VERSION]], - [UA_OS, [NAME, VERSION]] + [BROWSER, [NAME, VERSION]], + [CPU, [ARCHITECTURE]], + [DEVICE, [VENDOR, MODEL]], + [ENGINE, [NAME, VERSION]], + [OS, [NAME, VERSION]] ]); return props; })(); @@ -1114,14 +1114,14 @@ return item.detectFeature().get(); }; - if (itemType != UA_RESULT) { + if (itemType != RESULT) { IData.prototype.is = function (strToCheck) { var is = false; for (var i in this) { if (this.hasOwnProperty(i) && !has(is_ignoreProps, i) && lowerize(is_ignoreRgx ? strip(is_ignoreRgx, this[i]) : this[i]) == lowerize(is_ignoreRgx ? strip(is_ignoreRgx, strToCheck) : strToCheck)) { is = true; - if (strToCheck != UNDEF_TYPE) break; - } else if (strToCheck == UNDEF_TYPE && is) { + if (strToCheck != TYPEOF.UNDEFINED) break; + } else if (strToCheck == TYPEOF.UNDEFINED && is) { is = !is; break; } @@ -1131,11 +1131,11 @@ IData.prototype.toString = function () { var str = EMPTY; for (var i in toString_props) { - if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) { + if (typeof(this[toString_props[i]]) !== TYPEOF.UNDEFINED) { str += (str ? ' ' : EMPTY) + this[toString_props[i]]; } } - return str || UNDEF_TYPE; + return str || TYPEOF.UNDEFINED; }; } @@ -1171,19 +1171,19 @@ setProps.call(this, CH_ALL_VALUES); if (isHttpUACH) { setProps.call(this, [ - [BRANDS, itemListToArray(uach[CH_HEADER])], - [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])], - [MOBILE, /\?1/.test(uach[CH_HEADER_MOBILE])], - [MODEL, stripQuotes(uach[CH_HEADER_MODEL])], - [PLATFORM, stripQuotes(uach[CH_HEADER_PLATFORM])], - [PLATFORMVER, stripQuotes(uach[CH_HEADER_PLATFORM_VER])], - [ARCHITECTURE, stripQuotes(uach[CH_HEADER_ARCH])], - [FORMFACTORS, itemListToArray(uach[CH_HEADER_FORM_FACTORS])], - [BITNESS, stripQuotes(uach[CH_HEADER_BITNESS])] + [BRANDS, itemListToArray(uach[CH])], + [FULLVERLIST, itemListToArray(uach[CH_FULL_VER_LIST])], + [MOBILE, /\?1/.test(uach[CH_MOBILE])], + [MODEL, stripQuotes(uach[CH_MODEL])], + [PLATFORM, stripQuotes(uach[CH_PLATFORM])], + [PLATFORMVER, stripQuotes(uach[CH_PLATFORM_VER])], + [ARCHITECTURE, stripQuotes(uach[CH_ARCH])], + [FORMFACTORS, itemListToArray(uach[CH_FORM_FACTORS])], + [BITNESS, stripQuotes(uach[CH_BITNESS])] ]); } else { for (var prop in uach) { - if(this.hasOwnProperty(prop) && typeof uach[prop] !== UNDEF_TYPE) this[prop] = uach[prop]; + if(this.hasOwnProperty(prop) && typeof uach[prop] !== TYPEOF.UNDEFINED) this[prop] = uach[prop]; } } } @@ -1208,30 +1208,30 @@ this.detectFeature = function () { if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) { switch (this.itemType) { - case UA_BROWSER: + case BROWSER: // Brave-specific detection - if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { + if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) { this.set(NAME, 'Brave'); } break; - case UA_DEVICE: + case DEVICE: // Chrome-specific detection: check for 'mobile' value of navigator.userAgentData if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[MOBILE]) { this.set(TYPE, MOBILE); } // iPadOS-specific detection: identified as Mac, but has some iOS-only properties - if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { + if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { this.set(MODEL, 'iPad') .set(TYPE, TABLET); } break; - case UA_OS: + case OS: // Chrome-specific detection: check for 'platform' value of navigator.userAgentData if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM]) { this.set(NAME, NAVIGATOR_UADATA[PLATFORM]); } break; - case UA_RESULT: + case RESULT: var data = this.data; var detect = function (itemType) { return data[itemType] @@ -1239,25 +1239,25 @@ .detectFeature() .get(); }; - this.set(UA_BROWSER, detect(UA_BROWSER)) - .set(UA_CPU, detect(UA_CPU)) - .set(UA_DEVICE, detect(UA_DEVICE)) - .set(UA_ENGINE, detect(UA_ENGINE)) - .set(UA_OS, detect(UA_OS)); + this.set(BROWSER, detect(BROWSER)) + .set(CPU, detect(CPU)) + .set(DEVICE, detect(DEVICE)) + .set(ENGINE, detect(ENGINE)) + .set(OS, detect(OS)); } } return this; }; this.parseUA = function () { - if (this.itemType != UA_RESULT) { + if (this.itemType != RESULT) { rgxMapper.call(this.data, this.ua, this.rgxMap); } switch (this.itemType) { - case UA_BROWSER: + case BROWSER: this.set(MAJOR, majorize(this.get(VERSION))); break; - case UA_OS: + case OS: if (this.get(NAME) == 'iOS' && this.get(VERSION) == '18.6') { // Based on the assumption that iOS version is tightly coupled with Safari version var realVersion = /\) Version\/([\d\.]+)/.exec(this.ua); // Get Safari version @@ -1275,14 +1275,14 @@ rgxMap = this.rgxMap; switch (this.itemType) { - case UA_BROWSER: - case UA_ENGINE: + case BROWSER: + case ENGINE: var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName; if (brands) { for (var i=0; i Date: Sun, 5 Oct 2025 21:01:32 +0700 Subject: [PATCH 09/20] [fix] setUA(): remove trailing space from user-agent string --- src/main/ua-parser.js | 9 +++------ test/unit/main.js | 8 ++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 370116e..8292272 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -206,10 +206,8 @@ return strip(/\\?\"/g, str); }, trim = function (str, len) { - if (isString(str)) { - str = strip(/^\s\s*/, str); - return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, UA_MAX_LENGTH); - } + str = strip(/^\s\s*/, String(str)); + return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, len); }; /////////////// @@ -1464,8 +1462,7 @@ ['getResult', createItemFunc(RESULT)], ['getUA', function () { return userAgent; }], ['setUA', function (ua) { - if (isString(ua)) - userAgent = ua.length > UA_MAX_LENGTH ? trim(ua, UA_MAX_LENGTH) : ua; + if (isString(ua)) userAgent = trim(ua, UA_MAX_LENGTH); return this; }] ]) diff --git a/test/unit/main.js b/test/unit/main.js index b99e832..231b5c5 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -163,6 +163,14 @@ describe('Extending Regex', function () { assert.deepEqual(myParser3.setUA(myUA2).getDevice(), {vendor: "MyTab", model: "14 Pro Max", type: "tablet"}); }); +describe('User-agent with trailing space', function () { + it ('trailing space will be trimmed', function () { + const uastring = ' Opera/9.21 (Windows NT 5.1; U; ru) '; + const { ua } = UAParser(uastring); + assert.equal(ua, 'Opera/9.21 (Windows NT 5.1; U; ru) '); + }); +}); + describe('User-agent length', function () { var UA_MAX_LENGTH = 500; From 4e6259ad7fcf4e913721c69cf66690bd408dd084 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Sun, 5 Oct 2025 23:28:57 +0700 Subject: [PATCH 10/20] [feat] Add new CLI feature: processing batch user-agent data from file and output as JSON --- CHANGELOG.md | 8 +++- package.json | 2 +- script/cli.js | 94 ++++++++++++++++++++++++++++++++++++++- test/unit/cli/cli.spec.js | 37 +++++++++++++++ test/unit/cli/input.txt | 2 + test/unit/cli/output.json | 32 +++++++++++++ 6 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 test/unit/cli/cli.spec.js create mode 100644 test/unit/cli/input.txt create mode 100644 test/unit/cli/output.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea4bb8..fb55cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,12 @@ - **Support for Custom/Predefined Extensions:** - Pass custom regexes or predefined extensions as a list to `UAParser()` -- **Support for CLI Parsing:** - - Parse a user-agent directly from the command line using `npx ua-parser-js "[User-Agent]"` +- **Support for CLI Processing:** + - Directly parse user-agent strings from the command line: + `npx ua-parser-js ""` + - Process batch data from files: + `npx ua-parser-js --input-file=log.txt >> result.json` or + `npx ua-parser-js --input-file=log.txt --output-file=result.json` - **Enhanced Detection with Client Hints:** - `withClientHints()`: Improves detection accuracy by leveraging client hints diff --git a/package.json b/package.json index 580bbcc..06b4b7b 100755 --- a/package.json +++ b/package.json @@ -220,7 +220,7 @@ "test:eslint": "eslint src && eslint script", "test:jshint": "jshint src/main", "test:lockfile-lint": "npx lockfile-lint -p package-lock.json", - "test:mocha": "mocha test/unit", + "test:mocha": "mocha --recursive test/unit", "test:playwright": "npx playwright install && playwright test test/e2e --browser all" }, "dependencies": { diff --git a/script/cli.js b/script/cli.js index 785a104..368d6c5 100755 --- a/script/cli.js +++ b/script/cli.js @@ -1,4 +1,94 @@ #!/usr/bin/env node -const UAParser = require('ua-parser-js'); -console.log(JSON.stringify(process.argv.slice(2).map(ua => UAParser(ua)), null, 4)); \ No newline at end of file +try { + const fs = require('node:fs'); + const path = require('node:path'); + const { performance } = require('node:perf_hooks'); + const readline = require('node:readline'); + const { parseArgs } = require('node:util'); + const UAParser = require('ua-parser-js'); + const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('ua-parser-js/extensions'); + + if (!process.argv[2].startsWith('-')) { + + const results = process.argv.slice(2).map(ua => UAParser(ua)); + console.log(JSON.stringify(results, null, 4)); + process.exit(0); + + } else if (['-h', '--help'].includes(process.argv[2])) { + + console.log('Usage: npx ua-parser-js '); + console.log(' or npx ua-parser-js --input-file [--output-file ]'); + console.log('-i, --input-file'); + console.log('-o, --output-file'); + process.exit(0); + + } else { + + const startPerf = performance.now(); + const { + values: { + 'input-file': inputFile, + 'output-file': outputFile + }, + } = parseArgs({ + options: { + 'input-file': { type: 'string', short: 'i' }, + 'output-file': { type: 'string', short: 'o' } + } + }); + + if (!inputFile) { + console.error('Input file must be present'); + process.exit(1); + } + + const inputPath = path.resolve(__dirname, inputFile); + const outputPath = outputFile ? path.resolve(__dirname, outputFile) : null; + + if (!fs.existsSync(inputPath)) { + console.error(`Input file not found: ${inputPath}`); + process.exit(1); + } + + const inputStream = fs.createReadStream(inputPath, 'utf8'); + const rl = readline.createInterface({ + input: inputStream, + crlfDelay: Infinity + }); + + const outputStream = outputPath ? fs.createWriteStream(outputPath, { encoding : 'utf8' }) : process.stdout; + + const uap = new UAParser([Bots, Emails, ExtraDevices, InApps, Vehicles]); + let lineNumber = 0; + + outputStream.write('[\n'); + + rl.on('line', line => { + const result = uap.setUA(line).getResult(); + const json = JSON.stringify(result, null, 4); + if (lineNumber > 0) outputStream.write(',\n'); + outputStream.write(json); + lineNumber++; + }); + + rl.on('close', () => { + outputStream.write('\n]'); + if (outputPath) { + outputStream.end(() => { + const finishPerf = performance.now(); + console.log(`Done!`); + console.log(`Number of lines found: ${lineNumber}`); + console.log(`Task finished in: ${(finishPerf - startPerf).toFixed(3)}ms`); + console.log(`Output written to: ${outputPath}`); + process.exit(0); + }); + } else { + process.exit(0); + } + }); + } +} catch (err) { + console.error(err); + process.exit(1); +} \ No newline at end of file diff --git a/test/unit/cli/cli.spec.js b/test/unit/cli/cli.spec.js new file mode 100644 index 0000000..7365fe4 --- /dev/null +++ b/test/unit/cli/cli.spec.js @@ -0,0 +1,37 @@ +const assert = require('node:assert'); +const { exec } = require('node:child_process'); +const fs = require('node:fs'); +const { UAParser } = require('../../../src/main/ua-parser'); +const uap = new UAParser(); + +describe('npx ua-parser-js ', () => { + it ('print result to stdout', () => { + exec('npx ua-parser-js "TEST"', (err, stdout, stderr) => { + assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([uap.setUA("TEST").getResult()]))); + }); + }) +}); + +describe('npx ua-parser-js --input-file=', () => { + it ('load file and print result to stdout', () => { + exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt"', (err, stdout, stderr) => { + assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([ + uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), + uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() + ]))); + }); + }); +}); + +describe('npx ua-parser-js --input-file= --output-file=', () => { + it ('load file and save result to file', () => { + exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt" --output-file="../test/unit/cli/output.json"', (err, stdout, stderr) => { + fs.readFile('test/unit/cli/output.json', (err, data) => { + assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify([ + uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), + uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() + ]))); + }); + }); + }); +}); diff --git a/test/unit/cli/input.txt b/test/unit/cli/input.txt new file mode 100644 index 0000000..2d50fb8 --- /dev/null +++ b/test/unit/cli/input.txt @@ -0,0 +1,2 @@ +Opera/9.25 (Windows NT 6.0; U; ru) +Mozilla/4.0 (compatible; MSIE 5.5; Windows NT) \ No newline at end of file diff --git a/test/unit/cli/output.json b/test/unit/cli/output.json new file mode 100644 index 0000000..d5b92c8 --- /dev/null +++ b/test/unit/cli/output.json @@ -0,0 +1,32 @@ +[ +{ + "ua": "Opera/9.25 (Windows NT 6.0; U; ru)", + "browser": { + "name": "Opera", + "version": "9.25", + "major": "9" + }, + "cpu": {}, + "device": {}, + "engine": {}, + "os": { + "name": "Windows", + "version": "Vista" + } +}, +{ + "ua": "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)", + "browser": { + "name": "IE", + "version": "5.5", + "major": "5" + }, + "cpu": {}, + "device": {}, + "engine": {}, + "os": { + "name": "Windows", + "version": "NT" + } +} +] \ No newline at end of file From fc5125042c8123d213698eac8fe3c4c02a8356ed Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Mon, 6 Oct 2025 11:51:22 +0700 Subject: [PATCH 11/20] Improve browser detection: Mozilla, Pale Moon --- src/main/ua-parser.js | 8 ++++---- test/data/ua/browser/browser-all.json | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 8292272..9f50fad 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -375,8 +375,8 @@ /(avant|iemobile|slim(?:browser|boat|jet))[\/ ]?([\d\.]*)/i, // Avant/IEMobile/SlimBrowser/SlimBoat/Slimjet /(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer - // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble - /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser)\/([-\w\.]+)/i, + // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble/Palemoon + /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser|palemoon)\/([-\w\.]+)/i, // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio/Dragon /(heytap|ovi|115|surf|qwant)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf /(qwant)(?:ios|mobile)\/([\d\.]+)/i, // Qwant @@ -505,10 +505,10 @@ /(swiftfox)/i, // Swiftfox /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror)[\/ ]?([\w\.\+]+)/i, // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror - /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, + /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|basilisk|waterfox)\/([-\w\.]+)$/i, // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix /(firefox)\/([\w\.]+)/i, // Other Firefox-based - /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla + /(mozilla)\/([\w\.]+(?= .+rv\:.+gecko\/\d+)|[0-4][\w\.]+(?!.+compatible))/i, // Mozilla // Other /(amaya|dillo|doris|icab|ladybird|lynx|mosaic|netsurf|obigo|polaris|w3m|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json index 5293bc7..bea4eb9 100644 --- a/test/data/ua/browser/browser-all.json +++ b/test/data/ua/browser/browser-all.json @@ -611,6 +611,16 @@ "major" : "55" } }, + { + "desc" : "PaleMoon", + "ua" : "(Windows NT 6.2; WOW64) KHTML/4.11 Gecko/20130308 Firefox/23.0 (PaleMoon/20.3)", + "expect" : + { + "name" : "PaleMoon", + "version" : "20.3", + "major" : "20" + } + }, { "desc" : "PaleMoon", "ua" : "Mozilla/5.0 (X11; Linux x86_64; rv:52.9) Gecko/20100101 Goanna/3.4 Firefox/52.9 PaleMoon/27.6.1", @@ -1338,6 +1348,16 @@ "major" : "5" } }, + { + "desc" : "Mozilla", + "ua" : "Mozilla/2.02 [fr] (WinNT; I)", + "expect" : + { + "name" : "Mozilla", + "version" : "2.02", + "major" : "2" + } + }, { "desc" : "MSIE", "ua" : "Mozilla/4.0 (compatible; MSIE 5.0b1; Mac_PowerPC)", From 44165a6e01cc742f62433ad86de09daa7388ff58 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Mon, 6 Oct 2025 11:59:55 +0700 Subject: [PATCH 12/20] Improve CPU detection: 68k --- src/main/ua-parser.js | 2 ++ test/data/ua/cpu/cpu-all.json | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 9f50fad..8e35764 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -546,6 +546,8 @@ /((ppc|powerpc)(64)?)( mac|;|\))/i, // PowerPC /(?:osf1|[freopnt]{3,4}bsd) (alpha)/i // Alpha ], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [ + /mc680.0/i + ], [[ARCHITECTURE, '68k']], [ /winnt.+\[axp/i ], [[ARCHITECTURE, 'alpha']] ], diff --git a/test/data/ua/cpu/cpu-all.json b/test/data/ua/cpu/cpu-all.json index e9b749e..6a992e9 100644 --- a/test/data/ua/cpu/cpu-all.json +++ b/test/data/ua/cpu/cpu-all.json @@ -289,7 +289,15 @@ }, { "desc" : "68k", - "ua" : "'Mozilla/1.1 (Macintosh; U; 68K)'", + "ua" : "Mozilla/1.1 (Macintosh; U; 68K)", + "expect" : + { + "architecture" : "68k" + } + }, + { + "desc" : "MC680x0", + "ua" : "AmigaVoyager/3.2 (AmigaOS/MC680x0)", "expect" : { "architecture" : "68k" From ae7b5e15e5159b5c86fde4ad447e32c407940fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20R=C3=A8gne?= Date: Mon, 6 Oct 2025 15:59:29 +0200 Subject: [PATCH 13/20] chore: Replace Undici by native Headers (#805) --- package-lock.json | 12 +----------- package.json | 3 +-- src/main/ua-parser.d.ts | 1 - test/unit/main.js | 1 - 4 files changed, 2 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9334f2..17355c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,8 +25,7 @@ "dependencies": { "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", - "ua-is-frozen": "^0.1.2", - "undici": "^7.12.0" + "ua-is-frozen": "^0.1.2" }, "bin": { "ua-parser-js": "script/cli.js" @@ -2693,15 +2692,6 @@ "node": ">=0.8.0" } }, - "node_modules/undici": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.12.0.tgz", - "integrity": "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug==", - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 06b4b7b..1c3fd50 100755 --- a/package.json +++ b/package.json @@ -226,8 +226,7 @@ "dependencies": { "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", - "ua-is-frozen": "^0.1.2", - "undici": "^7.12.0" + "ua-is-frozen": "^0.1.2" }, "devDependencies": { "@babel/parser": "7.15.8", diff --git a/src/main/ua-parser.d.ts b/src/main/ua-parser.d.ts index 09c11ee..0dcbd6a 100644 --- a/src/main/ua-parser.d.ts +++ b/src/main/ua-parser.d.ts @@ -2,7 +2,6 @@ // Project: https://github.com/faisalman/ua-parser-js // Definitions by: Faisal Salman -import type { Headers } from "undici"; import { BrowserType, CPUArch, DeviceType, EngineName } from "../enums/ua-parser-enums"; declare namespace UAParser { diff --git a/test/unit/main.js b/test/unit/main.js index 231b5c5..01e0d4e 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -10,7 +10,6 @@ var cpus = require('../data/ua/cpu/cpu-all.json'); var devices = readJsonFiles('test/data/ua/device'); var engines = require('../data/ua/engine/engine-all.json'); var os = readJsonFiles('test/data/ua/os'); -var { Headers } = require('undici'); function readJsonFiles(dir) { var list = []; From 4c935c0139c10c2115f1c1f5856bb48620d8f284 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Mon, 6 Oct 2025 22:07:35 +0700 Subject: [PATCH 14/20] Improve device detection: Nokia --- src/main/ua-parser.js | 2 +- test/data/ua/device/nokia.json | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 8e35764..297804c 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -658,7 +658,7 @@ /(nokia) (t[12][01])/i ], [VENDOR, MODEL, [TYPE, TABLET]], [ /(?:maemo|nokia).*(n900|lumia \d+|rm-\d+)/i, - /nokia[-_ ]?(([-\w\. ]*))/i + /nokia[-_ ]?(([-\w\. ]*?))( bui|\)|;|\/)/i ], [[MODEL, /_/g, ' '], [TYPE, MOBILE], [VENDOR, 'Nokia']], [ // Google diff --git a/test/data/ua/device/nokia.json b/test/data/ua/device/nokia.json index 375cfcc..f1ff7d6 100644 --- a/test/data/ua/device/nokia.json +++ b/test/data/ua/device/nokia.json @@ -1,4 +1,13 @@ [ + { + "desc": "Nokia 1", + "ua": "Mozilla/5.0 (Linux; Android 10; Nokia 1 Build/QP1A.190711.020) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.15 Mobile Safari/537.36", + "expect": { + "vendor": "Nokia", + "model": "1", + "type": "mobile" + } + }, { "desc": "Nokia3xx", "ua": "Nokia303/14.87 CLDC-1.1", From 9ba4d2b207d19e741c537e7cd0f3f1973f49e5ea Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Mon, 6 Oct 2025 22:08:56 +0700 Subject: [PATCH 15/20] Add new device vendor: Hisense - https://global.hisense.com/ --- src/enums/ua-parser-enums.js | 1 + src/main/ua-parser.js | 1 + test/data/ua/device/hisense.json | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 test/data/ua/device/hisense.json diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 945dfaa..8955439 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -240,6 +240,7 @@ const DeviceVendor = Object.freeze({ GEEKSPHONE: 'GeeksPhone', GENERIC: 'Generic', GOOGLE: 'Google', + HISENSE: 'Hisense', HMD: 'HMD', HP: 'HP', HTC: 'HTC', diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 297804c..afff9b8 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -773,6 +773,7 @@ /(hp) ([\w ]+\w)/i, // HP iPAQ /(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia /(oppo) ?([\w ]+) bui/i, // OPPO + /(hisense) ([ehv][\w ]+)\)/i, // Hisense /droid[^;]+; (philips)[_ ]([sv-x][\d]{3,4}[xz]?)/i // Philips ], [VENDOR, MODEL, [TYPE, MOBILE]], [ diff --git a/test/data/ua/device/hisense.json b/test/data/ua/device/hisense.json new file mode 100644 index 0000000..d13f6c9 --- /dev/null +++ b/test/data/ua/device/hisense.json @@ -0,0 +1,20 @@ +[ + { + "desc": "Hisense E50 Lite", + "ua": "Mozilla/5.0 (Linux; Android 11; Hisense E50 Lite) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.126 Mobile Safari/537.36", + "expect": { + "vendor": "Hisense", + "model": "E50 Lite", + "type": "mobile" + } + }, + { + "desc": "Hisense V40s", + "ua": "Mozilla/5.0 (Linux; Android 11; Hisense V40s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.125 Mobile Safari/537.36", + "expect": { + "vendor": "Hisense", + "model": "V40s", + "type": "mobile" + } + } +] \ No newline at end of file From 5349bb52ed30f3048800ca92d5be6f264cf9f348 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Tue, 7 Oct 2025 14:51:46 +0700 Subject: [PATCH 16/20] Improve device detection: BlackBerry, Huawei, Xiaomi --- src/main/ua-parser.js | 12 +++++----- test/data/ua/device/blackberry.json | 22 ++++++++++++++++-- test/data/ua/device/huawei.json | 36 +++++++++++++++++++++++++++++ test/data/ua/device/xiaomi.json | 36 +++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index afff9b8..27e79ce 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -588,13 +588,13 @@ // Huawei /\b((?:ag[rs][2356]?k?|bah[234]?|bg[2o]|bt[kv]|cmr|cpn|db[ry]2?|jdn2|got|kob2?k?|mon|pce|scm|sht?|[tw]gr|vrd)-[ad]?[lw][0125][09]b?|605hw|bg2-u03|(?:gem|fdr|m2|ple|t1)-[7a]0[1-4][lu]|t1-a2[13][lw]|mediapad[\w\. ]*(?= bui|\)))\b(?!.+d\/s)/i ], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [ - /(?:huawei)([-\w ]+)[;\)]/i, - /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i + /(?:huawei) ?([-\w ]+)[;\)]/i, + /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][\dc][adnt]?)\b(?!.+d\/s)/i ], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [ // Xiaomi /oid[^\)]+; (2[\dbc]{4}(182|283|rp\w{2})[cgl]|m2105k81a?c)(?: bui|\))/i, - /\b((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets + /\b(?:xiao)?((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets ],[[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, TABLET]], [ /\b(poco[\w ]+|m2\d{3}j\d\d[a-z]{2})(?: bui|\))/i, // Xiaomi POCO @@ -602,7 +602,7 @@ /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi /oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models - /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite|pro)?)(?: bui|\))/i, // Xiaomi Mi + /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note|max|cc)?[_ ]?(?:\d{0,2}\w?)[_ ]?(?:plus|se|lite|pro)?( 5g|lte)?)(?: bui|\))/i, // Xiaomi Mi / ([\w ]+) miui\/v?\d/i ], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [ @@ -689,7 +689,7 @@ /(playbook);[-\w\),; ]+(rim)/i // BlackBerry PlayBook ], [MODEL, VENDOR, [TYPE, TABLET]], [ /\b((?:bb[a-f]|st[hv])100-\d)/i, - /\(bb10; (\w+)/i // BlackBerry 10 + /(?:blackberry|\(bb10;) (\w+)/i ], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [ // Asus @@ -913,7 +913,7 @@ ], [MODEL, [TYPE, SMARTTV]], [ /\b((4k|android|smart|opera)[- ]?tv|tv; rv:|large screen[\w ]+safari)\b/i ], [[TYPE, SMARTTV]], [ - /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew).+?(mobile|vr|\d) safari/i + /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew|; hmsc).+?(mobile|vr|\d) safari/i ], [MODEL, [TYPE, strMapper, { 'mobile' : 'Mobile', 'xr' : 'VR', '*' : TABLET }]], [ /\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i // Unidentifiable Tablet ], [[TYPE, TABLET]], [ diff --git a/test/data/ua/device/blackberry.json b/test/data/ua/device/blackberry.json index f809d55..396a139 100644 --- a/test/data/ua/device/blackberry.json +++ b/test/data/ua/device/blackberry.json @@ -1,7 +1,25 @@ [ + { + "desc": "BlackBerry 9650", + "ua": "Mozilla/5.0 (BlackBerry; U; BlackBerry 9650; en-US) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.524 Mobile Safari/534.8+", + "expect": { + "vendor": "BlackBerry", + "model": "9650", + "type": "mobile" + } + }, + { + "desc": "BlackBerry 9780", + "ua": "Mozilla/5.0 (BlackBerry; U; BlackBerry 9780; en) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.546 Mobile Safari/534.8+", + "expect": { + "vendor": "BlackBerry", + "model": "9780", + "type": "mobile" + } + }, { "desc": "BlackBerry Priv", - "ua": "User-Agent: Mozilla/5.0 (Linux; Android 5.1.1; STV100-1 Build/LMY47V; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36", + "ua": "Mozilla/5.0 (Linux; Android 5.1.1; STV100-1 Build/LMY47V; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/46.0.2490.76 Mobile Safari/537.36", "expect": { "vendor": "BlackBerry", "model": "STV100-1", @@ -28,7 +46,7 @@ }, { "desc": "BlackBerry Key2 LE", - "ua": "User-Agent: Mozilla/5.0 (Linux; Android 8.1.0; BBE100-1 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497 Mobile Safari/537.36", + "ua": "Mozilla/5.0 (Linux; Android 8.1.0; BBE100-1 Build/OPM1.171019.026) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497 Mobile Safari/537.36", "expect": { "vendor": "BlackBerry", "model": "BBE100-1", diff --git a/test/data/ua/device/huawei.json b/test/data/ua/device/huawei.json index 670879e..14af421 100644 --- a/test/data/ua/device/huawei.json +++ b/test/data/ua/device/huawei.json @@ -701,6 +701,15 @@ "type": "mobile" } }, + { + "desc": "Huawei P10 Lite", + "ua": "Mozilla/5.0 (Linux; Android 8.0.0; WAS-L03T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5199.205 Mobile Safari/537.36", + "expect": { + "vendor": "Huawei", + "model": "WAS-L03T", + "type": "mobile" + } + }, { "desc": "Huawei P20 Lite", "ua": "Mozilla/5.0 (Linux; Android 8.0.0; ANE-LX1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.143 Mobile Safari/537.36", @@ -728,6 +737,15 @@ "type": "mobile" } }, + { + "desc": "Huawei P30", + "ua": "Mozilla/5.0 (Linux; Android 10; ELE-L04) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.109 Mobile Safari/537.36", + "expect": { + "vendor": "Huawei", + "model": "ELE-L04", + "type": "mobile" + } + }, { "desc": "Huawei P30", "ua": "Mozilla/5.0 (Linux; Android 9; ELE-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.90 Mobile Safari/537.36", @@ -782,6 +800,15 @@ "type": "mobile" } }, + { + "desc": "Huawei Nova", + "ua": "Mozilla/5.0 (Linux; Android 7.0; HUAWEI CAN-L13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5113.212 Mobile Safari/537.36", + "expect": { + "vendor": "Huawei", + "model": "CAN-L13", + "type": "mobile" + } + }, { "desc": "Huawei Nova 5T", "ua": "Mozilla/5.0 (Linux; Android 10; YAL-L21) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Mobile Safari/537.36", @@ -899,6 +926,15 @@ "type": "mobile" } }, + { + "desc": "Huawei Y7p", + "ua": "Mozilla/5.0 (Linux; Android 10; ART-L28; HMSCore 6.8.0.311) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4476.0 HuaweiBrowser/12.1.2.312 Mobile Safari/537.36", + "expect": { + "vendor": "Huawei", + "model": "ART-L28", + "type": "mobile" + } + }, { "desc": "Huawei Y7p", "ua": "Mozilla/5.0 (Linux; Android 9; ART-L29) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.92 Mobile Safari/537.36", diff --git a/test/data/ua/device/xiaomi.json b/test/data/ua/device/xiaomi.json index 86593ef..053ef69 100644 --- a/test/data/ua/device/xiaomi.json +++ b/test/data/ua/device/xiaomi.json @@ -116,6 +116,15 @@ "type": "mobile" } }, + { + "desc": "Xiaomi Mi 11 Lite 5G", + "ua": "Mozilla/5.0 (Linux; U; Android 12; zh-CN; Mi 11 Lite 5G Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.136 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.17.0-gn", + "expect": { + "vendor": "Xiaomi", + "model": "Mi 11 Lite 5G", + "type": "mobile" + } + }, { "desc": "Xiaomi Mi 5s Plus", "ua": "Mozilla/5.0 (Linux; U; Android 6.0.1; zh-cn; MI 5s Plus Build/MXB48T) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.146 Mobile Safari/537.36 XiaoMi/MiuiBrowser/8.7.1", @@ -188,6 +197,15 @@ "type": "mobile" } }, + { + "desc": "Xiaomi Mi 10T", + "ua": "Mozilla/5.0 (Linux; U; Android 12; fr-CA; Mi 10T Build/SKQ1.211006.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.79 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.16.1-gn", + "expect": { + "vendor": "Xiaomi", + "model": "Mi 10T", + "type": "mobile" + } + }, { "desc": "Xiaomi Mi A2", "ua": "Mozilla/5.0 (Linux; Android 9; Mi A2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Mobile Safari/537.36", @@ -314,6 +332,15 @@ "type": "tablet" } }, + { + "desc": "Xiaomi Pad 5", + "ua": "Mozilla/5.0 (Linux; U; Android 12; ca-ES; Xiaomi Pad 5 Build/SKQ1.220303.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.86 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.6.0-gn", + "expect": { + "vendor": "Xiaomi", + "model": "mi Pad 5", + "type": "tablet" + } + }, { "desc": "Xiaomi Pad 6S Pro 12.4", "ua": "Mozilla/5.0 (Linux; Android 14; 24018RPACC) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", @@ -458,6 +485,15 @@ "type": "mobile" } }, + { + "desc": "XiaoMi Redmi Note 10 Lite", + "ua": "Mozilla/5.0 (Linux; U; Android 12; es-VE; Mi Note 10 Lite Build/SKQ1.210908.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4896.88 Mobile Safari/537.36 XiaoMi/MiuiBrowser/13.16.1-gn", + "expect": { + "vendor": "Xiaomi", + "model": "Mi Note 10 Lite", + "type": "mobile" + } + }, { "desc": "XiaoMi Redmi Note 10 Pro", "ua": "Mozilla/5.0 (Linux; Android 13; M2101K6P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36", From 5f1ed83225d81863a20c43ce13e73482c7835953 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 8 Oct 2025 13:13:17 +0700 Subject: [PATCH 17/20] [chore] Update CLI import & unit test --- script/cli.js | 4 ++-- test/unit/cli/cli.spec.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/script/cli.js b/script/cli.js index 368d6c5..813cfc3 100755 --- a/script/cli.js +++ b/script/cli.js @@ -6,8 +6,8 @@ try { const { performance } = require('node:perf_hooks'); const readline = require('node:readline'); const { parseArgs } = require('node:util'); - const UAParser = require('ua-parser-js'); - const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('ua-parser-js/extensions'); + const UAParser = require('../src/main/ua-parser'); + const { Bots, Emails, ExtraDevices, InApps, Vehicles } = require('../src/extensions/ua-parser-extensions'); if (!process.argv[2].startsWith('-')) { diff --git a/test/unit/cli/cli.spec.js b/test/unit/cli/cli.spec.js index 7365fe4..ab5523e 100644 --- a/test/unit/cli/cli.spec.js +++ b/test/unit/cli/cli.spec.js @@ -4,6 +4,12 @@ const fs = require('node:fs'); const { UAParser } = require('../../../src/main/ua-parser'); const uap = new UAParser(); +const input = [ + 'Opera/9.25 (Windows NT 6.0; U; ru)', + 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)' +]; +const output = input.map(x => uap.setUA(x).getResult()); + describe('npx ua-parser-js ', () => { it ('print result to stdout', () => { exec('npx ua-parser-js "TEST"', (err, stdout, stderr) => { @@ -15,10 +21,7 @@ describe('npx ua-parser-js ', () => { describe('npx ua-parser-js --input-file=', () => { it ('load file and print result to stdout', () => { exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt"', (err, stdout, stderr) => { - assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify([ - uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), - uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() - ]))); + assert.deepEqual(JSON.parse(stdout), JSON.parse(JSON.stringify(output))); }); }); }); @@ -27,10 +30,7 @@ describe('npx ua-parser-js --input-file= --output-file=', () it ('load file and save result to file', () => { exec('npx ua-parser-js --input-file="../test/unit/cli/input.txt" --output-file="../test/unit/cli/output.json"', (err, stdout, stderr) => { fs.readFile('test/unit/cli/output.json', (err, data) => { - assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify([ - uap.setUA('Opera/9.25 (Windows NT 6.0; U; ru)').getResult(), - uap.setUA('Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)').getResult() - ]))); + assert.deepEqual(JSON.parse(data), JSON.parse(JSON.stringify(output))); }); }); }); From 3eea0643c58ae4ff235bd343cc8c71585976e933 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Wed, 8 Oct 2025 21:51:36 +0700 Subject: [PATCH 18/20] Add new device vendor: Wiko - https://world.wikomobile.com/ --- src/enums/ua-parser-enums.js | 1 + src/main/ua-parser.js | 3 ++- test/data/ua/device/wiko.json | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 test/data/ua/device/wiko.json diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 8955439..202a8f6 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -289,6 +289,7 @@ const DeviceVendor = Object.freeze({ VIVO: 'Vivo', VIZIO: 'Vizio', VODAFONE: 'Vodafone', + WIKO: 'Wiko', XBOX: 'Xbox', XIAOMI: 'Xiaomi', ZEBRA: 'Zebra', diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 27e79ce..7c962a9 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -769,7 +769,8 @@ /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus(?! zenw)|dell|jolla|meizu|motorola|polytron|tecno|micromax|advan)[-_ ]?([-\w]*)/i, // BlackBerry/BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron/Tecno/Micromax/Advan - /; (blu|hmd|imo|infinix|lava|oneplus|tcl)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i, // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL + // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL/Wiko + /; (blu|hmd|imo|infinix|lava|oneplus|tcl|wiko)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i, /(hp) ([\w ]+\w)/i, // HP iPAQ /(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia /(oppo) ?([\w ]+) bui/i, // OPPO diff --git a/test/data/ua/device/wiko.json b/test/data/ua/device/wiko.json new file mode 100644 index 0000000..9030e7a --- /dev/null +++ b/test/data/ua/device/wiko.json @@ -0,0 +1,29 @@ +[ + { + "desc": "Wiko Life 3", + "ua": "Mozilla/5.0 (Linux; Android 11; Wiko U316AT) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5199.205 Mobile Safari/537.36", + "expect": { + "vendor": "Wiko", + "model": "U316AT", + "type": "mobile" + } + }, + { + "desc": "Wiko Ride 3", + "ua": "Mozilla/5.0 (Linux; Android 11; Wiko U614AS) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.181 Mobile Safari/537.36", + "expect": { + "vendor": "Wiko", + "model": "U614AS", + "type": "mobile" + } + }, + { + "desc": "Wiko T10", + "ua": "Mozilla/5.0 (Linux; Android 11; WIKO T10 Build/RP1A.200720.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.62 Mobile Safari/537.36", + "expect": { + "vendor": "WIKO", + "model": "T10", + "type": "mobile" + } + } +] \ No newline at end of file From 2882014f0ebf1b052b3bd128f5f5e83762aa7752 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Thu, 9 Oct 2025 11:34:03 +0700 Subject: [PATCH 19/20] Add new inApp browser: Bing --- src/enums/ua-parser-enums.js | 1 + src/main/ua-parser.js | 1 + test/data/ua/browser/browser-all.json | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 202a8f6..4867fec 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -20,6 +20,7 @@ const BrowserName = Object.freeze({ AVG: 'AVG Secure Browser', BAIDU: 'Baidu Browser', BASILISK: 'Basilisk', + BING: 'Bing', BLAZER: 'Blazer', BOLT: 'Bolt', BOWSER: 'Bowser', diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index 7c962a9..b461dc3 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -448,6 +448,7 @@ /\b(line)\/([\w\.]+)\/iab/i, // Line App for Android /(alipay)client\/([\w\.]+)/i, // Alipay /(twitter)(?:and| f.+e\/([\w\.]+))/i, // Twitter + /(bing)(?:web|sapphire)\/([\w\.]+)/i, // Bing /(instagram|snapchat|klarna)[\/ ]([-\w\.]+)/i // Instagram/Snapchat/Klarna ], [NAME, VERSION, [TYPE, INAPP]], [ /\bgsa\/([\w\.]+) .*safari\//i // Google Search Appliance on iOS diff --git a/test/data/ua/browser/browser-all.json b/test/data/ua/browser/browser-all.json index bea4eb9..a7372d5 100644 --- a/test/data/ua/browser/browser-all.json +++ b/test/data/ua/browser/browser-all.json @@ -271,6 +271,26 @@ "major" : "11" } }, + { + "desc" : "Bing", + "ua" : "Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/605.1.15 BingSapphire/31.8.430522001", + "expect" : + { + "name" : "Bing", + "version" : "31.8.430522001", + "major" : "31" + } + }, + { + "desc" : "Bing", + "ua" : "Mozilla/5.0 (Linux; Android 9; MIX 2 Build/PKQ1.190118.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/100.0.4893.0 Mobile Safari/537.36 BingWeb/6.9.12", + "expect" : + { + "name" : "Bing", + "version" : "6.9.12", + "major" : "6" + } + }, { "desc" : "Blazer", "ua" : "Mozilla/4.0 (compatible; MSIE 6.0; Windows 98; PalmSource/hspr-H102; Blazer/4.0) 16;320x320", From 061cf0e90f3651a040a1ee64fc5c63d442b93c64 Mon Sep 17 00:00:00 2001 From: Faisal Salman Date: Fri, 10 Oct 2025 09:58:40 +0700 Subject: [PATCH 20/20] Bump version `2.0.6` --- CHANGELOG.md | 14 ++ README.md | 46 ++-- dist/ua-parser.min.js | 4 +- dist/ua-parser.min.mjs | 4 +- dist/ua-parser.pack.js | 4 +- dist/ua-parser.pack.mjs | 4 +- package-lock.json | 4 +- package.json | 10 +- src/enums/ua-parser-enums.d.ts | 15 +- src/enums/ua-parser-enums.js | 2 +- src/enums/ua-parser-enums.mjs | 15 +- src/extensions/ua-parser-extensions.js | 2 +- src/extensions/ua-parser-extensions.mjs | 12 +- src/helpers/ua-parser-helpers.js | 2 +- src/helpers/ua-parser-helpers.mjs | 2 +- src/main/ua-parser.d.ts | 2 +- src/main/ua-parser.js | 4 +- src/main/ua-parser.mjs | 275 +++++++++++++----------- test/unit/main.js | 15 +- 19 files changed, 247 insertions(+), 189 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb55cf8..5ef6810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,20 @@ --- +## Version 2.0.6 +- Add new CLI feature: processing batch user-agent data from file and output as JSON +- Fix `setUA()`: trim leading space from user-agent string input +- Replace `undici` dependency with node's internal `Headers` +- Add new browser: Bing, Qwant +- Add new device vendor: Hisense, Wiko +- Improve browser detection: Mozilla, Pale Moon +- Improve CPU detection: 68k +- Improve device detection: Apple, BlackBerry, Huawei, Nokia, Xiaomi +- Improve OS detection: iOS 26 +- `extensions` submodule: + - Add new fetcher: Discordbot, KeybaseBot, Slackbot, Slackbot-LinkExpanding, Slack-ImgProxy, Twitterbot + - Add new crawler: Qwantbot-news, SurdotlyBot, SwiftBot + ## Version 2.0.5 - Add new browser: Zalo diff --git a/README.md b/README.md index 03eb487..7bb630f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ -Discord invite +Discord invite

# UAParser.js @@ -58,7 +58,7 @@ see what's new & breaking. PRO Enterprise - Browser detection + Browser Detection ⚠️ ✅ ✅ @@ -66,7 +66,7 @@ see what's new & breaking. ✅ - CPU detection + CPU Detection ⚠️ ✅ ✅ @@ -74,7 +74,7 @@ see what's new & breaking. ✅ - Device detection + Device Detection ⚠️ ✅ ✅ @@ -82,7 +82,7 @@ see what's new & breaking. ✅ - Engine detection + Rendering Engine Detection ⚠️ ✅ ✅ @@ -98,7 +98,7 @@ see what's new & breaking. ✅ - Bot detection + Enhanced+ Accuracy ❌ ✅ ✅ @@ -106,7 +106,7 @@ see what's new & breaking. ✅ - AI Bot detection + Bot Detection ❌ ✅ ✅ @@ -114,7 +114,7 @@ see what's new & breaking. ✅ - Extras (Apps, Libs, Emails, Media Players, etc) detection + AI Detection ❌ ✅ ✅ @@ -122,7 +122,7 @@ see what's new & breaking. ✅ - Enhanced detection result + Extra Detections (Apps, Libs, Emails, Media Players, Crawlers, and more) ❌ ✅ ✅ @@ -130,7 +130,7 @@ see what's new & breaking. ✅ - Client Hints support + Client Hints Support ❌ ✅ ✅ @@ -138,7 +138,7 @@ see what's new & breaking. ✅ - CommonJS support + CommonJS Support ✅ ✅ ✅ @@ -146,7 +146,7 @@ see what's new & breaking. ✅ - ES modules support + ESM Support ❌ ✅ ✅ @@ -154,15 +154,15 @@ see what's new & breaking. ✅ - TypeScript declarations - ⚠️ + TypeScript Definitions + ✅ ✅ ✅ ✅ - npm module available + npm Module Available ✅ ✅ ✅ @@ -170,7 +170,7 @@ see what's new & breaking. ✅ - Direct downloads available + Direct Downloads Available ✅ ✅ ✅ @@ -178,7 +178,7 @@ see what's new & breaking. ✅ - Allows commercial usage + Commercial Use Allowed ✅ ✅ ❌ @@ -186,7 +186,7 @@ see what's new & breaking. ✅ - Permissive (non-copyleft) license + Permissive (non-Copyleft) License ✅ ✅ @@ -194,7 +194,7 @@ see what's new & breaking. ✅ - No open-source obligations + No Open-Source Obligations ✅ ✅ @@ -202,7 +202,7 @@ see what's new & breaking. ✅ - Unlimited end-products + Unlimited End-Products ✅ ✅ ✅ @@ -210,7 +210,7 @@ see what's new & breaking. ✅ - Unlimited deployments + Unlimited Deployments ✅ ✅ ✅ @@ -218,7 +218,7 @@ see what's new & breaking. ✅ - 1-year product support + 1-year Product Support ❌ ❌ ✅ @@ -226,7 +226,7 @@ see what's new & breaking. ✅ - Lifetime updates + Lifetime Updates ✅ ✅ ✅ diff --git a/dist/ua-parser.min.js b/dist/ua-parser.min.js index 98f9ad6..ceb9508 100644 --- a/dist/ua-parser.min.js +++ b/dist/ua-parser.min.js @@ -1,4 +1,4 @@ -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License */ -(function(window,undefined){"use strict";var LIBVERSION="2.0.5",UA_MAX_LENGTH=500,USER_AGENT="user-agent",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",UA_BROWSER="browser",UA_CPU="cpu",UA_DEVICE="device",UA_ENGINE="engine",UA_OS="os",UA_RESULT="result",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",MAJOR="major",MODEL="model",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",XR="xr",EMBEDDED="embedded",INAPP="inapp",BRANDS="brands",FORMFACTORS="formFactors",FULLVERLIST="fullVersionList",PLATFORM="platform",PLATFORMVER="platformVersion",BITNESS="bitness",CH_HEADER="sec-ch-ua",CH_HEADER_FULL_VER_LIST=CH_HEADER+"-full-version-list",CH_HEADER_ARCH=CH_HEADER+"-arch",CH_HEADER_BITNESS=CH_HEADER+"-"+BITNESS,CH_HEADER_FORM_FACTORS=CH_HEADER+"-form-factors",CH_HEADER_MOBILE=CH_HEADER+"-"+MOBILE,CH_HEADER_MODEL=CH_HEADER+"-"+MODEL,CH_HEADER_PLATFORM=CH_HEADER+"-"+PLATFORM,CH_HEADER_PLATFORM_VER=CH_HEADER_PLATFORM+"-version",CH_ALL_VALUES=[BRANDS,FULLVERLIST,MOBILE,MODEL,PLATFORM,PLATFORMVER,ARCHITECTURE,FORMFACTORS,BITNESS],AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",GOOGLE="Google",HUAWEI="Huawei",LENOVO="Lenovo",HONOR="Honor",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",NVIDIA="Nvidia",ONEPLUS="OnePlus",OPPO="OPPO",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",CHROME="Chrome",CHROMIUM="Chromium",CHROMECAST="Chromecast",EDGE="Edge",FIREFOX="Firefox",OPERA="Opera",FACEBOOK="Facebook",SOGOU="Sogou",PREFIX_MOBILE="Mobile ",SUFFIX_BROWSER=" Browser",WINDOWS="Windows";var isWindow=typeof window!==UNDEF_TYPE,NAVIGATOR=isWindow&&window.navigator?window.navigator:undefined,NAVIGATOR_UADATA=NAVIGATOR&&NAVIGATOR.userAgentData?NAVIGATOR.userAgentData:undefined;var extend=function(defaultRgx,extensions){var mergedRgx={};var extraRgx=extensions;if(!isExtensions(extensions)){extraRgx={};for(var i in extensions){for(var j in extensions[i]){extraRgx[j]=extensions[i][j].concat(extraRgx[j]?extraRgx[j]:[])}}}for(var k in defaultRgx){mergedRgx[k]=extraRgx[k]&&extraRgx[k].length%2===0?extraRgx[k].concat(defaultRgx[k]):defaultRgx[k]}return mergedRgx},enumerize=function(arr){var enums={};for(var i=0;i0){for(var i in str1){if(lowerize(str2)==lowerize(str1[i]))return true}return false}return isString(str1)?lowerize(str2)==lowerize(str1):false},isExtensions=function(obj,deep){for(var prop in obj){return/^(browser|cpu|device|engine|os)$/.test(prop)||(deep?isExtensions(obj[prop]):false)}},isString=function(val){return typeof val===STR_TYPE},itemListToArray=function(header){if(!header)return undefined;var arr=[];var tokens=strip(/\\?\"/g,header).split(",");for(var i=0;i-1){var token=trim(tokens[i]).split(";v=");arr[i]={brand:token[0],version:token[1]}}else{arr[i]=trim(tokens[i])}}return arr},lowerize=function(str){return isString(str)?str.toLowerCase():str},majorize=function(version){return isString(version)?strip(/[^\d\.]/g,version).split(".")[0]:undefined},setProps=function(arr){for(var i in arr){if(!arr.hasOwnProperty(i))continue;var propName=arr[i];if(typeof propName==OBJ_TYPE&&propName.length==2){this[propName[0]]=propName[1]}else{this[propName]=undefined}}return this},strip=function(pattern,str){return isString(str)?str.replace(pattern,EMPTY):str},stripQuotes=function(str){return strip(/\\?\"/g,str)},trim=function(str,len){if(isString(str)){str=strip(/^\s\s*/,str);return typeof len===UNDEF_TYPE?str:str.substring(0,UA_MAX_LENGTH)}};var rgxMapper=function(ua,arrays){if(!ua||!arrays)return;var i=0,j,k,p,q,matches,match;while(i0){if(q.length===2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length>=3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){if(q.length>3){this[q[0]]=match?q[1].apply(this,q.slice(2)):undefined}else{this[q[0]]=match?q[1].call(this,match,q[2]):undefined}}else{if(q.length==3){this[q[0]]=match?match.replace(q[1],q[2]):undefined}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}else if(q.length>4){this[q[0]]=match?q[3].apply(this,[match.replace(q[1],q[2])].concat(q.slice(4))):undefined}}}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j2){this.set(MODEL,"iPad").set(TYPE,TABLET)}break;case UA_OS:if(!this.get(NAME)&&NAVIGATOR_UADATA&&NAVIGATOR_UADATA[PLATFORM]){this.set(NAME,NAVIGATOR_UADATA[PLATFORM])}break;case UA_RESULT:var data=this.data;var detect=function(itemType){return data[itemType].getItem().detectFeature().get()};this.set(UA_BROWSER,detect(UA_BROWSER)).set(UA_CPU,detect(UA_CPU)).set(UA_DEVICE,detect(UA_DEVICE)).set(UA_ENGINE,detect(UA_ENGINE)).set(UA_OS,detect(UA_OS))}}return this};this.parseUA=function(){if(this.itemType!=UA_RESULT){rgxMapper.call(this.data,this.ua,this.rgxMap)}if(this.itemType==UA_BROWSER){this.set(MAJOR,majorize(this.get(VERSION)))}return this};this.parseCH=function(){var uaCH=this.uaCH,rgxMap=this.rgxMap;switch(this.itemType){case UA_BROWSER:case UA_ENGINE:var brands=uaCH[FULLVERLIST]||uaCH[BRANDS],prevName;if(brands){for(var i=0;i=13?"11":"10";this.set(NAME,osName).set(VERSION,osVersion)}if(this.get(NAME)==WINDOWS&&uaCH[MODEL]=="Xbox"){this.set(NAME,"Xbox").set(VERSION,undefined)}break;case UA_RESULT:var data=this.data;var parse=function(itemType){return data[itemType].getItem().setCH(uaCH).parseCH().get()};this.set(UA_BROWSER,parse(UA_BROWSER)).set(UA_CPU,parse(UA_CPU)).set(UA_DEVICE,parse(UA_DEVICE)).set(UA_ENGINE,parse(UA_ENGINE)).set(UA_OS,parse(UA_OS))}return this};setProps.call(this,[["itemType",itemType],["ua",ua],["uaCH",uaCH],["rgxMap",rgxMap],["data",createIData(this,itemType)]]);return this}function UAParser(ua,extensions,headers){if(typeof ua===OBJ_TYPE){if(isExtensions(ua,true)){if(typeof extensions===OBJ_TYPE){headers=extensions}extensions=ua}else{headers=ua;extensions=undefined}ua=undefined}else if(typeof ua===STR_TYPE&&!isExtensions(extensions,true)){headers=extensions;extensions=undefined}if(headers){if(typeof headers.append===FUNC_TYPE){var kv={};headers.forEach(function(v,k){kv[String(k).toLowerCase()]=v});headers=kv}else{var normalized={};for(var header in headers){if(headers.hasOwnProperty(header)){normalized[String(header).toLowerCase()]=headers[header]}}headers=normalized}}if(!(this instanceof UAParser)){return new UAParser(ua,extensions,headers).getResult()}var userAgent=typeof ua===STR_TYPE?ua:headers&&headers[USER_AGENT]?headers[USER_AGENT]:NAVIGATOR&&NAVIGATOR.userAgent?NAVIGATOR.userAgent:EMPTY,httpUACH=new UACHData(headers,true),regexMap=extensions?extend(defaultRegexes,extensions):defaultRegexes,createItemFunc=function(itemType){if(itemType==UA_RESULT){return function(){return new UAItem(itemType,userAgent,regexMap,httpUACH).set("ua",userAgent).set(UA_BROWSER,this.getBrowser()).set(UA_CPU,this.getCPU()).set(UA_DEVICE,this.getDevice()).set(UA_ENGINE,this.getEngine()).set(UA_OS,this.getOS()).get()}}else{return function(){return new UAItem(itemType,userAgent,regexMap[itemType],httpUACH).parseUA().get()}}};setProps.call(this,[["getBrowser",createItemFunc(UA_BROWSER)],["getCPU",createItemFunc(UA_CPU)],["getDevice",createItemFunc(UA_DEVICE)],["getEngine",createItemFunc(UA_ENGINE)],["getOS",createItemFunc(UA_OS)],["getResult",createItemFunc(UA_RESULT)],["getUA",function(){return userAgent}],["setUA",function(ua){if(isString(ua))userAgent=ua.length>UA_MAX_LENGTH?trim(ua,UA_MAX_LENGTH):ua;return this}]]).setUA(userAgent);return this}UAParser.VERSION=LIBVERSION;UAParser.BROWSER=enumerize([NAME,VERSION,MAJOR,TYPE]);UAParser.CPU=enumerize([ARCHITECTURE]);UAParser.DEVICE=enumerize([MODEL,VENDOR,TYPE,CONSOLE,MOBILE,SMARTTV,TABLET,WEARABLE,EMBEDDED]);UAParser.ENGINE=UAParser.OS=enumerize([NAME,VERSION]);if(typeof exports!==UNDEF_TYPE){if(typeof module!==UNDEF_TYPE&&module.exports){exports=module.exports=UAParser}exports.UAParser=UAParser}else{if(typeof define===FUNC_TYPE&&define.amd){define(function(){return UAParser})}else if(isWindow){window.UAParser=UAParser}}var $=isWindow&&(window.jQuery||window.Zepto);if($&&!$.ua){var parser=new UAParser;$.ua=parser.getResult();$.ua.get=function(){return parser.getUA()};$.ua.set=function(ua){parser.setUA(ua);var result=parser.getResult();for(var prop in result){$.ua[prop]=result[prop]}}}})(typeof window==="object"?window:this); \ No newline at end of file +(function(window,undefined){"use strict";var LIBVERSION="2.0.6",UA_MAX_LENGTH=500,USER_AGENT="user-agent",EMPTY="",UNKNOWN="?",TYPEOF={FUNCTION:"function",OBJECT:"object",STRING:"string",UNDEFINED:"undefined"},BROWSER="browser",CPU="cpu",DEVICE="device",ENGINE="engine",OS="os",RESULT="result",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",MAJOR="major",MODEL="model",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",XR="xr",EMBEDDED="embedded",INAPP="inapp",BRANDS="brands",FORMFACTORS="formFactors",FULLVERLIST="fullVersionList",PLATFORM="platform",PLATFORMVER="platformVersion",BITNESS="bitness",CH="sec-ch-ua",CH_FULL_VER_LIST=CH+"-full-version-list",CH_ARCH=CH+"-arch",CH_BITNESS=CH+"-"+BITNESS,CH_FORM_FACTORS=CH+"-form-factors",CH_MOBILE=CH+"-"+MOBILE,CH_MODEL=CH+"-"+MODEL,CH_PLATFORM=CH+"-"+PLATFORM,CH_PLATFORM_VER=CH_PLATFORM+"-version",CH_ALL_VALUES=[BRANDS,FULLVERLIST,MOBILE,MODEL,PLATFORM,PLATFORMVER,ARCHITECTURE,FORMFACTORS,BITNESS],AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",GOOGLE="Google",HUAWEI="Huawei",LENOVO="Lenovo",HONOR="Honor",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",NVIDIA="Nvidia",ONEPLUS="OnePlus",OPPO="OPPO",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",CHROME="Chrome",CHROMIUM="Chromium",CHROMECAST="Chromecast",EDGE="Edge",FIREFOX="Firefox",OPERA="Opera",FACEBOOK="Facebook",SOGOU="Sogou",PREFIX_MOBILE="Mobile ",SUFFIX_BROWSER=" Browser",WINDOWS="Windows";var isWindow=typeof window!==TYPEOF.UNDEFINED,NAVIGATOR=isWindow&&window.navigator?window.navigator:undefined,NAVIGATOR_UADATA=NAVIGATOR&&NAVIGATOR.userAgentData?NAVIGATOR.userAgentData:undefined;var extend=function(defaultRgx,extensions){var mergedRgx={};var extraRgx=extensions;if(!isExtensions(extensions)){extraRgx={};for(var i in extensions){for(var j in extensions[i]){extraRgx[j]=extensions[i][j].concat(extraRgx[j]?extraRgx[j]:[])}}}for(var k in defaultRgx){mergedRgx[k]=extraRgx[k]&&extraRgx[k].length%2===0?extraRgx[k].concat(defaultRgx[k]):defaultRgx[k]}return mergedRgx},enumerize=function(arr){var enums={};for(var i=0;i0){for(var i in str1){if(lowerize(str2)==lowerize(str1[i]))return true}return false}return isString(str1)?lowerize(str2)==lowerize(str1):false},isExtensions=function(obj,deep){for(var prop in obj){return/^(browser|cpu|device|engine|os)$/.test(prop)||(deep?isExtensions(obj[prop]):false)}},isString=function(val){return typeof val===TYPEOF.STRING},itemListToArray=function(header){if(!header)return undefined;var arr=[];var tokens=strip(/\\?\"/g,header).split(",");for(var i=0;i-1){var token=trim(tokens[i]).split(";v=");arr[i]={brand:token[0],version:token[1]}}else{arr[i]=trim(tokens[i])}}return arr},lowerize=function(str){return isString(str)?str.toLowerCase():str},majorize=function(version){return isString(version)?strip(/[^\d\.]/g,version).split(".")[0]:undefined},setProps=function(arr){for(var i in arr){if(!arr.hasOwnProperty(i))continue;var propName=arr[i];if(typeof propName==TYPEOF.OBJECT&&propName.length==2){this[propName[0]]=propName[1]}else{this[propName]=undefined}}return this},strip=function(pattern,str){return isString(str)?str.replace(pattern,EMPTY):str},stripQuotes=function(str){return strip(/\\?\"/g,str)},trim=function(str,len){str=strip(/^\s\s*/,String(str));return typeof len===TYPEOF.UNDEFINED?str:str.substring(0,len)};var rgxMapper=function(ua,arrays){if(!ua||!arrays)return;var i=0,j,k,p,q,matches,match;while(i0){if(q.length===2){if(typeof q[1]==TYPEOF.FUNCTION){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length>=3){if(typeof q[1]===TYPEOF.FUNCTION&&!(q[1].exec&&q[1].test)){if(q.length>3){this[q[0]]=match?q[1].apply(this,q.slice(2)):undefined}else{this[q[0]]=match?q[1].call(this,match,q[2]):undefined}}else{if(q.length==3){this[q[0]]=match?match.replace(q[1],q[2]):undefined}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}else if(q.length>4){this[q[0]]=match?q[3].apply(this,[match.replace(q[1],q[2])].concat(q.slice(4))):undefined}}}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===TYPEOF.OBJECT&&map[i].length>0){for(var j=0;j2){this.set(MODEL,"iPad").set(TYPE,TABLET)}break;case OS:if(!this.get(NAME)&&NAVIGATOR_UADATA&&NAVIGATOR_UADATA[PLATFORM]){this.set(NAME,NAVIGATOR_UADATA[PLATFORM])}break;case RESULT:var data=this.data;var detect=function(itemType){return data[itemType].getItem().detectFeature().get()};this.set(BROWSER,detect(BROWSER)).set(CPU,detect(CPU)).set(DEVICE,detect(DEVICE)).set(ENGINE,detect(ENGINE)).set(OS,detect(OS))}}return this};this.parseUA=function(){if(this.itemType!=RESULT){rgxMapper.call(this.data,this.ua,this.rgxMap)}switch(this.itemType){case BROWSER:this.set(MAJOR,majorize(this.get(VERSION)));break;case OS:if(this.get(NAME)=="iOS"&&this.get(VERSION)=="18.6"){var realVersion=/\) Version\/([\d\.]+)/.exec(this.ua);if(realVersion&&parseInt(realVersion[1].substring(0,2),10)>=26){this.set(VERSION,realVersion[1])}}break}return this};this.parseCH=function(){var uaCH=this.uaCH,rgxMap=this.rgxMap;switch(this.itemType){case BROWSER:case ENGINE:var brands=uaCH[FULLVERLIST]||uaCH[BRANDS],prevName;if(brands){for(var i=0;i=13?"11":"10";this.set(NAME,osName).set(VERSION,osVersion)}if(this.get(NAME)==WINDOWS&&uaCH[MODEL]=="Xbox"){this.set(NAME,"Xbox").set(VERSION,undefined)}break;case RESULT:var data=this.data;var parse=function(itemType){return data[itemType].getItem().setCH(uaCH).parseCH().get()};this.set(BROWSER,parse(BROWSER)).set(CPU,parse(CPU)).set(DEVICE,parse(DEVICE)).set(ENGINE,parse(ENGINE)).set(OS,parse(OS))}return this};setProps.call(this,[["itemType",itemType],["ua",ua],["uaCH",uaCH],["rgxMap",rgxMap],["data",createIData(this,itemType)]]);return this}function UAParser(ua,extensions,headers){if(typeof ua===TYPEOF.OBJECT){if(isExtensions(ua,true)){if(typeof extensions===TYPEOF.OBJECT){headers=extensions}extensions=ua}else{headers=ua;extensions=undefined}ua=undefined}else if(typeof ua===TYPEOF.STRING&&!isExtensions(extensions,true)){headers=extensions;extensions=undefined}if(headers){if(typeof headers.append===TYPEOF.FUNCTION){var kv={};headers.forEach(function(v,k){kv[String(k).toLowerCase()]=v});headers=kv}else{var normalized={};for(var header in headers){if(headers.hasOwnProperty(header)){normalized[String(header).toLowerCase()]=headers[header]}}headers=normalized}}if(!(this instanceof UAParser)){return new UAParser(ua,extensions,headers).getResult()}var userAgent=typeof ua===TYPEOF.STRING?ua:headers&&headers[USER_AGENT]?headers[USER_AGENT]:NAVIGATOR&&NAVIGATOR.userAgent?NAVIGATOR.userAgent:EMPTY,httpUACH=new UACHData(headers,true),regexMap=extensions?extend(defaultRegexes,extensions):defaultRegexes,createItemFunc=function(itemType){if(itemType==RESULT){return function(){return new UAItem(itemType,userAgent,regexMap,httpUACH).set("ua",userAgent).set(BROWSER,this.getBrowser()).set(CPU,this.getCPU()).set(DEVICE,this.getDevice()).set(ENGINE,this.getEngine()).set(OS,this.getOS()).get()}}else{return function(){return new UAItem(itemType,userAgent,regexMap[itemType],httpUACH).parseUA().get()}}};setProps.call(this,[["getBrowser",createItemFunc(BROWSER)],["getCPU",createItemFunc(CPU)],["getDevice",createItemFunc(DEVICE)],["getEngine",createItemFunc(ENGINE)],["getOS",createItemFunc(OS)],["getResult",createItemFunc(RESULT)],["getUA",function(){return userAgent}],["setUA",function(ua){if(isString(ua))userAgent=trim(ua,UA_MAX_LENGTH);return this}]]).setUA(userAgent);return this}UAParser.VERSION=LIBVERSION;UAParser.BROWSER=enumerize([NAME,VERSION,MAJOR,TYPE]);UAParser.CPU=enumerize([ARCHITECTURE]);UAParser.DEVICE=enumerize([MODEL,VENDOR,TYPE,CONSOLE,MOBILE,SMARTTV,TABLET,WEARABLE,EMBEDDED]);UAParser.ENGINE=UAParser.OS=enumerize([NAME,VERSION]);if(typeof exports!==TYPEOF.UNDEFINED){if(typeof module!==TYPEOF.UNDEFINED&&module.exports){exports=module.exports=UAParser}exports.UAParser=UAParser}else{if(typeof define===TYPEOF.FUNCTION&&define.amd){define(function(){return UAParser})}else if(isWindow){window.UAParser=UAParser}}var $=isWindow&&(window.jQuery||window.Zepto);if($&&!$.ua){var parser=new UAParser;$.ua=parser.getResult();$.ua.get=function(){return parser.getUA()};$.ua.set=function(ua){parser.setUA(ua);var result=parser.getResult();for(var prop in result){$.ua[prop]=result[prop]}}}})(typeof window==="object"?window:this); \ No newline at end of file diff --git a/dist/ua-parser.min.mjs b/dist/ua-parser.min.mjs index ce5f6d4..c1a9f92 100644 --- a/dist/ua-parser.min.mjs +++ b/dist/ua-parser.min.mjs @@ -1,4 +1,4 @@ -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License */ -var LIBVERSION="2.0.5",UA_MAX_LENGTH=500,USER_AGENT="user-agent",EMPTY="",UNKNOWN="?",FUNC_TYPE="function",UNDEF_TYPE="undefined",OBJ_TYPE="object",STR_TYPE="string",UA_BROWSER="browser",UA_CPU="cpu",UA_DEVICE="device",UA_ENGINE="engine",UA_OS="os",UA_RESULT="result",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",MAJOR="major",MODEL="model",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",XR="xr",EMBEDDED="embedded",INAPP="inapp",BRANDS="brands",FORMFACTORS="formFactors",FULLVERLIST="fullVersionList",PLATFORM="platform",PLATFORMVER="platformVersion",BITNESS="bitness",CH_HEADER="sec-ch-ua",CH_HEADER_FULL_VER_LIST=CH_HEADER+"-full-version-list",CH_HEADER_ARCH=CH_HEADER+"-arch",CH_HEADER_BITNESS=CH_HEADER+"-"+BITNESS,CH_HEADER_FORM_FACTORS=CH_HEADER+"-form-factors",CH_HEADER_MOBILE=CH_HEADER+"-"+MOBILE,CH_HEADER_MODEL=CH_HEADER+"-"+MODEL,CH_HEADER_PLATFORM=CH_HEADER+"-"+PLATFORM,CH_HEADER_PLATFORM_VER=CH_HEADER_PLATFORM+"-version",CH_ALL_VALUES=[BRANDS,FULLVERLIST,MOBILE,MODEL,PLATFORM,PLATFORMVER,ARCHITECTURE,FORMFACTORS,BITNESS],AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",GOOGLE="Google",HUAWEI="Huawei",LENOVO="Lenovo",HONOR="Honor",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",NVIDIA="Nvidia",ONEPLUS="OnePlus",OPPO="OPPO",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",CHROME="Chrome",CHROMIUM="Chromium",CHROMECAST="Chromecast",EDGE="Edge",FIREFOX="Firefox",OPERA="Opera",FACEBOOK="Facebook",SOGOU="Sogou",PREFIX_MOBILE="Mobile ",SUFFIX_BROWSER=" Browser",WINDOWS="Windows";var isWindow=typeof window!==UNDEF_TYPE,NAVIGATOR=isWindow&&window.navigator?window.navigator:undefined,NAVIGATOR_UADATA=NAVIGATOR&&NAVIGATOR.userAgentData?NAVIGATOR.userAgentData:undefined;var extend=function(defaultRgx,extensions){var mergedRgx={};var extraRgx=extensions;if(!isExtensions(extensions)){extraRgx={};for(var i in extensions){for(var j in extensions[i]){extraRgx[j]=extensions[i][j].concat(extraRgx[j]?extraRgx[j]:[])}}}for(var k in defaultRgx){mergedRgx[k]=extraRgx[k]&&extraRgx[k].length%2===0?extraRgx[k].concat(defaultRgx[k]):defaultRgx[k]}return mergedRgx},enumerize=function(arr){var enums={};for(var i=0;i0){for(var i in str1){if(lowerize(str2)==lowerize(str1[i]))return true}return false}return isString(str1)?lowerize(str2)==lowerize(str1):false},isExtensions=function(obj,deep){for(var prop in obj){return/^(browser|cpu|device|engine|os)$/.test(prop)||(deep?isExtensions(obj[prop]):false)}},isString=function(val){return typeof val===STR_TYPE},itemListToArray=function(header){if(!header)return undefined;var arr=[];var tokens=strip(/\\?\"/g,header).split(",");for(var i=0;i-1){var token=trim(tokens[i]).split(";v=");arr[i]={brand:token[0],version:token[1]}}else{arr[i]=trim(tokens[i])}}return arr},lowerize=function(str){return isString(str)?str.toLowerCase():str},majorize=function(version){return isString(version)?strip(/[^\d\.]/g,version).split(".")[0]:undefined},setProps=function(arr){for(var i in arr){if(!arr.hasOwnProperty(i))continue;var propName=arr[i];if(typeof propName==OBJ_TYPE&&propName.length==2){this[propName[0]]=propName[1]}else{this[propName]=undefined}}return this},strip=function(pattern,str){return isString(str)?str.replace(pattern,EMPTY):str},stripQuotes=function(str){return strip(/\\?\"/g,str)},trim=function(str,len){if(isString(str)){str=strip(/^\s\s*/,str);return typeof len===UNDEF_TYPE?str:str.substring(0,UA_MAX_LENGTH)}};var rgxMapper=function(ua,arrays){if(!ua||!arrays)return;var i=0,j,k,p,q,matches,match;while(i0){if(q.length===2){if(typeof q[1]==FUNC_TYPE){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length>=3){if(typeof q[1]===FUNC_TYPE&&!(q[1].exec&&q[1].test)){if(q.length>3){this[q[0]]=match?q[1].apply(this,q.slice(2)):undefined}else{this[q[0]]=match?q[1].call(this,match,q[2]):undefined}}else{if(q.length==3){this[q[0]]=match?match.replace(q[1],q[2]):undefined}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}else if(q.length>4){this[q[0]]=match?q[3].apply(this,[match.replace(q[1],q[2])].concat(q.slice(4))):undefined}}}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===OBJ_TYPE&&map[i].length>0){for(var j=0;j2){this.set(MODEL,"iPad").set(TYPE,TABLET)}break;case UA_OS:if(!this.get(NAME)&&NAVIGATOR_UADATA&&NAVIGATOR_UADATA[PLATFORM]){this.set(NAME,NAVIGATOR_UADATA[PLATFORM])}break;case UA_RESULT:var data=this.data;var detect=function(itemType){return data[itemType].getItem().detectFeature().get()};this.set(UA_BROWSER,detect(UA_BROWSER)).set(UA_CPU,detect(UA_CPU)).set(UA_DEVICE,detect(UA_DEVICE)).set(UA_ENGINE,detect(UA_ENGINE)).set(UA_OS,detect(UA_OS))}}return this};this.parseUA=function(){if(this.itemType!=UA_RESULT){rgxMapper.call(this.data,this.ua,this.rgxMap)}if(this.itemType==UA_BROWSER){this.set(MAJOR,majorize(this.get(VERSION)))}return this};this.parseCH=function(){var uaCH=this.uaCH,rgxMap=this.rgxMap;switch(this.itemType){case UA_BROWSER:case UA_ENGINE:var brands=uaCH[FULLVERLIST]||uaCH[BRANDS],prevName;if(brands){for(var i=0;i=13?"11":"10";this.set(NAME,osName).set(VERSION,osVersion)}if(this.get(NAME)==WINDOWS&&uaCH[MODEL]=="Xbox"){this.set(NAME,"Xbox").set(VERSION,undefined)}break;case UA_RESULT:var data=this.data;var parse=function(itemType){return data[itemType].getItem().setCH(uaCH).parseCH().get()};this.set(UA_BROWSER,parse(UA_BROWSER)).set(UA_CPU,parse(UA_CPU)).set(UA_DEVICE,parse(UA_DEVICE)).set(UA_ENGINE,parse(UA_ENGINE)).set(UA_OS,parse(UA_OS))}return this};setProps.call(this,[["itemType",itemType],["ua",ua],["uaCH",uaCH],["rgxMap",rgxMap],["data",createIData(this,itemType)]]);return this}function UAParser(ua,extensions,headers){if(typeof ua===OBJ_TYPE){if(isExtensions(ua,true)){if(typeof extensions===OBJ_TYPE){headers=extensions}extensions=ua}else{headers=ua;extensions=undefined}ua=undefined}else if(typeof ua===STR_TYPE&&!isExtensions(extensions,true)){headers=extensions;extensions=undefined}if(headers){if(typeof headers.append===FUNC_TYPE){var kv={};headers.forEach(function(v,k){kv[String(k).toLowerCase()]=v});headers=kv}else{var normalized={};for(var header in headers){if(headers.hasOwnProperty(header)){normalized[String(header).toLowerCase()]=headers[header]}}headers=normalized}}if(!(this instanceof UAParser)){return new UAParser(ua,extensions,headers).getResult()}var userAgent=typeof ua===STR_TYPE?ua:headers&&headers[USER_AGENT]?headers[USER_AGENT]:NAVIGATOR&&NAVIGATOR.userAgent?NAVIGATOR.userAgent:EMPTY,httpUACH=new UACHData(headers,true),regexMap=extensions?extend(defaultRegexes,extensions):defaultRegexes,createItemFunc=function(itemType){if(itemType==UA_RESULT){return function(){return new UAItem(itemType,userAgent,regexMap,httpUACH).set("ua",userAgent).set(UA_BROWSER,this.getBrowser()).set(UA_CPU,this.getCPU()).set(UA_DEVICE,this.getDevice()).set(UA_ENGINE,this.getEngine()).set(UA_OS,this.getOS()).get()}}else{return function(){return new UAItem(itemType,userAgent,regexMap[itemType],httpUACH).parseUA().get()}}};setProps.call(this,[["getBrowser",createItemFunc(UA_BROWSER)],["getCPU",createItemFunc(UA_CPU)],["getDevice",createItemFunc(UA_DEVICE)],["getEngine",createItemFunc(UA_ENGINE)],["getOS",createItemFunc(UA_OS)],["getResult",createItemFunc(UA_RESULT)],["getUA",function(){return userAgent}],["setUA",function(ua){if(isString(ua))userAgent=ua.length>UA_MAX_LENGTH?trim(ua,UA_MAX_LENGTH):ua;return this}]]).setUA(userAgent);return this}UAParser.VERSION=LIBVERSION;UAParser.BROWSER=enumerize([NAME,VERSION,MAJOR,TYPE]);UAParser.CPU=enumerize([ARCHITECTURE]);UAParser.DEVICE=enumerize([MODEL,VENDOR,TYPE,CONSOLE,MOBILE,SMARTTV,TABLET,WEARABLE,EMBEDDED]);UAParser.ENGINE=UAParser.OS=enumerize([NAME,VERSION]);export{UAParser}; \ No newline at end of file +var LIBVERSION="2.0.6",UA_MAX_LENGTH=500,USER_AGENT="user-agent",EMPTY="",UNKNOWN="?",TYPEOF={FUNCTION:"function",OBJECT:"object",STRING:"string",UNDEFINED:"undefined"},BROWSER="browser",CPU="cpu",DEVICE="device",ENGINE="engine",OS="os",RESULT="result",NAME="name",TYPE="type",VENDOR="vendor",VERSION="version",ARCHITECTURE="architecture",MAJOR="major",MODEL="model",CONSOLE="console",MOBILE="mobile",TABLET="tablet",SMARTTV="smarttv",WEARABLE="wearable",XR="xr",EMBEDDED="embedded",INAPP="inapp",BRANDS="brands",FORMFACTORS="formFactors",FULLVERLIST="fullVersionList",PLATFORM="platform",PLATFORMVER="platformVersion",BITNESS="bitness",CH="sec-ch-ua",CH_FULL_VER_LIST=CH+"-full-version-list",CH_ARCH=CH+"-arch",CH_BITNESS=CH+"-"+BITNESS,CH_FORM_FACTORS=CH+"-form-factors",CH_MOBILE=CH+"-"+MOBILE,CH_MODEL=CH+"-"+MODEL,CH_PLATFORM=CH+"-"+PLATFORM,CH_PLATFORM_VER=CH_PLATFORM+"-version",CH_ALL_VALUES=[BRANDS,FULLVERLIST,MOBILE,MODEL,PLATFORM,PLATFORMVER,ARCHITECTURE,FORMFACTORS,BITNESS],AMAZON="Amazon",APPLE="Apple",ASUS="ASUS",BLACKBERRY="BlackBerry",GOOGLE="Google",HUAWEI="Huawei",LENOVO="Lenovo",HONOR="Honor",LG="LG",MICROSOFT="Microsoft",MOTOROLA="Motorola",NVIDIA="Nvidia",ONEPLUS="OnePlus",OPPO="OPPO",SAMSUNG="Samsung",SHARP="Sharp",SONY="Sony",XIAOMI="Xiaomi",ZEBRA="Zebra",CHROME="Chrome",CHROMIUM="Chromium",CHROMECAST="Chromecast",EDGE="Edge",FIREFOX="Firefox",OPERA="Opera",FACEBOOK="Facebook",SOGOU="Sogou",PREFIX_MOBILE="Mobile ",SUFFIX_BROWSER=" Browser",WINDOWS="Windows";var isWindow=typeof window!==TYPEOF.UNDEFINED,NAVIGATOR=isWindow&&window.navigator?window.navigator:undefined,NAVIGATOR_UADATA=NAVIGATOR&&NAVIGATOR.userAgentData?NAVIGATOR.userAgentData:undefined;var extend=function(defaultRgx,extensions){var mergedRgx={};var extraRgx=extensions;if(!isExtensions(extensions)){extraRgx={};for(var i in extensions){for(var j in extensions[i]){extraRgx[j]=extensions[i][j].concat(extraRgx[j]?extraRgx[j]:[])}}}for(var k in defaultRgx){mergedRgx[k]=extraRgx[k]&&extraRgx[k].length%2===0?extraRgx[k].concat(defaultRgx[k]):defaultRgx[k]}return mergedRgx},enumerize=function(arr){var enums={};for(var i=0;i0){for(var i in str1){if(lowerize(str2)==lowerize(str1[i]))return true}return false}return isString(str1)?lowerize(str2)==lowerize(str1):false},isExtensions=function(obj,deep){for(var prop in obj){return/^(browser|cpu|device|engine|os)$/.test(prop)||(deep?isExtensions(obj[prop]):false)}},isString=function(val){return typeof val===TYPEOF.STRING},itemListToArray=function(header){if(!header)return undefined;var arr=[];var tokens=strip(/\\?\"/g,header).split(",");for(var i=0;i-1){var token=trim(tokens[i]).split(";v=");arr[i]={brand:token[0],version:token[1]}}else{arr[i]=trim(tokens[i])}}return arr},lowerize=function(str){return isString(str)?str.toLowerCase():str},majorize=function(version){return isString(version)?strip(/[^\d\.]/g,version).split(".")[0]:undefined},setProps=function(arr){for(var i in arr){if(!arr.hasOwnProperty(i))continue;var propName=arr[i];if(typeof propName==TYPEOF.OBJECT&&propName.length==2){this[propName[0]]=propName[1]}else{this[propName]=undefined}}return this},strip=function(pattern,str){return isString(str)?str.replace(pattern,EMPTY):str},stripQuotes=function(str){return strip(/\\?\"/g,str)},trim=function(str,len){str=strip(/^\s\s*/,String(str));return typeof len===TYPEOF.UNDEFINED?str:str.substring(0,len)};var rgxMapper=function(ua,arrays){if(!ua||!arrays)return;var i=0,j,k,p,q,matches,match;while(i0){if(q.length===2){if(typeof q[1]==TYPEOF.FUNCTION){this[q[0]]=q[1].call(this,match)}else{this[q[0]]=q[1]}}else if(q.length>=3){if(typeof q[1]===TYPEOF.FUNCTION&&!(q[1].exec&&q[1].test)){if(q.length>3){this[q[0]]=match?q[1].apply(this,q.slice(2)):undefined}else{this[q[0]]=match?q[1].call(this,match,q[2]):undefined}}else{if(q.length==3){this[q[0]]=match?match.replace(q[1],q[2]):undefined}else if(q.length==4){this[q[0]]=match?q[3].call(this,match.replace(q[1],q[2])):undefined}else if(q.length>4){this[q[0]]=match?q[3].apply(this,[match.replace(q[1],q[2])].concat(q.slice(4))):undefined}}}}else{this[q]=match?match:undefined}}}}i+=2}},strMapper=function(str,map){for(var i in map){if(typeof map[i]===TYPEOF.OBJECT&&map[i].length>0){for(var j=0;j2){this.set(MODEL,"iPad").set(TYPE,TABLET)}break;case OS:if(!this.get(NAME)&&NAVIGATOR_UADATA&&NAVIGATOR_UADATA[PLATFORM]){this.set(NAME,NAVIGATOR_UADATA[PLATFORM])}break;case RESULT:var data=this.data;var detect=function(itemType){return data[itemType].getItem().detectFeature().get()};this.set(BROWSER,detect(BROWSER)).set(CPU,detect(CPU)).set(DEVICE,detect(DEVICE)).set(ENGINE,detect(ENGINE)).set(OS,detect(OS))}}return this};this.parseUA=function(){if(this.itemType!=RESULT){rgxMapper.call(this.data,this.ua,this.rgxMap)}switch(this.itemType){case BROWSER:this.set(MAJOR,majorize(this.get(VERSION)));break;case OS:if(this.get(NAME)=="iOS"&&this.get(VERSION)=="18.6"){var realVersion=/\) Version\/([\d\.]+)/.exec(this.ua);if(realVersion&&parseInt(realVersion[1].substring(0,2),10)>=26){this.set(VERSION,realVersion[1])}}break}return this};this.parseCH=function(){var uaCH=this.uaCH,rgxMap=this.rgxMap;switch(this.itemType){case BROWSER:case ENGINE:var brands=uaCH[FULLVERLIST]||uaCH[BRANDS],prevName;if(brands){for(var i=0;i=13?"11":"10";this.set(NAME,osName).set(VERSION,osVersion)}if(this.get(NAME)==WINDOWS&&uaCH[MODEL]=="Xbox"){this.set(NAME,"Xbox").set(VERSION,undefined)}break;case RESULT:var data=this.data;var parse=function(itemType){return data[itemType].getItem().setCH(uaCH).parseCH().get()};this.set(BROWSER,parse(BROWSER)).set(CPU,parse(CPU)).set(DEVICE,parse(DEVICE)).set(ENGINE,parse(ENGINE)).set(OS,parse(OS))}return this};setProps.call(this,[["itemType",itemType],["ua",ua],["uaCH",uaCH],["rgxMap",rgxMap],["data",createIData(this,itemType)]]);return this}function UAParser(ua,extensions,headers){if(typeof ua===TYPEOF.OBJECT){if(isExtensions(ua,true)){if(typeof extensions===TYPEOF.OBJECT){headers=extensions}extensions=ua}else{headers=ua;extensions=undefined}ua=undefined}else if(typeof ua===TYPEOF.STRING&&!isExtensions(extensions,true)){headers=extensions;extensions=undefined}if(headers){if(typeof headers.append===TYPEOF.FUNCTION){var kv={};headers.forEach(function(v,k){kv[String(k).toLowerCase()]=v});headers=kv}else{var normalized={};for(var header in headers){if(headers.hasOwnProperty(header)){normalized[String(header).toLowerCase()]=headers[header]}}headers=normalized}}if(!(this instanceof UAParser)){return new UAParser(ua,extensions,headers).getResult()}var userAgent=typeof ua===TYPEOF.STRING?ua:headers&&headers[USER_AGENT]?headers[USER_AGENT]:NAVIGATOR&&NAVIGATOR.userAgent?NAVIGATOR.userAgent:EMPTY,httpUACH=new UACHData(headers,true),regexMap=extensions?extend(defaultRegexes,extensions):defaultRegexes,createItemFunc=function(itemType){if(itemType==RESULT){return function(){return new UAItem(itemType,userAgent,regexMap,httpUACH).set("ua",userAgent).set(BROWSER,this.getBrowser()).set(CPU,this.getCPU()).set(DEVICE,this.getDevice()).set(ENGINE,this.getEngine()).set(OS,this.getOS()).get()}}else{return function(){return new UAItem(itemType,userAgent,regexMap[itemType],httpUACH).parseUA().get()}}};setProps.call(this,[["getBrowser",createItemFunc(BROWSER)],["getCPU",createItemFunc(CPU)],["getDevice",createItemFunc(DEVICE)],["getEngine",createItemFunc(ENGINE)],["getOS",createItemFunc(OS)],["getResult",createItemFunc(RESULT)],["getUA",function(){return userAgent}],["setUA",function(ua){if(isString(ua))userAgent=trim(ua,UA_MAX_LENGTH);return this}]]).setUA(userAgent);return this}UAParser.VERSION=LIBVERSION;UAParser.BROWSER=enumerize([NAME,VERSION,MAJOR,TYPE]);UAParser.CPU=enumerize([ARCHITECTURE]);UAParser.DEVICE=enumerize([MODEL,VENDOR,TYPE,CONSOLE,MOBILE,SMARTTV,TABLET,WEARABLE,EMBEDDED]);UAParser.ENGINE=UAParser.OS=enumerize([NAME,VERSION]);export{UAParser}; \ No newline at end of file diff --git a/dist/ua-parser.pack.js b/dist/ua-parser.pack.js index cd05ecb..c267160 100644 --- a/dist/ua-parser.pack.js +++ b/dist/ua-parser.pack.js @@ -1,4 +1,4 @@ -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License */ -((i,c)=>{function V(i){for(var e={},t=0;t{var t,o={},r=e;if(!zi(e))for(var a in r={},e)for(var s in e[a])r[s]=e[a][s].concat(r[s]||[]);for(t in i)o[t]=r[t]&&r[t].length%2==0?r[t].concat(i[t]):i[t];return o})(Ui,e):Ui,M.call(this,[["getBrowser",(b=function(i){return i==g?function(){return new Gi(i,s,w,n).set("ua",s).set(u,this.getBrowser()).set(h,this.getCPU()).set(p,this.getDevice()).set(m,this.getEngine()).set(f,this.getOS()).get()}:function(){return new Gi(i,s,w[i],n).parseUA().get()}})(u)],["getCPU",b(h)],["getDevice",b(p)],["getEngine",b(m)],["getOS",b(f)],["getResult",b(g)],["getUA",function(){return s}],["setUA",function(i){return Oi(i)&&(s=i.length>I?Mi(i,I):i),this}]]).setUA(s),this):new P(i,e,t).getResult()}P.VERSION="2.0.5",P.BROWSER=V([v,y,G,k]),P.CPU=V([C]),P.DEVICE=V([S,x,k,W,_,e,r,t,F]),P.ENGINE=P.OS=V([v,y]),typeof exports!==n?(exports=typeof module!==n&&module.exports?module.exports=P:exports).UAParser=P:typeof define===L&&define.amd?define(function(){return P}):_i&&(i.UAParser=P);var Wi,Ni=_i&&(i.jQuery||i.Zepto);Ni&&!Ni.ua&&(Wi=new P,Ni.ua=Wi.getResult(),Ni.ua.get=function(){return Wi.getUA()},Ni.ua.set=function(i){Wi.setUA(i);var e,t=Wi.getResult();for(e in t)Ni.ua[e]=t[e]})})("object"==typeof window?window:this); \ No newline at end of file +((i,c)=>{function A(i){for(var e={},t=0;t{var t,o={},r=e;if(!Si(e))for(var a in r={},e)for(var s in e[a])r[s]=e[a][s].concat(r[s]||[]);for(t in i)o[t]=r[t]&&r[t].length%2==0?r[t].concat(i[t]):i[t];return o})(Ai,e):Ai,q.call(this,[["getBrowser",(b=function(i){return i==g?function(){return new Mi(i,s,w,n).set("ua",s).set(u,this.getBrowser()).set(h,this.getCPU()).set(p,this.getDevice()).set(m,this.getEngine()).set(f,this.getOS()).get()}:function(){return new Mi(i,s,w[i],n).parseUA().get()}})(u)],["getCPU",b(h)],["getDevice",b(p)],["getEngine",b(m)],["getOS",b(f)],["getResult",b(g)],["getUA",function(){return s}],["setUA",function(i){return Ti(i)&&(s=_i(i,500)),this}]]).setUA(s),this):new F(i,e,t).getResult()}F.VERSION="2.0.6",F.BROWSER=A([v,y,j,k]),F.CPU=A([C]),F.DEVICE=A([E,x,k,M,N,e,r,t,P]),F.ENGINE=F.OS=A([v,y]),typeof exports!==l.UNDEFINED?(exports=typeof module!==l.UNDEFINED&&module.exports?module.exports=F:exports).UAParser=F:typeof define===l.FUNCTION&&define.amd?define(function(){return F}):Ei&&(i.UAParser=F);var Vi,Pi=Ei&&(i.jQuery||i.Zepto);Pi&&!Pi.ua&&(Vi=new F,Pi.ua=Vi.getResult(),Pi.ua.get=function(){return Vi.getUA()},Pi.ua.set=function(i){Vi.setUA(i);var e,t=Vi.getResult();for(e in t)Pi.ua[e]=t[e]})})("object"==typeof window?window:this); \ No newline at end of file diff --git a/dist/ua-parser.pack.mjs b/dist/ua-parser.pack.mjs index d8b552f..c3a3076 100644 --- a/dist/ua-parser.pack.mjs +++ b/dist/ua-parser.pack.mjs @@ -1,4 +1,4 @@ -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License */ -function I(i){for(var e={},o=0;o{var o,t={},r=e;if(!zi(e))for(var a in r={},e)for(var s in e[a])r[s]=e[a][s].concat(r[s]||[]);for(o in i)t[o]=r[o]&&r[o].length%2==0?r[o].concat(i[o]):i[o];return t})(Ii,e):Ii,A.call(this,[["getBrowser",(d=function(i){return i==f?function(){return new Li(i,s,w,n).set("ua",s).set(c,this.getBrowser()).set(h,this.getCPU()).set(u,this.getDevice()).set(p,this.getEngine()).set(m,this.getOS()).get()}:function(){return new Li(i,s,w[i],n).parseUA().get()}})(c)],["getCPU",d(h)],["getDevice",d(u)],["getEngine",d(p)],["getOS",d(m)],["getResult",d(f)],["getUA",function(){return s}],["setUA",function(i){return H(i)&&(s=i.length>P?Mi(i,P):i),this}]]).setUA(s),this):new V(i,e,o).getResult()}V.VERSION="2.0.5",V.BROWSER=I([g,x,G,v]),V.CPU=I([y]),V.DEVICE=I([C,k,v,W,S,i,r,e,F]),V.ENGINE=V.OS=I([g,x]);export{V as UAParser}; \ No newline at end of file +function F(i){for(var e={},o=0;o{var o,t={},r=e;if(!Ci(e))for(var a in r={},e)for(var s in e[a])r[s]=e[a][s].concat(r[s]||[]);for(o in i)t[o]=r[o]&&r[o].length%2==0?r[o].concat(i[o]):i[o];return t})(zi,e):zi,U.call(this,[["getBrowser",(b=function(i){return i==g?function(){return new Hi(i,s,w,n).set("ua",s).set(c,this.getBrowser()).set(h,this.getCPU()).set(u,this.getDevice()).set(p,this.getEngine()).set(m,this.getOS()).get()}:function(){return new Hi(i,s,w[i],n).parseUA().get()}})(c)],["getCPU",b(h)],["getDevice",b(u)],["getEngine",b(p)],["getOS",b(m)],["getResult",b(g)],["getUA",function(){return s}],["setUA",function(i){return Ei(i)&&(s=Oi(i,500)),this}]]).setUA(s),this):new B(i,e,o).getResult()}B.VERSION="2.0.6",B.BROWSER=F([f,x,C,v]),B.CPU=F([y]),B.DEVICE=F([E,k,v,i,S,e,r,o,V]),B.ENGINE=B.OS=F([f,x]);export{B as UAParser}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 17355c1..87de782 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ua-parser-js", - "version": "2.0.5", + "version": "2.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ua-parser-js", - "version": "2.0.5", + "version": "2.0.6", "funding": [ { "type": "opencollective", diff --git a/package.json b/package.json index 1c3fd50..b39ab3c 100755 --- a/package.json +++ b/package.json @@ -1,25 +1,25 @@ { "title": "UAParser.js", "name": "ua-parser-js", - "version": "2.0.5", + "version": "2.0.6", "author": "Faisal Salman (http://faisalman.com)", "description": "Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent & Client Hints data. Supports browser & node.js environment", "keywords": [ "user-agent", "client-hints", - "parser", "browser", "engine", "os", "device", "cpu", - "jquery-plugin", - "ecosystem:jquery", "ua-parser-js", "browser-detection", "device-detection", "os-detection", - "bot-detection" + "bot-detection", + "ai-detection", + "app-detection", + "crawler-detection" ], "homepage": "https://uaparser.dev", "contributors": [ diff --git a/src/enums/ua-parser-enums.d.ts b/src/enums/ua-parser-enums.d.ts index c19cd80..ec2bade 100644 --- a/src/enums/ua-parser-enums.d.ts +++ b/src/enums/ua-parser-enums.d.ts @@ -3,7 +3,7 @@ // Source: /src/enums/ua-parser-enums.js /////////////////////////////////////////////// -/* Enums for UAParser.js v2.0.5 +/* Enums for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ @@ -24,6 +24,7 @@ export const BrowserName: Readonly<{ AVG: 'AVG Secure Browser', BAIDU: 'Baidu Browser', BASILISK: 'Basilisk', + BING: 'Bing', BLAZER: 'Blazer', BOLT: 'Bolt', BOWSER: 'Bowser', @@ -129,6 +130,7 @@ export const BrowserName: Readonly<{ QUARK: 'Quark', QUPZILLA: 'QupZilla', QUTEBROWSER: 'qutebrowser', + QWANT: 'Qwant', REKONQ: 'rekonq', ROCKMELT: 'Rockmelt', SAFARI: 'Safari', @@ -243,6 +245,7 @@ export const DeviceVendor: Readonly<{ GEEKSPHONE: 'GeeksPhone', GENERIC: 'Generic', GOOGLE: 'Google', + HISENSE: 'Hisense', HMD: 'HMD', HP: 'HP', HTC: 'HTC', @@ -291,6 +294,7 @@ export const DeviceVendor: Readonly<{ VIVO: 'Vivo', VIZIO: 'Vizio', VODAFONE: 'Vodafone', + WIKO: 'Wiko', XBOX: 'Xbox', XIAOMI: 'Xiaomi', ZEBRA: 'Zebra', @@ -489,6 +493,7 @@ export const Extension: Readonly<{ DUCKDUCKGO_BOT: 'DuckDuckBot', DUCKDUCKGO_FAVICONS_BOT: 'DuckDuckGo-Favicons-Bot', ELASTIC: 'Elastic', + ELASTIC_SWIFTYPE_BOT: 'Swiftbot', EXALEAD_EXABOT: 'Exabot', FIRECRAWL_AGENT: 'FirecrawlAgent', FREESPOKE: 'Freespoke', @@ -538,6 +543,7 @@ export const Extension: Readonly<{ PERPLEXITY_BOT: 'PerplexityBot', QIHOO_360_SPIDER: '360Spider', QWANT_BOT: 'Qwantbot', + QWANT_BOT_NEWS: 'Qwantbot-news', REPLICATE_BOT: 'Replicate-Bot', RUNPOD_BOT: 'RunPod-Bot', SB_INTUITIONS_BOT: 'SBIntuitionsBot', @@ -551,6 +557,7 @@ export const Extension: Readonly<{ SOGOU_PIC_SPIDER: 'Sogou Pic Spider', SOGOU_WEB_SPIDER: 'Sogou web spider', STARTPAGE: 'Startpage', + SURLY_BOT: 'SurdotlyBot', TIMPI_BOT: 'Timpibot', TOGETHER_BOT: 'Together-Bot', TURNITIN_BOT: 'TurnitinBot', @@ -631,6 +638,7 @@ export const Extension: Readonly<{ BLUESKY: 'Bluesky', BUFFER_LINKPREVIEWBOT: 'BufferLinkPreviewBot', COHERE_AI: 'Cohere-AI', + DISCORD_BOT: 'Discordbot', DUCKDUCKGO_ASSISTBOT: 'DuckAssistBot', GOOGLE_CHROME_LIGHTHOUSE: 'Chrome-Lighthouse', GOOGLE_FEEDFETCHER: 'FeedFetcher-Google', @@ -643,6 +651,7 @@ export const Extension: Readonly<{ HUBSPOT_PAGE_FETCHER: 'HubSpot Page Fetcher', IFRAMELY: 'Iframely', KAKAOTALK_SCRAP: 'kakaotalk-scrap', + KEYBASE_BOT: 'KeybaseBot', META_EXTERNALFETCHER: 'meta-externalfetcher', META_WHATSAPP: 'WhatsApp', MICROSOFT_BINGPREVIEW: 'BingPreview', @@ -654,6 +663,9 @@ export const Extension: Readonly<{ PERPLEXITY_USER: 'Perplexity-User', PINTEREST_BOT: 'Pinterestbot', SEMRUSH_SITEAUDITBOT: 'SiteAuditBot', + SLACK_BOT: 'Slackbot', + SLACK_BOT_LINKEXPANDING: 'Slackbot-LinkExpanding', + SLACK_IMGPROXY: 'Slack-ImgProxy', SNAP_URL_PREVIEW: 'Snap URL Preview', SKYPE_URIPREVIEW: 'SkypeUriPreview', TELEGRAM_BOT: 'TelegramBot', @@ -663,6 +675,7 @@ export const Extension: Readonly<{ VERCEL_BOT: 'Vercelbot', VERCEL_FLAGS: 'vercelflags', VERCEL_TRACING: 'verceltracing', + X_TWITTERBOT: 'Twitterbot', YANDEX_CALENDAR: 'YandexCalendar', YANDEX_DIRECT: 'YandexDirect', YANDEX_DIRECTDYN: 'YandexDirectDyn', diff --git a/src/enums/ua-parser-enums.js b/src/enums/ua-parser-enums.js index 4867fec..ea7dc3c 100644 --- a/src/enums/ua-parser-enums.js +++ b/src/enums/ua-parser-enums.js @@ -1,5 +1,5 @@ /////////////////////////////////////////////// -/* Enums for UAParser.js v2.0.5 +/* Enums for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ diff --git a/src/enums/ua-parser-enums.mjs b/src/enums/ua-parser-enums.mjs index 9c1f3f1..18f1d29 100644 --- a/src/enums/ua-parser-enums.mjs +++ b/src/enums/ua-parser-enums.mjs @@ -3,7 +3,7 @@ // Source: /src/enums/ua-parser-enums.js /////////////////////////////////////////////// -/* Enums for UAParser.js v2.0.5 +/* Enums for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ @@ -24,6 +24,7 @@ const BrowserName = Object.freeze({ AVG: 'AVG Secure Browser', BAIDU: 'Baidu Browser', BASILISK: 'Basilisk', + BING: 'Bing', BLAZER: 'Blazer', BOLT: 'Bolt', BOWSER: 'Bowser', @@ -129,6 +130,7 @@ const BrowserName = Object.freeze({ QUARK: 'Quark', QUPZILLA: 'QupZilla', QUTEBROWSER: 'qutebrowser', + QWANT: 'Qwant', REKONQ: 'rekonq', ROCKMELT: 'Rockmelt', SAFARI: 'Safari', @@ -243,6 +245,7 @@ const DeviceVendor = Object.freeze({ GEEKSPHONE: 'GeeksPhone', GENERIC: 'Generic', GOOGLE: 'Google', + HISENSE: 'Hisense', HMD: 'HMD', HP: 'HP', HTC: 'HTC', @@ -291,6 +294,7 @@ const DeviceVendor = Object.freeze({ VIVO: 'Vivo', VIZIO: 'Vizio', VODAFONE: 'Vodafone', + WIKO: 'Wiko', XBOX: 'Xbox', XIAOMI: 'Xiaomi', ZEBRA: 'Zebra', @@ -489,6 +493,7 @@ const Extension = Object.freeze({ DUCKDUCKGO_BOT: 'DuckDuckBot', DUCKDUCKGO_FAVICONS_BOT: 'DuckDuckGo-Favicons-Bot', ELASTIC: 'Elastic', + ELASTIC_SWIFTYPE_BOT: 'Swiftbot', EXALEAD_EXABOT: 'Exabot', FIRECRAWL_AGENT: 'FirecrawlAgent', FREESPOKE: 'Freespoke', @@ -538,6 +543,7 @@ const Extension = Object.freeze({ PERPLEXITY_BOT: 'PerplexityBot', QIHOO_360_SPIDER: '360Spider', QWANT_BOT: 'Qwantbot', + QWANT_BOT_NEWS: 'Qwantbot-news', REPLICATE_BOT: 'Replicate-Bot', RUNPOD_BOT: 'RunPod-Bot', SB_INTUITIONS_BOT: 'SBIntuitionsBot', @@ -551,6 +557,7 @@ const Extension = Object.freeze({ SOGOU_PIC_SPIDER: 'Sogou Pic Spider', SOGOU_WEB_SPIDER: 'Sogou web spider', STARTPAGE: 'Startpage', + SURLY_BOT: 'SurdotlyBot', TIMPI_BOT: 'Timpibot', TOGETHER_BOT: 'Together-Bot', TURNITIN_BOT: 'TurnitinBot', @@ -631,6 +638,7 @@ const Extension = Object.freeze({ BLUESKY: 'Bluesky', BUFFER_LINKPREVIEWBOT: 'BufferLinkPreviewBot', COHERE_AI: 'Cohere-AI', + DISCORD_BOT: 'Discordbot', DUCKDUCKGO_ASSISTBOT: 'DuckAssistBot', GOOGLE_CHROME_LIGHTHOUSE: 'Chrome-Lighthouse', GOOGLE_FEEDFETCHER: 'FeedFetcher-Google', @@ -643,6 +651,7 @@ const Extension = Object.freeze({ HUBSPOT_PAGE_FETCHER: 'HubSpot Page Fetcher', IFRAMELY: 'Iframely', KAKAOTALK_SCRAP: 'kakaotalk-scrap', + KEYBASE_BOT: 'KeybaseBot', META_EXTERNALFETCHER: 'meta-externalfetcher', META_WHATSAPP: 'WhatsApp', MICROSOFT_BINGPREVIEW: 'BingPreview', @@ -654,6 +663,9 @@ const Extension = Object.freeze({ PERPLEXITY_USER: 'Perplexity-User', PINTEREST_BOT: 'Pinterestbot', SEMRUSH_SITEAUDITBOT: 'SiteAuditBot', + SLACK_BOT: 'Slackbot', + SLACK_BOT_LINKEXPANDING: 'Slackbot-LinkExpanding', + SLACK_IMGPROXY: 'Slack-ImgProxy', SNAP_URL_PREVIEW: 'Snap URL Preview', SKYPE_URIPREVIEW: 'SkypeUriPreview', TELEGRAM_BOT: 'TelegramBot', @@ -663,6 +675,7 @@ const Extension = Object.freeze({ VERCEL_BOT: 'Vercelbot', VERCEL_FLAGS: 'vercelflags', VERCEL_TRACING: 'verceltracing', + X_TWITTERBOT: 'Twitterbot', YANDEX_CALENDAR: 'YandexCalendar', YANDEX_DIRECT: 'YandexDirect', YANDEX_DIRECTDYN: 'YandexDirectDyn', diff --git a/src/extensions/ua-parser-extensions.js b/src/extensions/ua-parser-extensions.js index af8ca1d..eee4014 100644 --- a/src/extensions/ua-parser-extensions.js +++ b/src/extensions/ua-parser-extensions.js @@ -1,5 +1,5 @@ /////////////////////////////////////////////// -/* Extensions for UAParser.js v2.0.5 +/* Extensions for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ diff --git a/src/extensions/ua-parser-extensions.mjs b/src/extensions/ua-parser-extensions.mjs index 2260057..f40fd47 100644 --- a/src/extensions/ua-parser-extensions.mjs +++ b/src/extensions/ua-parser-extensions.mjs @@ -3,7 +3,7 @@ // Source: /src/extensions/ua-parser-extensions.js /////////////////////////////////////////////// -/* Extensions for UAParser.js v2.0.5 +/* Extensions for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ @@ -67,8 +67,10 @@ const Crawlers = Object.freeze({ // PerplexityBot - https://perplexity.ai/perplexitybot // SBIntuitionsBot - https://www.sbintuitions.co.jp/bot/ // SeznamBot - http://napoveda.seznam.cz/seznambot-intro + // SurdotlyBot - http://sur.ly/bot.html + // Swiftbot - https://swiftype.com/swiftbot // YepBot - https://yep.com/yepbot/ - /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|yep)bot)\/([\w\.-]+)/i, + /((?:adidx|ahrefs|amazon|bing|brave|cc|contx|coveo|criteo|dot|duckduck(?:go-favicons-)?|exa|facebook|gpt|iask|kagi|kangaroo |linkedin|mj12|mojeek|oai-search|onespot-scraper|perplexity|sbintuitions|semrush|seznam|surdotly|swift|yep)bot)\/([\w\.-]+)/i, // Algolia Crawler /(algolia crawler(?: renderscript)?)\/?([\w\.]*)/i, @@ -102,7 +104,7 @@ const Crawlers = Object.freeze({ /(oncrawl) mobile\/([\w\.]+)/i, // Qwantbot - https://help.qwant.com/bot - /(qwantbot)[-\w]*\/?([\w\.]*)/i, + /(qwantbot(?:-news)?)[-\w]*\/?([\w\.]*)/i, // SemrushBot - http://www.semrush.com/bot.html /((?:semrush|splitsignal)bot[-abcfimostw]*)\/?([\w\.-]*)/i, @@ -287,8 +289,8 @@ const Fetchers = Object.freeze({ [NAME, VERSION, [TYPE, FETCHER]], [ - // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / Snapchat / Vercelbot / Yandex Bots - /((?:better uptime |telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i + // Google Bots / Chrome-Lighthouse / Gemini-Deep-Research / KeybaseBot / Snapchat / Vercelbot / Yandex Bots + /((?:better uptime |keybase|telegram|vercel)bot|chrome-lighthouse|feedfetcher-google|gemini-deep-research|google(?:imageproxy|-read-aloud|-pagerenderer|producer)|snap url preview|vercel(flags|tracing|-(favicon|screenshot)-bot)|yandex(?:sitelinks|userproxy))/i ], [NAME, [TYPE, FETCHER]], ], diff --git a/src/helpers/ua-parser-helpers.js b/src/helpers/ua-parser-helpers.js index b19d0bf..08975ba 100644 --- a/src/helpers/ua-parser-helpers.js +++ b/src/helpers/ua-parser-helpers.js @@ -1,5 +1,5 @@ /////////////////////////////////////////////// -/* Helpers for UAParser.js v2.0.5 +/* Helpers for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ diff --git a/src/helpers/ua-parser-helpers.mjs b/src/helpers/ua-parser-helpers.mjs index 5cdcdf0..409de41 100644 --- a/src/helpers/ua-parser-helpers.mjs +++ b/src/helpers/ua-parser-helpers.mjs @@ -3,7 +3,7 @@ // Source: /src/helpers/ua-parser-helpers.js /////////////////////////////////////////////// -/* Helpers for UAParser.js v2.0.5 +/* Helpers for UAParser.js v2.0.6 https://github.com/faisalman/ua-parser-js Author: Faisal Salman AGPLv3 License */ diff --git a/src/main/ua-parser.d.ts b/src/main/ua-parser.d.ts index 0dcbd6a..9383d6a 100644 --- a/src/main/ua-parser.d.ts +++ b/src/main/ua-parser.d.ts @@ -1,4 +1,4 @@ -// Type definitions for UAParser.js v2.0.5 +// Type definitions for UAParser.js v2.0.6 // Project: https://github.com/faisalman/ua-parser-js // Definitions by: Faisal Salman diff --git a/src/main/ua-parser.js b/src/main/ua-parser.js index b461dc3..4123c89 100755 --- a/src/main/ua-parser.js +++ b/src/main/ua-parser.js @@ -1,5 +1,5 @@ ///////////////////////////////////////////////////////////////////////////////// -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License *//* Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data. @@ -19,7 +19,7 @@ // Constants ///////////// - var LIBVERSION = '2.0.5', + var LIBVERSION = '2.0.6', UA_MAX_LENGTH = 500, USER_AGENT = 'user-agent', EMPTY = '', diff --git a/src/main/ua-parser.mjs b/src/main/ua-parser.mjs index 04545da..8377af0 100644 --- a/src/main/ua-parser.mjs +++ b/src/main/ua-parser.mjs @@ -3,7 +3,7 @@ // Source: /src/main/ua-parser.js ///////////////////////////////////////////////////////////////////////////////// -/* UAParser.js v2.0.5 +/* UAParser.js v2.0.6 Copyright © 2012-2025 Faisal Salman AGPLv3 License *//* Detect Browser, Engine, OS, CPU, and Device type/model from User-Agent data. @@ -21,26 +21,26 @@ // Constants ///////////// - var LIBVERSION = '2.0.5', + var LIBVERSION = '2.0.6', UA_MAX_LENGTH = 500, USER_AGENT = 'user-agent', EMPTY = '', UNKNOWN = '?', - - // typeof - FUNC_TYPE = 'function', - UNDEF_TYPE = 'undefined', - OBJ_TYPE = 'object', - STR_TYPE = 'string', + TYPEOF = { + FUNCTION : 'function', + OBJECT : 'object', + STRING : 'string', + UNDEFINED : 'undefined' + }, // properties - UA_BROWSER = 'browser', - UA_CPU = 'cpu', - UA_DEVICE = 'device', - UA_ENGINE = 'engine', - UA_OS = 'os', - UA_RESULT = 'result', - + BROWSER = 'browser', + CPU = 'cpu', + DEVICE = 'device', + ENGINE = 'engine', + OS = 'os', + RESULT = 'result', + NAME = 'name', TYPE = 'type', VENDOR = 'vendor', @@ -68,16 +68,16 @@ PLATFORM = 'platform', PLATFORMVER = 'platformVersion', BITNESS = 'bitness', - CH_HEADER = 'sec-ch-ua', - CH_HEADER_FULL_VER_LIST = CH_HEADER + '-full-version-list', - CH_HEADER_ARCH = CH_HEADER + '-arch', - CH_HEADER_BITNESS = CH_HEADER + '-' + BITNESS, - CH_HEADER_FORM_FACTORS = CH_HEADER + '-form-factors', - CH_HEADER_MOBILE = CH_HEADER + '-' + MOBILE, - CH_HEADER_MODEL = CH_HEADER + '-' + MODEL, - CH_HEADER_PLATFORM = CH_HEADER + '-' + PLATFORM, - CH_HEADER_PLATFORM_VER = CH_HEADER_PLATFORM + '-version', - CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS], + CH = 'sec-ch-ua', + CH_FULL_VER_LIST= CH + '-full-version-list', + CH_ARCH = CH + '-arch', + CH_BITNESS = CH + '-' + BITNESS, + CH_FORM_FACTORS = CH + '-form-factors', + CH_MOBILE = CH + '-' + MOBILE, + CH_MODEL = CH + '-' + MODEL, + CH_PLATFORM = CH + '-' + PLATFORM, + CH_PLATFORM_VER = CH_PLATFORM + '-version', + CH_ALL_VALUES = [BRANDS, FULLVERLIST, MOBILE, MODEL, PLATFORM, PLATFORMVER, ARCHITECTURE, FORMFACTORS, BITNESS], // device vendors AMAZON = 'Amazon', @@ -116,7 +116,7 @@ // os WINDOWS = 'Windows'; - var isWindow = typeof window !== UNDEF_TYPE, + var isWindow = typeof window !== TYPEOF.UNDEFINED, NAVIGATOR = (isWindow && window.navigator) ? window.navigator : undefined, @@ -152,7 +152,7 @@ return enums; }, has = function (str1, str2) { - if (typeof str1 === OBJ_TYPE && str1.length > 0) { + if (typeof str1 === TYPEOF.OBJECT && str1.length > 0) { for (var i in str1) { if (lowerize(str2) == lowerize(str1[i])) return true; } @@ -166,7 +166,7 @@ } }, isString = function (val) { - return typeof val === STR_TYPE; + return typeof val === TYPEOF.STRING; }, itemListToArray = function (header) { if (!header) return undefined; @@ -193,7 +193,7 @@ if (!arr.hasOwnProperty(i)) continue; var propName = arr[i]; - if (typeof propName == OBJ_TYPE && propName.length == 2) { + if (typeof propName == TYPEOF.OBJECT && propName.length == 2) { this[propName[0]] = propName[1]; } else { this[propName] = undefined; @@ -208,10 +208,8 @@ return strip(/\\?\"/g, str); }, trim = function (str, len) { - if (isString(str)) { - str = strip(/^\s\s*/, str); - return typeof len === UNDEF_TYPE ? str : str.substring(0, UA_MAX_LENGTH); - } + str = strip(/^\s\s*/, String(str)); + return typeof len === TYPEOF.UNDEFINED ? str : str.substring(0, len); }; /////////////// @@ -242,9 +240,9 @@ match = matches[++k]; q = props[p]; // check if given property is actually array - if (typeof q === OBJ_TYPE && q.length > 0) { + if (typeof q === TYPEOF.OBJECT && q.length > 0) { if (q.length === 2) { - if (typeof q[1] == FUNC_TYPE) { + if (typeof q[1] == TYPEOF.FUNCTION) { // assign modified match this[q[0]] = q[1].call(this, match); } else { @@ -253,7 +251,7 @@ } } else if (q.length >= 3) { // Check whether q[1] FUNCTION or REGEX - if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { + if (typeof q[1] === TYPEOF.FUNCTION && !(q[1].exec && q[1].test)) { if (q.length > 3) { this[q[0]] = match ? q[1].apply(this, q.slice(2)) : undefined; } else { @@ -285,7 +283,7 @@ for (var i in map) { // check if current value is array - if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { + if (typeof map[i] === TYPEOF.OBJECT && map[i].length > 0) { for (var j = 0; j < map[i].length; j++) { if (has(map[i][j], str)) { return (i === UNKNOWN) ? undefined : i; @@ -379,10 +377,11 @@ /(avant|iemobile|slim(?:browser|boat|jet))[\/ ]?([\d\.]*)/i, // Avant/IEMobile/SlimBrowser/SlimBoat/Slimjet /(?:ms|\()(ie) ([\w\.]+)/i, // Internet Explorer - // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble - /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser)\/([-\w\.]+)/i, + // Blink/Webkit/KHTML based // Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser/QupZilla/Falkon/LG Browser/Otter/qutebrowser/Dooble/Palemoon + /(flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser|qupzilla|falkon|rekonq|puffin|brave|whale(?!.+naver)|qqbrowserlite|duckduckgo|klar|helio|(?=comodo_)?dragon|otter|dooble|(?:lg |qute)browser|palemoon)\/([-\w\.]+)/i, // Rekonq/Puffin/Brave/Whale/QQBrowserLite/QQ//Vivaldi/DuckDuckGo/Klar/Helio/Dragon - /(heytap|ovi|115|surf)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf + /(heytap|ovi|115|surf|qwant)browser\/([\d\.]+)/i, // HeyTap/Ovi/115/Surf + /(qwant)(?:ios|mobile)\/([\d\.]+)/i, // Qwant /(ecosia|weibo)(?:__| \w+@)([\d\.]+)/i // Ecosia/Weibo ], [NAME, VERSION], [ /quark(?:pc)?\/([-\w\.]+)/i // Quark @@ -451,6 +450,7 @@ /\b(line)\/([\w\.]+)\/iab/i, // Line App for Android /(alipay)client\/([\w\.]+)/i, // Alipay /(twitter)(?:and| f.+e\/([\w\.]+))/i, // Twitter + /(bing)(?:web|sapphire)\/([\w\.]+)/i, // Bing /(instagram|snapchat|klarna)[\/ ]([-\w\.]+)/i // Instagram/Snapchat/Klarna ], [NAME, VERSION, [TYPE, INAPP]], [ /\bgsa\/([\w\.]+) .*safari\//i // Google Search Appliance on iOS @@ -508,10 +508,10 @@ /(swiftfox)/i, // Swiftfox /(icedragon|iceweasel|camino|chimera|fennec|maemo browser|minimo|conkeror)[\/ ]?([\w\.\+]+)/i, // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror - /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|palemoon|basilisk|waterfox)\/([-\w\.]+)$/i, + /(seamonkey|k-meleon|icecat|iceape|firebird|phoenix|basilisk|waterfox)\/([-\w\.]+)$/i, // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix /(firefox)\/([\w\.]+)/i, // Other Firefox-based - /(mozilla)\/([\w\.]+) .+rv\:.+gecko\/\d+/i, // Mozilla + /(mozilla)\/([\w\.]+(?= .+rv\:.+gecko\/\d+)|[0-4][\w\.]+(?!.+compatible))/i, // Mozilla // Other /(amaya|dillo|doris|icab|ladybird|lynx|mosaic|netsurf|obigo|polaris|w3m|(?:go|ice|up)[\. ]?browser)[-\/ ]?v?([\w\.]+)/i, @@ -549,6 +549,8 @@ /((ppc|powerpc)(64)?)( mac|;|\))/i, // PowerPC /(?:osf1|[freopnt]{3,4}bsd) (alpha)/i // Alpha ], [[ARCHITECTURE, /ower/, EMPTY, lowerize]], [ + /mc680.0/i + ], [[ARCHITECTURE, '68k']], [ /winnt.+\[axp/i ], [[ARCHITECTURE, 'alpha']] ], @@ -568,11 +570,10 @@ ], [MODEL, [VENDOR, SAMSUNG], [TYPE, MOBILE]], [ // Apple - /(?:\/|\()(ip(?:hone|od)[\w, ]*)(?:\/|;)/i // iPod/iPhone + /(?:\/|\()(ip(?:hone|od)[\w, ]*)[\/\);]/i // iPod/iPhone ], [MODEL, [VENDOR, APPLE], [TYPE, MOBILE]], [ - /\((ipad);[-\w\),; ]+apple/i, // iPad - /applecoremedia\/[\w\.]+ \((ipad)/i, - /\b(ipad)\d\d?,\d\d?[;\]].+ios/i + /\b(?:ios|apple\w+)\/.+[\(\/](ipad)/i, // iPad + /\b(ipad)[\d,]*[;\] ].+(mac |i(pad)?)os/i ], [MODEL, [VENDOR, APPLE], [TYPE, TABLET]], [ /(macintosh);/i ], [MODEL, [VENDOR, APPLE]], [ @@ -590,13 +591,13 @@ // Huawei /\b((?:ag[rs][2356]?k?|bah[234]?|bg[2o]|bt[kv]|cmr|cpn|db[ry]2?|jdn2|got|kob2?k?|mon|pce|scm|sht?|[tw]gr|vrd)-[ad]?[lw][0125][09]b?|605hw|bg2-u03|(?:gem|fdr|m2|ple|t1)-[7a]0[1-4][lu]|t1-a2[13][lw]|mediapad[\w\. ]*(?= bui|\)))\b(?!.+d\/s)/i ], [MODEL, [VENDOR, HUAWEI], [TYPE, TABLET]], [ - /(?:huawei)([-\w ]+)[;\)]/i, - /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i + /(?:huawei) ?([-\w ]+)[;\)]/i, + /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][\dc][adnt]?)\b(?!.+d\/s)/i ], [MODEL, [VENDOR, HUAWEI], [TYPE, MOBILE]], [ // Xiaomi /oid[^\)]+; (2[\dbc]{4}(182|283|rp\w{2})[cgl]|m2105k81a?c)(?: bui|\))/i, - /\b((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets + /\b(?:xiao)?((?:red)?mi[-_ ]?pad[\w- ]*)(?: bui|\))/i // Mi Pad tablets ],[[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, TABLET]], [ /\b(poco[\w ]+|m2\d{3}j\d\d[a-z]{2})(?: bui|\))/i, // Xiaomi POCO @@ -604,7 +605,7 @@ /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi /oid[^\)]+; (m?[12][0-389][01]\w{3,6}[c-y])( bui|; wv|\))/i, // Xiaomi Redmi 'numeric' models - /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite|pro)?)(?: bui|\))/i, // Xiaomi Mi + /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note|max|cc)?[_ ]?(?:\d{0,2}\w?)[_ ]?(?:plus|se|lite|pro)?( 5g|lte)?)(?: bui|\))/i, // Xiaomi Mi / ([\w ]+) miui\/v?\d/i ], [[MODEL, /_/g, ' '], [VENDOR, XIAOMI], [TYPE, MOBILE]], [ @@ -660,7 +661,7 @@ /(nokia) (t[12][01])/i ], [VENDOR, MODEL, [TYPE, TABLET]], [ /(?:maemo|nokia).*(n900|lumia \d+|rm-\d+)/i, - /nokia[-_ ]?(([-\w\. ]*))/i + /nokia[-_ ]?(([-\w\. ]*?))( bui|\)|;|\/)/i ], [[MODEL, /_/g, ' '], [TYPE, MOBILE], [VENDOR, 'Nokia']], [ // Google @@ -691,7 +692,7 @@ /(playbook);[-\w\),; ]+(rim)/i // BlackBerry PlayBook ], [MODEL, VENDOR, [TYPE, TABLET]], [ /\b((?:bb[a-f]|st[hv])100-\d)/i, - /\(bb10; (\w+)/i // BlackBerry 10 + /(?:blackberry|\(bb10;) (\w+)/i ], [MODEL, [VENDOR, BLACKBERRY], [TYPE, MOBILE]], [ // Asus @@ -771,10 +772,12 @@ /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus(?! zenw)|dell|jolla|meizu|motorola|polytron|tecno|micromax|advan)[-_ ]?([-\w]*)/i, // BlackBerry/BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron/Tecno/Micromax/Advan - /; (blu|hmd|imo|infinix|lava|oneplus|tcl)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i, // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL + // BLU/HMD/IMO/Infinix/Lava/OnePlus/TCL/Wiko + /; (blu|hmd|imo|infinix|lava|oneplus|tcl|wiko)[_ ]([\w\+ ]+?)(?: bui|\)|; r)/i, /(hp) ([\w ]+\w)/i, // HP iPAQ /(microsoft); (lumia[\w ]+)/i, // Microsoft Lumia /(oppo) ?([\w ]+) bui/i, // OPPO + /(hisense) ([ehv][\w ]+)\)/i, // Hisense /droid[^;]+; (philips)[_ ]([sv-x][\d]{3,4}[xz]?)/i // Philips ], [VENDOR, MODEL, [TYPE, MOBILE]], [ @@ -914,7 +917,7 @@ ], [MODEL, [TYPE, SMARTTV]], [ /\b((4k|android|smart|opera)[- ]?tv|tv; rv:|large screen[\w ]+safari)\b/i ], [[TYPE, SMARTTV]], [ - /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew).+?(mobile|vr|\d) safari/i + /droid .+?; ([^;]+?)(?: bui|; wv\)|\) applew|; hmsc).+?(mobile|vr|\d) safari/i ], [MODEL, [TYPE, strMapper, { 'mobile' : 'Mobile', 'xr' : 'VR', '*' : TABLET }]], [ /\b((tablet|tab)[;\/]|focus\/\d(?!.+mobile))/i // Unidentifiable Tablet ], [[TYPE, TABLET]], [ @@ -967,7 +970,7 @@ // iOS/macOS /[adehimnop]{4,7}\b(?:.*os ([\w]+) like mac|; opera)/i, // iOS - /(?:ios;fbsv\/|iphone.+ios[\/ ])([\d\.]+)/i, + /(?:ios;fbsv|ios(?=.+ip(?:ad|hone))|ip(?:ad|hone)(?: |.+i(?:pad)?)os)[\/ ]([\w\.]+)/i, /cfnetwork\/.+darwin/i ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [ /(mac os x) ?([\w\. ]*)/i, @@ -1052,27 +1055,27 @@ var defaultProps = (function () { var props = { init : {}, isIgnore : {}, isIgnoreRgx : {}, toString : {}}; setProps.call(props.init, [ - [UA_BROWSER, [NAME, VERSION, MAJOR, TYPE]], - [UA_CPU, [ARCHITECTURE]], - [UA_DEVICE, [TYPE, MODEL, VENDOR]], - [UA_ENGINE, [NAME, VERSION]], - [UA_OS, [NAME, VERSION]] + [BROWSER, [NAME, VERSION, MAJOR, TYPE]], + [CPU, [ARCHITECTURE]], + [DEVICE, [TYPE, MODEL, VENDOR]], + [ENGINE, [NAME, VERSION]], + [OS, [NAME, VERSION]] ]); setProps.call(props.isIgnore, [ - [UA_BROWSER, [VERSION, MAJOR]], - [UA_ENGINE, [VERSION]], - [UA_OS, [VERSION]] + [BROWSER, [VERSION, MAJOR]], + [ENGINE, [VERSION]], + [OS, [VERSION]] ]); setProps.call(props.isIgnoreRgx, [ - [UA_BROWSER, / ?browser$/i], - [UA_OS, / ?os$/i] + [BROWSER, / ?browser$/i], + [OS, / ?os$/i] ]); setProps.call(props.toString, [ - [UA_BROWSER, [NAME, VERSION]], - [UA_CPU, [ARCHITECTURE]], - [UA_DEVICE, [VENDOR, MODEL]], - [UA_ENGINE, [NAME, VERSION]], - [UA_OS, [NAME, VERSION]] + [BROWSER, [NAME, VERSION]], + [CPU, [ARCHITECTURE]], + [DEVICE, [VENDOR, MODEL]], + [ENGINE, [NAME, VERSION]], + [OS, [NAME, VERSION]] ]); return props; })(); @@ -1116,14 +1119,14 @@ return item.detectFeature().get(); }; - if (itemType != UA_RESULT) { + if (itemType != RESULT) { IData.prototype.is = function (strToCheck) { var is = false; for (var i in this) { if (this.hasOwnProperty(i) && !has(is_ignoreProps, i) && lowerize(is_ignoreRgx ? strip(is_ignoreRgx, this[i]) : this[i]) == lowerize(is_ignoreRgx ? strip(is_ignoreRgx, strToCheck) : strToCheck)) { is = true; - if (strToCheck != UNDEF_TYPE) break; - } else if (strToCheck == UNDEF_TYPE && is) { + if (strToCheck != TYPEOF.UNDEFINED) break; + } else if (strToCheck == TYPEOF.UNDEFINED && is) { is = !is; break; } @@ -1133,11 +1136,11 @@ IData.prototype.toString = function () { var str = EMPTY; for (var i in toString_props) { - if (typeof(this[toString_props[i]]) !== UNDEF_TYPE) { + if (typeof(this[toString_props[i]]) !== TYPEOF.UNDEFINED) { str += (str ? ' ' : EMPTY) + this[toString_props[i]]; } } - return str || UNDEF_TYPE; + return str || TYPEOF.UNDEFINED; }; } @@ -1173,19 +1176,19 @@ setProps.call(this, CH_ALL_VALUES); if (isHttpUACH) { setProps.call(this, [ - [BRANDS, itemListToArray(uach[CH_HEADER])], - [FULLVERLIST, itemListToArray(uach[CH_HEADER_FULL_VER_LIST])], - [MOBILE, /\?1/.test(uach[CH_HEADER_MOBILE])], - [MODEL, stripQuotes(uach[CH_HEADER_MODEL])], - [PLATFORM, stripQuotes(uach[CH_HEADER_PLATFORM])], - [PLATFORMVER, stripQuotes(uach[CH_HEADER_PLATFORM_VER])], - [ARCHITECTURE, stripQuotes(uach[CH_HEADER_ARCH])], - [FORMFACTORS, itemListToArray(uach[CH_HEADER_FORM_FACTORS])], - [BITNESS, stripQuotes(uach[CH_HEADER_BITNESS])] + [BRANDS, itemListToArray(uach[CH])], + [FULLVERLIST, itemListToArray(uach[CH_FULL_VER_LIST])], + [MOBILE, /\?1/.test(uach[CH_MOBILE])], + [MODEL, stripQuotes(uach[CH_MODEL])], + [PLATFORM, stripQuotes(uach[CH_PLATFORM])], + [PLATFORMVER, stripQuotes(uach[CH_PLATFORM_VER])], + [ARCHITECTURE, stripQuotes(uach[CH_ARCH])], + [FORMFACTORS, itemListToArray(uach[CH_FORM_FACTORS])], + [BITNESS, stripQuotes(uach[CH_BITNESS])] ]); } else { for (var prop in uach) { - if(this.hasOwnProperty(prop) && typeof uach[prop] !== UNDEF_TYPE) this[prop] = uach[prop]; + if(this.hasOwnProperty(prop) && typeof uach[prop] !== TYPEOF.UNDEFINED) this[prop] = uach[prop]; } } } @@ -1210,30 +1213,30 @@ this.detectFeature = function () { if (NAVIGATOR && NAVIGATOR.userAgent == this.ua) { switch (this.itemType) { - case UA_BROWSER: + case BROWSER: // Brave-specific detection - if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == FUNC_TYPE) { + if (NAVIGATOR.brave && typeof NAVIGATOR.brave.isBrave == TYPEOF.FUNCTION) { this.set(NAME, 'Brave'); } break; - case UA_DEVICE: + case DEVICE: // Chrome-specific detection: check for 'mobile' value of navigator.userAgentData if (!this.get(TYPE) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[MOBILE]) { this.set(TYPE, MOBILE); } // iPadOS-specific detection: identified as Mac, but has some iOS-only properties - if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== UNDEF_TYPE && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { + if (this.get(MODEL) == 'Macintosh' && NAVIGATOR && typeof NAVIGATOR.standalone !== TYPEOF.UNDEFINED && NAVIGATOR.maxTouchPoints && NAVIGATOR.maxTouchPoints > 2) { this.set(MODEL, 'iPad') .set(TYPE, TABLET); } break; - case UA_OS: + case OS: // Chrome-specific detection: check for 'platform' value of navigator.userAgentData if (!this.get(NAME) && NAVIGATOR_UADATA && NAVIGATOR_UADATA[PLATFORM]) { this.set(NAME, NAVIGATOR_UADATA[PLATFORM]); } break; - case UA_RESULT: + case RESULT: var data = this.data; var detect = function (itemType) { return data[itemType] @@ -1241,22 +1244,33 @@ .detectFeature() .get(); }; - this.set(UA_BROWSER, detect(UA_BROWSER)) - .set(UA_CPU, detect(UA_CPU)) - .set(UA_DEVICE, detect(UA_DEVICE)) - .set(UA_ENGINE, detect(UA_ENGINE)) - .set(UA_OS, detect(UA_OS)); + this.set(BROWSER, detect(BROWSER)) + .set(CPU, detect(CPU)) + .set(DEVICE, detect(DEVICE)) + .set(ENGINE, detect(ENGINE)) + .set(OS, detect(OS)); } } return this; }; this.parseUA = function () { - if (this.itemType != UA_RESULT) { + if (this.itemType != RESULT) { rgxMapper.call(this.data, this.ua, this.rgxMap); } - if (this.itemType == UA_BROWSER) { - this.set(MAJOR, majorize(this.get(VERSION))); + switch (this.itemType) { + case BROWSER: + this.set(MAJOR, majorize(this.get(VERSION))); + break; + case OS: + if (this.get(NAME) == 'iOS' && this.get(VERSION) == '18.6') { + // Based on the assumption that iOS version is tightly coupled with Safari version + var realVersion = /\) Version\/([\d\.]+)/.exec(this.ua); // Get Safari version + if (realVersion && parseInt(realVersion[1].substring(0,2), 10) >= 26) { + this.set(VERSION, realVersion[1]); // Set as iOS version + } + } + break; } return this; }; @@ -1266,14 +1280,14 @@ rgxMap = this.rgxMap; switch (this.itemType) { - case UA_BROWSER: - case UA_ENGINE: + case BROWSER: + case ENGINE: var brands = uaCH[FULLVERLIST] || uaCH[BRANDS], prevName; if (brands) { for (var i=0; i UA_MAX_LENGTH ? trim(ua, UA_MAX_LENGTH) : ua; + if (isString(ua)) userAgent = trim(ua, UA_MAX_LENGTH); return this; }] ]) diff --git a/test/unit/main.js b/test/unit/main.js index 01e0d4e..053939f 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -382,12 +382,15 @@ describe('Read user-agent data from req.headers', function () { assert.strictEqual(engine.name, "EdgeHTML"); }); - it('Fetch API\'s Header can be passed directly into headers', () => { - const reqHeaders = new Headers(); - reqHeaders.append('User-Agent', 'Midori/0.2.2 (X11; Linux i686; U; en-us) WebKit/531.2+'); - const { browser } = UAParser(reqHeaders); - assert.strictEqual(browser.is('Midori'), true); - }); + // Headers supported in node 18+ - https://developer.mozilla.org/en-US/docs/Web/API/Headers + if (typeof Headers !== 'undefined') { + it('Fetch API\'s Header can be passed directly into headers', () => { + const reqHeaders = new Headers(); + reqHeaders.append('User-Agent', 'Midori/0.2.2 (X11; Linux i686; U; en-us) WebKit/531.2+'); + const { browser } = UAParser(reqHeaders); + assert.strictEqual(browser.is('Midori'), true); + }); + } it('Headers field name should be case insensitive', function () { const hEaDeRs = {