snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
265
server/public/shared/markdown/autolink.go
Normal file
265
server/public/shared/markdown/autolink.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Based off of extensions/autolink.c from https://github.com/github/cmark
|
||||
|
||||
var (
|
||||
DefaultURLSchemes = []string{"http", "https", "ftp", "mailto", "tel"}
|
||||
wwwAutoLinkRegex = regexp.MustCompile(`^www\d{0,3}\.`)
|
||||
)
|
||||
|
||||
// Given a string with a w at the given position, tries to parse and return a range containing a www link.
|
||||
// if one exists. If the text at the given position isn't a link, returns an empty string. Equivalent to
|
||||
// www_match from the reference code.
|
||||
func parseWWWAutolink(data string, position int) (Range, bool) {
|
||||
// Check that this isn't part of another word
|
||||
if position > 1 {
|
||||
prevChar := data[position-1]
|
||||
|
||||
if !isWhitespaceByte(prevChar) && !isAllowedBeforeWWWLink(prevChar) {
|
||||
return Range{}, false
|
||||
}
|
||||
}
|
||||
|
||||
// Check that this starts with www
|
||||
if len(data)-position < 4 || !wwwAutoLinkRegex.MatchString(data[position:]) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end := checkDomain(data[position:], false)
|
||||
if end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end += position
|
||||
|
||||
// Grab all text until the end of the string or the next whitespace character
|
||||
for end < len(data) && !isWhitespaceByte(data[end]) {
|
||||
end += 1
|
||||
}
|
||||
|
||||
// Trim trailing punctuation
|
||||
end = trimTrailingCharactersFromLink(data, position, end)
|
||||
if position == end {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
return Range{position, end}, true
|
||||
}
|
||||
|
||||
func isAllowedBeforeWWWLink(c byte) bool {
|
||||
switch c {
|
||||
case '*', '_', '~', ')', '<', '(', '>':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Given a string with a : at the given position, tried to parse and return a range containing a URL scheme
|
||||
// if one exists. If the text around the given position isn't a link, returns an empty string. Equivalent to
|
||||
// url_match from the reference code.
|
||||
func parseURLAutolink(data string, position int) (Range, bool) {
|
||||
// Check that a :// exists. This doesn't match the clients that treat the slashes as optional.
|
||||
if len(data)-position < 4 || data[position+1] != '/' || data[position+2] != '/' {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
start := position - 1
|
||||
for start > 0 && isAlphanumericByte(data[start-1]) {
|
||||
start -= 1
|
||||
}
|
||||
|
||||
if start < 0 || position >= len(data) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
// Ensure that the URL scheme is allowed and that at least one character after the scheme is valid.
|
||||
scheme := data[start:position]
|
||||
if !isSchemeAllowed(scheme) || !isValidHostCharacter(data[position+3:]) {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end := checkDomain(data[position+3:], true)
|
||||
if end == 0 {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
end += position
|
||||
|
||||
// Grab all text until the end of the string or the next whitespace character
|
||||
for end < len(data) && !isWhitespaceByte(data[end]) {
|
||||
end += 1
|
||||
}
|
||||
|
||||
// Trim trailing punctuation
|
||||
end = trimTrailingCharactersFromLink(data, start, end)
|
||||
if start == end {
|
||||
return Range{}, false
|
||||
}
|
||||
|
||||
return Range{start, end}, true
|
||||
}
|
||||
|
||||
func isSchemeAllowed(scheme string) bool {
|
||||
// Note that this doesn't support the custom URL schemes implemented by the client
|
||||
for _, allowed := range DefaultURLSchemes {
|
||||
if strings.EqualFold(allowed, scheme) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Given a string starting with a URL, returns the number of valid characters that make up the URL's domain.
|
||||
// Returns 0 if the string doesn't start with a domain name. allowShort determines whether or not the domain
|
||||
// needs to contain a period to be considered valid. Equivalent to check_domain from the reference code.
|
||||
func checkDomain(data string, allowShort bool) int {
|
||||
foundUnderscore := false
|
||||
foundPeriod := false
|
||||
|
||||
i := 1
|
||||
for ; i < len(data)-1; i++ {
|
||||
if data[i] == '_' {
|
||||
foundUnderscore = true
|
||||
break
|
||||
} else if data[i] == '.' {
|
||||
foundPeriod = true
|
||||
} else if !isValidHostCharacter(data[i:]) && data[i] != '-' {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundUnderscore {
|
||||
return 0
|
||||
}
|
||||
|
||||
if allowShort {
|
||||
// If allowShort is set, accept any string of valid domain characters
|
||||
return i
|
||||
}
|
||||
|
||||
// If allowShort isn't set, a valid domain just requires at least a single period. Note that this
|
||||
// logic isn't entirely necessary because we already know the string starts with "www." when
|
||||
// this is called from parseWWWAutolink
|
||||
if foundPeriod {
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Returns true if the provided link starts with a valid character for a domain name. Equivalent to
|
||||
// is_valid_hostchar from the reference code.
|
||||
func isValidHostCharacter(link string) bool {
|
||||
c, _ := utf8.DecodeRuneInString(link)
|
||||
if c == utf8.RuneError {
|
||||
return false
|
||||
}
|
||||
|
||||
return !unicode.IsSpace(c) && !unicode.IsPunct(c)
|
||||
}
|
||||
|
||||
// Removes any trailing characters such as punctuation or stray brackets that shouldn't be part of the link.
|
||||
// Returns a new end position for the link. Equivalent to autolink_delim from the reference code.
|
||||
func trimTrailingCharactersFromLink(markdown string, start int, end int) int {
|
||||
runes := []rune(markdown[start:end])
|
||||
linkEnd := len(runes)
|
||||
|
||||
// Cut off the link before an angle bracket if it contains one
|
||||
for i, c := range runes {
|
||||
if c == '<' || c == '>' {
|
||||
linkEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
numClosing := 0
|
||||
numOpening := 0
|
||||
for i := range linkEnd {
|
||||
if runes[i] == '(' {
|
||||
numOpening += 1
|
||||
} else if runes[i] == ')' {
|
||||
numClosing += 1
|
||||
}
|
||||
}
|
||||
|
||||
trimLinkEnd := func(newEnd int) {
|
||||
for i := newEnd; i < linkEnd; i++ {
|
||||
if runes[i] == '(' {
|
||||
numOpening -= 1
|
||||
} else if runes[i] == ')' {
|
||||
numClosing -= 1
|
||||
}
|
||||
}
|
||||
linkEnd = newEnd
|
||||
}
|
||||
|
||||
for linkEnd > 0 {
|
||||
c := runes[linkEnd-1]
|
||||
|
||||
if !canEndAutolink(c) {
|
||||
// Trim trailing quotes, periods, etc
|
||||
trimLinkEnd(linkEnd - 1)
|
||||
} else if c == ';' {
|
||||
// Trim a trailing HTML entity
|
||||
newEnd := linkEnd - 2
|
||||
|
||||
for newEnd > 0 && ((runes[newEnd] >= 'a' && runes[newEnd] <= 'z') || (runes[newEnd] >= 'A' && runes[newEnd] <= 'Z')) {
|
||||
newEnd -= 1
|
||||
}
|
||||
|
||||
if newEnd < linkEnd-2 && runes[newEnd] == '&' {
|
||||
trimLinkEnd(newEnd)
|
||||
} else {
|
||||
// This isn't actually an HTML entity, so just trim the semicolon
|
||||
trimLinkEnd(linkEnd - 1)
|
||||
}
|
||||
} else if c == ')' {
|
||||
// Only allow an autolink ending with a bracket if that bracket is part of a matching pair of brackets.
|
||||
// If there are more closing brackets than opening ones, remove the extra bracket
|
||||
|
||||
// Examples (input text => output linked portion):
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_(Electric)
|
||||
// => http://www.pokemon.com/Pikachu_(Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_((Electric)
|
||||
// => http://www.pokemon.com/Pikachu_((Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_(Electric))
|
||||
// => http://www.pokemon.com/Pikachu_(Electric)
|
||||
//
|
||||
// http://www.pokemon.com/Pikachu_((Electric))
|
||||
// => http://www.pokemon.com/Pikachu_((Electric))
|
||||
|
||||
if numClosing <= numOpening {
|
||||
// There's fewer or equal closing brackets, so we've found the end of the link
|
||||
break
|
||||
}
|
||||
|
||||
trimLinkEnd(linkEnd - 1)
|
||||
} else {
|
||||
// There's no special characters at the end of the link, so we're at the end
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return start + len(string(runes[:linkEnd]))
|
||||
}
|
||||
|
||||
func canEndAutolink(c rune) bool {
|
||||
switch c {
|
||||
case '?', '!', '.', ',', ':', '*', '_', '~', '\'', '"':
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user