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"
"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
2017-11-04 13:18:05 +03:00
var customCSS [ ] byte
2017-11-04 13:41:56 +03:00
var defaultLock string
2017-11-05 00:30:44 +03:00
var debounceTime int
2017-11-05 01:09:10 +03:00
var diaryMode bool
2018-02-16 02:50:32 +03:00
var allowFileUploads bool
var maxUploadMB uint
2018-03-31 06:33:10 +03:00
var needSitemapUpdate = true
2018-04-28 04:32:08 +03:00
var pathToData string
var log * lumber . ConsoleLogger
2017-11-04 13:18:05 +03:00
2018-04-28 04:42:51 +03:00
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
}
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-01-31 06:15:16 +03:00
hotTemplateReloading bool ,
2018-04-28 04:32:08 +03:00
fileuploads bool ,
maxUploadSize uint ,
logger * lumber . ConsoleLogger ,
2018-01-18 12:50:13 +03:00
) {
2018-04-28 04:42:51 +03:00
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
2018-04-28 04:32:08 +03:00
if log == nil {
log = lumber . NewConsoleLogger ( lumber . TRACE )
}
2018-02-16 02:50:32 +03:00
2018-04-28 04:42:51 +03:00
if s . 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-01-31 06:15:16 +03:00
2018-04-01 04:31:26 +03:00
router . SetFuncMap ( template . FuncMap {
"sniffContentType" : sniffContentType ,
} )
2018-04-28 04:42:51 +03:00
if s . HotTemplateReloading {
2018-01-31 06:15:16 +03:00
router . LoadHTMLGlob ( "templates/*.tmpl" )
} else {
router . HTMLRender = loadTemplates ( "index.tmpl" )
}
2018-04-28 04:42:51 +03:00
store := sessions . NewCookieStore ( [ ] byte ( s . Secret ) )
2017-09-29 15:48:32 +03:00
router . Use ( sessions . Sessions ( "mysession" , store ) )
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" {
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 )
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
router . POST ( "/uploads" , handleUpload )
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
} )
router . GET ( "/:page/*command" , handlePageRequest )
router . POST ( "/update" , handlePageUpdate )
2017-06-28 06:32:38 +03:00
router . POST ( "/relinquish" , handlePageRelinquish ) // relinquish returns the page no matter what (and destroys if nessecary)
2017-07-05 19:49:06 +03:00
router . POST ( "/exists" , handlePageExists )
2017-03-24 02:47:41 +03:00
router . POST ( "/prime" , handlePrime )
router . POST ( "/lock" , handleLock )
2017-10-15 16:49:40 +03:00
router . POST ( "/publish" , handlePublish )
2017-03-24 02:47:41 +03:00
router . POST ( "/encrypt" , handleEncrypt )
router . DELETE ( "/oldlist" , handleClearOldListItems )
router . DELETE ( "/listitem" , deleteListItem )
2017-10-15 17:18:48 +03:00
// start long-processes as threads
go thread_SiteMap ( )
2017-11-04 13:18:05 +03:00
// collect custom CSS
2018-04-28 04:42:51 +03:00
if len ( s . CssFile ) > 0 {
2017-11-04 13:18:05 +03:00
var errRead error
2018-04-28 04:42:51 +03:00
customCSS , errRead = ioutil . ReadFile ( s . CssFile )
2017-11-04 13:18:05 +03:00
if errRead != nil {
fmt . Println ( errRead . Error ( ) )
return
}
fmt . Printf ( "Loaded CSS file, %d bytes\n" , len ( customCSS ) )
}
2017-11-05 00:30:44 +03:00
// lock all pages automatically
2018-04-28 04:42:51 +03:00
if s . DefaultPassword != "" {
2017-11-04 13:41:56 +03:00
fmt . Println ( "running with locked pages" )
2018-04-28 04:42:51 +03:00
defaultLock = HashPassword ( s . DefaultPassword )
2017-11-04 13:41:56 +03:00
}
2017-11-05 00:30:44 +03:00
// set the debounce time
2018-04-28 04:42:51 +03:00
debounceTime = s . Debounce
2017-11-05 00:30:44 +03:00
2017-11-05 01:09:10 +03:00
// set diary mode
2018-04-28 04:42:51 +03:00
diaryMode = s . Diary
2017-11-05 01:09:10 +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-01-21 04:09:16 +03:00
2018-04-28 04:42:51 +03:00
if s . TLS {
http . ListenAndServeTLS ( s . Host + ":" + s . Port , s . CertPath , s . KeyPath , router )
2017-06-23 16:40:35 +03:00
} else {
2018-04-28 04:42:51 +03:00
panic ( router . Run ( s . Host + ":" + s . Port ) )
2017-06-23 16:40:35 +03:00
}
2017-03-24 02:47:41 +03:00
}
func loadTemplates ( list ... string ) multitemplate . Render {
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 {
"sniffContentType" : sniffContentType ,
} ) . 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
}
2017-06-28 06:32:38 +03:00
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 )
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
}
2017-10-15 17:18:48 +03:00
func thread_SiteMap ( ) {
for {
2018-03-31 06:33:10 +03:00
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 )
2017-10-15 17:18:48 +03:00
}
}
2017-10-15 16:49:40 +03:00
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 ]
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
2017-03-24 02:47:41 +03:00
func handlePageRequest ( c * gin . Context ) {
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" {
2017-10-15 17:18:48 +03:00
siteMap , err := ioutil . ReadFile ( path . Join ( pathToData , "sitemap.xml" ) )
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" {
data = customCSS
} 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" {
if ! allowFileUploads {
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"
}
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
2018-02-16 02:50:32 +03:00
}
2017-03-24 02:47:41 +03:00
}
2018-02-16 02:50:32 +03:00
2017-11-02 15:27:39 +03:00
p := 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
if defaultLock != "" && p . IsNew ( ) {
p . IsLocked = true
p . PassphraseToUnlock = defaultLock
}
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-01-18 09:05:07 +03:00
DirectoryEntries = 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
DirectoryEntries , err = UploadList ( )
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 ,
2017-11-04 13:18:05 +03:00
"CustomCSS" : len ( customCSS ) > 0 ,
2017-11-05 00:30:44 +03:00
"Debounce" : debounceTime ,
2017-11-05 01:09:10 +03:00
"DiaryMode" : diaryMode ,
"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-02-16 02:50:32 +03:00
"AllowFileUploads" : allowFileUploads ,
"MaxUploadMB" : maxUploadMB ,
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 ]
}
2017-07-05 19:49:06 +03:00
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 } )
}
}
2017-03-24 02:47:41 +03:00
func handlePageUpdate ( c * gin . Context ) {
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 {
log . Trace ( err . Error ( ) )
2017-03-24 02:47:41 +03:00
c . JSON ( http . StatusOK , gin . H { "success" : false , "message" : "Wrong JSON" } )
return
}
2017-08-17 17:36:07 +03:00
if len ( json . NewText ) > 100000000 {
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
}
2017-03-24 02:47:41 +03:00
log . Trace ( "Update: %v" , json )
p := 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 {
needSitemapUpdate = true
}
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
}
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 )
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" } )
}
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 )
2017-11-04 13:41:56 +03:00
if defaultLock != "" && p . IsNew ( ) {
2018-03-12 01:46:29 +03:00
p . IsLocked = true // IsLocked was replaced by variable wrt Context
2017-11-04 13:41:56 +03:00
p . PassphraseToUnlock = defaultLock
}
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
}
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 } )
2017-03-24 02:47:41 +03:00
}
2018-02-16 02:50:32 +03:00
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
}
2017-03-24 02:47:41 +03:00
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 )
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
}
q := Open ( json . Page )
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 ( )
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 ( )
2017-06-30 00:24:34 +03:00
encrypted , _ := encrypt . EncryptString ( currentText , json . Passphrase )
2017-03-24 02:47:41 +03:00
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
}
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" } )
}