package common
import (
"bytes"
//"fmt"
"net/url"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
// TODO: Somehow localise these?
var SpaceGap = []byte(" ")
var httpProtBytes = []byte("http://")
var InvalidURL = []byte("[Invalid URL]")
var InvalidTopic = []byte("[Invalid Topic]")
var InvalidProfile = []byte("[Invalid Profile]")
var InvalidForum = []byte("[Invalid Forum]")
var unknownMedia = []byte("[Unknown Media]")
var URLOpen = []byte("")
var bytesSinglequote = []byte("'")
var bytesGreaterthan = []byte(">")
var urlMention = []byte(" class='mention'")
var URLClose = []byte("")
var imageOpen = []byte("")
var attachOpen = []byte("Attachment")
var urlPattern = `(?s)([ {1}])((http|https|ftp|mailto)*)(:{??)\/\/([\.a-zA-Z\/]+)([ {1}])`
var urlReg *regexp.Regexp
func init() {
urlReg = regexp.MustCompile(urlPattern)
}
// TODO: Write a test for this
func shortcodeToUnicode(msg string) string {
//re := regexp.MustCompile(":(.):")
msg = strings.Replace(msg, ":grinning:", "đ", -1)
msg = strings.Replace(msg, ":grin:", "đ", -1)
msg = strings.Replace(msg, ":joy:", "đ", -1)
msg = strings.Replace(msg, ":rofl:", "đ¤Ŗ", -1)
msg = strings.Replace(msg, ":smiley:", "đ", -1)
msg = strings.Replace(msg, ":smile:", "đ", -1)
msg = strings.Replace(msg, ":sweat_smile:", "đ
", -1)
msg = strings.Replace(msg, ":laughing:", "đ", -1)
msg = strings.Replace(msg, ":satisfied:", "đ", -1)
msg = strings.Replace(msg, ":wink:", "đ", -1)
msg = strings.Replace(msg, ":blush:", "đ", -1)
msg = strings.Replace(msg, ":yum:", "đ", -1)
msg = strings.Replace(msg, ":sunglasses:", "đ", -1)
msg = strings.Replace(msg, ":heart_eyes:", "đ", -1)
msg = strings.Replace(msg, ":kissing_heart:", "đ", -1)
msg = strings.Replace(msg, ":kissing:", "đ", -1)
msg = strings.Replace(msg, ":kissing_smiling_eyes:", "đ", -1)
msg = strings.Replace(msg, ":kissing_closed_eyes:", "đ", -1)
msg = strings.Replace(msg, ":relaxed:", "âēī¸", -1)
msg = strings.Replace(msg, ":slight_smile:", "đ", -1)
msg = strings.Replace(msg, ":hugging:", "đ¤", -1)
msg = strings.Replace(msg, ":thinking:", "đ¤", -1)
msg = strings.Replace(msg, ":neutral_face:", "đ", -1)
msg = strings.Replace(msg, ":expressionless:", "đ", -1)
msg = strings.Replace(msg, ":no_mouth:", "đļ", -1)
msg = strings.Replace(msg, ":rolling_eyes:", "đ", -1)
msg = strings.Replace(msg, ":smirk:", "đ", -1)
msg = strings.Replace(msg, ":persevere:", "đŖ", -1)
msg = strings.Replace(msg, ":disappointed_relieved:", "đĨ", -1)
msg = strings.Replace(msg, ":open_mouth:", "đŽ", -1)
msg = strings.Replace(msg, ":zipper_mouth:", "đ¤", -1)
msg = strings.Replace(msg, ":hushed:", "đ¯", -1)
msg = strings.Replace(msg, ":sleepy:", "đĒ", -1)
msg = strings.Replace(msg, ":tired_face:", "đĢ", -1)
msg = strings.Replace(msg, ":sleeping:", "đ´", -1)
msg = strings.Replace(msg, ":relieved:", "đ", -1)
msg = strings.Replace(msg, ":nerd:", "đ¤", -1)
msg = strings.Replace(msg, ":stuck_out_tongue:", "đ", -1)
msg = strings.Replace(msg, ":worried:", "đ", -1)
msg = strings.Replace(msg, ":drooling_face:", "đ¤¤", -1)
msg = strings.Replace(msg, ":disappointed:", "đ", -1)
msg = strings.Replace(msg, ":astonished:", "đ˛", -1)
msg = strings.Replace(msg, ":slight_frown:", "đ", -1)
msg = strings.Replace(msg, ":skull_crossbones:", "â ī¸", -1)
msg = strings.Replace(msg, ":skull:", "đ", -1)
msg = strings.Replace(msg, ":point_up:", "âī¸", -1)
msg = strings.Replace(msg, ":v:", "âī¸ī¸", -1)
msg = strings.Replace(msg, ":writing_hand:", "âī¸", -1)
msg = strings.Replace(msg, ":heart:", "â¤ī¸ī¸", -1)
msg = strings.Replace(msg, ":heart_exclamation:", "âŖī¸", -1)
msg = strings.Replace(msg, ":hotsprings:", "â¨ī¸", -1)
msg = strings.Replace(msg, ":airplane:", "âī¸ī¸", -1)
msg = strings.Replace(msg, ":hourglass:", "â", -1)
msg = strings.Replace(msg, ":watch:", "â", -1)
msg = strings.Replace(msg, ":comet:", "âī¸", -1)
msg = strings.Replace(msg, ":snowflake:", "âī¸", -1)
msg = strings.Replace(msg, ":cloud:", "âī¸", -1)
msg = strings.Replace(msg, ":sunny:", "âī¸", -1)
msg = strings.Replace(msg, ":spades:", "â ī¸", -1)
msg = strings.Replace(msg, ":hearts:", "âĨī¸ī¸", -1)
msg = strings.Replace(msg, ":diamonds:", "âĻī¸", -1)
msg = strings.Replace(msg, ":clubs:", "âŖī¸", -1)
msg = strings.Replace(msg, ":phone:", "âī¸", -1)
msg = strings.Replace(msg, ":telephone:", "âī¸", -1)
msg = strings.Replace(msg, ":biohazard:", "âŖī¸", -1)
msg = strings.Replace(msg, ":radioactive:", "âĸī¸", -1)
msg = strings.Replace(msg, ":scissors:", "âī¸", -1)
msg = strings.Replace(msg, ":arrow_upper_right:", "âī¸", -1)
msg = strings.Replace(msg, ":arrow_right:", "âĄī¸", -1)
msg = strings.Replace(msg, ":arrow_lower_right:", "âī¸", -1)
msg = strings.Replace(msg, ":arrow_lower_left:", "âī¸", -1)
msg = strings.Replace(msg, ":arrow_upper_left:", "âī¸", -1)
msg = strings.Replace(msg, ":arrow_up_down:", "âī¸", -1)
msg = strings.Replace(msg, ":left_right_arrow:", "âī¸", -1)
msg = strings.Replace(msg, ":leftwards_arrow_with_hook:", "âŠī¸", -1)
msg = strings.Replace(msg, ":arrow_right_hook:", "âĒī¸", -1)
msg = strings.Replace(msg, ":arrow_forward:", "âļī¸", -1)
msg = strings.Replace(msg, ":arrow_backward:", "âī¸", -1)
msg = strings.Replace(msg, ":female:", "âī¸", -1)
msg = strings.Replace(msg, ":male:", "âī¸", -1)
msg = strings.Replace(msg, ":ballot_box_with_check:", "âī¸", -1)
msg = strings.Replace(msg, ":heavy_check_mark:", "âī¸ī¸", -1)
msg = strings.Replace(msg, ":heavy_multiplication_x:", "âī¸", -1)
msg = strings.Replace(msg, ":pisces:", "â", -1)
msg = strings.Replace(msg, ":aquarius:", "â", -1)
msg = strings.Replace(msg, ":capricorn:", "â", -1)
msg = strings.Replace(msg, ":sagittarius:", "â", -1)
msg = strings.Replace(msg, ":scorpius:", "â", -1)
msg = strings.Replace(msg, ":libra:", "â", -1)
msg = strings.Replace(msg, ":virgo:", "â", -1)
msg = strings.Replace(msg, ":leo:", "â", -1)
msg = strings.Replace(msg, ":cancer:", "â", -1)
msg = strings.Replace(msg, ":gemini:", "â", -1)
msg = strings.Replace(msg, ":taurus:", "â", -1)
msg = strings.Replace(msg, ":aries:", "â", -1)
msg = strings.Replace(msg, ":peace:", "âŽī¸", -1)
msg = strings.Replace(msg, ":eight_spoked_asterisk:", "âŗī¸", -1)
msg = strings.Replace(msg, ":eight_pointed_black_star:", "â´ī¸", -1)
msg = strings.Replace(msg, ":snowman2:", "âī¸", -1)
msg = strings.Replace(msg, ":umbrella2:", "âī¸", -1)
msg = strings.Replace(msg, ":pencil2:", "âī¸", -1)
msg = strings.Replace(msg, ":black_nib:", "âī¸", -1)
msg = strings.Replace(msg, ":email:", "âī¸", -1)
msg = strings.Replace(msg, ":envelope:", "âī¸", -1)
msg = strings.Replace(msg, ":keyboard:", "â¨ī¸", -1)
msg = strings.Replace(msg, ":white_small_square:", "âĢī¸", -1)
msg = strings.Replace(msg, ":black_small_square:", "âĒī¸", -1)
msg = strings.Replace(msg, ":secret:", "ãī¸", -1)
msg = strings.Replace(msg, ":congratulations:", "ãī¸", -1)
msg = strings.Replace(msg, ":m:", "âī¸", -1)
msg = strings.Replace(msg, ":tm:", "âĸī¸ī¸", -1)
msg = strings.Replace(msg, ":registered:", "ÂŽī¸", -1)
msg = strings.Replace(msg, ":copyright:", "Šī¸", -1)
msg = strings.Replace(msg, ":wavy_dash:", "ã°ī¸", -1)
msg = strings.Replace(msg, ":bangbang:", "âŧī¸", -1)
msg = strings.Replace(msg, ":sparkle:", "âī¸", -1)
msg = strings.Replace(msg, ":star_of_david:", "âĄī¸", -1)
msg = strings.Replace(msg, ":wheel_of_dharma:", "â¸ī¸", -1)
msg = strings.Replace(msg, ":yin_yang:", "â¯ī¸", -1)
msg = strings.Replace(msg, ":cross:", "âī¸", -1)
msg = strings.Replace(msg, ":orthodox_cross:", "âĻī¸", -1)
msg = strings.Replace(msg, ":star_and_crescent:", "âĒī¸", -1)
msg = strings.Replace(msg, ":frowning2:", "âšī¸", -1)
msg = strings.Replace(msg, ":information_source:", "âšī¸", -1)
msg = strings.Replace(msg, ":interrobang:", "âī¸", -1)
return msg
}
type TagToAction struct {
Suffix string
Do func(*TagToAction, bool, int, []rune) (int, string) // func(tagToAction,open,i,runes) (newI, output)
Depth int // For use by Do
PartialMode bool
}
// TODO: Write a test for this
func tryStepForward(i int, step int, runes []rune) (int, bool) {
i += step
if i < len(runes) {
return i, true
}
return i - step, false
}
// TODO: Write a test for this
func tryStepBackward(i int, step int, runes []rune) (int, bool) {
if i == 0 {
return i, false
}
return i - 1, true
}
// TODO: Preparse Markdown and normalize it into HTML?
func PreparseMessage(msg string) string {
// TODO: Kick this check down a level into SanitiseBody?
if !utf8.ValidString(msg) {
return ""
}
msg = strings.Replace(msg, "
", "\n\n", -1)
msg = strings.Replace(msg, "
", "\n\n", -1)
msg = strings.Replace(msg, "
", "", -1)
// TODO: Make this looser by moving it to the reverse HTML parser?
msg = strings.Replace(msg, "
", "\n\n", -1)
msg = strings.Replace(msg, "
", "\n\n", -1) // XHTML style
msg = strings.Replace(msg, " ", "", -1)
msg = strings.Replace(msg, "\r", "", -1) // Windows artifact
//msg = strings.Replace(msg, "\n\n\n\n", "\n\n\n", -1)
msg = GetHookTable().Sshook("preparse_preassign", msg)
// There are a few useful cases for having spaces, but I'd like to stop the WYSIWYG from inserting random lines here and there
msg = SanitiseBody(msg)
var runes = []rune(msg)
msg = ""
// TODO: We can maybe reduce the size of this by using an offset?
// TODO: Move some of these closures out of this function to make things a little more efficient
var allowedTags = [][]string{
'e': []string{"m"},
's': []string{"", "trong", "pan"},
'd': []string{"el"},
'u': []string{""},
'b': []string{"", "lockquote"},
'i': []string{""},
'h': []string{"1", "2", "3"},
//'p': []string{""},
'g': []string{""}, // Quick and dirty fix for Grammarly
}
var buildLitMatch = func(tag string) func(*TagToAction, bool, int, []rune) (int, string) {
return func(action *TagToAction, open bool, _ int, _ []rune) (int, string) {
if open {
action.Depth++
return -1, "<" + tag + ">"
}
if action.Depth <= 0 {
return -1, ""
}
action.Depth--
return -1, "" + tag + ">"
}
}
var tagToAction = [][]*TagToAction{
'e': []*TagToAction{&TagToAction{"m", buildLitMatch("em"), 0, false}},
's': []*TagToAction{
&TagToAction{"", buildLitMatch("del"), 0, false},
&TagToAction{"trong", buildLitMatch("strong"), 0, false},
// Hides the span tags Trumbowyg loves blasting out randomly
&TagToAction{"pan", func(act *TagToAction, open bool, i int, runes []rune) (int, string) {
if open {
act.Depth++
//fmt.Println("skipping attributes")
for ; i < len(runes); i++ {
if runes[i] == '&' && peekMatch(i, "gt;", runes) {
//fmt.Println("found tag exit")
return i + 3, " "
}
}
return -1, " "
}
if act.Depth <= 0 {
return -1, " "
}
act.Depth--
return -1, " "
}, 0, true},
},
'd': []*TagToAction{&TagToAction{"el", buildLitMatch("del"), 0, false}},
'u': []*TagToAction{&TagToAction{"", buildLitMatch("u"), 0, false}},
'b': []*TagToAction{
&TagToAction{"", buildLitMatch("strong"), 0, false},
&TagToAction{"lockquote", buildLitMatch("blockquote"), 0, false},
},
'i': []*TagToAction{&TagToAction{"", buildLitMatch("em"), 0, false}},
'h': []*TagToAction{
&TagToAction{"1", buildLitMatch("h2"), 0, false},
&TagToAction{"2", buildLitMatch("h3"), 0, false},
&TagToAction{"3", buildLitMatch("h4"), 0, false},
},
//'p': []*TagToAction{&TagToAction{"", buildLitMatch2("\n\n", ""), 0, false}},
'g': []*TagToAction{
&TagToAction{"", func(act *TagToAction, open bool, i int, runes []rune) (int, string) {
if open {
act.Depth++
//fmt.Println("skipping attributes")
for ; i < len(runes); i++ {
if runes[i] == '&' && peekMatch(i, "gt;", runes) {
//fmt.Println("found tag exit")
return i + 3, " "
}
}
return -1, " "
}
if act.Depth <= 0 {
return -1, " "
}
act.Depth--
return -1, " "
}, 0, true},
},
}
// TODO: Implement a less literal parser
for i := 0; i < len(runes); i++ {
char := runes[i]
// TODO: Make the slashes escapable too in case someone means to use a literaly slash, maybe as an example of how to escape elements?
if char == '\\' {
if peekMatch(i, "<", runes) {
msg += "&"
i++
}
} else if char == '&' && peekMatch(i, "lt;", runes) {
var ok bool
i, ok = tryStepForward(i, 4, runes)
if !ok {
msg += "<"
break
}
char := runes[i]
if int(char) >= len(allowedTags) {
//fmt.Println("sentinel char out of bounds")
msg += "&"
i -= 4
continue
}
var closeTag bool
if char == '/' {
//fmt.Println("found close tag")
i, ok = tryStepForward(i, 1, runes)
if !ok {
msg += "</"
break
}
char = runes[i]
closeTag = true
}
tags := allowedTags[char]
if len(tags) == 0 {
//fmt.Println("couldn't find char in allowedTags")
if closeTag {
//msg += "</"
msg += "&"
i -= 5
} else {
msg += "&"
i -= 4
}
continue
}
// TODO: Scan through tags and make sure the suffix is present to reduce the number of false positives which hit the loop below
//fmt.Printf("tags: %+v\n", tags)
var newI = -1
var out string
toActionList := tagToAction[char]
for _, toAction := range toActionList {
// TODO: Optimise this, maybe with goto or a function call to avoid scanning the text twice?
if (toAction.PartialMode && !closeTag && peekMatch(i, toAction.Suffix, runes)) || peekMatch(i, toAction.Suffix+">", runes) {
newI, out = toAction.Do(toAction, !closeTag, i, runes)
if newI != -1 {
i = newI
} else if out != "" {
i += len(toAction.Suffix + ">")
}
break
}
}
if out == "" {
msg += "&"
if closeTag {
i -= 5
} else {
i -= 4
}
} else if out != " " {
msg += out
}
} else if char == '@' && (i == 0 || runes[i-1] < 33) {
// TODO: Handle usernames containing spaces, maybe in the front-end with AJAX
// Do not mention-ify ridiculously long things
var ok bool
i, ok = tryStepForward(i, 1, runes)
if !ok {
msg += "@"
continue
}
start := i
for j := 0; i < len(runes) && j < Config.MaxUsernameLength; j++ {
cchar := runes[i]
if cchar < 33 {
break
}
i++
}
username := string(runes[start:i])
if username == "" {
msg += "@"
i = start - 1
continue
}
user, err := Users.GetByName(username)
if err != nil {
if err != ErrNoRows {
LogError(err)
}
msg += "@"
i = start - 1
continue
}
msg += "@" + strconv.Itoa(user.ID)
i--
} else {
msg += string(char)
}
}
for _, actionList := range tagToAction {
for _, toAction := range actionList {
if toAction.Depth > 0 {
for ; toAction.Depth > 0; toAction.Depth-- {
_, out := toAction.Do(toAction, false, len(runes), runes)
if out != "" {
msg += out
}
}
}
}
}
return strings.TrimSpace(shortcodeToUnicode(msg))
}
// TODO: Test this
// TODO: Use this elsewhere in the parser?
func peek(cur int, skip int, runes []rune) rune {
if (cur + skip) < len(runes) {
return runes[cur+skip]
}
return 0 // null byte
}
// TODO: Test this
func peekMatch(cur int, phrase string, runes []rune) bool {
if cur+len(phrase) > len(runes) {
return false
}
for i, char := range phrase {
if cur+i+1 >= len(runes) {
return false
}
if runes[cur+i+1] != char {
return false
}
}
return true
}
// ! Not concurrency safe
func AddHashLinkType(prefix string, handler func(*strings.Builder, string, *int)) {
// There can only be one hash link type starting with a specific character at the moment
hashType := hashLinkTypes[prefix[0]]
if hashType != "" {
return
}
hashLinkMap[prefix] = handler
hashLinkTypes[prefix[0]] = prefix
}
func WriteURL(sb *strings.Builder, url string, label string) {
sb.Write(URLOpen)
sb.WriteString(url)
sb.Write(URLOpen2)
sb.WriteString(label)
sb.Write(URLClose)
}
var hashLinkTypes = []string{'t': "tid-", 'r': "rid-", 'f': "fid-"}
var hashLinkMap = map[string]func(*strings.Builder, string, *int){
"tid-": func(sb *strings.Builder, msg string, i *int) {
tid, intLen := CoerceIntString(msg[*i:])
*i += intLen
topic, err := Topics.Get(tid)
if err != nil || !Forums.Exists(topic.ParentID) {
sb.Write(InvalidTopic)
return
}
WriteURL(sb, BuildTopicURL("", tid), "#tid-"+strconv.Itoa(tid))
},
"rid-": func(sb *strings.Builder, msg string, i *int) {
rid, intLen := CoerceIntString(msg[*i:])
*i += intLen
topic, err := TopicByReplyID(rid)
if err != nil || !Forums.Exists(topic.ParentID) {
sb.Write(InvalidTopic)
return
}
// TODO: Send the user to the right page and post not just the right topic?
WriteURL(sb, BuildTopicURL("", topic.ID), "#rid-"+strconv.Itoa(rid))
},
"fid-": func(sb *strings.Builder, msg string, i *int) {
fid, intLen := CoerceIntString(msg[*i:])
*i += intLen
if !Forums.Exists(fid) {
sb.Write(InvalidForum)
return
}
WriteURL(sb, BuildForumURL("", fid), "#fid-"+strconv.Itoa(fid))
},
// TODO: Forum Shortcode Link
}
// TODO: Write a test for this
// TODO: We need a lot more hooks here. E.g. To add custom media types and handlers.
// TODO: Use templates to reduce the amount of boilerplate?
func ParseMessage(msg string, sectionID int, sectionType string /*, user User*/) string {
// TODO: Word boundary detection for these to avoid mangling code
msg = strings.Replace(msg, ":)", "đ", -1)
msg = strings.Replace(msg, ":(", "đ", -1)
msg = strings.Replace(msg, ":D", "đ", -1)
msg = strings.Replace(msg, ":P", "đ", -1)
msg = strings.Replace(msg, ":O", "đ˛", -1)
msg = strings.Replace(msg, ":p", "đ", -1)
msg = strings.Replace(msg, ":o", "đ˛", -1)
msg = strings.Replace(msg, ";)", "đ", -1)
// Word filter list. E.g. Swear words and other things the admins don't like
wordFilters, err := WordFilters.GetAll()
if err != nil {
LogError(err)
return ""
}
for _, filter := range wordFilters {
msg = strings.Replace(msg, filter.Find, filter.Replacement, -1)
}
// Search for URLs, mentions and hashlinks in the messages...
var sb strings.Builder
var lastItem = 0
var i = 0
//var c bool
//fmt.Println("msg:", "'"+msg+"'")
for ; len(msg) > i; i++ {
//fmt.Printf("msg[%d]: %s\n",i,string(msg[i]))
if (i == 0 && (msg[0] > 32)) || (len(msg) > (i+1) && (msg[i] < 33) && (msg[i+1] > 32)) {
//fmt.Println("s1")
if (i != 0) || msg[i] < 33 {
i++
}
if len(msg) <= (i+1) {
break
}
//fmt.Println("s2")
if msg[i] == '#' {
//fmt.Println("msg[i+1]:", msg[i+1])
//fmt.Println("string(msg[i+1]):", string(msg[i+1]))
hashType := hashLinkTypes[msg[i+1]]
if hashType == "" {
//fmt.Println("uh1")
sb.WriteString(msg[lastItem:i])
i++
lastItem = i
continue
}
//fmt.Println("hashType:", hashType)
if len(msg) <= (i + len(hashType) + 1) {
sb.WriteString(msg[lastItem:i])
lastItem = i
continue
}
if msg[i+1:i+len(hashType)+1] != hashType {
continue
}
//fmt.Println("msg[lastItem:i]:", msg[lastItem:i])
sb.WriteString(msg[lastItem:i])
i += len(hashType) + 1
hashLinkMap[hashType](&sb, msg, &i)
lastItem = i
i--
} else if msg[i] == '@' {
sb.WriteString(msg[lastItem:i])
i++
start := i
uid, intLen := CoerceIntString(msg[start:])
i += intLen
menUser, err := Users.Get(uid)
if err != nil {
sb.Write(InvalidProfile)
lastItem = i
i--
continue
}
sb.Write(URLOpen)
sb.WriteString(menUser.Link)
sb.Write(bytesSinglequote)
sb.Write(urlMention)
sb.Write(bytesGreaterthan)
sb.WriteString("@" + menUser.Name)
sb.Write(URLClose)
lastItem = i
i--
} else if msg[i] == 'h' || msg[i] == 'f' || msg[i] == 'g' || msg[i] == '/' {
//fmt.Println("s3")
if len(msg) > i+3 && msg[i+1] == 't' && msg[i+2] == 't' && msg[i+3] == 'p' {
if len(msg) > i+6 && msg[i+4] == 's' && msg[i+5] == ':' && msg[i+6] == '/' {
// Do nothing
} else if len(msg) > i+5 && msg[i+4] == ':' && msg[i+5] == '/' {
// Do nothing
} else {
continue
}
} else if len(msg) > i+4 && msg[i+1] == 't' && msg[i+2] == 'p' && msg[i+3] == ':' && msg[i+4] == '/' {
// Do nothing
} else if len(msg) > i+4 && msg[i+1] == 'i' && msg[i+2] == 't' && msg[i+3] == ':' && msg[i+4] == '/' {
// Do nothing
} else if msg[i+1] == '/' {
// Do nothing
} else {
continue
}
//fmt.Println("p1:",i)
sb.WriteString(msg[lastItem:i])
urlLen, ok := PartialURLStringLen(msg[i:])
if len(msg) < i+urlLen {
//fmt.Println("o1")
sb.Write(InvalidURL)
i += len(msg) - 1
lastItem = i
break
}
//fmt.Println("msg[i:i+urlLen]:", "'"+msg[i:i+urlLen]+"'")
if !ok {
//fmt.Printf("o2: i = %d; i+urlLen = %d\n",i,i+urlLen)
sb.Write(InvalidURL)
i += urlLen
lastItem = i
i--
continue
}
media, ok := parseMediaString(msg[i:i+urlLen])
if !ok {
//fmt.Println("o3")
sb.Write(InvalidURL)
i += urlLen
lastItem = i
continue
}
//fmt.Println("p2")
var addImage = func(url string) {
sb.Write(imageOpen)
sb.WriteString(url)
sb.Write(imageOpen2)
sb.WriteString(url)
sb.Write(imageClose)
i += urlLen
lastItem = i
}
// TODO: Reduce the amount of code duplication
if media.Type == "attach" {
addImage(media.URL + "?sectionID=" + strconv.Itoa(sectionID) + "§ionType=" + sectionType)
continue
} else if media.Type == "image" {
addImage(media.URL)
continue
} else if media.Type == "aother" {
sb.Write(attachOpen)
sb.WriteString(media.URL + "?sectionID=" + strconv.Itoa(sectionID) + "§ionType=" + sectionType)
sb.Write(attachClose)
i += urlLen
lastItem = i
continue
} else if media.Type == "raw" {
sb.WriteString(media.Body)
i += urlLen
lastItem = i
continue
} else if media.Type != "" {
sb.Write(unknownMedia)
i += urlLen
continue
}
//fmt.Println("p3")
sb.Write(URLOpen)
sb.WriteString(msg[i : i+urlLen])
sb.Write(URLOpen2)
sb.WriteString(msg[i : i+urlLen])
sb.Write(URLClose)
i += urlLen
lastItem = i
i--
}
}
}
if lastItem != i && sb.Len() != 0 {
/*calclen := len(msg)
if calclen <= lastItem {
calclen = lastItem
}*/
//if i == len(msg) {
sb.WriteString(msg[lastItem:])
/*} else {
sb.WriteString(msg[lastItem:calclen])
}*/
}
if sb.Len() != 0 {
msg = sb.String()
//fmt.Println("sb.String():", "'"+sb.String()+"'")
}
msg = strings.Replace(msg, "\n", "
", -1)
msg = GetHookTable().Sshook("parse_assign", msg)
return msg
}
// 6, 7, 8, 6, 2, 7
// ftp://, http://, https:// git://, //, mailto: (not a URL, just here for length comparison purposes)
// TODO: Write a test for this
func validateURLString(data string) bool {
i := 0
if len(data) >= 6 {
if data[0:6] == "ftp://" || data[0:6] == "git://" {
i = 6
} else if len(data) >= 7 && data[0:7] == "http://" {
i = 7
} else if len(data) >= 8 && data[0:8] == "https://" {
i = 8
}
} else if len(data) >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
}
// ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s.
for ; len(data) > i; i++ {
if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && data[i] != '#' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
return false
}
}
return true
}
// TODO: Write a test for this
func validatedURLBytes(data []byte) (url []byte) {
datalen := len(data)
i := 0
if datalen >= 6 {
if bytes.Equal(data[0:6], []byte("ftp://")) || bytes.Equal(data[0:6], []byte("git://")) {
i = 6
} else if datalen >= 7 && bytes.Equal(data[0:7], httpProtBytes) {
i = 7
} else if datalen >= 8 && bytes.Equal(data[0:8], []byte("https://")) {
i = 8
}
} else if datalen >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
}
// ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s.
for ; datalen > i; i++ {
if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && data[i] != '#' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
return InvalidURL
}
}
url = append(url, data...)
return url
}
// TODO: Write a test for this
func PartialURLString(data string) (url []byte) {
i := 0
end := len(data) - 1
if len(data) >= 6 {
if data[0:6] == "ftp://" || data[0:6] == "git://" {
i = 6
} else if len(data) >= 7 && data[0:7] == "http://" {
i = 7
} else if len(data) >= 8 && data[0:8] == "https://" {
i = 8
}
} else if len(data) >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
}
// ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s.
for ; end >= i; i++ {
if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && data[i] != '#' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
end = i
}
}
url = append(url, []byte(data[0:end])...)
return url
}
// TODO: Write a test for this
func PartialURLStringLen(data string) (int, bool) {
i := 0
if len(data) >= 6 {
//log.Print(string(data[0:5]))
if data[0:6] == "ftp://" || data[0:6] == "git://" {
i = 6
} else if len(data) >= 7 && data[0:7] == "http://" {
i = 7
} else if len(data) >= 8 && data[0:8] == "https://" {
i = 8
}
} else if len(data) >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
}
//fmt.Println("Data Length: ",len(data))
if len(data) < i {
//fmt.Println("e1:",i)
return i+1, false
}
// ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s.
f := i
//fmt.Println("f:",f)
for ; len(data) > i; i++ {
if data[i] < 33 { // space and invisibles
//fmt.Println("e2:",i)
return i, i != f
} else if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && data[i] != '#' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
//log.Print("Bad Character: ", data[i])
//fmt.Println("e3")
return i, false
}
}
//fmt.Println("e4:", i)
/*if data[i-1] < 33 {
return i-1, i != f
}*/
//fmt.Println("e5")
return i, i != f
}
// TODO: Write a test for this
func PartialURLStringLen2(data string) int {
i := 0
if len(data) >= 6 {
//log.Print(string(data[0:5]))
if data[0:6] == "ftp://" || data[0:6] == "git://" {
i = 6
} else if len(data) >= 7 && data[0:7] == "http://" {
i = 7
} else if len(data) >= 8 && data[0:8] == "https://" {
i = 8
}
} else if len(data) >= 2 && data[0] == '/' && data[1] == '/' {
i = 2
}
// ? - There should only be one : and that's only if the URL is on a non-standard port. Same for ?s.
for ; len(data) > i; i++ {
if data[i] != '\\' && data[i] != '_' && data[i] != ':' && data[i] != '?' && data[i] != '&' && data[i] != '=' && data[i] != ';' && data[i] != '@' && data[i] != '#' && !(data[i] > 44 && data[i] < 58) && !(data[i] > 64 && data[i] < 91) && !(data[i] > 96 && data[i] < 123) {
//log.Print("Bad Character: ", data[i])
return i
}
}
//log.Print("Data Length: ",len(data))
return len(data)
}
type MediaEmbed struct {
Type string //image
URL string
Body string
}
// TODO: Write a test for this
func parseMediaString(data string) (media MediaEmbed, ok bool) {
if !validateURLString(data) {
return media, false
}
url, err := url.Parse(data)
if err != nil {
return media, false
}
hostname := url.Hostname()
scheme := url.Scheme
port := url.Port()
query := url.Query()
// TODO: Treat 127.0.0.1 and [::1] as localhost too
var samesite = hostname == "localhost" || hostname == Site.URL
if samesite {
hostname = strings.Split(Site.URL, ":")[0]
// ?- Test this as I'm not sure it'll do what it should. If someone's running SSL on port 80 or non-SSL on port 443 then... Well... They're in far worse trouble than this...
port = Site.Port
if Site.EnableSsl {
scheme = "https"
}
}
if scheme == "" {
scheme = "http"
}
path := url.EscapedPath()
pathFrags := strings.Split(path, "/")
if len(pathFrags) >= 2 {
if samesite && pathFrags[1] == "attachs" && (scheme == "http" || scheme == "https") {
var sport string
// ? - Assumes the sysadmin hasn't mixed up the two standard ports
if port != "443" && port != "80" {
sport = ":" + port
}
media.URL = scheme + "://" + hostname + sport + path
var extarr = strings.Split(path, ".")
if len(extarr) == 0 {
// TODO: Write a unit test for this
return media, false
}
var ext = extarr[len(extarr)-1]
if ImageFileExts.Contains(ext) {
media.Type = "attach"
} else {
media.Type = "aother"
}
return media, true
}
}
// ? - I don't think this hostname will hit every YT domain
// TODO: Make this a more customisable handler rather than hard-coding it in here
if hostname == "www.youtube.com" && path == "/watch" {
video, ok := query["v"]
if ok && len(video) >= 1 && video[0] != "" {
media.Type = "raw"
// TODO: Filter the URL to make sure no nasties end up in there
media.Body = ""
return media, true
}
}
lastFrag := pathFrags[len(pathFrags)-1]
if lastFrag != "" {
// TODO: Write a function for getting the file extension of a string
extarr := strings.Split(lastFrag, ".")
if len(extarr) >= 2 {
ext := extarr[len(extarr)-1]
if ImageFileExts.Contains(ext) {
media.Type = "image"
var sport string
if port != "443" && port != "80" {
sport = ":" + port
}
media.URL = scheme + "://" + hostname + sport + path
return media, true
}
}
}
return media, true
}
// TODO: Write a test for this
func CoerceIntString(data string) (res int, length int) {
if !(data[0] > 47 && data[0] < 58) {
return 0, 1
}
i := 0
for ; len(data) > i; i++ {
if !(data[i] > 47 && data[i] < 58) {
conv, err := strconv.Atoi(data[0:i])
if err != nil {
return 0, i
}
return conv, i
}
}
conv, err := strconv.Atoi(data)
if err != nil {
return 0, i
}
return conv, i
}
// TODO: Write tests for this
// Make sure we reflect changes to this in the JS port in /public/global.js
func Paginate(count int, perPage int, maxPages int) []int {
if count < perPage {
return []int{1}
}
var page int
var out []int
for current := 0; current < count; current += perPage {
page++
out = append(out, page)
if len(out) >= maxPages {
break
}
}
return out
}
// TODO: Write tests for this
// Make sure we reflect changes to this in the JS port in /public/global.js
func PageOffset(count int, page int, perPage int) (int, int, int) {
var offset int
lastPage := LastPage(count, perPage)
if page > 1 {
offset = (perPage * page) - perPage
} else if page == -1 {
page = lastPage
offset = (perPage * page) - perPage
} else {
page = 1
}
// ? - This has been commented out as it created a bug in the user manager where the first user on a page wouldn't be accessible
// We don't want the offset to overflow the slices, if everything's in memory
/*if offset >= (count - 1) {
offset = 0
}*/
return offset, page, lastPage
}
// TODO: Write tests for this
// Make sure we reflect changes to this in the JS port in /public/global.js
func LastPage(count int, perPage int) int {
return (count / perPage) + 1
}