package server
import (
"crypto/sha256"
"fmt"
"html/template"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
secretRequired "github.com/danielheath/gin-teeny-security"
"github.com/gin-contrib/multitemplate"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/jcelliott/lumber"
"github.com/schollz/cowyo/encrypt"
)
const minutesToUnlock = 10.0
var customCSS []byte
var defaultLock string
var debounceTime int
var diaryMode bool
var allowFileUploads bool
var maxUploadMB uint
var needSitemapUpdate = true
var pathToData string
var log *lumber.ConsoleLogger
type Site struct {
PathToData string
Host string
Port string
CertPath string
KeyPath string
TLS bool
CssFile string
DefaultPage string
DefaultPassword string
Debounce int
Diary bool
Secret string
SecretCode string
AllowInsecure bool
HotTemplateReloading bool
Fileuploads bool
MaxUploadSize uint
Logger *lumber.ConsoleLogger
}
func Serve(
filepathToData,
host,
port,
crt_path,
key_path string,
TLS bool,
cssFile string,
defaultPage string,
defaultPassword string,
debounce int,
diary bool,
secret string,
secretCode string,
allowInsecure bool,
hotTemplateReloading bool,
fileuploads bool,
maxUploadSize uint,
logger *lumber.ConsoleLogger,
) {
Site{
filepathToData,
host,
port,
crt_path,
key_path,
TLS,
cssFile,
defaultPage,
defaultPassword,
debounce,
diary,
secret,
secretCode,
allowInsecure,
hotTemplateReloading,
fileuploads,
maxUploadSize,
logger,
}.Serve()
}
func (s Site) Serve() {
pathToData = s.PathToData
allowFileUploads = s.Fileuploads
maxUploadMB = s.MaxUploadSize
log = s.Logger
if log == nil {
log = lumber.NewConsoleLogger(lumber.TRACE)
}
if s.HotTemplateReloading {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router := gin.Default()
router.SetFuncMap(template.FuncMap{
"sniffContentType": sniffContentType,
})
if s.HotTemplateReloading {
router.LoadHTMLGlob("templates/*.tmpl")
} else {
router.HTMLRender = loadTemplates("index.tmpl")
}
store := sessions.NewCookieStore([]byte(s.Secret))
router.Use(sessions.Sessions("mysession", store))
if s.SecretCode != "" {
cfg := &secretRequired.Config{
Secret: s.SecretCode,
Path: "/login/",
RequireAuth: func(c *gin.Context) bool {
page := c.Param("page")
cmd := c.Param("command")
if page == "sitemap.xml" || page == "favicon.ico" || page == "static" || page == "uploads" {
return false // no auth for these
}
if page != "" && cmd == "/read" {
p := Open(page)
fmt.Printf("p: '%+v'\n", p)
if p != nil && p.IsPublished {
return false // Published pages don't require auth.
}
}
return true
},
}
router.Use(cfg.Middleware)
}
// router.Use(static.Serve("/static/", static.LocalFile("./static", true)))
router.GET("/", func(c *gin.Context) {
if s.DefaultPage != "" {
c.Redirect(302, "/"+s.DefaultPage+"/read")
} else {
c.Redirect(302, "/"+randomAlliterateCombo())
}
})
router.POST("/uploads", handleUpload)
router.GET("/:page", func(c *gin.Context) {
page := c.Param("page")
c.Redirect(302, "/"+page+"/")
})
router.GET("/:page/*command", handlePageRequest)
router.POST("/update", handlePageUpdate)
router.POST("/relinquish", handlePageRelinquish) // relinquish returns the page no matter what (and destroys if nessecary)
router.POST("/exists", handlePageExists)
router.POST("/prime", handlePrime)
router.POST("/lock", handleLock)
router.POST("/publish", handlePublish)
router.POST("/encrypt", handleEncrypt)
router.DELETE("/oldlist", handleClearOldListItems)
router.DELETE("/listitem", deleteListItem)
// start long-processes as threads
go thread_SiteMap()
// collect custom CSS
if len(s.CssFile) > 0 {
var errRead error
customCSS, errRead = ioutil.ReadFile(s.CssFile)
if errRead != nil {
fmt.Println(errRead.Error())
return
}
fmt.Printf("Loaded CSS file, %d bytes\n", len(customCSS))
}
// lock all pages automatically
if s.DefaultPassword != "" {
fmt.Println("running with locked pages")
defaultLock = HashPassword(s.DefaultPassword)
}
// set the debounce time
debounceTime = s.Debounce
// set diary mode
diaryMode = s.Diary
// Allow iframe/scripts in markup?
allowInsecureHtml = s.AllowInsecure
if s.TLS {
http.ListenAndServeTLS(s.Host+":"+s.Port, s.CertPath, s.KeyPath, router)
} else {
panic(router.Run(s.Host + ":" + s.Port))
}
}
func loadTemplates(list ...string) multitemplate.Render {
r := multitemplate.New()
for _, x := range list {
templateString, err := Asset("templates/" + x)
if err != nil {
panic(err)
}
tmplMessage, err := template.New(x).Funcs(template.FuncMap{
"sniffContentType": sniffContentType,
}).Parse(string(templateString))
if err != nil {
panic(err)
}
r.Add(x, tmplMessage)
}
return r
}
func pageIsLocked(p *Page, c *gin.Context) bool {
// it is easier to reason about when the page is actually unlocked
var unlocked = !p.IsLocked ||
(p.IsLocked && p.UnlockedFor == getSetSessionID(c))
return !unlocked
}
func handlePageRelinquish(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
}
var json QueryJSON
err := c.BindJSON(&json)
if err != nil {
log.Trace(err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
return
}
if len(json.Page) == 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
return
}
message := "Relinquished"
p := Open(json.Page)
name := p.Meta
if name == "" {
name = json.Page
}
text := p.Text.GetCurrent()
isLocked := pageIsLocked(p, c)
isEncrypted := p.IsEncrypted
destroyed := p.IsPrimedForSelfDestruct
if !isLocked && p.IsPrimedForSelfDestruct {
p.Erase()
message = "Relinquished and erased"
}
c.JSON(http.StatusOK, gin.H{"success": true,
"name": name,
"message": message,
"text": text,
"locked": isLocked,
"encrypted": isEncrypted,
"destroyed": destroyed})
}
func getSetSessionID(c *gin.Context) (sid string) {
var (
session = sessions.Default(c)
v = session.Get("sid")
)
if v != nil {
sid = v.(string)
}
if v == nil || sid == "" {
sid = RandStringBytesMaskImprSrc(8)
session.Set("sid", sid)
session.Save()
}
return sid
}
func thread_SiteMap() {
for {
if needSitemapUpdate {
log.Info("Generating sitemap...")
needSitemapUpdate = false
ioutil.WriteFile(path.Join(pathToData, "sitemap.xml"), []byte(generateSiteMap()), 0644)
log.Info("..finished generating sitemap")
}
time.Sleep(time.Second)
}
}
func generateSiteMap() (sitemap string) {
files, _ := ioutil.ReadDir(pathToData)
lastEdited := make([]string, len(files))
names := make([]string, len(files))
i := 0
for _, f := range files {
names[i] = DecodeFileName(f.Name())
p := Open(names[i])
if p.IsPublished {
lastEdited[i] = time.Unix(p.Text.LastEditTime()/1000000000, 0).Format("2006-01-02")
i++
}
}
names = names[:i]
lastEdited = lastEdited[:i]
sitemap = ""
for i := range names {
sitemap += fmt.Sprintf(`
{{ .Request.Host }}/%s/read
%s
monthly
0.8
`, names[i], lastEdited[i])
}
sitemap += ""
return
}
func handlePageRequest(c *gin.Context) {
page := c.Param("page")
command := c.Param("command")
if page == "sitemap.xml" {
siteMap, err := ioutil.ReadFile(path.Join(pathToData, "sitemap.xml"))
if err != nil {
c.Data(http.StatusInternalServerError, contentType("sitemap.xml"), []byte(""))
} else {
fmt.Fprintln(c.Writer, ``)
template.Must(template.New("sitemap").Parse(string(siteMap))).Execute(c.Writer, c)
}
return
} else if page == "favicon.ico" {
data, _ := Asset("/static/img/cowyo/favicon.ico")
c.Data(http.StatusOK, contentType("/static/img/cowyo/favicon.ico"), data)
return
} else if page == "static" {
filename := page + command
var data []byte
if filename == "static/css/custom.css" {
data = customCSS
} else {
var errAssset error
data, errAssset = Asset(filename)
if errAssset != nil {
c.String(http.StatusInternalServerError, "Could not find data")
}
}
c.Data(http.StatusOK, contentType(filename), data)
return
} else if page == "uploads" {
if len(command) == 0 || command == "/" || command == "/edit" {
if !allowFileUploads {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Uploads are disabled on this server"))
return
}
} else {
command = command[1:]
if !strings.HasSuffix(command, ".upload") {
command = command + ".upload"
}
pathname := path.Join(pathToData, command)
if allowInsecureHtml {
c.Header(
"Content-Disposition",
`inline; filename="`+c.DefaultQuery("filename", "upload")+`"`,
)
} else {
// Prevent malicious html uploads by forcing type to plaintext and 'download-instead-of-view'
c.Header("Content-Type", "text/plain")
c.Header(
"Content-Disposition",
`attachment; filename="`+c.DefaultQuery("filename", "upload")+`"`,
)
}
c.File(pathname)
return
}
}
p := Open(page)
if len(command) < 2 {
if p.IsPublished {
c.Redirect(302, "/"+page+"/read")
} else {
c.Redirect(302, "/"+page+"/edit")
}
return
}
// use the default lock
if defaultLock != "" && p.IsNew() {
p.IsLocked = true
p.PassphraseToUnlock = defaultLock
}
version := c.DefaultQuery("version", "ajksldfjl")
isLocked := pageIsLocked(p, c)
// Disallow anything but viewing locked/encrypted pages
if (p.IsEncrypted || isLocked) &&
(command[0:2] != "/v" && command[0:2] != "/r") {
c.Redirect(302, "/"+page+"/view")
return
}
// Destroy page if it is opened and primed
if p.IsPrimedForSelfDestruct && !isLocked && !p.IsEncrypted {
p.Update("This page has self-destructed. You cannot return to it.\n\n" + p.Text.GetCurrent())
p.Erase()
if p.IsPublished {
command = "/read"
} else {
command = "/view"
}
}
if command == "/erase" {
if !isLocked && !p.IsEncrypted {
p.Erase()
c.Redirect(302, "/"+page+"/edit")
} else {
c.Redirect(302, "/"+page+"/view")
}
return
}
rawText := p.Text.GetCurrent()
rawHTML := p.RenderedPage
// Check to see if an old version is requested
versionInt, versionErr := strconv.Atoi(version)
if versionErr == nil && versionInt > 0 {
versionText, err := p.Text.GetPreviousByTimestamp(int64(versionInt))
if err == nil {
rawText = versionText
rawHTML = GithubMarkdownToHTML(rawText)
}
}
// Get history
var versionsInt64 []int64
var versionsChangeSums []int
var versionsText []string
if command[0:2] == "/h" {
versionsInt64, versionsChangeSums = p.Text.GetMajorSnapshotsAndChangeSums(60) // get snapshots 60 seconds apart
versionsText = make([]string, len(versionsInt64))
for i, v := range versionsInt64 {
versionsText[i] = time.Unix(v/1000000000, 0).Format("Mon Jan 2 15:04:05 MST 2006")
}
versionsText = reverseSliceString(versionsText)
versionsInt64 = reverseSliceInt64(versionsInt64)
versionsChangeSums = reverseSliceInt(versionsChangeSums)
}
if len(command) > 3 && command[0:3] == "/ra" {
c.Writer.Header().Set("Content-Type", "text/plain")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-Max")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Data(200, contentType(p.Name), []byte(rawText))
return
}
var DirectoryEntries []os.FileInfo
if page == "ls" {
command = "/view"
DirectoryEntries = DirectoryList()
}
if page == "uploads" {
command = "/view"
var err error
DirectoryEntries, err = UploadList()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
}
// swap out /view for /read if it is published
if p.IsPublished {
rawHTML = strings.Replace(rawHTML, "/view", "/read", -1)
}
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"EditPage": command[0:2] == "/e", // /edit
"ViewPage": command[0:2] == "/v", // /view
"ListPage": command[0:2] == "/l", // /list
"HistoryPage": command[0:2] == "/h", // /history
"ReadPage": command[0:2] == "/r", // /history
"DontKnowPage": command[0:2] != "/e" &&
command[0:2] != "/v" &&
command[0:2] != "/l" &&
command[0:2] != "/r" &&
command[0:2] != "/h",
"DirectoryPage": page == "ls" || page == "uploads",
"UploadPage": page == "uploads",
"DirectoryEntries": DirectoryEntries,
"Page": page,
"RenderedPage": template.HTML([]byte(rawHTML)),
"RawPage": rawText,
"Versions": versionsInt64,
"VersionsText": versionsText,
"VersionsChangeSums": versionsChangeSums,
"IsLocked": isLocked,
"IsEncrypted": p.IsEncrypted,
"ListItems": renderList(rawText),
"Route": "/" + page + command,
"HasDotInName": strings.Contains(page, "."),
"RecentlyEdited": getRecentlyEdited(page, c),
"IsPublished": p.IsPublished,
"CustomCSS": len(customCSS) > 0,
"Debounce": debounceTime,
"DiaryMode": diaryMode,
"Date": time.Now().Format("2006-01-02"),
"UnixTime": time.Now().Unix(),
"ChildPageNames": p.ChildPageNames(),
"AllowFileUploads": allowFileUploads,
"MaxUploadMB": maxUploadMB,
})
}
func getRecentlyEdited(title string, c *gin.Context) []string {
session := sessions.Default(c)
var recentlyEdited string
v := session.Get("recentlyEdited")
editedThings := []string{}
if v == nil {
recentlyEdited = title
} else {
editedThings = strings.Split(v.(string), "|||")
if !stringInSlice(title, editedThings) {
recentlyEdited = v.(string) + "|||" + title
} else {
recentlyEdited = v.(string)
}
}
session.Set("recentlyEdited", recentlyEdited)
session.Save()
editedThingsWithoutCurrent := make([]string, len(editedThings))
i := 0
for _, thing := range editedThings {
if thing == title {
continue
}
if strings.Contains(thing, "icon-") {
continue
}
editedThingsWithoutCurrent[i] = thing
i++
}
return editedThingsWithoutCurrent[:i]
}
func handlePageExists(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
}
var json QueryJSON
err := c.BindJSON(&json)
if err != nil {
log.Trace(err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON", "exists": false})
return
}
p := Open(json.Page)
if len(p.Text.GetCurrent()) > 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " found", "exists": true})
} else {
c.JSON(http.StatusOK, gin.H{"success": true, "message": json.Page + " not found", "exists": false})
}
}
func handlePageUpdate(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
NewText string `json:"new_text"`
FetchedAt int64 `json:"fetched_at"`
IsEncrypted bool `json:"is_encrypted"`
IsPrimed bool `json:"is_primed"`
Meta string `json:"meta"`
}
var json QueryJSON
err := c.BindJSON(&json)
if err != nil {
log.Trace(err.Error())
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong JSON"})
return
}
if len(json.NewText) > 100000000 {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Too much"})
return
}
if len(json.Page) == 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Must specify `page`"})
return
}
log.Trace("Update: %v", json)
p := Open(json.Page)
var (
message string
sinceLastEdit = time.Since(p.LastEditTime())
)
success := false
if pageIsLocked(p, c) {
if sinceLastEdit < minutesToUnlock {
message = "This page is being edited by someone else"
} else {
// here what might have happened is that two people unlock without
// editing thus they both suceeds but only one is able to edit
message = "Locked, must unlock first"
}
} else if p.IsEncrypted {
message = "Encrypted, must decrypt first"
} else if json.FetchedAt > 0 && p.LastEditUnixTime() > json.FetchedAt {
message = "Refusing to overwrite others work"
} else {
p.Meta = json.Meta
p.Update(json.NewText)
if json.IsEncrypted {
p.IsEncrypted = true
}
if json.IsPrimed {
p.IsPrimedForSelfDestruct = true
}
p.Save()
message = "Saved"
if p.IsPublished {
needSitemapUpdate = true
}
success = true
}
c.JSON(http.StatusOK, gin.H{"success": success, "message": message, "unix_time": time.Now().Unix()})
}
func handlePrime(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
}
var json QueryJSON
if c.BindJSON(&json) != nil {
c.String(http.StatusBadRequest, "Problem binding keys")
return
}
log.Trace("Update: %v", json)
p := Open(json.Page)
if pageIsLocked(p, c) {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
return
} else if p.IsEncrypted {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
return
}
p.IsPrimedForSelfDestruct = true
p.Save()
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Primed"})
}
func handleLock(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
Passphrase string `json:"passphrase"`
}
var json QueryJSON
if c.BindJSON(&json) != nil {
c.String(http.StatusBadRequest, "Problem binding keys")
return
}
p := Open(json.Page)
if defaultLock != "" && p.IsNew() {
p.IsLocked = true // IsLocked was replaced by variable wrt Context
p.PassphraseToUnlock = defaultLock
}
if p.IsEncrypted {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
return
}
var (
message string
sessionID = getSetSessionID(c)
sinceLastEdit = time.Since(p.LastEditTime())
)
// both lock/unlock ends here on locked&timeout combination
if p.IsLocked &&
p.UnlockedFor != sessionID &&
p.UnlockedFor != "" &&
sinceLastEdit.Minutes() < minutesToUnlock {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": fmt.Sprintf("This page is being edited by someone else! Will unlock automatically %2.0f minutes after the last change.", minutesToUnlock-sinceLastEdit.Minutes()),
})
return
}
if !pageIsLocked(p, c) {
p.IsLocked = true
p.PassphraseToUnlock = HashPassword(json.Passphrase)
p.UnlockedFor = ""
message = "Locked"
} else {
err2 := CheckPasswordHash(json.Passphrase, p.PassphraseToUnlock)
if err2 != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Can't unlock"})
return
}
p.UnlockedFor = sessionID
message = "Unlocked only for you"
}
p.Save()
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
}
func handlePublish(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
Publish bool `json:"publish"`
}
var json QueryJSON
if c.BindJSON(&json) != nil {
c.String(http.StatusBadRequest, "Problem binding keys")
return
}
p := Open(json.Page)
p.IsPublished = json.Publish
p.Save()
message := "Published"
if !p.IsPublished {
message = "Unpublished"
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
}
func handleUpload(c *gin.Context) {
if !allowFileUploads {
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("Uploads are disabled on this server"))
return
}
file, info, err := c.Request.FormFile("file")
defer file.Close()
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
h := sha256.New()
if _, err := io.Copy(h, file); err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
newName := "sha256-" + encodeBytesToBase32(h.Sum(nil))
// Replaces any existing version, but sha256 collisions are rare as anything.
outfile, err := os.Create(path.Join(pathToData, newName+".upload"))
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
file.Seek(0, io.SeekStart)
_, err = io.Copy(outfile, file)
if err != nil {
c.AbortWithError(http.StatusInternalServerError, err)
return
}
c.Header("Location", "/uploads/"+newName+"?filename="+url.QueryEscape(info.Filename))
return
}
func handleEncrypt(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
Passphrase string `json:"passphrase"`
}
var json QueryJSON
if c.BindJSON(&json) != nil {
c.String(http.StatusBadRequest, "Problem binding keys")
return
}
p := Open(json.Page)
if pageIsLocked(p, c) {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
return
}
q := Open(json.Page)
var message string
if p.IsEncrypted {
decrypted, err2 := encrypt.DecryptString(p.Text.GetCurrent(), json.Passphrase)
if err2 != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Wrong password"})
return
}
q.Erase()
q = Open(json.Page)
q.Update(decrypted)
q.IsEncrypted = false
q.IsLocked = p.IsLocked
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
message = "Decrypted"
} else {
currentText := p.Text.GetCurrent()
encrypted, _ := encrypt.EncryptString(currentText, json.Passphrase)
q.Erase()
q = Open(json.Page)
q.Update(encrypted)
q.IsEncrypted = true
q.IsLocked = p.IsLocked
q.IsPrimedForSelfDestruct = p.IsPrimedForSelfDestruct
message = "Encrypted"
}
q.Save()
c.JSON(http.StatusOK, gin.H{"success": true, "message": message})
}
func deleteListItem(c *gin.Context) {
lineNum, err := strconv.Atoi(c.DefaultQuery("lineNum", "None"))
page := c.Query("page") // shortcut for c.Request.URL.Query().Get("lastname")
if err == nil {
p := Open(page)
_, listItems := reorderList(p.Text.GetCurrent())
newText := p.Text.GetCurrent()
for i, lineString := range listItems {
// fmt.Println(i, lineString, lineNum)
if i+1 == lineNum {
// fmt.Println("MATCHED")
if strings.Contains(lineString, "~~") == false {
// fmt.Println(p.Text, "("+lineString[2:]+"\n"+")", "~~"+lineString[2:]+"~~"+"\n")
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", "~~"+strings.TrimSpace(lineString[2:])+"~~"+"\n", 1)
} else {
newText = strings.Replace(newText+"\n", lineString[2:]+"\n", lineString[4:len(lineString)-2]+"\n", 1)
}
p.Update(newText)
break
}
}
c.JSON(200, gin.H{
"success": true,
"message": "Done.",
})
} else {
c.JSON(200, gin.H{
"success": false,
"message": err.Error(),
})
}
}
func handleClearOldListItems(c *gin.Context) {
type QueryJSON struct {
Page string `json:"page"`
}
var json QueryJSON
if c.BindJSON(&json) != nil {
c.String(http.StatusBadRequest, "Problem binding keys")
return
}
p := Open(json.Page)
if p.IsEncrypted {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Encrypted"})
return
}
if pageIsLocked(p, c) {
c.JSON(http.StatusOK, gin.H{"success": false, "message": "Locked"})
return
}
lines := strings.Split(p.Text.GetCurrent(), "\n")
newLines := make([]string, len(lines))
newLinesI := 0
for _, line := range lines {
if strings.Count(line, "~~") != 2 {
newLines[newLinesI] = line
newLinesI++
}
}
p.Update(strings.Join(newLines[0:newLinesI], "\n"))
p.Save()
c.JSON(http.StatusOK, gin.H{"success": true, "message": "Cleared"})
}