github.com/dop251/goja_nodejs@v0.0.0-20240418154818-2aae10d4cbcf/url/url.go (about)

     1  package url
     2  
     3  import (
     4  	"math"
     5  	"net/url"
     6  	"reflect"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/dop251/goja"
    11  	"github.com/dop251/goja_nodejs/errors"
    12  
    13  	"golang.org/x/net/idna"
    14  )
    15  
    16  const (
    17  	URLNotAbsolute  = "URL is not absolute"
    18  	InvalidURL      = "Invalid URL"
    19  	InvalidBaseURL  = "Invalid base URL"
    20  	InvalidHostname = "Invalid hostname"
    21  )
    22  
    23  var (
    24  	reflectTypeURL = reflect.TypeOf((*nodeURL)(nil))
    25  	reflectTypeInt = reflect.TypeOf(int64(0))
    26  )
    27  
    28  func toURL(r *goja.Runtime, v goja.Value) *nodeURL {
    29  	if v.ExportType() == reflectTypeURL {
    30  		if u := v.Export().(*nodeURL); u != nil {
    31  			return u
    32  		}
    33  	}
    34  
    35  	panic(errors.NewTypeError(r, errors.ErrCodeInvalidThis, `Value of "this" must be of type URL`))
    36  }
    37  
    38  func (m *urlModule) newInvalidURLError(msg, input string) *goja.Object {
    39  	o := errors.NewTypeError(m.r, "ERR_INVALID_URL", msg)
    40  	o.Set("input", m.r.ToValue(input))
    41  	return o
    42  }
    43  
    44  func (m *urlModule) defineURLAccessorProp(p *goja.Object, name string, getter func(*nodeURL) interface{}, setter func(*nodeURL, goja.Value)) {
    45  	var getterVal, setterVal goja.Value
    46  	if getter != nil {
    47  		getterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value {
    48  			return m.r.ToValue(getter(toURL(m.r, call.This)))
    49  		})
    50  	}
    51  	if setter != nil {
    52  		setterVal = m.r.ToValue(func(call goja.FunctionCall) goja.Value {
    53  			setter(toURL(m.r, call.This), call.Argument(0))
    54  			return goja.Undefined()
    55  		})
    56  	}
    57  	p.DefineAccessorProperty(name, getterVal, setterVal, goja.FLAG_FALSE, goja.FLAG_TRUE)
    58  }
    59  
    60  func valueToURLPort(v goja.Value) (portNum int, empty bool) {
    61  	portNum = -1
    62  	if et := v.ExportType(); et == reflectTypeInt {
    63  		num := v.ToInteger()
    64  		if num < 0 {
    65  			empty = true
    66  		} else if num <= math.MaxUint16 {
    67  			portNum = int(num)
    68  		}
    69  	} else {
    70  		s := v.String()
    71  		if s == "" {
    72  			return 0, true
    73  		}
    74  		firstDigitIdx := -1
    75  		for i := 0; i < len(s); i++ {
    76  			if c := s[i]; c >= '0' && c <= '9' {
    77  				firstDigitIdx = i
    78  				break
    79  			}
    80  		}
    81  
    82  		if firstDigitIdx == -1 {
    83  			return -1, false
    84  		}
    85  
    86  		if firstDigitIdx > 0 {
    87  			return 0, true
    88  		}
    89  
    90  		for i := 0; i < len(s); i++ {
    91  			if c := s[i]; c >= '0' && c <= '9' {
    92  				if portNum == -1 {
    93  					portNum = 0
    94  				}
    95  				portNum = portNum*10 + int(c-'0')
    96  				if portNum > math.MaxUint16 {
    97  					portNum = -1
    98  					break
    99  				}
   100  			} else {
   101  				break
   102  			}
   103  		}
   104  	}
   105  	return
   106  }
   107  
   108  func isDefaultURLPort(protocol string, port int) bool {
   109  	switch port {
   110  	case 21:
   111  		if protocol == "ftp" {
   112  			return true
   113  		}
   114  	case 80:
   115  		if protocol == "http" || protocol == "ws" {
   116  			return true
   117  		}
   118  	case 443:
   119  		if protocol == "https" || protocol == "wss" {
   120  			return true
   121  		}
   122  	}
   123  	return false
   124  }
   125  
   126  func isSpecialProtocol(protocol string) bool {
   127  	switch protocol {
   128  	case "ftp", "file", "http", "https", "ws", "wss":
   129  		return true
   130  	}
   131  	return false
   132  }
   133  
   134  func clearURLPort(u *url.URL) {
   135  	u.Host = u.Hostname()
   136  }
   137  
   138  func setURLPort(nu *nodeURL, v goja.Value) {
   139  	u := nu.url
   140  	if u.Scheme == "file" {
   141  		return
   142  	}
   143  	portNum, empty := valueToURLPort(v)
   144  	if empty {
   145  		clearURLPort(u)
   146  		return
   147  	}
   148  	if portNum == -1 {
   149  		return
   150  	}
   151  	if isDefaultURLPort(u.Scheme, portNum) {
   152  		clearURLPort(u)
   153  	} else {
   154  		u.Host = u.Hostname() + ":" + strconv.Itoa(portNum)
   155  	}
   156  }
   157  
   158  func (m *urlModule) parseURL(s string, isBase bool) *url.URL {
   159  	u, err := url.Parse(s)
   160  	if err != nil {
   161  		if isBase {
   162  			panic(m.newInvalidURLError(InvalidBaseURL, s))
   163  		} else {
   164  			panic(m.newInvalidURLError(InvalidURL, s))
   165  		}
   166  	}
   167  	if isBase && !u.IsAbs() {
   168  		panic(m.newInvalidURLError(URLNotAbsolute, s))
   169  	}
   170  	if portStr := u.Port(); portStr != "" {
   171  		if port, err := strconv.Atoi(portStr); err != nil || isDefaultURLPort(u.Scheme, port) {
   172  			u.Host = u.Hostname() // Clear port
   173  		}
   174  	}
   175  	m.fixURL(u)
   176  	return u
   177  }
   178  
   179  func fixRawQuery(u *url.URL) {
   180  	if u.RawQuery != "" {
   181  		u.RawQuery = escape(u.RawQuery, &tblEscapeURLQuery, false)
   182  	}
   183  }
   184  
   185  func (m *urlModule) fixURL(u *url.URL) {
   186  	switch u.Scheme {
   187  	case "https", "http", "ftp", "wss", "ws":
   188  		if u.Path == "" {
   189  			u.Path = "/"
   190  		}
   191  		hostname := u.Hostname()
   192  		lh := strings.ToLower(hostname)
   193  		ch, err := idna.Punycode.ToASCII(lh)
   194  		if err != nil {
   195  			panic(m.newInvalidURLError(InvalidHostname, lh))
   196  		}
   197  		if ch != hostname {
   198  			if port := u.Port(); port != "" {
   199  				u.Host = ch + ":" + port
   200  			} else {
   201  				u.Host = ch
   202  			}
   203  		}
   204  	}
   205  	fixRawQuery(u)
   206  }
   207  
   208  func (m *urlModule) createURLPrototype() *goja.Object {
   209  	p := m.r.NewObject()
   210  
   211  	// host
   212  	m.defineURLAccessorProp(p, "host", func(u *nodeURL) interface{} {
   213  		return u.url.Host
   214  	}, func(u *nodeURL, arg goja.Value) {
   215  		host := arg.String()
   216  		if _, err := url.ParseRequestURI(u.url.Scheme + "://" + host); err == nil {
   217  			u.url.Host = host
   218  			m.fixURL(u.url)
   219  		}
   220  	})
   221  
   222  	// hash
   223  	m.defineURLAccessorProp(p, "hash", func(u *nodeURL) interface{} {
   224  		if u.url.Fragment != "" {
   225  			return "#" + u.url.EscapedFragment()
   226  		}
   227  		return ""
   228  	}, func(u *nodeURL, arg goja.Value) {
   229  		h := arg.String()
   230  		if len(h) > 0 && h[0] == '#' {
   231  			h = h[1:]
   232  		}
   233  		u.url.Fragment = h
   234  	})
   235  
   236  	// hostname
   237  	m.defineURLAccessorProp(p, "hostname", func(u *nodeURL) interface{} {
   238  		return strings.Split(u.url.Host, ":")[0]
   239  	}, func(u *nodeURL, arg goja.Value) {
   240  		h := arg.String()
   241  		if strings.IndexByte(h, ':') >= 0 {
   242  			return
   243  		}
   244  		if _, err := url.ParseRequestURI(u.url.Scheme + "://" + h); err == nil {
   245  			if port := u.url.Port(); port != "" {
   246  				u.url.Host = h + ":" + port
   247  			} else {
   248  				u.url.Host = h
   249  			}
   250  			m.fixURL(u.url)
   251  		}
   252  	})
   253  
   254  	// href
   255  	m.defineURLAccessorProp(p, "href", func(u *nodeURL) interface{} {
   256  		return u.String()
   257  	}, func(u *nodeURL, arg goja.Value) {
   258  		u.url = m.parseURL(arg.String(), true)
   259  	})
   260  
   261  	// pathname
   262  	m.defineURLAccessorProp(p, "pathname", func(u *nodeURL) interface{} {
   263  		return u.url.EscapedPath()
   264  	}, func(u *nodeURL, arg goja.Value) {
   265  		p := arg.String()
   266  		if _, err := url.Parse(p); err == nil {
   267  			switch u.url.Scheme {
   268  			case "https", "http", "ftp", "ws", "wss":
   269  				if !strings.HasPrefix(p, "/") {
   270  					p = "/" + p
   271  				}
   272  			}
   273  			u.url.Path = p
   274  		}
   275  	})
   276  
   277  	// origin
   278  	m.defineURLAccessorProp(p, "origin", func(u *nodeURL) interface{} {
   279  		return u.url.Scheme + "://" + u.url.Hostname()
   280  	}, nil)
   281  
   282  	// password
   283  	m.defineURLAccessorProp(p, "password", func(u *nodeURL) interface{} {
   284  		p, _ := u.url.User.Password()
   285  		return p
   286  	}, func(u *nodeURL, arg goja.Value) {
   287  		user := u.url.User
   288  		u.url.User = url.UserPassword(user.Username(), arg.String())
   289  	})
   290  
   291  	// username
   292  	m.defineURLAccessorProp(p, "username", func(u *nodeURL) interface{} {
   293  		return u.url.User.Username()
   294  	}, func(u *nodeURL, arg goja.Value) {
   295  		p, has := u.url.User.Password()
   296  		if !has {
   297  			u.url.User = url.User(arg.String())
   298  		} else {
   299  			u.url.User = url.UserPassword(arg.String(), p)
   300  		}
   301  	})
   302  
   303  	// port
   304  	m.defineURLAccessorProp(p, "port", func(u *nodeURL) interface{} {
   305  		return u.url.Port()
   306  	}, func(u *nodeURL, arg goja.Value) {
   307  		setURLPort(u, arg)
   308  	})
   309  
   310  	// protocol
   311  	m.defineURLAccessorProp(p, "protocol", func(u *nodeURL) interface{} {
   312  		return u.url.Scheme + ":"
   313  	}, func(u *nodeURL, arg goja.Value) {
   314  		s := arg.String()
   315  		pos := strings.IndexByte(s, ':')
   316  		if pos >= 0 {
   317  			s = s[:pos]
   318  		}
   319  		s = strings.ToLower(s)
   320  		if isSpecialProtocol(u.url.Scheme) == isSpecialProtocol(s) {
   321  			if _, err := url.ParseRequestURI(s + "://" + u.url.Host); err == nil {
   322  				u.url.Scheme = s
   323  			}
   324  		}
   325  	})
   326  
   327  	// Search
   328  	m.defineURLAccessorProp(p, "search", func(u *nodeURL) interface{} {
   329  		u.syncSearchParams()
   330  		if u.url.RawQuery != "" {
   331  			return "?" + u.url.RawQuery
   332  		}
   333  		return ""
   334  	}, func(u *nodeURL, arg goja.Value) {
   335  		u.url.RawQuery = arg.String()
   336  		fixRawQuery(u.url)
   337  		if u.searchParams != nil {
   338  			u.searchParams = parseSearchQuery(u.url.RawQuery)
   339  			if u.searchParams == nil {
   340  				u.searchParams = make(searchParams, 0)
   341  			}
   342  		}
   343  	})
   344  
   345  	// search Params
   346  	m.defineURLAccessorProp(p, "searchParams", func(u *nodeURL) interface{} {
   347  		if u.searchParams == nil {
   348  			sp := parseSearchQuery(u.url.RawQuery)
   349  			if sp == nil {
   350  				sp = make(searchParams, 0)
   351  			}
   352  			u.searchParams = sp
   353  		}
   354  		return m.newURLSearchParams((*urlSearchParams)(u))
   355  	}, nil)
   356  
   357  	p.Set("toString", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   358  		u := toURL(m.r, call.This)
   359  		u.syncSearchParams()
   360  		return m.r.ToValue(u.url.String())
   361  	}))
   362  
   363  	p.Set("toJSON", m.r.ToValue(func(call goja.FunctionCall) goja.Value {
   364  		u := toURL(m.r, call.This)
   365  		u.syncSearchParams()
   366  		return m.r.ToValue(u.url.String())
   367  	}))
   368  
   369  	return p
   370  }
   371  
   372  func (m *urlModule) createURLConstructor() goja.Value {
   373  	f := m.r.ToValue(func(call goja.ConstructorCall) *goja.Object {
   374  		var u *url.URL
   375  		if baseArg := call.Argument(1); !goja.IsUndefined(baseArg) {
   376  			base := m.parseURL(baseArg.String(), true)
   377  			ref := m.parseURL(call.Argument(0).String(), false)
   378  			u = base.ResolveReference(ref)
   379  		} else {
   380  			u = m.parseURL(call.Argument(0).String(), true)
   381  		}
   382  		res := m.r.ToValue(&nodeURL{url: u}).(*goja.Object)
   383  		res.SetPrototype(call.This.Prototype())
   384  		return res
   385  	}).(*goja.Object)
   386  
   387  	proto := m.createURLPrototype()
   388  	f.Set("prototype", proto)
   389  	proto.DefineDataProperty("constructor", f, goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
   390  	return f
   391  }
   392  
   393  func (m *urlModule) domainToASCII(domUnicode string) string {
   394  	res, err := idna.ToASCII(domUnicode)
   395  	if err != nil {
   396  		return ""
   397  	}
   398  	return res
   399  }
   400  
   401  func (m *urlModule) domainToUnicode(domASCII string) string {
   402  	res, err := idna.ToUnicode(domASCII)
   403  	if err != nil {
   404  		return ""
   405  	}
   406  	return res
   407  }