1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-08-18 07:36:24 -07:00

Add webm support and update jQuery

This commit is contained in:
Joshua Merrell 2018-01-28 15:01:59 -08:00
parent 5a0d4e1ff8
commit 4fcb43330f
15 changed files with 395 additions and 219 deletions

View file

@ -42,15 +42,89 @@ function preparePostPreviews(is_inline) {
if($(this).next().attr("class") != "inlinepostprev") {
$(".postprev").remove();
$(this).after($("div#"+this.innerHTML.replace(">>","")).clone().attr({"class":"inlinepostprev","id":"i"+$(this).parent().attr("id")+"-"+($(this).parent().find("div#i"+$(this).parent().attr("id")).length+1)}));
console.debug("honk");
preparePostPreviews(true);
} else {
$(this).next().remove();
}
}
});
}
}
function getUploadPostID(upload, container) {
// if container, upload is div.upload-container
// otherwise it's img or video
var jqu = container? $jq(upload) : $jq(upload).parent();
if(insideOP(jqu)) return jqu.siblings().eq(4).text();
else return jqu.siblings().eq(3).text();
}
function insideOP(elem) {
return $jq(elem).parents("div.op-post").length > 0;
}
function prepareThumbnails() {
// set thumbnails to expand when clicked
$jq("a.upload-container").click(function(e) {
var a = $jq(this);
var thumb = a.find("img.thumbnail");
var video;
var thumbURL;
if(thumb.attr("alt") == undefined) thumbURL = thumb.attr("src");
else thumbURL = thumb.attr("alt");
var thumb_width = thumb.attr("width");
var thumb_height = thumb.attr("height");
var file_info_elem = a.prevAll(".file-info:first");
var uploadURL = file_info_elem.children("a:first")[0].href;
if(thumb.attr("src") == thumbURL) {
// Expanding thumbnail
if(uploadURL.indexOf(".webm") > 0) {
// Upload is a video
thumb.hide();
video = $jq("<video />")
.prop({
src: uploadURL,
autoplay: true,
controls: true,
loop: true
}).click(function(e) {
e.preventDefault();
if(this.pause) this.pause();
this.removeAttribute("src");
this.remove();
}).insertAfter(a);
var close_video_btn = $jq("<a />")
.prop("href", "javascript:;")
.click(function(e) {
video.remove();
thumb.show();
this.remove();
}).css({
"padding-left": "8px"
})
.text("[Close]")
.insertAfter(file_info_elem);
} else {
thumb.attr({
src: uploadURL,
alt: thumbURL
});
thumb.removeAttr("width");
thumb.removeAttr("height");
}
} else {
// Shrinking back to thumbnail
thumb.attr({
"src": thumbURL,
"width": thumb_width,
"height": thumb_height
});
}
return false;
});
}
var TopBarButton = function(title,callback_open, callback_close) {
this.title = title;
$jq("div#topbar").append("<a href=\"javascript:void(0)\" class=\"dropdown-button\" id=\""+title.toLowerCase()+"\">"+title+down_arrow_symbol+"</a>");
@ -113,7 +187,7 @@ function showMessage(msg) {
lightbox_css_added = true;
}
$jq(document.body).prepend("<div class=\"lightbox-bg\"></div><div class=\"lightbox-msg\">"+msg+"<br /><button class=\"lightbox-msg-ok\" style=\"float: right; margin-top:8px;\">OK</button></div>");
console.debug($jq(".lightbox-msg").width());
console.log($jq(".lightbox-msg").width());
var centeroffset = parseInt($jq(".lightbox-msg").css("transform-origin").replace("px",""),10)+$jq(".lightbox-msg").width()/2
$jq(".lightbox-msg").css({
@ -191,19 +265,19 @@ function changeFrontPage(page_name) {
// heavily based on 4chan's quote() function, with a few tweaks
function quote(e) {
var msgbox_id = "postmsg";
if(qr_enabled) msgbox_id = "postmsg-qr";
if (document.selection) {
document.getElementById(msgbox_id).focus();
var t = document.getselection.createRange();
t.text = ">>" + e + "\n"
} else if (document.getElementById(msgbox_id).selectionStart || "0" == document.getElementById(msgbox_id).selectionStart) {
var n = document.getElementById(msgbox_id).selectionStart,
o = document.getElementById(msgbox_id).selectionEnd;
document.getElementById(msgbox_id).value = document.getElementById(msgbox_id).value.substring(0, n) + ">>" + e + "\n" + document.getElementById(msgbox_id).value.substring(o, document.getElementById(msgbox_id).value.length)
} else document.getElementById(msgbox_id).value += ">>" + e + "\n"
window.scroll(0,document.getElementById(msgbox_id).offsetTop-48);
var msgbox_id = "postmsg";
if(qr_enabled) msgbox_id = "postmsg-qr";
if (document.selection) {
document.getElementById(msgbox_id).focus();
var t = document.getselection.createRange();
t.text = ">>" + e + "\n"
} else if (document.getElementById(msgbox_id).selectionStart || "0" == document.getElementById(msgbox_id).selectionStart) {
var n = document.getElementById(msgbox_id).selectionStart,
o = document.getElementById(msgbox_id).selectionEnd;
document.getElementById(msgbox_id).value = document.getElementById(msgbox_id).value.substring(0, n) + ">>" + e + "\n" + document.getElementById(msgbox_id).value.substring(o, document.getElementById(msgbox_id).value.length)
} else document.getElementById(msgbox_id).value += ">>" + e + "\n"
window.scroll(0,document.getElementById(msgbox_id).offsetTop-48);
}
function deletePost(id) {
@ -284,49 +358,11 @@ function getCookie(name) {
for(var i = 0; i < cookie_arr.length; i++) {
pair = cookie_arr[i].split("=");
if(pair[0] == name) {
//var val = decodeURIComponent(pair[1]);
val = pair[1].replace("+", " ");
val = val.replace("%2B", "+");
val = decodeURIComponent(val);
return val;
return decodeURIComponent(pair[1].replace("+", " ").replace("%2B", "+"))
}
}
}
/*function preparePostPreviews(is_inline) {
var m_type = "mousemove";
if(!movable_postpreviews) m_type = "mouseover";
if(expandable_postrefs) $("a.postref").attr("href","javascript:void(0);");
var hvr_str = "a.postref";
if(is_inline) hvr_str = "div.inlinepostprev "+hvr_str;
$(hvr_str).hover(function(){
$(document.body).append($("div#"+this.innerHTML.replace("&gt;&gt;","")).clone().attr("class","postprev"))
$(document).bind(m_type, function(e){
$('.postprev').css({
left: e.pageX + 8,
top: e.pageY + 8
});
})
},
function() {
$(".postprev").remove();
});
if(expandable_postrefs) {
var clk_str = "a.postref";
if(is_inline) clk_str = "div.inlinepostprev "+clk_str;
$(clk_str).click(function() {
if($(this).next().attr("class") != "inlinepostprev") {
$(".postprev").remove();
$(this).after($("div#"+this.innerHTML.replace("&gt;&gt;","")).clone().attr({"class":"inlinepostprev","id":"i"+$(this).parent().attr("id")+"-"+($(this).parent().find("div#i"+$(this).parent().attr("id")).length+1)}));
preparePostPreviews(true);
} else {
$(this).next().remove();
}
});
}
} */
function reportPost(id) {
var reason = prompt("Reason");
}
@ -359,9 +395,8 @@ $jq(document).ready(function() {
addStaffButtons();
}
if(isFrontPage()) {
changeFrontPage(getHashVal());
}
if(isFrontPage()) changeFrontPage(getHashVal());
else prepareThumbnails();
$jq(".plus").click(function() {
var block = $jq(this).parent().next();
@ -397,30 +432,4 @@ $jq(document).ready(function() {
}
}
});
// set thumbnails to expand when clicked
var thumbnails = document.getElementsByClassName("thumbnail");
for(var i = 0; i < thumbnails.length; i++) {
var is_thumb = true;
thumbnails[i].onclick = function(e) {
var src = this.parentNode.getAttribute("href");
if(this.getAttribute("width") != null) {
// change the width and height constraints
this.setAttribute("old-width",this.getAttribute("width"));
this.removeAttribute("width");
this.setAttribute("old-height",this.getAttribute("height"));
this.removeAttribute("height");
} else {
// this is an expanded image, shrink it
var src = this.parentNode.getAttribute("href");
this.setAttribute("width", this.getAttribute("old-width"));
this.removeAttribute("old-width");
this.setAttribute("height", this.getAttribute("old-height"));
this.removeAttribute("old-height");
}
this.setAttribute("src", src);
//thumbnails[i].setAttribute("onclick", "function() { alert(\"hi!\"); return ")
return false;
}
}
});

File diff suppressed because one or more lines are too long

2
html/javascript/jquery-3.3.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -19,11 +19,6 @@ type ManageFunction struct {
Callback func() string //return string of html output
}
var (
rebuildfront func() string
rebuildboards func() string
)
func callManageFunction(w http.ResponseWriter, r *http.Request, data interface{}) {
request = *r
writer = w
@ -53,10 +48,6 @@ func callManageFunction(w http.ResponseWriter, r *http.Request, data interface{}
if _, ok := manage_functions[action]; ok {
if staffRank >= manage_functions[action].Permissions {
if action == "rebuildall" || action == "purgeeverything" {
rebuildfront = manage_functions["rebuildfront"].Callback
rebuildboards = manage_functions["rebuildboards"].Callback
}
managePageBuffer.Write([]byte(manage_functions[action].Callback()))
} else if staffRank == 0 && manage_functions[action].Permissions == 0 {
managePageBuffer.Write([]byte(manage_functions[action].Callback()))
@ -89,7 +80,6 @@ func getCurrentStaff() (string, error) {
current_session := new(SessionsTable)
err := row.Scan(&current_session.Data)
if err != nil {
return "", err
}
@ -264,14 +254,16 @@ var manage_functions = map[string]ManageFunction{
return
}
}
_, err = db.Exec("truncate `" + config.DBprefix + "posts`")
_, err = db.Exec("TRUNCATE `" + config.DBprefix + "posts`")
if err != nil {
html += err.Error() + "<br />"
return
}
_, _ = db.Exec("ALTER TABLE `" + config.DBprefix + "posts` AUTO_INCREMENT = 1")
html += "<br />Everything purged, rebuilding all<br />"
html += rebuildboards() + "<hr />\n"
db.Exec("ALTER TABLE `" + config.DBprefix + "posts` AUTO_INCREMENT = 1")
html += "<br />Everything purged, rebuilding all<br />" +
buildBoards(true, 0) + "<hr />\n" +
buildFrontPage()
return
}},
"executesql": {
@ -282,10 +274,12 @@ var manage_functions = map[string]ManageFunction{
if statement != "" {
html += "<hr />"
result, sqlerr := db.Exec(statement)
fmt.Println(&result)
println(1, &result)
if sqlerr != nil {
html += sqlerr.Error()
errortext := sqlerr.Error()
html += errortext
println(1, errortext)
} else {
html += "Statement esecuted successfully."
}
@ -366,7 +360,7 @@ var manage_functions = map[string]ManageFunction{
"<div class=\"section-title-block\"><b>" + announcement.Subject + "</b> by " + announcement.Poster + " at " + humanReadableTime(announcement.Timestamp) + "</div>\n" +
"<div class=\"section-body\">" + announcement.Message + "\n</div></div>\n"
}
iterations += 1
iterations++
}
if iterations == 0 {
@ -609,28 +603,28 @@ var manage_functions = map[string]ManageFunction{
if err != nil {
do = ""
board_creation_status = err.Error()
continue
break
}
err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "res"), 0777)
if err != nil {
do = ""
board_creation_status = err.Error()
continue
break
}
err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "thumb"), 0777)
if err != nil {
do = ""
board_creation_status = err.Error()
continue
break
}
err = os.Mkdir(path.Join(config.DocumentRoot, board.Dir, "src"), 0777)
if err != nil {
do = ""
board_creation_status = err.Error()
continue
break
}
stmt, err := db.Prepare(
"INSERT INTO `" + config.DBprefix + "boards` (`order`,`dir`,`type`,`upload_type`,`title`,`subtitle`," +
@ -647,7 +641,7 @@ var manage_functions = map[string]ManageFunction{
if err != nil {
do = ""
board_creation_status = err.Error()
continue
break
}
boardCreationTimestamp := getSpecificSQLDateTime(board.CreatedOn)
@ -664,13 +658,18 @@ var manage_functions = map[string]ManageFunction{
if err != nil {
do = ""
board_creation_status = err.Error()
continue
println(1, "Error creating board: "+board_creation_status)
break
} else {
board_creation_status = "Board created successfully"
rebuildboards()
println(1, board_creation_status)
buildBoards(true, 0)
resetBoardSectionArrays()
println(1, "Boards rebuilt successfully")
done = true
break
}
resetBoardSectionArrays()
case do == "del":
// resetBoardSectionArrays()
case do == "edit":
@ -680,6 +679,7 @@ var manage_functions = map[string]ManageFunction{
rows, err = db.Query("SELECT `column_name`,`column_default` FROM `information_schema`.`columns` WHERE `table_name` = '" + config.DBprefix + "boards'")
if err != nil {
html += err.Error()
println(1, err.Error())
return
}
@ -687,8 +687,14 @@ var manage_functions = map[string]ManageFunction{
var column_name string
var column_default string
err = rows.Scan(&column_name, &column_default)
if err != nil {
html += err.Error()
println(1, err.Error())
return
}
column_default_int, _ := strconv.Atoi(column_default)
column_default_bool := (column_default_int == 1)
println(1, "Got this far...")
switch column_name {
case "id":
board.ID = column_default_int
@ -739,6 +745,7 @@ var manage_functions = map[string]ManageFunction{
case "enable_catalog":
board.EnableCatalog = column_default_bool
}
println(1, "Done with the switch")
}
}
@ -810,15 +817,16 @@ var manage_functions = map[string]ManageFunction{
"rebuildfront": {
Permissions: 3,
Callback: func() (html string) {
initTemplates()
return buildFrontPage()
}},
"rebuildall": {
Permissions: 3,
Callback: func() (html string) {
html += rebuildfront() + "<hr />\n"
html += buildBoardListJSON() + "<hr />\n"
html += rebuildboards() + "<hr />\n"
return
initTemplates()
return buildFrontPage() + "<hr />\n" +
buildBoardListJSON() + "<hr />\n" +
buildBoards(true, 0) + "<hr />\n"
}},
"rebuildboards": {
Permissions: 3,

View file

@ -7,6 +7,7 @@ import (
"crypto/md5"
"database/sql"
"encoding/json"
"errors"
"fmt"
"html"
"image"
@ -15,6 +16,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path"
"regexp"
"strconv"
@ -792,18 +794,18 @@ func sinceLastPost(post *PostTable) int {
return -1
}
func createThumbnail(image_obj image.Image, size string) image.Image {
func createImageThumbnail(image_obj image.Image, size string) image.Image {
var thumb_width int
var thumb_height int
switch {
case size == "op":
switch size {
case "op":
thumb_width = config.ThumbWidth
thumb_height = config.ThumbHeight
case size == "reply":
case "reply":
thumb_width = config.ThumbWidth_reply
thumb_height = config.ThumbHeight_reply
case size == "catalog":
case "catalog":
thumb_width = config.ThumbWidth_catalog
thumb_height = config.ThumbHeight_catalog
}
@ -817,6 +819,40 @@ func createThumbnail(image_obj image.Image, size string) image.Image {
return image_obj
}
func createVideoThumbnail(video, thumb string, size int) error {
sizeStr := strconv.Itoa(size)
outputBytes, err := exec.Command("ffmpeg", "-y", "-itsoffset", "-1", "-i", video, "-vframes", "1", "-filter:v", "scale='min("+sizeStr+"\\, "+sizeStr+"):-1'", thumb).CombinedOutput()
if err != nil {
outputStringArr := strings.Split(string(outputBytes), "\n")
if len(outputStringArr) > 1 {
outputString := outputStringArr[len(outputStringArr)-2]
err = errors.New(outputString)
}
}
return err
}
func getVideoInfo(path string) (map[string]int, error) {
var vidInfo map[string]int
outputBytes, err := exec.Command("ffprobe", "-v quiet", "-show_format", "-show_streams", path).CombinedOutput()
if err == nil && outputBytes != nil {
outputStringArr := strings.Split(string(outputBytes), "\n")
for _, line := range outputStringArr {
lineArr := strings.Split(line, "=")
if len(lineArr) < 2 {
continue
}
if lineArr[0] == "width" || lineArr[0] == "height" || lineArr[0] == "size" {
value, _ := strconv.Atoi(lineArr[1])
vidInfo[lineArr[0]] = value
}
}
}
return vidInfo, err
}
func getNewFilename() string {
now := time.Now().Unix()
rand.Seed(now)
@ -972,7 +1008,7 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
serveErrorPage(w, "Post body is too long")
return
}
post.MessageHTML = sanitizeHTML(post.MessageText)
post.MessageHTML = html.EscapeString(post.MessageText)
formatMessage(&post)
post.Password = md5Sum(request.FormValue("postpassword"))
@ -1025,9 +1061,11 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
post.FilenameOriginal = html.EscapeString(handler.Filename)
filetype := getFileExtension(post.FilenameOriginal)
thumbFiletype := filetype
if thumbFiletype == "gif" {
if thumbFiletype == "gif" || thumbFiletype == "webm" {
thumbFiletype = "jpg"
}
post.Filename = getNewFilename() + "." + getFileExtension(post.FilenameOriginal)
boardArr, _ := getBoardArr(map[string]interface{}{"id": request.FormValue("boardid")}, "")
if len(boardArr) == 0 {
@ -1036,7 +1074,9 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
_boardDir, _ := getBoardArr(map[string]interface{}{"id": request.FormValue("boardid")}, "")
boardDir := _boardDir[0].Dir
filePath := path.Join(config.DocumentRoot, "/"+boardDir+"/src/", post.Filename)
thumbPath := path.Join(config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "t."+thumbFiletype, -1))
catalogThumbPath := path.Join(config.DocumentRoot, "/"+boardDir+"/thumb/", strings.Replace(post.Filename, "."+filetype, "c."+thumbFiletype, -1))
err := ioutil.WriteFile(filePath, data, 0777)
@ -1051,89 +1091,181 @@ func makePost(w http.ResponseWriter, r *http.Request, data interface{}) {
// Calculate image checksum
post.FileChecksum = fmt.Sprintf("%x", md5.Sum(data))
// Attempt to load uploaded file with imaging library
img, err := imaging.Open(filePath)
var allowsVids bool
vidStmt, err := db.Prepare("SELECT `embeds_allowed` FROM `" + config.DBprefix + "boards` WHERE `id` = ? LIMIT 1")
if err != nil {
errorText = "Couldn't open uploaded file \"" + post.Filename + "\"" + err.Error()
errortext := err.Error()
errorLog.Println(errorText)
println(1, errorText)
serveErrorPage(w, "Upload filetype not supported")
serveErrorPage(w, "Couldn't get board info: "+errorText)
println(1, errortext)
return
} else {
// Get image filesize
stat, err := os.Stat(filePath)
if err != nil {
errorLog.Println("Couldn't get image filesize: " + err.Error())
println(1, "Couldn't get image filesize: "+err.Error())
serveErrorPage(w, "Couldn't get image filesize: "+err.Error())
} else {
post.Filesize = int(stat.Size())
}
defer func() {
if vidStmt != nil {
vidStmt.Close()
}
}()
err = vidStmt.QueryRow(post.BoardID).Scan(&allowsVids)
if err != nil {
errortext := err.Error()
errorLog.Println(errorText)
serveErrorPage(w, "Couldn't get board info: "+errorText)
println(1, errortext)
return
}
if filetype == "webm" {
if !allowsVids || !config.AllowVideoUploads {
serveErrorPage(w, "Video uploading is not currently enabled for this board.")
os.Remove(filePath)
return
}
// Get image width and height, as well as thumbnail width and height
post.ImageW = img.Bounds().Max.X
post.ImageH = img.Bounds().Max.Y
accessLog.Print("Receiving post with video: " + handler.Filename + " from " + request.RemoteAddr + ", referrer: " + request.Referer())
if post.ParentID == 0 {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth)
if err != nil {
serveErrorPage(w, err.Error())
printf(1, err.Error())
return
}
} else {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
err := createVideoThumbnail(filePath, thumbPath, config.ThumbWidth_reply)
if err != nil {
serveErrorPage(w, err.Error())
printf(1, err.Error())
return
}
}
accessLog.Print("Receiving post with image: " + handler.Filename + " from " + request.RemoteAddr + ", referrer: " + request.Referer())
err := createVideoThumbnail(filePath, catalogThumbPath, config.ThumbWidth_catalog)
if err != nil {
serveErrorPage(w, err.Error())
printf(1, err.Error())
return
}
if request.FormValue("spoiler") == "on" {
// If spoiler is enabled, symlink thumbnail to spoiler image
_, err := os.Stat(path.Join(config.DocumentRoot, "spoiler.png"))
outputBytes, err := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", filePath).CombinedOutput()
if err != nil {
errortext := "Error getting video info: " + err.Error()
serveErrorPage(w, errortext)
printf(1, errortext)
return
}
if err == nil && outputBytes != nil {
outputStringArr := strings.Split(string(outputBytes), "\n")
for _, line := range outputStringArr {
lineArr := strings.Split(line, "=")
if len(lineArr) < 2 {
continue
}
value, _ := strconv.Atoi(lineArr[1])
switch lineArr[0] {
case "width":
post.ImageW = value
case "height":
post.ImageH = value
case "size":
post.Filesize = value
}
}
if post.ParentID == 0 {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
} else {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
}
}
} else {
// Attempt to load uploaded file with imaging library
img, err := imaging.Open(filePath)
if err != nil {
errorText = "Couldn't open uploaded file \"" + post.Filename + "\"" + err.Error()
errorLog.Println(errorText)
println(1, errorText)
os.Remove(filePath)
serveErrorPage(w, "Upload filetype not supported")
return
} else {
// Get image filesize
stat, err := os.Stat(filePath)
if err != nil {
serveErrorPage(w, "missing /spoiler.png")
errortext := "Couldn't get image filesize: " + err.Error()
errorLog.Println(errortext)
println(1, errortext)
serveErrorPage(w, errortext)
return
} else {
err = syscall.Symlink(path.Join(config.DocumentRoot, "spoiler.png"), thumbPath)
post.Filesize = int(stat.Size())
}
// Get image width and height, as well as thumbnail width and height
post.ImageW = img.Bounds().Max.X
post.ImageH = img.Bounds().Max.Y
if post.ParentID == 0 {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "op")
} else {
post.ThumbW, post.ThumbH = getThumbnailSize(post.ImageW, post.ImageH, "reply")
}
accessLog.Print("Receiving post with image: " + handler.Filename + " from " + request.RemoteAddr + ", referrer: " + request.Referer())
if request.FormValue("spoiler") == "on" {
// If spoiler is enabled, symlink thumbnail to spoiler image
_, err := os.Stat(path.Join(config.DocumentRoot, "spoiler.png"))
if err != nil {
serveErrorPage(w, "missing /spoiler.png")
return
} else {
err = syscall.Symlink(path.Join(config.DocumentRoot, "spoiler.png"), thumbPath)
if err != nil {
serveErrorPage(w, err.Error())
return
}
}
} else if config.ThumbWidth >= post.ImageW && config.ThumbHeight >= post.ImageH {
// If image fits in thumbnail size, symlink thumbnail to original
post.ThumbW = img.Bounds().Max.X
post.ThumbH = img.Bounds().Max.Y
err := syscall.Symlink(filePath, thumbPath)
if err != nil {
serveErrorPage(w, err.Error())
return
}
}
} else if config.ThumbWidth >= post.ImageW && config.ThumbHeight >= post.ImageH {
// If image fits in thumbnail size, symlink thumbnail to original
post.ThumbW = img.Bounds().Max.X
post.ThumbH = img.Bounds().Max.Y
err := syscall.Symlink(filePath, thumbPath)
if err != nil {
serveErrorPage(w, err.Error())
return
}
} else {
var thumbnail image.Image
var catalogThumbnail image.Image
if post.ParentID == 0 {
// If this is a new thread, generate thumbnail and catalog thumbnail
thumbnail = createThumbnail(img, "op")
catalogThumbnail = createThumbnail(img, "catalog")
println(1, catalogThumbPath)
err = imaging.Save(catalogThumbnail, catalogThumbPath)
} else {
var thumbnail image.Image
var catalogThumbnail image.Image
if post.ParentID == 0 {
// If this is a new thread, generate thumbnail and catalog thumbnail
thumbnail = createImageThumbnail(img, "op")
catalogThumbnail = createImageThumbnail(img, "catalog")
println(1, catalogThumbPath)
err = imaging.Save(catalogThumbnail, catalogThumbPath)
if err != nil {
errorLog.Println("Couldn't generate catalog thumbnail: " + err.Error())
serveErrorPage(w, "Couldn't generate catalog thumbnail: "+err.Error())
return
}
} else {
thumbnail = createImageThumbnail(img, "reply")
}
err = imaging.Save(thumbnail, thumbPath)
if err != nil {
errorLog.Println("Couldn't generate catalog thumbnail: " + err.Error())
serveErrorPage(w, "Couldn't generate catalog thumbnail: "+err.Error())
errortext := "Couldn't save thumbnail: " + err.Error()
println(0, errortext)
errorLog.Println(errortext)
serveErrorPage(w, errortext)
return
}
} else {
thumbnail = createThumbnail(img, "reply")
}
err = imaging.Save(thumbnail, thumbPath)
if err != nil {
println(0, "Couldn't save thumbnail: "+err.Error())
errorLog.Println("Couldn't save thumbnail: " + err.Error())
serveErrorPage(w, "Couldn't save thumbnail: "+err.Error())
return
}
}
}
}
}
if post.FilenameOriginal != "" {
//post.FilenameOriginal = sanitizeHTML(post.FilenameOriginal)
//post.FilenameOriginal = html.EscapeString(post.FilenameOriginal)
}
if strings.TrimSpace(post.MessageText) == "" && post.Filename == "" {

View file

@ -32,12 +32,12 @@ func (s GochanServer) AddNamespace(basePath string, namespaceFunction func(http.
s.namespaces[basePath] = namespaceFunction
}
func (s GochanServer) getFileData(writer http.ResponseWriter, url string) []byte {
func (s GochanServer) getFileData(writer http.ResponseWriter, url string) (fileBytes []byte) {
filePath := path.Join(config.DocumentRoot, url)
results, err := os.Stat(filePath)
if err != nil {
// the requested path isn't a file or directory, 404
return nil
fileBytes = nil
} else {
//the file exists, or there is a folder here
if results.IsDir() {
@ -54,7 +54,7 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) []byte
}
} else {
//the file exists, and is not a folder
fileBytes, _ := ioutil.ReadFile(filePath)
fileBytes, _ = ioutil.ReadFile(filePath)
extension := getFileExtension(url)
switch extension {
case "png":
@ -75,24 +75,29 @@ func (s GochanServer) getFileData(writer http.ResponseWriter, url string) []byte
case "json":
writer.Header().Add("Content-Type", "application/json")
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
case "webm":
writer.Header().Add("Content-Type", "video/webm")
writer.Header().Add("Cache-Control", "max-age=86400")
}
if strings.HasPrefix(extension, "htm") {
writer.Header().Add("Content-Type", "text/html")
writer.Header().Add("Cache-Control", "max-age=5, must-revalidate")
}
accessLog.Print("Success: 200 from " + getRealIP(&request) + " @ " + request.RequestURI)
return fileBytes
}
}
return nil
return
}
func serveNotFound(writer http.ResponseWriter, request *http.Request) {
writer.Header().Add("Content-Type", "text/html; charset=utf-8")
writer.WriteHeader(404)
errorPage, err := ioutil.ReadFile(config.DocumentRoot + "/error/404.html")
if err != nil {
writer.Write([]byte("Requested page not found, and 404 error page not found"))
} else {
writer.Write(errorPage)
}
errorLog.Print("Error: 404 Not Found from " + getRealIP(request) + " @ " + request.RequestURI)
}

View file

@ -142,13 +142,32 @@ var funcMap = template.FuncMap{
if name == "" || name == "deleted" {
return ""
}
if name[len(name)-3:] == "gif" || name[len(name)-3:] == "gif" {
if name[len(name)-3:] == "gif" {
name = name[:len(name)-3] + "jpg"
} else if name[len(name)-4:] == "webm" {
name = name[:len(name)-4] + "jpg"
}
ext_begin := strings.LastIndex(name, ".")
new_name := name[:ext_begin] + "t." + getFileExtension(name)
return new_name
},
"getUploadType": func(name string) string {
extension := getFileExtension(name)
var uploadType string
switch extension {
case "":
case "deleted":
uploadType = ""
case "webm":
case "jpg":
case "gif":
uploadType = "jpg"
case "png":
uploadType = "png"
}
return uploadType
},
"formatFilesize": func(size_int int) string {
size := float32(size_int)
if size < 1000 {
@ -163,8 +182,8 @@ var funcMap = template.FuncMap{
return fmt.Sprintf("%0.2f GB", size/1024/1024/1024)
},
"imageToThumbnailPath": func(img string) string {
filetype := img[strings.LastIndex(img, ".")+1:]
if filetype == "gif" || filetype == "GIF" {
filetype := strings.ToLower(img[strings.LastIndex(img, ".")+1:])
if filetype == "gif" || filetype == "webm" {
filetype = "jpg"
}
index := strings.LastIndex(img, ".")

View file

@ -365,6 +365,7 @@ type GochanConfig struct {
DefaultStyle_txt string
AllowDuplicateImages bool
AllowVideoUploads bool
NewThreadDelay int
ReplyDelay int
MaxLineLength int
@ -568,6 +569,18 @@ func initConfig() {
config.DefaultStyle_txt = config.Styles_txt[0]
}
if config.NewThreadDelay == 0 {
config.NewThreadDelay = 30
}
if config.ReplyDelay == 0 {
config.ReplyDelay = 7
}
if config.MaxLineLength == 0 {
config.MaxLineLength = 150
}
//ReservedTrips string //eventually this will be map[string]string
if config.ThumbWidth == 0 {

View file

@ -418,19 +418,14 @@ func reverse(arr []interface{}) (reversed []interface{}) {
return
}
func sanitizeHTML(input string) (output string) {
output = html.EscapeString(input)
return
}
// sanitize/escape HTML strings in a post. This should be run immediately before
// the post is inserted into the database
func sanitizePost(post PostTable) PostTable {
sanitized := post
sanitized.Name = sanitizeHTML(sanitized.Name)
sanitized.Email = sanitizeHTML(sanitized.Email)
sanitized.Subject = sanitizeHTML(sanitized.Subject)
sanitized.Password = sanitizeHTML(sanitized.Password)
sanitized.Name = html.EscapeString(sanitized.Name)
sanitized.Email = html.EscapeString(sanitized.Email)
sanitized.Subject = html.EscapeString(sanitized.Subject)
sanitized.Password = html.EscapeString(sanitized.Password)
return sanitized
}

View file

@ -10,7 +10,7 @@
var styles = [{{range $i, $style := $config.Styles_img}}{{if gt $i 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{$config.SiteWebfolder}}"
</script>
<script type="text/javascript" src="/javascript/jquery/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/javascript/gochan.js"></script>
<script type="text/javascript" src="/javascript/manage.js"></script>
</head>

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>{{$config.SiteName}}</title>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/javascript/msgpack.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];

View file

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<title>/{{$board.Dir}}/ - {{$board.Title}}</title>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{$config.SiteWebfolder}}";
@ -56,8 +56,8 @@
<div class="op-post" id="op{{$op.ID}}">
{{if stringNeq $op.Filename ""}}
{{if stringNeq $op.Filename "deleted"}}
<div class="file-info">File: <a href="src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}} )</div>
<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$op.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="thumbnail" alt="{{imageToThumbnailPath $op.Filename}}" /></a>
<div class="file-info">File: <a href="src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div>
<a class="upload-container" href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$op.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="thumbnail" /></a>
{{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}}
@ -72,8 +72,8 @@
<div class="reply-container">
<div class="reply" id="{{$reply.ID}}">
<div><input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if stringNeq $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if stringNeq $reply.Name ""}}{{$reply.Name}}{{else}}{{if stringEq $reply.Tripcode ""}}{{$board.Anonymous}}{{end}}{{end}}{{if stringNeq $reply.Email ""}}</a>{{end}}</span>{{if stringNeq $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="/{{$board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-clink">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span></div>
{{if stringNeq $reply.Filename ""}}<span class="file-info">File: <a href="src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}} )</span><br />
<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$reply.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="thumbnail" alt="{{imageToThumbnailPath $reply.Filename}}" /></a>{{end}}
{{if stringNeq $reply.Filename ""}}<span class="file-info">File: <a href="src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$reply.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="thumbnail" /></a></div>{{end}}
{{if stringNeq $reply.MessageHTML ""}}
<div class="post-text">{{$reply.MessageHTML}}</div>

View file

@ -8,7 +8,7 @@
<title>/{{$board.Dir}}/ - {{truncateString $op.MessageHTML 20 true}}</title>
{{end}}{{end}}
<title>/{{$board.Dir}} - {{$board.Title}}</title>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
var styles = [{{range $ii, $style := $config.Styles_img}}{{if gt $ii 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{$config.SiteWebfolder}}";
@ -73,24 +73,23 @@
<div class="op-post" id="op{{$op.ID}}">
{{if stringNeq $op.Filename ""}}
{{if stringNeq $op.Filename "deleted"}}
<div class="file-info">File: <a href="src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}} )</div>
<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$op.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="thumbnail" alt="{{imageToThumbnailPath $op.Filename}}" /></a>
<div class="file-info">File: <a href="../src/{{$op.Filename}}" target="_blank">{{$op.Filename}}</a> - ({{formatFilesize $op.Filesize}} , {{$op.ImageW}}x{{$op.ImageH}}, {{$op.FilenameOriginal}})</div>
<a class="upload-container" href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$op.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $op.Filename}}" width="{{$op.ThumbW}}" height="{{$op.ThumbH}}" class="thumbnail" /></a>
{{else}}
<div class="file-deleted-box" style="text-align:center;">File removed</div>
{{end}}
{{end}}
<input type="checkbox" id="check{{$op.ID}}" name="check{{$op.ID}}" /><label class="post-info" for="check{{$op.ID}}"> <span class="subject">{{$op.Subject}}</span> <span class="postername">{{if stringNeq $op.Email ""}}<a href="mailto:{{$op.Email}}">{{end}}{{if stringNeq $op.Name ""}}{{$op.Name}}{{else}}{{if stringEq $op.Tripcode ""}}{{$board.Anonymous}}{{end}}{{end}}{{if stringNeq $op.Email ""}}</a>{{end}}</span>{{if stringNeq $op.Tripcode ""}}<span class="tripcode">!{{$op.Tripcode}}</span>{{end}} {{formatTimestamp $op.Timestamp}} </label><a href="/{{$board.Dir}}/res/{{$op.ID}}.html#{{$op.ID}}">No.</a> <a href="javascript:quote({{$op.ID}})" class="backlink-click">{{$op.ID}}</a> <span class="post-links"> <span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if stringNeq $op.MessageHTML ""}}
<div class="post-text">{{$op.MessageHTML}}</div>
{{end}}
<div class="post-text">{{$op.MessageHTML}}</div>
</div>
{{range $reply_num,$reply := $post_arr}}{{if gt $reply_num 0}}
<div class="reply-container" id="replycontainer{{$reply.ID}}">
<a class="anchor" id="{{$reply.ID}}"></a>
<div class="reply" id="reply{{$reply.ID}}">
<input type="checkbox" id="check{{$reply.ID}}" name="check{{$reply.ID}}" /> <label class="post-info" for="check{{$reply.ID}}"> <span class="subject">{{$reply.Subject}}</span> <span class="postername">{{if stringNeq $reply.Email ""}}<a href="mailto:{{$reply.Email}}">{{end}}{{if stringNeq $reply.Name ""}}{{$reply.Name}}{{else}}{{if stringEq $reply.Tripcode ""}}{{$board.Anonymous}}{{end}}{{end}}{{if stringNeq $reply.Email ""}}</a>{{end}}</span>{{if stringNeq $reply.Tripcode ""}}<span class="tripcode">!{{$reply.Tripcode}}</span>{{end}} {{formatTimestamp $reply.Timestamp}} </label><a href="/{{$board.Dir}}/res/{{$op.ID}}.html#{{$reply.ID}}">No.</a> <a href="javascript:quote({{$reply.ID}})" class="backlink-click">{{$reply.ID}}</a> <span class="post-links"><span class="thread-ddown">[<a href="javascript:void(0)">&#9660;</a>]</span></span><br />
{{if stringNeq $reply.Filename ""}}<span class="file-info">File: <a href="../src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}} )</span><br />
<a href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$reply.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="thumbnail" alt="{{imageToThumbnailPath $reply.Filename}}" /></a>{{end}}
{{if stringNeq $reply.Filename ""}}<span class="file-info">File: <a href="../src/{{$reply.Filename}}" target="_blank">{{$reply.Filename}}</a> - ({{formatFilesize $reply.Filesize}} , {{$reply.ImageW}}x{{$reply.ImageH}}, {{$reply.FilenameOriginal}})</span><br />
<a class="upload-container" href="{{$config.SiteWebfolder}}{{$board.Dir}}/src/{{$reply.Filename}}"><img src="{{$config.SiteWebfolder}}{{$board.Dir}}/thumb/{{imageToThumbnailPath $reply.Filename}}" width="{{$reply.ThumbW}}" height="{{$reply.ThumbH}}" class="thumbnail" /></a>{{end}}
<div class="post-text">{{$reply.MessageHTML}}</div>
</div>
</div>

View file

@ -7,7 +7,7 @@
var styles = [{{range $i, $style := .Styles_img}}{{if gt $i 0}}, {{end}}"{{$style}}"{{end}}];
var webroot = "{{.SiteWebfolder}}"
</script>
<script type="text/javascript" src="/javascript/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="/javascript/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/javascript/gochan.js"></script>
<script type="text/javascript" src="/javascript/manage.js"></script>
</head>

View file

@ -8,7 +8,7 @@ export GOPATH=/vagrant/lib
apt-get update
apt-get -y upgrade
apt-get -y install git subversion mercurial golang nginx redis-server mariadb-server mariadb-client
apt-get -y install git subversion mercurial golang nginx redis-server mariadb-server mariadb-client ffmpeg
# Make sure any imported database is utf8mb4
# http://mathiasbynens.be/notes/mysql-utf8mb4
@ -101,4 +101,4 @@ echo
echo "Server set up, please run \"vagrant ssh\" on your host machine, and"
echo "\"cd /home/vagrant/gochan && ./gochan\" in the guest. Then browse to http://172.27.0.3/manage"
echo "to complete installation (TODO: add further instructions as default initial announcement"
echo "or /manage?action=firstrun)"
echo "or /manage?action=firstrun)"