2018-04-28 04:32:08 +03:00
package server
2017-03-24 02:47:41 +03:00
import (
2018-02-16 02:50:32 +03:00
"crypto/sha256"
2017-10-15 16:49:40 +03:00
"fmt"
2017-03-24 02:47:41 +03:00
"html/template"
2018-02-16 02:50:32 +03:00
"io"
2017-10-15 16:49:40 +03:00
"io/ioutil"
2017-03-24 02:47:41 +03:00
"net/http"
2018-02-16 02:50:32 +03:00
"net/url"
"os"
2017-10-15 17:18:48 +03:00
"path"
2017-03-24 02:47:41 +03:00
"strconv"
"strings"
2018-04-28 12:32:48 +03:00
"sync"
2017-03-24 02:47:41 +03:00
"time"
2018-01-21 04:09:16 +03:00
secretRequired "github.com/danielheath/gin-teeny-security"
2017-03-24 02:47:41 +03:00
"github.com/gin-contrib/multitemplate"
2017-09-29 15:48:32 +03:00
"github.com/gin-contrib/sessions"
2017-03-24 02:47:41 +03:00
"github.com/gin-gonic/gin"
2018-04-28 04:32:08 +03:00
"github.com/jcelliott/lumber"
2017-06-30 00:24:34 +03:00
"github.com/schollz/cowyo/encrypt"
2017-03-24 02:47:41 +03:00
)
2018-03-12 01:46:29 +03:00
const minutesToUnlock = 10.0
2018-04-28 04:42:51 +03:00
type Site struct {
2018-04-30 13:19:11 +03:00
PathToData string
Css [ ] byte
DefaultPage string
DefaultPassword string
Debounce int
Diary bool
SessionStore sessions . Store
SecretCode string
AllowInsecure bool
Fileuploads bool
MaxUploadSize uint
Logger * lumber . ConsoleLogger
2018-05-02 09:27:35 +03:00
MaxDocumentSize uint // in runes; about a 10mb limit by default
2018-04-28 12:32:48 +03:00
saveMut sync . Mutex
2018-04-28 05:19:43 +03:00
sitemapUpToDate bool // TODO this makes everything use a pointer
2018-04-28 04:42:51 +03:00
}
2018-04-28 05:19:43 +03:00
func ( s * Site ) defaultLock ( ) string {
2018-04-28 05:16:10 +03:00
if s . DefaultPassword == "" {
return ""
}
return HashPassword ( s . DefaultPassword )
}
2018-04-30 13:19:11 +03:00
var hotTemplateReloading bool
2018-04-28 04:32:08 +03:00
func Serve (
filepathToData ,
2018-01-18 12:50:13 +03:00
host ,
port ,
crt_path ,
key_path string ,
TLS bool ,
cssFile string ,
defaultPage string ,
defaultPassword string ,
debounce int ,
diary bool ,
2018-01-18 12:51:08 +03:00
secret string ,
2018-01-21 04:09:16 +03:00
secretCode string ,
allowInsecure bool ,
2018-04-28 04:32:08 +03:00
fileuploads bool ,
maxUploadSize uint ,
2018-05-02 09:27:35 +03:00
maxDocumentSize uint ,
2018-04-28 04:32:08 +03:00
logger * lumber . ConsoleLogger ,
2018-01-18 12:50:13 +03:00
) {
2018-04-28 04:56:07 +03:00
var customCSS [ ] byte
// collect custom CSS
if len ( cssFile ) > 0 {
var errRead error
customCSS , errRead = ioutil . ReadFile ( cssFile )
if errRead != nil {
fmt . Println ( errRead )
return
}
fmt . Printf ( "Loaded CSS file, %d bytes\n" , len ( customCSS ) )
}
router := Site {
2018-05-02 09:27:35 +03:00
PathToData : filepathToData ,
Css : customCSS ,
DefaultPage : defaultPage ,
DefaultPassword : defaultPassword ,
Debounce : debounce ,
Diary : diary ,
SessionStore : sessions . NewCookieStore ( [ ] byte ( secret ) ) ,
SecretCode : secretCode ,
AllowInsecure : allowInsecure ,
Fileuploads : fileuploads ,
MaxUploadSize : maxUploadSize ,
Logger : logger ,
MaxDocumentSize : maxDocumentSize ,
2018-04-28 04:56:07 +03:00
} . Router ( )
if TLS {
http . ListenAndServeTLS ( host + ":" + port , crt_path , key_path , router )
} else {
panic ( router . Run ( host + ":" + port ) )
}
2018-04-28 04:42:51 +03:00
}
2018-04-28 04:56:07 +03:00
func ( s Site ) Router ( ) * gin . Engine {
2018-04-28 05:30:13 +03:00
if s . Logger == nil {
s . Logger = lumber . NewConsoleLogger ( lumber . TRACE )
2018-04-28 04:32:08 +03:00
}
2018-02-16 02:50:32 +03:00
2018-04-30 13:19:11 +03:00
if hotTemplateReloading {
2018-01-31 06:15:16 +03:00
gin . SetMode ( gin . DebugMode )
} else {
gin . SetMode ( gin . ReleaseMode )
}
2018-01-21 04:09:16 +03:00
2017-03-24 02:47:41 +03:00
router := gin . Default ( )
2018-04-01 04:31:26 +03:00
router . SetFuncMap ( template . FuncMap {
2018-04-28 12:29:35 +03:00
"sniffContentType" : s . sniffContentType ,
2018-04-01 04:31:26 +03:00
} )
2018-04-30 13:19:11 +03:00
if hotTemplateReloading {
2018-01-31 06:15:16 +03:00
router . LoadHTMLGlob ( "templates/*.tmpl" )
} else {
2018-04-28 12:29:35 +03:00
router . HTMLRender = s . loadTemplates ( "index.tmpl" )
2018-01-31 06:15:16 +03:00
}
2018-05-27 14:49:34 +03:00
router . Use ( sessions . Sessions ( "_session" , s . SessionStore ) )
2018-04-28 04:42:51 +03:00
if s . SecretCode != "" {
2018-02-07 07:09:01 +03:00
cfg := & secretRequired . Config {
2018-04-28 04:42:51 +03:00
Secret : s . SecretCode ,
2018-02-07 07:09:01 +03:00
Path : "/login/" ,
RequireAuth : func ( c * gin . Context ) bool {
page := c . Param ( "page" )
cmd := c . Param ( "command" )
2018-02-16 02:50:32 +03:00
if page == "sitemap.xml" || page == "favicon.ico" || page == "static" || page == "uploads" {
return false // no auth for these
2018-02-07 07:09:01 +03:00
}
if page != "" && cmd == "/read" {
2018-04-28 05:30:13 +03:00
p := s . Open ( page )
2018-02-07 07:09:01 +03:00
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 )
2018-01-21 04:09:16 +03:00
}
2018-01-31 06:15:16 +03:00
2017-03-24 02:47:41 +03:00
// router.Use(static.Serve("/static/", static.LocalFile("./static", true)))
router . GET ( "/" , func ( c * gin . Context ) {
2018-04-28 04:42:51 +03:00
if s . DefaultPage != "" {
c . Redirect ( 302 , "/" + s . DefaultPage + "/read" )
2017-11-04 13:21:51 +03:00
} else {
c . Redirect ( 302 , "/" + randomAlliterateCombo ( ) )
}
2017-03-24 02:47:41 +03:00
} )
2018-02-16 02:50:32 +03:00
2018-04-28 05:13:19 +03:00
router . POST ( "/uploads" , s . handleUpload )
2018-02-16 02:50:32 +03:00
2017-03-24 02:47:41 +03:00
router . GET ( "/:page" , func ( c * gin . Context ) {
page := c . Param ( "page" )
2017-11-04 21:49:02 +03:00
c . Redirect ( 302 , "/" + page + "/" )
2017-03-24 02:47:41 +03:00
} )
2018-04-28 04:56:07 +03:00
router . GET ( "/:page/*command" , s . handlePageRequest )
2018-04-28 05:19:43 +03:00
router . POST ( "/update" , s . handlePageUpdate )
2018-04-28 05:30:13 +03:00
router . POST ( "/relinquish" , s . handlePageRelinquish ) // relinquish returns the page no matter what (and destroys if nessecary)
router . POST ( "/exists" , s . handlePageExists )
router . POST ( "/prime" , s . handlePrime )
2018-04-28 05:16:10 +03:00
router . POST ( "/lock" , s . handleLock )
2018-04-28 05:30:13 +03:00
router . POST ( "/publish" , s . handlePublish )
router . POST ( "/encrypt" , s . handleEncrypt )
router . DELETE ( "/oldlist" , s . handleClearOldListItems )
router . DELETE ( "/listitem" , s . deleteListItem )
2017-03-24 02:47:41 +03:00
2017-10-15 17:18:48 +03:00
// start long-processes as threads
2018-04-28 05:19:43 +03:00
go s . thread_SiteMap ( )
2017-10-15 17:18:48 +03:00
2018-01-21 04:09:16 +03:00
// Allow iframe/scripts in markup?
2018-04-28 04:42:51 +03:00
allowInsecureHtml = s . AllowInsecure
2018-04-28 04:56:07 +03:00
return router
2017-03-24 02:47:41 +03:00
}
2018-04-28 12:29:35 +03:00
func ( s * Site ) loadTemplates ( list ... string ) multitemplate . Render {
2017-03-24 02:47:41 +03:00
r := multitemplate . New ( )
for _ , x := range list {
templateString , err := Asset ( "templates/" + x )
if err != nil {
panic ( err )
}
2018-04-20 06:59:14 +03:00
tmplMessage , err := template . New ( x ) . Funcs ( template . FuncMap {
2018-04-28 12:29:35 +03:00
"sniffContentType" : s . sniffContentType ,
2018-04-20 06:59:14 +03:00
} ) . Parse ( string ( templateString ) )
2017-03-24 02:47:41 +03:00
if err != nil {
panic ( err )
}
r . Add ( x , tmplMessage )
}
return r
}
2018-03-12 01:46:29 +03:00
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
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handlePageRelinquish ( c * gin . Context ) {
2017-06-28 06:32:38 +03:00
type QueryJSON struct {
Page string ` json:"page" `
}
var json QueryJSON
err := c . BindJSON ( & json )
if err != nil {
2018-04-28 05:30:13 +03:00
s . Logger . Trace ( err . Error ( ) )
2017-06-28 06:32:38 +03:00
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"
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2017-08-17 17:36:07 +03:00
name := p . Meta
if name == "" {
name = json . Page
}
2017-06-28 06:32:38 +03:00
text := p . Text . GetCurrent ( )
2018-03-12 01:46:29 +03:00
isLocked := pageIsLocked ( p , c )
2017-06-28 19:10:32 +03:00
isEncrypted := p . IsEncrypted
destroyed := p . IsPrimedForSelfDestruct
2018-03-12 01:46:29 +03:00
if ! isLocked && p . IsPrimedForSelfDestruct {
2017-06-28 06:32:38 +03:00
p . Erase ( )
message = "Relinquished and erased"
}
2017-08-12 17:05:01 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : true ,
"name" : name ,
"message" : message ,
"text" : text ,
"locked" : isLocked ,
"encrypted" : isEncrypted ,
"destroyed" : destroyed } )
2017-06-28 06:32:38 +03:00
}
2018-03-12 01:46:29 +03:00
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
}
2018-04-28 05:19:43 +03:00
func ( s * Site ) thread_SiteMap ( ) {
2017-10-15 17:18:48 +03:00
for {
2018-04-28 05:19:43 +03:00
if ! s . sitemapUpToDate {
2018-04-28 05:30:13 +03:00
s . Logger . Info ( "Generating sitemap..." )
2018-04-28 05:19:43 +03:00
s . sitemapUpToDate = true
2018-04-28 12:29:35 +03:00
ioutil . WriteFile ( path . Join ( s . PathToData , "sitemap.xml" ) , [ ] byte ( s . generateSiteMap ( ) ) , 0644 )
2018-04-28 05:30:13 +03:00
s . Logger . Info ( "..finished generating sitemap" )
2018-03-31 06:33:10 +03:00
}
time . Sleep ( time . Second )
2017-10-15 17:18:48 +03:00
}
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) generateSiteMap ( ) ( sitemap string ) {
2018-04-28 12:29:35 +03:00
files , _ := ioutil . ReadDir ( s . PathToData )
2017-10-15 16:49:40 +03:00
lastEdited := make ( [ ] string , len ( files ) )
names := make ( [ ] string , len ( files ) )
i := 0
for _ , f := range files {
names [ i ] = DecodeFileName ( f . Name ( ) )
2018-04-28 05:30:13 +03:00
p := s . Open ( names [ i ] )
2017-10-15 16:49:40 +03:00
if p . IsPublished {
lastEdited [ i ] = time . Unix ( p . Text . LastEditTime ( ) / 1000000000 , 0 ) . Format ( "2006-01-02" )
i ++
}
}
names = names [ : i ]
lastEdited = lastEdited [ : i ]
2018-03-31 06:33:10 +03:00
sitemap = ""
2017-10-15 16:49:40 +03:00
for i := range names {
sitemap += fmt . Sprintf ( `
2017-10-15 17:18:48 +03:00
< url >
2018-03-31 06:33:10 +03:00
< loc > { { . Request . Host } } / % s / read < / loc >
2017-10-15 17:18:48 +03:00
< lastmod > % s < / lastmod >
< changefreq > monthly < / changefreq >
< priority > 0.8 < / priority >
< / url >
` , names [ i ] , lastEdited [ i ] )
2017-10-15 16:49:40 +03:00
}
sitemap += "</urlset>"
return
}
2017-11-02 15:27:39 +03:00
2018-04-28 05:19:43 +03:00
func ( s * Site ) handlePageRequest ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
page := c . Param ( "page" )
2017-10-15 16:58:43 +03:00
command := c . Param ( "command" )
2017-10-15 16:49:40 +03:00
if page == "sitemap.xml" {
2018-04-28 12:29:35 +03:00
siteMap , err := ioutil . ReadFile ( path . Join ( s . PathToData , "sitemap.xml" ) )
2017-10-15 17:18:48 +03:00
if err != nil {
c . Data ( http . StatusInternalServerError , contentType ( "sitemap.xml" ) , [ ] byte ( "" ) )
} else {
2018-03-31 06:33:10 +03:00
fmt . Fprintln ( c . Writer , ` <?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> ` )
template . Must ( template . New ( "sitemap" ) . Parse ( string ( siteMap ) ) ) . Execute ( c . Writer , c )
2017-10-15 17:18:48 +03:00
}
2017-10-15 16:49:40 +03:00
return
2017-10-15 16:58:43 +03:00
} else if page == "favicon.ico" {
data , _ := Asset ( "/static/img/cowyo/favicon.ico" )
c . Data ( http . StatusOK , contentType ( "/static/img/cowyo/favicon.ico" ) , data )
2017-05-24 19:42:37 +03:00
return
2017-10-15 16:58:43 +03:00
} else if page == "static" {
2017-03-24 02:47:41 +03:00
filename := page + command
2017-11-04 13:18:05 +03:00
var data [ ] byte
if filename == "static/css/custom.css" {
2018-04-28 04:56:07 +03:00
data = s . Css
2017-11-04 13:18:05 +03:00
} else {
var errAssset error
data , errAssset = Asset ( filename )
if errAssset != nil {
c . String ( http . StatusInternalServerError , "Could not find data" )
}
2017-03-24 02:47:41 +03:00
}
c . Data ( http . StatusOK , contentType ( filename ) , data )
return
2018-02-16 02:50:32 +03:00
} else if page == "uploads" {
2018-04-01 04:31:26 +03:00
if len ( command ) == 0 || command == "/" || command == "/edit" {
2018-04-28 05:13:19 +03:00
if ! s . Fileuploads {
2018-04-01 04:31:26 +03:00
c . AbortWithError ( http . StatusInternalServerError , fmt . Errorf ( "Uploads are disabled on this server" ) )
return
}
2018-02-16 02:50:32 +03:00
} else {
2018-04-01 04:31:26 +03:00
command = command [ 1 : ]
if ! strings . HasSuffix ( command , ".upload" ) {
command = command + ".upload"
}
2018-04-28 12:29:35 +03:00
pathname := path . Join ( s . PathToData , command )
2018-04-01 04:31:26 +03:00
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
2018-02-16 02:50:32 +03:00
}
2017-03-24 02:47:41 +03:00
}
2018-02-16 02:50:32 +03:00
2018-04-28 05:30:13 +03:00
p := s . Open ( page )
2017-10-15 16:58:43 +03:00
if len ( command ) < 2 {
2017-11-02 15:27:39 +03:00
if p . IsPublished {
c . Redirect ( 302 , "/" + page + "/read" )
} else {
c . Redirect ( 302 , "/" + page + "/edit" )
}
2017-10-15 16:58:43 +03:00
return
}
2017-03-24 02:47:41 +03:00
2017-11-04 13:41:56 +03:00
// use the default lock
2018-04-28 05:16:10 +03:00
if s . defaultLock ( ) != "" && p . IsNew ( ) {
2017-11-04 13:41:56 +03:00
p . IsLocked = true
2018-04-28 05:16:10 +03:00
p . PassphraseToUnlock = s . defaultLock ( )
2017-11-04 13:41:56 +03:00
}
2018-03-12 01:46:29 +03:00
version := c . DefaultQuery ( "version" , "ajksldfjl" )
isLocked := pageIsLocked ( p , c )
2017-03-24 02:47:41 +03:00
// Disallow anything but viewing locked/encrypted pages
2018-03-12 01:46:29 +03:00
if ( p . IsEncrypted || isLocked ) &&
2017-03-24 02:47:41 +03:00
( command [ 0 : 2 ] != "/v" && command [ 0 : 2 ] != "/r" ) {
c . Redirect ( 302 , "/" + page + "/view" )
return
}
// Destroy page if it is opened and primed
2018-03-12 01:46:29 +03:00
if p . IsPrimedForSelfDestruct && ! isLocked && ! p . IsEncrypted {
2018-02-18 23:04:23 +03:00
p . Update ( "<center><em>This page has self-destructed. You cannot return to it.</em></center>\n\n" + p . Text . GetCurrent ( ) )
2017-03-24 02:47:41 +03:00
p . Erase ( )
2018-02-18 22:33:32 +03:00
if p . IsPublished {
command = "/read"
} else {
command = "/view"
}
2017-03-24 02:47:41 +03:00
}
if command == "/erase" {
2018-03-12 01:46:29 +03:00
if ! isLocked && ! p . IsEncrypted {
2017-03-24 02:47:41 +03:00
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 )
}
}
2017-03-24 16:09:41 +03:00
// Get history
var versionsInt64 [ ] int64
var versionsChangeSums [ ] int
var versionsText [ ] string
if command [ 0 : 2 ] == "/h" {
2017-03-24 16:12:09 +03:00
versionsInt64 , versionsChangeSums = p . Text . GetMajorSnapshotsAndChangeSums ( 60 ) // get snapshots 60 seconds apart
2017-03-24 16:09:41 +03:00
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 )
2017-03-24 02:47:41 +03:00
}
2017-11-13 21:32:32 +03:00
if len ( command ) > 3 && command [ 0 : 3 ] == "/ra" {
2018-04-01 03:06:51 +03:00
c . Writer . Header ( ) . Set ( "Content-Type" , "text/plain" )
2017-03-24 02:47:41 +03:00
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
}
2018-04-01 04:31:26 +03:00
var DirectoryEntries [ ] os . FileInfo
2017-03-24 16:58:37 +03:00
if page == "ls" {
command = "/view"
2018-04-28 05:30:13 +03:00
DirectoryEntries = s . DirectoryList ( )
2017-03-24 16:58:37 +03:00
}
2018-04-01 04:31:26 +03:00
if page == "uploads" {
command = "/view"
var err error
2018-04-28 12:29:35 +03:00
DirectoryEntries , err = s . UploadList ( )
2018-04-01 04:31:26 +03:00
if err != nil {
c . AbortWithError ( http . StatusInternalServerError , err )
return
}
}
2017-03-24 16:58:37 +03:00
2017-11-05 00:23:38 +03:00
// swap out /view for /read if it is published
if p . IsPublished {
rawHTML = strings . Replace ( rawHTML , "/view" , "/read" , - 1 )
}
2017-03-24 02:47:41 +03:00
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
2017-11-02 15:27:39 +03:00
"ReadPage" : command [ 0 : 2 ] == "/r" , // /history
2017-03-24 02:47:41 +03:00
"DontKnowPage" : command [ 0 : 2 ] != "/e" &&
command [ 0 : 2 ] != "/v" &&
command [ 0 : 2 ] != "/l" &&
2018-01-29 14:33:11 +03:00
command [ 0 : 2 ] != "/r" &&
2017-03-24 02:47:41 +03:00
command [ 0 : 2 ] != "/h" ,
2018-04-01 04:31:26 +03:00
"DirectoryPage" : page == "ls" || page == "uploads" ,
"UploadPage" : page == "uploads" ,
2018-01-18 09:05:07 +03:00
"DirectoryEntries" : DirectoryEntries ,
2017-03-24 16:09:41 +03:00
"Page" : page ,
"RenderedPage" : template . HTML ( [ ] byte ( rawHTML ) ) ,
"RawPage" : rawText ,
"Versions" : versionsInt64 ,
"VersionsText" : versionsText ,
"VersionsChangeSums" : versionsChangeSums ,
2018-03-12 01:46:29 +03:00
"IsLocked" : isLocked ,
2017-03-24 16:09:41 +03:00
"IsEncrypted" : p . IsEncrypted ,
"ListItems" : renderList ( rawText ) ,
"Route" : "/" + page + command ,
"HasDotInName" : strings . Contains ( page , "." ) ,
2017-09-29 15:48:32 +03:00
"RecentlyEdited" : getRecentlyEdited ( page , c ) ,
2017-10-15 16:49:40 +03:00
"IsPublished" : p . IsPublished ,
2018-04-28 04:56:07 +03:00
"CustomCSS" : len ( s . Css ) > 0 ,
2018-04-28 05:12:10 +03:00
"Debounce" : s . Debounce ,
"DiaryMode" : s . Diary ,
2017-11-05 01:09:10 +03:00
"Date" : time . Now ( ) . Format ( "2006-01-02" ) ,
2018-01-31 01:33:23 +03:00
"UnixTime" : time . Now ( ) . Unix ( ) ,
2018-01-31 05:53:37 +03:00
"ChildPageNames" : p . ChildPageNames ( ) ,
2018-04-28 05:13:19 +03:00
"AllowFileUploads" : s . Fileuploads ,
2018-04-28 05:10:47 +03:00
"MaxUploadMB" : s . MaxUploadSize ,
2017-03-24 02:47:41 +03:00
} )
}
2017-09-29 15:48:32 +03:00
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
}
2017-09-29 19:21:10 +03:00
if strings . Contains ( thing , "icon-" ) {
continue
}
2017-09-29 15:48:32 +03:00
editedThingsWithoutCurrent [ i ] = thing
i ++
}
return editedThingsWithoutCurrent [ : i ]
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handlePageExists ( c * gin . Context ) {
2017-07-05 19:49:06 +03:00
type QueryJSON struct {
Page string ` json:"page" `
}
var json QueryJSON
err := c . BindJSON ( & json )
if err != nil {
2018-04-28 05:30:13 +03:00
s . Logger . Trace ( err . Error ( ) )
2017-07-05 19:49:06 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Wrong JSON" , "exists" : false } )
return
}
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2017-07-05 19:49:06 +03:00
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 } )
}
}
2018-04-28 05:19:43 +03:00
func ( s * Site ) handlePageUpdate ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
type QueryJSON struct {
2017-06-28 06:32:38 +03:00
Page string ` json:"page" `
NewText string ` json:"new_text" `
2018-01-31 01:33:23 +03:00
FetchedAt int64 ` json:"fetched_at" `
2017-06-28 06:32:38 +03:00
IsEncrypted bool ` json:"is_encrypted" `
IsPrimed bool ` json:"is_primed" `
2017-08-17 17:36:07 +03:00
Meta string ` json:"meta" `
2017-03-24 02:47:41 +03:00
}
var json QueryJSON
2017-06-28 06:32:38 +03:00
err := c . BindJSON ( & json )
if err != nil {
2018-04-28 05:30:13 +03:00
s . Logger . Trace ( err . Error ( ) )
2017-03-24 02:47:41 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Wrong JSON" } )
return
}
2018-05-02 09:27:35 +03:00
if uint ( len ( json . NewText ) ) > s . MaxDocumentSize {
2017-03-24 02:47:41 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Too much" } )
return
}
2017-06-28 06:32:38 +03:00
if len ( json . Page ) == 0 {
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Must specify `page`" } )
return
}
2018-04-28 05:30:13 +03:00
s . Logger . Trace ( "Update: %v" , json )
p := s . Open ( json . Page )
2018-03-12 01:46:29 +03:00
var (
message string
sinceLastEdit = time . Since ( p . LastEditTime ( ) )
)
2017-07-05 19:49:06 +03:00
success := false
2018-03-12 01:46:29 +03:00
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"
}
2017-03-24 02:47:41 +03:00
} else if p . IsEncrypted {
2017-06-28 06:32:38 +03:00
message = "Encrypted, must decrypt first"
2018-01-31 01:33:23 +03:00
} else if json . FetchedAt > 0 && p . LastEditUnixTime ( ) > json . FetchedAt {
message = "Refusing to overwrite others work"
2017-03-24 02:47:41 +03:00
} else {
2017-08-17 17:36:07 +03:00
p . Meta = json . Meta
2017-03-24 02:47:41 +03:00
p . Update ( json . NewText )
2017-06-28 06:32:38 +03:00
if json . IsEncrypted {
p . IsEncrypted = true
}
if json . IsPrimed {
p . IsPrimedForSelfDestruct = true
}
2017-08-17 17:44:18 +03:00
p . Save ( )
2017-03-24 02:47:41 +03:00
message = "Saved"
2018-03-31 06:33:10 +03:00
if p . IsPublished {
2018-04-28 05:19:43 +03:00
s . sitemapUpToDate = false
2018-03-31 06:33:10 +03:00
}
2017-07-05 19:49:06 +03:00
success = true
2017-03-24 02:47:41 +03:00
}
2018-01-31 01:33:23 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : success , "message" : message , "unix_time" : time . Now ( ) . Unix ( ) } )
2017-03-24 02:47:41 +03:00
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handlePrime ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
type QueryJSON struct {
Page string ` json:"page" `
}
var json QueryJSON
if c . BindJSON ( & json ) != nil {
c . String ( http . StatusBadRequest , "Problem binding keys" )
return
}
2018-04-28 05:30:13 +03:00
s . Logger . Trace ( "Update: %v" , json )
p := s . Open ( json . Page )
2018-03-12 01:46:29 +03:00
if pageIsLocked ( p , c ) {
2017-03-24 02:47:41 +03:00
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" } )
}
2018-04-28 05:19:43 +03:00
func ( s * Site ) handleLock ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
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
}
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2018-04-28 05:16:10 +03:00
if s . defaultLock ( ) != "" && p . IsNew ( ) {
2018-03-12 01:46:29 +03:00
p . IsLocked = true // IsLocked was replaced by variable wrt Context
2018-04-28 05:16:10 +03:00
p . PassphraseToUnlock = s . defaultLock ( )
2017-11-04 13:41:56 +03:00
}
2017-03-24 02:47:41 +03:00
if p . IsEncrypted {
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Encrypted" } )
return
}
2018-03-12 01:46:29 +03:00
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 {
2017-03-24 02:47:41 +03:00
err2 := CheckPasswordHash ( json . Passphrase , p . PassphraseToUnlock )
if err2 != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Can't unlock" } )
return
}
2018-03-12 01:46:29 +03:00
p . UnlockedFor = sessionID
message = "Unlocked only for you"
2017-03-24 02:47:41 +03:00
}
p . Save ( )
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : message } )
2017-10-15 16:49:40 +03:00
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handlePublish ( c * gin . Context ) {
2017-10-15 16:49:40 +03:00
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
}
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2017-10-15 16:49:40 +03:00
p . IsPublished = json . Publish
p . Save ( )
message := "Published"
if ! p . IsPublished {
message = "Unpublished"
}
c . JSON ( http . StatusOK , gin . H { "success" : true , "message" : message } )
2017-03-24 02:47:41 +03:00
}
2018-04-28 05:19:43 +03:00
func ( s * Site ) handleUpload ( c * gin . Context ) {
2018-04-28 05:13:19 +03:00
if ! s . Fileuploads {
2018-02-16 02:50:32 +03:00
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.
2018-04-28 12:29:35 +03:00
outfile , err := os . Create ( path . Join ( s . PathToData , newName + ".upload" ) )
2018-02-16 02:50:32 +03:00
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
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handleEncrypt ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
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
}
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2018-03-12 01:46:29 +03:00
if pageIsLocked ( p , c ) {
2017-03-24 02:47:41 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Locked" } )
return
}
2018-04-28 05:30:13 +03:00
q := s . Open ( json . Page )
2017-03-24 02:47:41 +03:00
var message string
if p . IsEncrypted {
2017-06-30 00:24:34 +03:00
decrypted , err2 := encrypt . DecryptString ( p . Text . GetCurrent ( ) , json . Passphrase )
2017-03-24 02:47:41 +03:00
if err2 != nil {
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Wrong password" } )
return
}
q . Erase ( )
2018-04-28 05:30:13 +03:00
q = s . Open ( json . Page )
2017-03-24 02:47:41 +03:00
q . Update ( decrypted )
q . IsEncrypted = false
q . IsLocked = p . IsLocked
q . IsPrimedForSelfDestruct = p . IsPrimedForSelfDestruct
message = "Decrypted"
} else {
currentText := p . Text . GetCurrent ( )
2017-06-30 00:24:34 +03:00
encrypted , _ := encrypt . EncryptString ( currentText , json . Passphrase )
2017-03-24 02:47:41 +03:00
q . Erase ( )
2018-04-28 05:30:13 +03:00
q = s . Open ( json . Page )
2017-03-24 02:47:41 +03:00
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 } )
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) deleteListItem ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
lineNum , err := strconv . Atoi ( c . DefaultQuery ( "lineNum" , "None" ) )
page := c . Query ( "page" ) // shortcut for c.Request.URL.Query().Get("lastname")
if err == nil {
2018-04-28 05:30:13 +03:00
p := s . Open ( page )
2017-03-24 02:47:41 +03:00
_ , 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 ( ) ,
} )
}
}
2018-04-28 05:30:13 +03:00
func ( s * Site ) handleClearOldListItems ( c * gin . Context ) {
2017-03-24 02:47:41 +03:00
type QueryJSON struct {
Page string ` json:"page" `
}
var json QueryJSON
if c . BindJSON ( & json ) != nil {
c . String ( http . StatusBadRequest , "Problem binding keys" )
return
}
2018-04-28 05:30:13 +03:00
p := s . Open ( json . Page )
2017-03-24 02:47:41 +03:00
if p . IsEncrypted {
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Encrypted" } )
return
}
2018-03-12 01:46:29 +03:00
if pageIsLocked ( p , c ) {
2017-03-24 02:47:41 +03:00
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" } )
}