package main import ( //"fmt" "bytes" "html/template" "net/url" "regexp" "strconv" "strings" "sync" "time" ) type HeaderVars struct { NoticeList []string Scripts []string Stylesheets []string Widgets PageWidgets Site *Site Settings map[string]interface{} Themes map[string]Theme // TODO: Use a slice containing every theme instead of the main map for speed ThemeName string ExtData ExtData } // TODO: Add this to routes which don't use templates. E.g. Json APIs. type HeaderLite struct { Site *Site Settings SettingBox ExtData ExtData } type PageWidgets struct { LeftSidebar template.HTML RightSidebar template.HTML } // TODO: Add a ExtDataHolder interface with methods for manipulating the contents? // ? - Could we use a sync.Map instead? type ExtData struct { items map[string]interface{} // Key: pluginname sync.RWMutex } type Page struct { Title string CurrentUser User Header *HeaderVars ItemList []interface{} Something interface{} } type TopicPage struct { Title string CurrentUser User Header *HeaderVars ItemList []ReplyUser Topic TopicUser Page int LastPage int } type TopicsPage struct { Title string CurrentUser User Header *HeaderVars TopicList []*TopicsRow ForumList []Forum DefaultForum int } type ForumPage struct { Title string CurrentUser User Header *HeaderVars ItemList []*TopicsRow Forum *Forum Page int LastPage int } type ForumsPage struct { Title string CurrentUser User Header *HeaderVars ItemList []Forum } type ProfilePage struct { Title string CurrentUser User Header *HeaderVars ItemList []ReplyUser ProfileOwner User } type CreateTopicPage struct { Title string CurrentUser User Header *HeaderVars ItemList []Forum FID int } type IPSearchPage struct { Title string CurrentUser User Header *HeaderVars ItemList map[int]*User IP string } type PanelStats struct { Users int Groups int Forums int Settings int WordFilters int Themes int Reports int } type PanelPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ItemList []interface{} Something interface{} } type GridElement struct { ID string Body string Order int // For future use Class string Background string TextColour string Note string } type PanelDashboardPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats GridItems []GridElement } type PanelThemesPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats PrimaryThemes []Theme VariantThemes []Theme } type PanelUserPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ItemList []User PageList []int Page int LastPage int } type PanelGroupPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ItemList []GroupAdmin PageList []int Page int LastPage int } type PanelEditGroupPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ID int Name string Tag string Rank string DisableRank bool } type GroupForumPermPreset struct { Group *Group Preset string } type PanelEditForumPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ID int Name string Desc string Active bool Preset string Groups []GroupForumPermPreset } /*type NameLangPair struct { Name string LangStr string }*/ type NameLangToggle struct { Name string LangStr string Toggle bool } type PanelEditGroupPermsPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats ID int Name string LocalPerms []NameLangToggle GlobalPerms []NameLangToggle } type backupItem struct { SQLURL string // TODO: Add an easier to parse format here for Gosora to be able to more easily reimport portions of the dump and to strip unneccesary data (e.g. table defs and parsed post data) Timestamp time.Time } type PanelBackupPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats Backups []backupItem } type logItem struct { Action template.HTML IPAddress string DoneAt string } type PanelLogsPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats Logs []logItem PageList []int Page int LastPage int } type PanelDebugPage struct { Title string CurrentUser User Header *HeaderVars Stats PanelStats Uptime string OpenConns int DBAdapter string } type PageSimple struct { Title string Something interface{} } type AreYouSure struct { URL string Message string } 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 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 } func preparseMessage(msg string) string { if sshooks["preparse_preassign"] != nil { msg = runSshook("preparse_preassign", msg) } return shortcodeToUnicode(msg) } // TODO: Write a test for this // TODO: We need a lot more hooks here. E.g. To add custom media types and handlers. func parseMessage(msg string, sectionID int, sectionType string /*, user User*/) string { 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 = url_reg.ReplaceAllString(msg,"$2$3//$4") // Word filter list. E.g. Swear words and other things the admins don't like wordFilters := wordFilterBox.Load().(WordFilterBox) for _, filter := range wordFilters { msg = strings.Replace(msg, filter.Find, filter.Replacement, -1) } // Search for URLs, mentions and hashlinks in the messages... //log.Print("Parser Loop!") var msgbytes = []byte(msg) var outbytes []byte msgbytes = append(msgbytes, spaceGap...) //log.Printf("string(msgbytes) %+v\n", `"`+string(msgbytes)+`"`) var lastItem = 0 var i = 0 for ; len(msgbytes) > (i + 1); i++ { //log.Print("Index:",i) //log.Print("Index Item: ",msgbytes[i]) //log.Print("string(msgbytes[i]): ",string(msgbytes[i])) //log.Print("End Index") if (i == 0 && (msgbytes[0] > 32)) || ((msgbytes[i] < 33) && (msgbytes[i+1] > 32)) { //log.Print("IN") //log.Print(msgbytes[i]) if (i != 0) || msgbytes[i] < 33 { i++ } if msgbytes[i] == '#' { //log.Print("IN #") if bytes.Equal(msgbytes[i+1:i+5], []byte("tid-")) { outbytes = append(outbytes, msgbytes[lastItem:i]...) i += 5 start := i tid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen topic, err := topics.Get(tid) if err != nil || !fstore.Exists(topic.ParentID) { outbytes = append(outbytes, invalidTopic...) lastItem = i continue } outbytes = append(outbytes, urlOpen...) var urlBit = []byte(buildTopicURL("", tid)) outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlOpen2...) var tidBit = []byte("#tid-" + strconv.Itoa(tid)) outbytes = append(outbytes, tidBit...) outbytes = append(outbytes, urlClose...) lastItem = i //log.Print("string(msgbytes) ",string(msgbytes)) //log.Print("msgbytes ",msgbytes) //log.Print(msgbytes[lastItem - 1]) //log.Print(lastItem - 1) //log.Print(msgbytes[lastItem]) //log.Print("lastItem ",lastItem) } else if bytes.Equal(msgbytes[i+1:i+5], []byte("rid-")) { outbytes = append(outbytes, msgbytes[lastItem:i]...) i += 5 start := i rid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen topic, err := getTopicByReply(rid) if err != nil || !fstore.Exists(topic.ParentID) { outbytes = append(outbytes, invalidTopic...) lastItem = i continue } outbytes = append(outbytes, urlOpen...) var urlBit = []byte(buildTopicURL("", topic.ID)) outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlOpen2...) var ridBit = []byte("#rid-" + strconv.Itoa(rid)) outbytes = append(outbytes, ridBit...) outbytes = append(outbytes, urlClose...) lastItem = i } else if bytes.Equal(msgbytes[i+1:i+5], []byte("fid-")) { outbytes = append(outbytes, msgbytes[lastItem:i]...) i += 5 start := i fid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen if !fstore.Exists(fid) { outbytes = append(outbytes, invalidForum...) lastItem = i continue } outbytes = append(outbytes, urlOpen...) var urlBit = []byte(buildForumURL("", fid)) outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, urlOpen2...) var fidBit = []byte("#fid-" + strconv.Itoa(fid)) outbytes = append(outbytes, fidBit...) outbytes = append(outbytes, urlClose...) lastItem = i } else { // TODO: Forum Shortcode Link } } else if msgbytes[i] == '@' { //log.Print("IN @") outbytes = append(outbytes, msgbytes[lastItem:i]...) i++ start := i uid, intLen := coerceIntBytes(msgbytes[start:]) i += intLen menUser, err := users.Get(uid) if err != nil { outbytes = append(outbytes, invalidProfile...) lastItem = i continue } outbytes = append(outbytes, urlOpen...) var urlBit = []byte(menUser.Link) outbytes = append(outbytes, urlBit...) outbytes = append(outbytes, bytesSinglequote...) outbytes = append(outbytes, urlMention...) outbytes = append(outbytes, bytesGreaterthan...) var uidBit = []byte("@" + menUser.Name) outbytes = append(outbytes, uidBit...) outbytes = append(outbytes, urlClose...) lastItem = i //log.Print(string(msgbytes)) //log.Print(msgbytes) //log.Print(msgbytes[lastItem - 1]) //log.Print("lastItem - 1",lastItem - 1) //log.Print("msgbytes[lastItem]",msgbytes[lastItem]) //log.Print("lastItem",lastItem) } else if msgbytes[i] == 'h' || msgbytes[i] == 'f' || msgbytes[i] == 'g' { //log.Print("IN hfg") if msgbytes[i+1] == 't' && msgbytes[i+2] == 't' && msgbytes[i+3] == 'p' { if msgbytes[i+4] == 's' && msgbytes[i+5] == ':' && msgbytes[i+6] == '/' && msgbytes[i+7] == '/' { // Do nothing } else if msgbytes[i+4] == ':' && msgbytes[i+5] == '/' && msgbytes[i+6] == '/' { // Do nothing } else { continue } } else if msgbytes[i+1] == 't' && msgbytes[i+2] == 'p' && msgbytes[i+3] == ':' && msgbytes[i+4] == '/' && msgbytes[i+5] == '/' { // Do nothing } else if msgbytes[i+1] == 'i' && msgbytes[i+2] == 't' && msgbytes[i+3] == ':' && msgbytes[i+4] == '/' && msgbytes[i+5] == '/' { // Do nothing } else { continue } //log.Print("Normal URL") outbytes = append(outbytes, msgbytes[lastItem:i]...) urlLen := partialURLBytesLen(msgbytes[i:]) if msgbytes[i+urlLen] > 32 { // space and invisibles //log.Print("INVALID URL") //log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) //log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen])) //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) outbytes = append(outbytes, invalidURL...) i += urlLen continue } media, ok := parseMediaBytes(msgbytes[i : i+urlLen]) if !ok { outbytes = append(outbytes, invalidURL...) i += urlLen continue } if media.Type == "attach" { outbytes = append(outbytes, imageOpen...) outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) outbytes = append(outbytes, imageOpen2...) outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) outbytes = append(outbytes, imageClose...) i += urlLen lastItem = i continue } else if media.Type == "image" { outbytes = append(outbytes, imageOpen...) outbytes = append(outbytes, []byte(media.URL)...) outbytes = append(outbytes, imageOpen2...) outbytes = append(outbytes, []byte(media.URL)...) outbytes = append(outbytes, imageClose...) i += urlLen lastItem = i continue } else if media.Type == "raw" { outbytes = append(outbytes, []byte(media.Body)...) i += urlLen lastItem = i continue } else if media.Type != "" { outbytes = append(outbytes, unknownMedia...) i += urlLen continue } outbytes = append(outbytes, urlOpen...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlOpen2...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlClose...) i += urlLen lastItem = i } else if msgbytes[i] == '/' && msgbytes[i+1] == '/' { outbytes = append(outbytes, msgbytes[lastItem:i]...) urlLen := partialURLBytesLen(msgbytes[i:]) if msgbytes[i+urlLen] > 32 { // space and invisibles //log.Print("INVALID URL") //log.Print("msgbytes[i+urlLen]", msgbytes[i+urlLen]) //log.Print("string(msgbytes[i+urlLen])", string(msgbytes[i+urlLen])) //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) outbytes = append(outbytes, invalidURL...) i += urlLen continue } //log.Print("VALID URL") //log.Print("msgbytes[i:i+urlLen]", msgbytes[i:i+urlLen]) //log.Print("string(msgbytes[i:i+urlLen])", string(msgbytes[i:i+urlLen])) media, ok := parseMediaBytes(msgbytes[i : i+urlLen]) if !ok { outbytes = append(outbytes, invalidURL...) i += urlLen continue } if media.Type == "attach" { outbytes = append(outbytes, imageOpen...) outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) outbytes = append(outbytes, imageOpen2...) outbytes = append(outbytes, []byte(media.URL+"?sectionID="+strconv.Itoa(sectionID)+"§ionType="+sectionType)...) outbytes = append(outbytes, imageClose...) i += urlLen lastItem = i continue } else if media.Type == "image" { outbytes = append(outbytes, imageOpen...) outbytes = append(outbytes, []byte(media.URL)...) outbytes = append(outbytes, imageOpen2...) outbytes = append(outbytes, []byte(media.URL)...) outbytes = append(outbytes, imageClose...) i += urlLen lastItem = i continue } else if media.Type == "raw" { outbytes = append(outbytes, []byte(media.Body)...) i += urlLen lastItem = i continue } else if media.Type != "" { outbytes = append(outbytes, unknownMedia...) i += urlLen continue } outbytes = append(outbytes, urlOpen...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlOpen2...) outbytes = append(outbytes, msgbytes[i:i+urlLen]...) outbytes = append(outbytes, urlClose...) i += urlLen lastItem = i } } } if lastItem != i && len(outbytes) != 0 { //log.Print("lastItem: ",msgbytes[lastItem]) //log.Print("lastItem index: ",lastItem) //log.Print("i: ",i) //log.Print("lastItem to end: ",msgbytes[lastItem:]) //log.Print("-----") calclen := len(msgbytes) - 10 if calclen <= lastItem { calclen = lastItem } outbytes = append(outbytes, msgbytes[lastItem:calclen]...) msg = string(outbytes) } //log.Print(`"`+string(outbytes)+`"`) //log.Print("msg",`"`+msg+`"`) msg = strings.Replace(msg, "\n", "
", -1) if sshooks["parse_assign"] != nil { msg = runSshook("parse_assign", msg) } return msg } // TODO: Write a test for this func regexParseMessage(msg string) string { msg = strings.Replace(msg, ":)", "😀", -1) msg = strings.Replace(msg, ":D", "😃", -1) msg = strings.Replace(msg, ":P", "😛", -1) msg = urlReg.ReplaceAllString(msg, "$2$3//$4") msg = strings.Replace(msg, "\n", "
", -1) if sshooks["parse_assign"] != nil { msg = runSshook("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 validateURLBytes(data []byte) bool { 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] > 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] > 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 partialURLBytes(data []byte) (url []byte) { datalen := len(data) i := 0 end := datalen - 1 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 ; end >= i; i++ { if 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, data[0:end]...) return url } // TODO: Write a test for this func partialURLBytesLen(data []byte) int { datalen := len(data) i := 0 if datalen >= 6 { //log.Print(string(data[0:5])) 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] > 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: ",datalen) return datalen } type MediaEmbed struct { Type string //image URL string Body string } // TODO: Write a test for this func parseMediaBytes(data []byte) (media MediaEmbed, ok bool) { if !validateURLBytes(data) { return media, false } url, err := parseURL(data) if err != nil { return media, false } //log.Print("url ", url) hostname := url.Hostname() scheme := url.Scheme port := url.Port() //log.Print("hostname ", hostname) //log.Print("scheme ", scheme) query := url.Query() //log.Printf("query %+v\n", query) var samesite = hostname == "localhost" || hostname == site.URL if samesite { //log.Print("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 scheme == "" && site.EnableSsl { scheme = "https" } } if scheme == "" { scheme = "http" } path := url.EscapedPath() //log.Print("path", path) pathFrags := strings.Split(path, "/") //log.Printf("pathFrags %+v\n", pathFrags) //log.Print("scheme ", scheme) //log.Print("hostname ", hostname) if len(pathFrags) >= 2 { if samesite && pathFrags[1] == "attachs" && (scheme == "http" || scheme == "https") { //log.Print("Attachment") media.Type = "attach" 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 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 } func parseURL(data []byte) (*url.URL, error) { return url.Parse(string(data)) } // TODO: Write a test for this func coerceIntBytes(data []byte) (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(string(data[0:i])) if err != nil { return 0, i } return conv, i } } conv, err := strconv.Atoi(string(data)) if err != nil { return 0, i } return conv, i } // TODO: Write tests for this 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 func pageOffset(count int, page int, perPage int) (int, int, int) { var offset int lastPage := (count / perPage) + 1 if page > 1 { offset = (perPage * page) - perPage } else if page == -1 { page = lastPage offset = (perPage * page) - perPage } else { page = 1 } // 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 }