2018-12-24 18:45:15 +01:00
/ *
2022-11-11 05:49:16 +01:00
* Copyright © 2018 - 2021 Musing Studio LLC .
2018-12-24 18:45:15 +01:00
*
* This file is part of WriteFreely .
*
* WriteFreely is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , included
* in the LICENSE file in this source code package .
* /
2018-12-31 07:05:26 +01:00
2018-10-17 04:31:27 +02:00
package writefreely
import (
2018-11-08 05:43:11 +01:00
"database/sql"
"encoding/json"
"fmt"
2019-06-05 18:39:22 +02:00
"html/template"
"net/http"
2020-02-19 23:07:02 +01:00
"net/url"
2019-06-05 18:39:22 +02:00
"regexp"
"strings"
"time"
2018-11-08 05:43:11 +01:00
"github.com/gorilla/mux"
2018-10-17 04:31:27 +02:00
"github.com/guregu/null"
"github.com/guregu/null/zero"
"github.com/kylemcc/twitter-text-go/extract"
2019-03-14 13:58:37 +01:00
"github.com/microcosm-cc/bluemonday"
2021-06-25 17:16:03 +02:00
stripmd "github.com/writeas/go-strip-markdown/v2"
2018-11-08 05:43:11 +01:00
"github.com/writeas/impart"
2018-10-17 04:31:27 +02:00
"github.com/writeas/monday"
"github.com/writeas/slug"
2018-11-08 05:43:11 +01:00
"github.com/writeas/web-core/activitystreams"
"github.com/writeas/web-core/bots"
2018-10-17 04:31:27 +02:00
"github.com/writeas/web-core/converter"
2018-11-08 05:43:11 +01:00
"github.com/writeas/web-core/i18n"
"github.com/writeas/web-core/log"
2018-10-17 04:31:27 +02:00
"github.com/writeas/web-core/tags"
2021-04-06 23:24:07 +02:00
"github.com/writefreely/writefreely/page"
"github.com/writefreely/writefreely/parse"
2018-10-17 04:31:27 +02:00
)
const (
// Post ID length bounds
minIDLen = 10
maxIDLen = 10
userPostIDLen = 10
postIDLen = 10
postMetaDateFormat = "2006-01-02 15:04:05"
2021-06-07 21:52:24 +02:00
shortCodePaid = "<!--paid-->"
2018-10-17 04:31:27 +02:00
)
type (
2018-11-08 05:43:11 +01:00
AnonymousPost struct {
ID string
Content string
HTMLContent template . HTML
Font string
Language string
Direction string
Title string
GenTitle string
Description string
Author string
Views int64
2020-02-19 22:38:50 +01:00
Images [ ] string
2018-11-08 05:43:11 +01:00
IsPlainText bool
IsCode bool
IsLinkable bool
}
2018-10-17 04:31:27 +02:00
AuthenticatedPost struct {
2019-05-19 05:16:42 +02:00
ID string ` json:"id" schema:"id" `
Web bool ` json:"web" schema:"web" `
2018-10-17 04:31:27 +02:00
* SubmittedPost
}
// SubmittedPost represents a post supplied by a client for publishing or
// updating. Since Title and Content can be updated to "", they are
// pointers that can be easily tested to detect changes.
SubmittedPost struct {
Slug * string ` json:"slug" schema:"slug" `
Title * string ` json:"title" schema:"title" `
Content * string ` json:"body" schema:"body" `
Font string ` json:"font" schema:"font" `
IsRTL converter . NullJSONBool ` json:"rtl" schema:"rtl" `
Language converter . NullJSONString ` json:"lang" schema:"lang" `
Created * string ` json:"created" schema:"created" `
}
// Post represents a post as found in the database.
Post struct {
ID string ` db:"id" json:"id" `
Slug null . String ` db:"slug" json:"slug,omitempty" `
Font string ` db:"text_appearance" json:"appearance" `
Language zero . String ` db:"language" json:"language" `
RTL zero . Bool ` db:"rtl" json:"rtl" `
Privacy int64 ` db:"privacy" json:"-" `
OwnerID null . Int ` db:"owner_id" json:"-" `
CollectionID null . Int ` db:"collection_id" json:"-" `
PinnedPosition null . Int ` db:"pinned_position" json:"-" `
Created time . Time ` db:"created" json:"created" `
Updated time . Time ` db:"updated" json:"updated" `
ViewCount int64 ` db:"view_count" json:"-" `
Title zero . String ` db:"title" json:"title" `
HTMLTitle template . HTML ` db:"title" json:"-" `
Content string ` db:"content" json:"body" `
HTMLContent template . HTML ` db:"content" json:"-" `
HTMLExcerpt template . HTML ` db:"content" json:"-" `
Tags [ ] string ` json:"tags" `
Images [ ] string ` json:"images,omitempty" `
2021-06-07 21:52:24 +02:00
IsPaid bool ` json:"paid" `
2018-10-17 04:31:27 +02:00
OwnerName string ` json:"owner,omitempty" `
}
// PublicPost holds properties for a publicly returned post, i.e. a post in
// a context where the viewer may not be the owner. As such, sensitive
// metadata for the post is hidden and properties supporting the display of
// the post are added.
PublicPost struct {
* Post
IsSubdomain bool ` json:"-" `
IsTopLevel bool ` json:"-" `
DisplayDate string ` json:"-" `
Views int64 ` json:"views" `
Owner * PublicUser ` json:"-" `
IsOwner bool ` json:"-" `
2021-05-21 02:44:59 +02:00
URL string ` json:"url,omitempty" `
2018-10-17 04:31:27 +02:00
Collection * CollectionObj ` json:"collection,omitempty" `
}
2021-06-07 21:52:24 +02:00
CollectionPostPage struct {
* PublicPost
page . StaticPage
IsOwner bool
IsPinned bool
IsCustomDomain bool
Monetization string
PinnedPosts * [ ] PublicPost
IsFound bool
IsAdmin bool
CanInvite bool
Silenced bool
2021-06-09 16:04:28 +02:00
// Helper field for Chorus mode
CollAlias string
2021-06-07 21:52:24 +02:00
}
2018-11-08 05:43:11 +01:00
RawPost struct {
Id , Slug string
Title string
Content string
Views int64
Font string
Created time . Time
2020-06-11 17:45:12 +02:00
Updated time . Time
2018-11-08 05:43:11 +01:00
IsRTL sql . NullBool
Language sql . NullString
OwnerID int64
CollectionID sql . NullInt64
Found bool
Gone bool
}
2018-10-17 04:31:27 +02:00
AnonymousAuthPost struct {
ID string ` json:"id" `
Token string ` json:"token" `
}
ClaimPostRequest struct {
* AnonymousAuthPost
CollectionAlias string ` json:"collection" `
CreateCollection bool ` json:"create_collection" `
// Generated properties
Slug string ` json:"-" `
}
ClaimPostResult struct {
ID string ` json:"id,omitempty" `
Code int ` json:"code,omitempty" `
ErrorMessage string ` json:"error_msg,omitempty" `
Post * PublicPost ` json:"post,omitempty" `
}
)
2018-11-08 05:43:11 +01:00
func ( p * Post ) Direction ( ) string {
if p . RTL . Valid {
if p . RTL . Bool {
return "rtl"
}
return "ltr"
}
return "auto"
}
// DisplayTitle dynamically generates a title from the Post's contents if it
// doesn't already have an explicit title.
func ( p * Post ) DisplayTitle ( ) string {
if p . Title . String != "" {
return p . Title . String
}
t := friendlyPostTitle ( p . Content , p . ID )
return t
}
// PlainDisplayTitle dynamically generates a title from the Post's contents if it
// doesn't already have an explicit title.
func ( p * Post ) PlainDisplayTitle ( ) string {
if t := stripmd . Strip ( p . DisplayTitle ( ) ) ; t != "" {
return t
}
return p . ID
}
// FormattedDisplayTitle dynamically generates a title from the Post's contents if it
// doesn't already have an explicit title.
func ( p * Post ) FormattedDisplayTitle ( ) template . HTML {
if p . HTMLTitle != "" {
return p . HTMLTitle
}
return template . HTML ( p . DisplayTitle ( ) )
}
// Summary gives a shortened summary of the post based on the post's title,
// especially for display in a longer list of posts. It extracts a summary for
// posts in the Title\n\nBody format, returning nothing if the entire was short
// enough that the extracted title == extracted summary.
func ( p Post ) Summary ( ) string {
if p . Content == "" {
return ""
}
2020-09-05 01:58:45 +02:00
p . Content = stripHTMLWithoutEscaping ( p . Content )
2019-03-14 13:58:37 +01:00
// and Markdown
2021-06-25 17:16:03 +02:00
p . Content = stripmd . StripOptions ( p . Content , stripmd . Options { SkipImages : true } )
2018-11-08 05:43:11 +01:00
title := p . Title . String
var desc string
if title == "" {
// No title, so generate one
title = friendlyPostTitle ( p . Content , p . ID )
desc = postDescription ( p . Content , title , p . ID )
if desc == title {
return ""
}
return desc
}
return shortPostDescription ( p . Content )
}
2020-01-20 21:20:45 +01:00
func ( p Post ) SummaryHTML ( ) template . HTML {
return template . HTML ( p . Summary ( ) )
}
2018-11-08 05:43:11 +01:00
// Excerpt shows any text that comes before a (more) tag.
// TODO: use HTMLExcerpt in templates instead of this method
func ( p * Post ) Excerpt ( ) template . HTML {
return p . HTMLExcerpt
}
func ( p * Post ) CreatedDate ( ) string {
return p . Created . Format ( "2006-01-02" )
}
func ( p * Post ) Created8601 ( ) string {
return p . Created . Format ( "2006-01-02T15:04:05Z" )
}
func ( p * Post ) IsScheduled ( ) bool {
return p . Created . After ( time . Now ( ) )
}
func ( p * Post ) HasTag ( tag string ) bool {
// Regexp looks for tag and has a non-capturing group at the end looking
// for the end of the word.
// Assisted by: https://stackoverflow.com/a/35192941/1549194
hasTag , _ := regexp . MatchString ( "#" + tag + ` (?:[[:punct:]]|\s|\z) ` , p . Content )
return hasTag
}
func ( p * Post ) HasTitleLink ( ) bool {
if p . Title . String == "" {
return false
}
hasLink , _ := regexp . MatchString ( ` ([^!]+|^)\[.+\]\(.+\) ` , p . Title . String )
return hasLink
}
2021-06-07 21:52:24 +02:00
func ( c CollectionPostPage ) DisplayMonetization ( ) string {
if c . Collection == nil {
log . Info ( "CollectionPostPage.DisplayMonetization: c.Collection is nil" )
return ""
}
return displayMonetization ( c . Monetization , c . Collection . Alias )
}
2019-05-12 22:55:30 +02:00
func handleViewPost ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 05:43:11 +01:00
vars := mux . Vars ( r )
friendlyID := vars [ "post" ]
2019-07-02 01:10:29 +02:00
// NOTE: until this is done better, be sure to keep this in parity with
// isRaw() and viewCollectionPost()
2018-11-08 05:43:11 +01:00
isJSON := strings . HasSuffix ( friendlyID , ".json" )
isXML := strings . HasSuffix ( friendlyID , ".xml" )
isCSS := strings . HasSuffix ( friendlyID , ".css" )
isMarkdown := strings . HasSuffix ( friendlyID , ".md" )
isRaw := strings . HasSuffix ( friendlyID , ".txt" ) || isJSON || isXML || isCSS || isMarkdown
// Display reserved page if that is requested resource
if t , ok := pages [ r . URL . Path [ 1 : ] + ".tmpl" ] ; ok {
2018-11-19 03:58:50 +01:00
return handleTemplatedPage ( app , w , r , t )
2018-11-08 05:43:11 +01:00
} else if ( strings . Contains ( r . URL . Path , "." ) && ! isRaw && ! isMarkdown ) || r . URL . Path == "/robots.txt" || r . URL . Path == "/manifest.json" {
// Serve static file
2019-06-14 00:22:18 +02:00
app . shttp . ServeHTTP ( w , r )
2018-11-08 05:43:11 +01:00
return nil
}
// Display collection if this is a collection
c , _ := app . db . GetCollection ( friendlyID )
if c != nil {
return impart . HTTPError { http . StatusMovedPermanently , fmt . Sprintf ( "/%s/" , friendlyID ) }
}
// Normalize the URL, redirecting user to consistent post URL
if friendlyID != strings . ToLower ( friendlyID ) {
return impart . HTTPError { http . StatusMovedPermanently , fmt . Sprintf ( "/%s" , strings . ToLower ( friendlyID ) ) }
}
ext := ""
if isRaw {
parts := strings . Split ( friendlyID , "." )
friendlyID = parts [ 0 ]
if len ( parts ) > 1 {
ext = "." + parts [ 1 ]
}
}
var ownerID sql . NullInt64
var title string
var content string
var font string
var language [ ] byte
var rtl [ ] byte
var views int64
var post * AnonymousPost
var found bool
var gone bool
fixedID := slug . Make ( friendlyID )
if fixedID != friendlyID {
return impart . HTTPError { http . StatusFound , fmt . Sprintf ( "/%s%s" , fixedID , ext ) }
}
err := app . db . QueryRow ( fmt . Sprintf ( "SELECT owner_id, title, content, text_appearance, view_count, language, rtl FROM posts WHERE id = ?" ) , friendlyID ) . Scan ( & ownerID , & title , & content , & font , & views , & language , & rtl )
switch {
case err == sql . ErrNoRows :
found = false
// Output the error in the correct format
if isJSON {
content = "{\"error\": \"Post not found.\"}"
} else if isRaw {
content = "Post not found."
} else {
return ErrPostNotFound
}
case err != nil :
found = false
log . Error ( "Post loading err: %s\n" , err )
return ErrInternalGeneral
default :
found = true
var d string
if len ( rtl ) == 0 {
d = "auto"
} else if rtl [ 0 ] == 49 {
// TODO: find a cleaner way to get this (possibly NULL) value
d = "rtl"
} else {
d = "ltr"
}
generatedTitle := friendlyPostTitle ( content , friendlyID )
sanitizedContent := content
if font != "code" {
sanitizedContent = template . HTMLEscapeString ( content )
}
var desc string
if title == "" {
desc = postDescription ( content , title , friendlyID )
} else {
desc = shortPostDescription ( content )
}
post = & AnonymousPost {
ID : friendlyID ,
Content : sanitizedContent ,
Title : title ,
GenTitle : generatedTitle ,
Description : desc ,
Author : "" ,
Font : font ,
IsPlainText : isRaw ,
IsCode : font == "code" ,
IsLinkable : font != "code" ,
Views : views ,
Language : string ( language ) ,
Direction : d ,
}
if ! isRaw {
2019-08-07 15:26:07 +02:00
post . HTMLContent = template . HTML ( applyMarkdown ( [ ] byte ( content ) , "" , app . cfg ) )
2020-02-19 22:38:50 +01:00
post . Images = extractImages ( post . Content )
2018-11-08 05:43:11 +01:00
}
}
2020-02-09 17:14:14 +01:00
var silenced bool
2019-12-18 02:58:32 +01:00
if found {
2020-02-09 17:24:48 +01:00
silenced , err = app . db . IsUserSilenced ( ownerID . Int64 )
2019-12-18 02:58:32 +01:00
if err != nil {
log . Error ( "view post: %v" , err )
}
2019-08-28 21:37:45 +02:00
}
2018-11-08 05:43:11 +01:00
// Check if post has been unpublished
2021-05-25 23:04:17 +02:00
if title == "" && content == "" {
2018-11-08 05:43:11 +01:00
gone = true
if isJSON {
content = "{\"error\": \"Post was unpublished.\"}"
} else if isCSS {
content = ""
} else if isRaw {
content = "Post was unpublished."
} else {
return ErrPostUnpublished
}
}
var u = & User { }
if isRaw {
contentType := "text/plain"
if isJSON {
contentType = "application/json"
} else if isCSS {
contentType = "text/css"
} else if isXML {
contentType = "application/xml"
} else if isMarkdown {
contentType = "text/markdown"
}
w . Header ( ) . Set ( "Content-Type" , fmt . Sprintf ( "%s; charset=utf-8" , contentType ) )
if isMarkdown && post . Title != "" {
fmt . Fprintf ( w , "%s\n" , post . Title )
for i := 1 ; i <= len ( post . Title ) ; i ++ {
fmt . Fprintf ( w , "=" )
}
fmt . Fprintf ( w , "\n\n" )
}
fmt . Fprint ( w , content )
if ! found {
return ErrPostNotFound
} else if gone {
return ErrPostUnpublished
}
} else {
var err error
page := struct {
* AnonymousPost
page . StaticPage
2019-11-12 00:21:45 +01:00
Username string
IsOwner bool
SiteURL string
Silenced bool
2018-11-08 05:43:11 +01:00
} {
AnonymousPost : post ,
StaticPage : pageForReq ( app , r ) ,
SiteURL : app . cfg . App . Host ,
}
if u = getUserSession ( app , r ) ; u != nil {
page . Username = u . Username
page . IsOwner = ownerID . Valid && ownerID . Int64 == u . ID
}
2019-11-12 00:21:45 +01:00
if ! page . IsOwner && silenced {
2019-10-25 22:40:32 +02:00
return ErrPostNotFound
}
2019-11-12 00:21:45 +01:00
page . Silenced = silenced
2018-11-08 05:43:11 +01:00
err = templates [ "post" ] . ExecuteTemplate ( w , "post" , page )
if err != nil {
log . Error ( "Post template execute error: %v" , err )
}
}
go func ( ) {
if u != nil && ownerID . Valid && ownerID . Int64 == u . ID {
// Post is owned by someone; skip view increment since that person is viewing this post.
return
}
// Update stats for non-raw post views
if ! isRaw && r . Method != "HEAD" && ! bots . IsBot ( r . UserAgent ( ) ) {
_ , err := app . db . Exec ( "UPDATE posts SET view_count = view_count + 1 WHERE id = ?" , friendlyID )
if err != nil {
log . Error ( "Unable to update posts count: %v" , err )
}
}
} ( )
return nil
}
// API v2 funcs
// newPost creates a new post with or without an owning Collection.
//
// Endpoints:
// /posts
// /posts?collection={alias}
// ? /collections/{alias}/posts
2019-05-12 22:55:30 +02:00
func newPost ( app * App , w http . ResponseWriter , r * http . Request ) error {
2019-09-18 21:39:53 +02:00
reqJSON := IsJSON ( r )
2018-11-08 05:43:11 +01:00
vars := mux . Vars ( r )
collAlias := vars [ "alias" ]
if collAlias == "" {
collAlias = r . FormValue ( "collection" )
}
accessToken := r . Header . Get ( "Authorization" )
if accessToken == "" {
// TODO: remove this
accessToken = r . FormValue ( "access_token" )
}
// FIXME: determine web submission with Content-Type header
var u * User
var userID int64 = - 1
var username string
if accessToken == "" {
u = getUserSession ( app , r )
if u != nil {
userID = u . ID
username = u . Username
}
} else {
userID = app . db . GetUserID ( accessToken )
}
2019-11-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( userID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "new post: %v" , err )
2019-08-28 21:37:45 +02:00
}
2019-11-12 00:21:45 +01:00
if silenced {
return ErrUserSilenced
2019-08-28 21:37:45 +02:00
}
2018-11-08 05:43:11 +01:00
if userID == - 1 {
return ErrNotLoggedIn
}
if accessToken == "" && u == nil && collAlias != "" {
return impart . HTTPError { http . StatusBadRequest , "Parameter `access_token` required." }
}
// Get post data
var p * SubmittedPost
if reqJSON {
decoder := json . NewDecoder ( r . Body )
2019-08-28 21:37:45 +02:00
err = decoder . Decode ( & p )
2018-11-08 05:43:11 +01:00
if err != nil {
log . Error ( "Couldn't parse new post JSON request: %v\n" , err )
return ErrBadJSON
}
if p . Title == nil {
t := ""
p . Title = & t
}
2021-05-25 23:04:17 +02:00
if strings . TrimSpace ( * ( p . Title ) ) == "" && ( p . Content == nil || strings . TrimSpace ( * ( p . Content ) ) == "" ) {
2018-11-08 05:43:11 +01:00
return ErrNoPublishableContent
}
2021-05-25 23:04:17 +02:00
if p . Content == nil {
c := ""
p . Content = & c
}
2018-11-08 05:43:11 +01:00
} else {
post := r . FormValue ( "body" )
appearance := r . FormValue ( "font" )
title := r . FormValue ( "title" )
rtlValue := r . FormValue ( "rtl" )
langValue := r . FormValue ( "lang" )
if strings . TrimSpace ( post ) == "" {
return ErrNoPublishableContent
}
var isRTL , rtlValid bool
if rtlValue == "auto" && langValue != "" {
isRTL = i18n . LangIsRTL ( langValue )
rtlValid = true
} else {
isRTL = rtlValue == "true"
rtlValid = rtlValue != "" && langValue != ""
}
// Create a new post
p = & SubmittedPost {
Title : & title ,
Content : & post ,
Font : appearance ,
IsRTL : converter . NullJSONBool { sql . NullBool { Bool : isRTL , Valid : rtlValid } } ,
Language : converter . NullJSONString { sql . NullString { String : langValue , Valid : langValue != "" } } ,
}
}
if ! p . isFontValid ( ) {
p . Font = "norm"
}
var newPost * PublicPost = & PublicPost { }
var coll * Collection
if accessToken != "" {
2019-07-22 20:02:53 +02:00
newPost , err = app . db . CreateOwnedPost ( p , accessToken , collAlias , app . cfg . App . Host )
2018-11-08 05:43:11 +01:00
} else {
//return ErrNotLoggedIn
// TODO: verify user is logged in
2018-11-18 20:39:50 +01:00
var collID int64
2018-11-08 05:43:11 +01:00
if collAlias != "" {
coll , err = app . db . GetCollection ( collAlias )
if err != nil {
return err
}
2019-06-21 03:08:30 +02:00
coll . hostName = app . cfg . App . Host
2018-11-08 05:43:11 +01:00
if coll . OwnerID != u . ID {
return ErrForbiddenCollection
}
collID = coll . ID
}
// TODO: return PublicPost from createPost
newPost . Post , err = app . db . CreatePost ( userID , collID , p )
}
if err != nil {
return err
}
if coll != nil {
coll . ForPublic ( )
newPost . Collection = & CollectionObj { Collection : * coll }
}
newPost . extractData ( )
newPost . OwnerName = username
2021-05-21 02:44:59 +02:00
newPost . URL = newPost . CanonicalURL ( app . cfg . App . Host )
2018-11-08 05:43:11 +01:00
// Write success now
response := impart . WriteSuccess ( w , newPost , http . StatusCreated )
2019-06-17 02:34:32 +02:00
if newPost . Collection != nil && ! app . cfg . App . Private && app . cfg . App . Federation && ! newPost . Created . After ( time . Now ( ) ) {
2018-11-18 20:39:50 +01:00
go federatePost ( app , newPost , newPost . Collection . ID , false )
2018-11-08 05:43:11 +01:00
}
return response
}
2019-05-12 22:55:30 +02:00
func existingPost ( app * App , w http . ResponseWriter , r * http . Request ) error {
2019-09-18 21:39:53 +02:00
reqJSON := IsJSON ( r )
2018-11-08 05:43:11 +01:00
vars := mux . Vars ( r )
postID := vars [ "post" ]
p := AuthenticatedPost { ID : postID }
var err error
if reqJSON {
// Decode JSON request
decoder := json . NewDecoder ( r . Body )
err = decoder . Decode ( & p )
if err != nil {
log . Error ( "Couldn't parse post update JSON request: %v\n" , err )
return ErrBadJSON
}
} else {
err = r . ParseForm ( )
if err != nil {
log . Error ( "Couldn't parse post update form request: %v\n" , err )
return ErrBadFormData
}
// Can't decode to a nil SubmittedPost property, so create instance now
p . SubmittedPost = & SubmittedPost { }
err = app . formDecoder . Decode ( & p , r . PostForm )
if err != nil {
log . Error ( "Couldn't decode post update form request: %v\n" , err )
return ErrBadFormData
}
}
2019-05-19 05:16:42 +02:00
if p . Web {
p . IsRTL . Valid = true
}
2018-11-08 05:43:11 +01:00
if p . SubmittedPost == nil {
return ErrPostNoUpdatableVals
}
// Ensure an access token was given
accessToken := r . Header . Get ( "Authorization" )
// Get user's cookie session if there's no token
var u * User
//var username string
if accessToken == "" {
u = getUserSession ( app , r )
if u != nil {
//username = u.Username
}
}
if u == nil && accessToken == "" {
return ErrNoAccessToken
}
// Get user ID from current session or given access token, if one was given.
var userID int64
if u != nil {
userID = u . ID
} else if accessToken != "" {
userID , err = AuthenticateUser ( app . db , accessToken )
if err != nil {
return err
}
}
2019-11-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( userID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "existing post: %v" , err )
2019-08-28 21:37:45 +02:00
}
2019-11-12 00:21:45 +01:00
if silenced {
return ErrUserSilenced
2019-08-28 21:37:45 +02:00
}
2018-11-08 05:43:11 +01:00
// Modify post struct
p . ID = postID
err = app . db . UpdateOwnedPost ( & p , userID )
if err != nil {
if reqJSON {
return err
}
if err , ok := err . ( impart . HTTPError ) ; ok {
addSessionFlash ( app , w , r , err . Message , nil )
} else {
addSessionFlash ( app , w , r , err . Error ( ) , nil )
}
}
var pRes * PublicPost
pRes , err = app . db . GetPost ( p . ID , 0 )
if reqJSON {
if err != nil {
return err
}
pRes . extractData ( )
}
if pRes . CollectionID . Valid {
coll , err := app . db . GetCollectionBy ( "id = ?" , pRes . CollectionID . Int64 )
2019-06-17 02:34:32 +02:00
if err == nil && ! app . cfg . App . Private && app . cfg . App . Federation {
2019-06-21 03:08:30 +02:00
coll . hostName = app . cfg . App . Host
2018-11-08 05:43:11 +01:00
pRes . Collection = & CollectionObj { Collection : * coll }
go federatePost ( app , pRes , pRes . Collection . ID , true )
}
}
// Write success now
if reqJSON {
return impart . WriteSuccess ( w , pRes , http . StatusOK )
}
addSessionFlash ( app , w , r , "Changes saved." , nil )
collectionAlias := vars [ "alias" ]
redirect := "/" + postID + "/meta"
if collectionAlias != "" {
2018-12-01 22:27:14 +01:00
collPre := "/" + collectionAlias
if app . cfg . App . SingleUser {
collPre = ""
}
redirect = collPre + "/" + pRes . Slug . String + "/edit/meta"
2018-12-24 16:33:40 +01:00
} else {
if app . cfg . App . SingleUser {
redirect = "/d" + redirect
}
2018-11-08 05:43:11 +01:00
}
w . Header ( ) . Set ( "Location" , redirect )
w . WriteHeader ( http . StatusFound )
return nil
}
2019-05-12 22:55:30 +02:00
func deletePost ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 05:43:11 +01:00
vars := mux . Vars ( r )
friendlyID := vars [ "post" ]
editToken := r . FormValue ( "token" )
var ownerID int64
var u * User
accessToken := r . Header . Get ( "Authorization" )
if accessToken == "" && editToken == "" {
u = getUserSession ( app , r )
if u == nil {
return ErrNoAccessToken
}
}
var res sql . Result
var t * sql . Tx
var err error
var collID sql . NullInt64
var coll * Collection
var pp * PublicPost
2019-06-05 18:39:22 +02:00
if editToken != "" {
// TODO: SELECT owner_id, as well, and return appropriate error if NULL instead of running two queries
var dummy int64
err = app . db . QueryRow ( "SELECT 1 FROM posts WHERE id = ?" , friendlyID ) . Scan ( & dummy )
switch {
case err == sql . ErrNoRows :
return impart . HTTPError { http . StatusNotFound , "Post not found." }
}
err = app . db . QueryRow ( "SELECT 1 FROM posts WHERE id = ? AND owner_id IS NULL" , friendlyID ) . Scan ( & dummy )
switch {
case err == sql . ErrNoRows :
// Post already has an owner. This could provide a bad experience
// for the user, but it's more important to ensure data isn't lost
// unexpectedly. So prevent deletion via token.
return impart . HTTPError { http . StatusConflict , "This post belongs to some user (hopefully yours). Please log in and delete it from that user's account." }
}
res , err = app . db . Exec ( "DELETE FROM posts WHERE id = ? AND modify_token = ? AND owner_id IS NULL" , friendlyID , editToken )
} else if accessToken != "" || u != nil {
2018-11-08 05:43:11 +01:00
// Caller provided some way to authenticate; assume caller expects the
// post to be deleted based on a specific post owner, thus we should
// return corresponding errors.
if accessToken != "" {
ownerID = app . db . GetUserID ( accessToken )
if ownerID == - 1 {
return ErrBadAccessToken
}
} else {
ownerID = u . ID
}
// TODO: don't make two queries
var realOwnerID sql . NullInt64
err = app . db . QueryRow ( "SELECT collection_id, owner_id FROM posts WHERE id = ?" , friendlyID ) . Scan ( & collID , & realOwnerID )
if err != nil {
return err
}
if ! collID . Valid {
// There's no collection; simply delete the post
res , err = app . db . Exec ( "DELETE FROM posts WHERE id = ? AND owner_id = ?" , friendlyID , ownerID )
} else {
// Post belongs to a collection; do any additional clean up
coll , err = app . db . GetCollectionBy ( "id = ?" , collID . Int64 )
if err != nil {
log . Error ( "Unable to get collection: %v" , err )
return err
}
if app . cfg . App . Federation {
// First fetch full post for federation
pp , err = app . db . GetOwnedPost ( friendlyID , ownerID )
if err != nil {
log . Error ( "Unable to get owned post: %v" , err )
return err
}
collObj := & CollectionObj { Collection : * coll }
pp . Collection = collObj
}
t , err = app . db . Begin ( )
if err != nil {
log . Error ( "No begin: %v" , err )
return err
}
res , err = t . Exec ( "DELETE FROM posts WHERE id = ? AND owner_id = ?" , friendlyID , ownerID )
}
} else {
2019-06-05 18:39:22 +02:00
return impart . HTTPError { http . StatusBadRequest , "No authenticated user or post token given." }
2018-11-08 05:43:11 +01:00
}
if err != nil {
return err
}
affected , err := res . RowsAffected ( )
if err != nil {
if t != nil {
t . Rollback ( )
log . Error ( "Rows affected err! Rolling back" )
}
return err
} else if affected == 0 {
if t != nil {
t . Rollback ( )
log . Error ( "No rows affected! Rolling back" )
}
return impart . HTTPError { http . StatusForbidden , "Post not found, or you're not the owner." }
}
if t != nil {
t . Commit ( )
}
2019-06-17 02:34:32 +02:00
if coll != nil && ! app . cfg . App . Private && app . cfg . App . Federation {
2018-11-08 05:43:11 +01:00
go deleteFederatedPost ( app , pp , collID . Int64 )
}
return impart . HTTPError { Status : http . StatusNoContent }
}
// addPost associates a post with the authenticated user.
2019-05-12 22:55:30 +02:00
func addPost ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 05:43:11 +01:00
var ownerID int64
// Authenticate user
at := r . Header . Get ( "Authorization" )
if at != "" {
ownerID = app . db . GetUserID ( at )
if ownerID == - 1 {
return ErrBadAccessToken
}
} else {
u := getUserSession ( app , r )
if u == nil {
return ErrNotLoggedIn
}
ownerID = u . ID
}
2019-11-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( ownerID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "add post: %v" , err )
2019-08-28 21:37:45 +02:00
}
2019-11-12 00:21:45 +01:00
if silenced {
return ErrUserSilenced
2019-08-28 21:37:45 +02:00
}
2018-11-08 05:43:11 +01:00
// Parse claimed posts in format:
// [{"id": "...", "token": "..."}]
var claims * [ ] ClaimPostRequest
decoder := json . NewDecoder ( r . Body )
2019-08-28 21:37:45 +02:00
err = decoder . Decode ( & claims )
2018-11-08 05:43:11 +01:00
if err != nil {
return ErrBadJSONArray
}
vars := mux . Vars ( r )
collAlias := vars [ "alias" ]
// Update all given posts
2019-08-07 22:20:32 +02:00
res , err := app . db . ClaimPosts ( app . cfg , ownerID , collAlias , claims )
2018-11-08 05:43:11 +01:00
if err != nil {
return err
}
2018-11-16 18:42:21 +01:00
2019-06-17 02:34:32 +02:00