1  
     2  
     3  
     4  
     5  package template
     6  
     7  import (
     8  	"fmt"
     9  	"strings"
    10  )
    11  
    12  
    13  
    14  
    15  
    16  
    17  
    18  
    19  
    20  
    21  
    22  
    23  
    24  
    25  
    26  
    27  
    28  
    29  
    30  
    31  
    32  
    33  
    34  func urlFilter(args ...any) string {
    35  	s, t := stringify(args...)
    36  	if t == contentTypeURL {
    37  		return s
    38  	}
    39  	if !isSafeURL(s) {
    40  		return "#" + filterFailsafe
    41  	}
    42  	return s
    43  }
    44  
    45  
    46  
    47  func isSafeURL(s string) bool {
    48  	if protocol, _, ok := strings.Cut(s, ":"); ok && !strings.Contains(protocol, "/") {
    49  		if !strings.EqualFold(protocol, "http") && !strings.EqualFold(protocol, "https") && !strings.EqualFold(protocol, "mailto") {
    50  			return false
    51  		}
    52  	}
    53  	return true
    54  }
    55  
    56  
    57  
    58  func urlEscaper(args ...any) string {
    59  	return urlProcessor(false, args...)
    60  }
    61  
    62  
    63  
    64  
    65  
    66  
    67  func urlNormalizer(args ...any) string {
    68  	return urlProcessor(true, args...)
    69  }
    70  
    71  
    72  
    73  func urlProcessor(norm bool, args ...any) string {
    74  	s, t := stringify(args...)
    75  	if t == contentTypeURL {
    76  		norm = true
    77  	}
    78  	var b strings.Builder
    79  	if processURLOnto(s, norm, &b) {
    80  		return b.String()
    81  	}
    82  	return s
    83  }
    84  
    85  
    86  
    87  func processURLOnto(s string, norm bool, b *strings.Builder) bool {
    88  	b.Grow(len(s) + 16)
    89  	written := 0
    90  	
    91  	
    92  	
    93  	
    94  	
    95  	
    96  	for i, n := 0, len(s); i < n; i++ {
    97  		c := s[i]
    98  		switch c {
    99  		
   100  		
   101  		
   102  		
   103  		
   104  		
   105  		case '!', '#', '$', '&', '*', '+', ',', '/', ':', ';', '=', '?', '@', '[', ']':
   106  			if norm {
   107  				continue
   108  			}
   109  		
   110  		
   111  		
   112  		
   113  		
   114  		case '-', '.', '_', '~':
   115  			continue
   116  		case '%':
   117  			
   118  			if norm && i+2 < len(s) && isHex(s[i+1]) && isHex(s[i+2]) {
   119  				continue
   120  			}
   121  		default:
   122  			
   123  			if 'a' <= c && c <= 'z' {
   124  				continue
   125  			}
   126  			if 'A' <= c && c <= 'Z' {
   127  				continue
   128  			}
   129  			if '0' <= c && c <= '9' {
   130  				continue
   131  			}
   132  		}
   133  		b.WriteString(s[written:i])
   134  		fmt.Fprintf(b, "%%%02x", c)
   135  		written = i + 1
   136  	}
   137  	b.WriteString(s[written:])
   138  	return written != 0
   139  }
   140  
   141  
   142  
   143  func srcsetFilterAndEscaper(args ...any) string {
   144  	s, t := stringify(args...)
   145  	switch t {
   146  	case contentTypeSrcset:
   147  		return s
   148  	case contentTypeURL:
   149  		
   150  		
   151  		var b strings.Builder
   152  		if processURLOnto(s, true, &b) {
   153  			s = b.String()
   154  		}
   155  		
   156  		return strings.ReplaceAll(s, ",", "%2c")
   157  	}
   158  
   159  	var b strings.Builder
   160  	written := 0
   161  	for i := 0; i < len(s); i++ {
   162  		if s[i] == ',' {
   163  			filterSrcsetElement(s, written, i, &b)
   164  			b.WriteString(",")
   165  			written = i + 1
   166  		}
   167  	}
   168  	filterSrcsetElement(s, written, len(s), &b)
   169  	return b.String()
   170  }
   171  
   172  
   173  const htmlSpaceAndASCIIAlnumBytes = "\x00\x36\x00\x00\x01\x00\xff\x03\xfe\xff\xff\x07\xfe\xff\xff\x07"
   174  
   175  
   176  
   177  func isHTMLSpace(c byte) bool {
   178  	return (c <= 0x20) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
   179  }
   180  
   181  func isHTMLSpaceOrASCIIAlnum(c byte) bool {
   182  	return (c < 0x80) && 0 != (htmlSpaceAndASCIIAlnumBytes[c>>3]&(1<<uint(c&0x7)))
   183  }
   184  
   185  func filterSrcsetElement(s string, left int, right int, b *strings.Builder) {
   186  	start := left
   187  	for start < right && isHTMLSpace(s[start]) {
   188  		start++
   189  	}
   190  	end := right
   191  	for i := start; i < right; i++ {
   192  		if isHTMLSpace(s[i]) {
   193  			end = i
   194  			break
   195  		}
   196  	}
   197  	if url := s[start:end]; isSafeURL(url) {
   198  		
   199  		
   200  		metadataOk := true
   201  		for i := end; i < right; i++ {
   202  			if !isHTMLSpaceOrASCIIAlnum(s[i]) {
   203  				metadataOk = false
   204  				break
   205  			}
   206  		}
   207  		if metadataOk {
   208  			b.WriteString(s[left:start])
   209  			processURLOnto(url, true, b)
   210  			b.WriteString(s[end:right])
   211  			return
   212  		}
   213  	}
   214  	b.WriteString("#")
   215  	b.WriteString(filterFailsafe)
   216  }
   217  
View as plain text