package url import ( "math" "net/url" "reflect" "strconv" "strings" "apigo.cc/ai/ai/goja" "apigo.cc/ai/ai/goja_nodejs/errors" "golang.org/x/net/idna" ) const ( URLNotAbsolute = "URL is not absolute" InvalidURL = "Invalid URL" InvalidBaseURL = "Invalid base URL" InvalidHostname = "Invalid hostname" ) var ( reflectTypeURL = reflect.TypeOf((*nodeURL)(nil)) reflectTypeInt = reflect.TypeOf(int64(0)) ) func toURL(r *goja.Runtime, v goja.Value) *nodeURL { if v.ExportType() == reflectTypeURL { if u := v.Export().(*nodeURL); u != nil { return u } } panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URL`)) } func (m *urlModule) newInvalidURLError(msg, input string) *goja.Object { o := errors.NewTypeError(m.r, "ERR_INVALID_URL", msg) o.Set("input", m.r.ToValue(input)) return o } func (m *urlModule) defineURLAccessorProp(p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) { var getterVal, setterVal goja.Value if getter != nil { getterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { return m.r.ToValue(getter(toURL(m.r, call.This))) }) } if setter != nil { setterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value { setter(toURL(m.r, call.This), call.Argument(0)) return goja.Undefined() }) } p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE) } func valueToURLPort(v goja.Value) (portNum int, empty bool) { portNum = -1 if et := v.ExportType(); et == reflectTypeInt { num := v.ToInteger() if num < 0 { empty = true } else if num <= math.MaxUint16 { portNum = int(num) } } else { s := v.String() if s == "" { return 0, true } firstDigitIdx := -1 for i := 0; i < len(s); i++ { if c := s[i]; c >= '0' && c <= '9' { firstDigitIdx = i break } } if firstDigitIdx == -1 { return -1, false } if firstDigitIdx > 0 { return 0, true } for i := 0; i < len(s); i++ { if c := s[i]; c >= '0' && c <= '9' { if portNum == -1 { portNum = 0 } portNum = portNum*10 + int(c-'0') if portNum > math.MaxUint16 { portNum = -1 break } } else { break } } } return } func isDefaultURLPort(protocol string, port int) bool { switch port { case 21: if protocol == "ftp" { return true } case 80: if protocol == "http" || protocol == "ws" { return true } case 443: if protocol == "https" || protocol == "wss" { return true } } return false } func isSpecialProtocol(protocol string) bool { switch protocol { case "ftp", "file", "http", "https", "ws", "wss": return true } return false } func clearURLPort(u *url.URL) { u.Host = u.Hostname() } func setURLPort(nu *nodeURL, v goja.Value) { u := nu.url if u.Scheme == "file" { return } portNum, empty := valueToURLPort(v) if empty { clearURLPort(u) return } if portNum == -1 { return } if isDefaultURLPort(u.Scheme, portNum) { clearURLPort(u) } else { u.Host = u.Hostname() + ":" + strconv.Itoa(portNum) } } func (m *urlModule) parseURL(s string, isBase bool) *url.URL { u, err := url.Parse(s) if err != nil { if isBase { panic(m.newInvalidURLError(InvalidBaseURL, s)) } else { panic(m.newInvalidURLError(InvalidURL, s)) } } if isBase && !u.IsAbs() { panic(m.newInvalidURLError(URLNotAbsolute, s)) } if portStr := u.Port(); portStr != "" { if port, err := strconv.Atoi(portStr); err != nil || isDefaultURLPort(u.Scheme, port) { u.Host = u.Hostname() // Clear port } } m.fixURL(u) return u } func fixRawQuery(u *url.URL) { if u.RawQuery != "" { u.RawQuery = escape(u.RawQuery, &tblEscapeURLQuery, false) } } func (m *urlModule) fixURL(u *url.URL) { switch u.Scheme { case "https", "http", "ftp", "wss", "ws": if u.Path == "" { u.Path = "/" } hostname := u.Hostname() lh := strings.ToLower(hostname) ch, err := idna.Punycode.ToASCII(lh) if err != nil { panic(m.newInvalidURLError(InvalidHostname, lh)) } if ch != hostname { if port := u.Port(); port != "" { u.Host = ch + ":" + port } else { u.Host = ch } } } fixRawQuery(u) } func (m *urlModule) createURLPrototype() *goja.Object { p := m.r.NewObject() // host m.defineURLAccessorProp(p, "host", func(u *nodeURL) interface{} { return u.url.Host }, func(u *nodeURL, arg goja.Value) { host := arg.String() if _, err := url.ParseRequestURI(u.url.Scheme + "://" + host); err == nil { u.url.Host = host m.fixURL(u.url) } }) // hash m.defineURLAccessorProp(p, "hash", func(u *nodeURL) interface{} { if u.url.Fragment != "" { return "#" + u.url.EscapedFragment() } return "" }, func(u *nodeURL, arg goja.Value) { h := arg.String() if len(h) > 0 && h[0] == '#' { h = h[1:] } u.url.Fragment = h }) // hostname m.defineURLAccessorProp(p, "hostname", func(u *nodeURL) interface{} { return strings.Split(u.url.Host, ":")[0] }, func(u *nodeURL, arg goja.Value) { h := arg.String() if strings.IndexByte(h, ':') >= 0 { return } if _, err := url.ParseRequestURI(u.url.Scheme + "://" + h); err == nil { if port := u.url.Port(); port != "" { u.url.Host = h + ":" + port } else { u.url.Host = h } m.fixURL(u.url) } }) // href m.defineURLAccessorProp(p, "href", func(u *nodeURL) interface{} { return u.String() }, func(u *nodeURL, arg goja.Value) { u.url = m.parseURL(arg.String(), true) }) // pathname m.defineURLAccessorProp(p, "pathname", func(u *nodeURL) interface{} { return u.url.EscapedPath() }, func(u *nodeURL, arg goja.Value) { p := arg.String() if _, err := url.Parse(p); err == nil { switch u.url.Scheme { case "https", "http", "ftp", "ws", "wss": if !strings.HasPrefix(p, "/") { p = "/" + p } } u.url.Path = p } }) // origin m.defineURLAccessorProp(p, "origin", func(u *nodeURL) interface{} { return u.url.Scheme + "://" + u.url.Hostname() }, nil) // password m.defineURLAccessorProp(p, "password", func(u *nodeURL) interface{} { p, _ := u.url.User.Password() return p }, func(u *nodeURL, arg goja.Value) { user := u.url.User u.url.User = url.UserPassword(user.Username(), arg.String()) }) // username m.defineURLAccessorProp(p, "username", func(u *nodeURL) interface{} { return u.url.User.Username() }, func(u *nodeURL, arg goja.Value) { p, has := u.url.User.Password() if !has { u.url.User = url.User(arg.String()) } else { u.url.User = url.UserPassword(arg.String(), p) } }) // port m.defineURLAccessorProp(p, "port", func(u *nodeURL) interface{} { return u.url.Port() }, func(u *nodeURL, arg goja.Value) { setURLPort(u, arg) }) // protocol m.defineURLAccessorProp(p, "protocol", func(u *nodeURL) interface{} { return u.url.Scheme + ":" }, func(u *nodeURL, arg goja.Value) { s := arg.String() pos := strings.IndexByte(s, ':') if pos >= 0 { s = s[:pos] } s = strings.ToLower(s) if isSpecialProtocol(u.url.Scheme) == isSpecialProtocol(s) { if _, err := url.ParseRequestURI(s + "://" + u.url.Host); err == nil { u.url.Scheme = s } } }) // Search m.defineURLAccessorProp(p, "search", func(u *nodeURL) interface{} { u.syncSearchParams() if u.url.RawQuery != "" { return "?" + u.url.RawQuery } return "" }, func(u *nodeURL, arg goja.Value) { u.url.RawQuery = arg.String() fixRawQuery(u.url) if u.searchParams != nil { u.searchParams = parseSearchQuery(u.url.RawQuery) if u.searchParams == nil { u.searchParams = make(searchParams, 0) } } }) // search Params m.defineURLAccessorProp(p, "searchParams", func(u *nodeURL) interface{} { if u.searchParams == nil { sp := parseSearchQuery(u.url.RawQuery) if sp == nil { sp = make(searchParams, 0) } u.searchParams = sp } return m.newURLSearchParams((*urlSearchParams)(u)) }, nil) p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value { u := toURL(m.r, call.This) u.syncSearchParams() return m.r.ToValue(u.url.String()) })) p.Set("toJSON", m.r.ToValue(func(call goja.FunctionCall) goja.Value { u := toURL(m.r, call.This) u.syncSearchParams() return m.r.ToValue(u.url.String()) })) return p } func (m *urlModule) createURLConstructor() goja.Value { f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object { var u *url.URL if baseArg := call.Argument(1); !goja.IsUndefined(baseArg) { base := m.parseURL(baseArg.String(), true) ref := m.parseURL(call.Argument(0).String(), false) u = base.ResolveReference(ref) } else { u = m.parseURL(call.Argument(0).String(), true) } res := m.r.ToValue(&nodeURL{url: u}).(*goja.Object) res.SetPrototype(call.This.Prototype()) return res }).(*goja.Object) proto := m.createURLPrototype() f.Set("prototype", proto) proto.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE) return f } func (m *urlModule) domainToASCII(domUnicode string) string { res, err := idna.ToASCII(domUnicode) if err != nil { return "" } return res } func (m *urlModule) domainToUnicode(domASCII string) string { res, err := idna.ToUnicode(domASCII) if err != nil { return "" } return res }