mirror of https://github.com/writeas/writefreely
This includes config changes, collections, posts, some post rendering funcs, and actual database connection when the server starts up.pull/24/head
parent
f7430fb8bc
commit
0c1e1dd57e
@ -0,0 +1,69 @@ |
|||||||
|
package writefreely |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
) |
||||||
|
|
||||||
|
type ( |
||||||
|
Collection struct { |
||||||
|
ID int64 `datastore:"id" json:"-"` |
||||||
|
Alias string `datastore:"alias" schema:"alias" json:"alias"` |
||||||
|
Title string `datastore:"title" schema:"title" json:"title"` |
||||||
|
Description string `datastore:"description" schema:"description" json:"description"` |
||||||
|
Direction string `schema:"dir" json:"dir,omitempty"` |
||||||
|
Language string `schema:"lang" json:"lang,omitempty"` |
||||||
|
StyleSheet string `datastore:"style_sheet" schema:"style_sheet" json:"style_sheet"` |
||||||
|
Script string `datastore:"script" schema:"script" json:"script,omitempty"` |
||||||
|
Public bool `datastore:"public" json:"public"` |
||||||
|
Visibility collVisibility `datastore:"private" json:"-"` |
||||||
|
Format string `datastore:"format" json:"format,omitempty"` |
||||||
|
Views int64 `json:"views"` |
||||||
|
OwnerID int64 `datastore:"owner_id" json:"-"` |
||||||
|
PublicOwner bool `datastore:"public_owner" json:"-"` |
||||||
|
PreferSubdomain bool `datastore:"prefer_subdomain" json:"-"` |
||||||
|
Domain string `datastore:"domain" json:"domain,omitempty"` |
||||||
|
IsDomainActive bool `datastore:"is_active" json:"-"` |
||||||
|
IsSecure bool `datastore:"is_secure" json:"-"` |
||||||
|
CustomHandle string `datastore:"handle" json:"-"` |
||||||
|
Email string `json:"email,omitempty"` |
||||||
|
URL string `json:"url,omitempty"` |
||||||
|
|
||||||
|
app *app |
||||||
|
} |
||||||
|
CollectionObj struct { |
||||||
|
Collection |
||||||
|
TotalPosts int `json:"total_posts"` |
||||||
|
Owner *User `json:"owner,omitempty"` |
||||||
|
Posts *[]PublicPost `json:"posts,omitempty"` |
||||||
|
} |
||||||
|
SubmittedCollection struct { |
||||||
|
// Data used for updating a given collection
|
||||||
|
ID int64 |
||||||
|
OwnerID uint64 |
||||||
|
|
||||||
|
// Form helpers
|
||||||
|
PreferURL string `schema:"prefer_url" json:"prefer_url"` |
||||||
|
Privacy int `schema:"privacy" json:"privacy"` |
||||||
|
Pass string `schema:"password" json:"password"` |
||||||
|
Federate bool `schema:"federate" json:"federate"` |
||||||
|
MathJax bool `schema:"mathjax" json:"mathjax"` |
||||||
|
Handle string `schema:"handle" json:"handle"` |
||||||
|
|
||||||
|
// Actual collection values updated in the DB
|
||||||
|
Alias *string `schema:"alias" json:"alias"` |
||||||
|
Title *string `schema:"title" json:"title"` |
||||||
|
Description *string `schema:"description" json:"description"` |
||||||
|
StyleSheet *sql.NullString `schema:"style_sheet" json:"style_sheet"` |
||||||
|
Script *sql.NullString `schema:"script" json:"script"` |
||||||
|
Visibility *int `schema:"visibility" json:"public"` |
||||||
|
Format *sql.NullString `schema:"format" json:"format"` |
||||||
|
PreferSubdomain *bool `schema:"prefer_subdomain" json:"prefer_subdomain"` |
||||||
|
Domain *sql.NullString `schema:"domain" json:"domain"` |
||||||
|
} |
||||||
|
CollectionFormat struct { |
||||||
|
Format string |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// collVisibility represents the visibility level for the collection.
|
||||||
|
type collVisibility int |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,135 @@ |
|||||||
|
package writefreely |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"github.com/microcosm-cc/bluemonday" |
||||||
|
stripmd "github.com/writeas/go-strip-markdown" |
||||||
|
"github.com/writeas/saturday" |
||||||
|
"html" |
||||||
|
"html/template" |
||||||
|
"regexp" |
||||||
|
"strings" |
||||||
|
"unicode" |
||||||
|
"unicode/utf8" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
blockReg = regexp.MustCompile("<(ul|ol|blockquote)>\n") |
||||||
|
endBlockReg = regexp.MustCompile("</([a-z]+)>\n</(ul|ol|blockquote)>") |
||||||
|
youtubeReg = regexp.MustCompile("(https?://www.youtube.com/embed/[a-zA-Z0-9\\-_]+)(\\?[^\t\n\f\r \"']+)?") |
||||||
|
titleElementReg = regexp.MustCompile("</?h[1-6]>") |
||||||
|
hashtagReg = regexp.MustCompile(`#([\p{L}\p{M}\d]+)`) |
||||||
|
markeddownReg = regexp.MustCompile("<p>(.+)</p>") |
||||||
|
) |
||||||
|
|
||||||
|
func (p *Post) formatContent(c *Collection, isOwner bool) { |
||||||
|
baseURL := c.CanonicalURL() |
||||||
|
if isOwner { |
||||||
|
baseURL = "/" + c.Alias + "/" |
||||||
|
} |
||||||
|
newCon := hashtagReg.ReplaceAllFunc([]byte(p.Content), func(b []byte) []byte { |
||||||
|
// Ensure we only replace "hashtags" that have already been extracted.
|
||||||
|
// `hashtagReg` catches everything, including any hash on the end of a
|
||||||
|
// URL, so we rely on p.Tags as the final word on whether or not to link
|
||||||
|
// a tag.
|
||||||
|
for _, t := range p.Tags { |
||||||
|
if string(b) == "#"+t { |
||||||
|
return bytes.Replace(b, []byte("#"+t), []byte("<a href=\""+baseURL+"tag:"+t+"\" class=\"hashtag\"><span>#</span><span class=\"p-category\">"+t+"</span></a>"), -1) |
||||||
|
} |
||||||
|
} |
||||||
|
return b |
||||||
|
}) |
||||||
|
p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) |
||||||
|
p.HTMLContent = template.HTML(applyMarkdown([]byte(newCon))) |
||||||
|
if exc := strings.Index(string(newCon), "<!--more-->"); exc > -1 { |
||||||
|
p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(newCon[:exc]))) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (p *PublicPost) formatContent(isOwner bool) { |
||||||
|
p.Post.formatContent(&p.Collection.Collection, isOwner) |
||||||
|
} |
||||||
|
|
||||||
|
func applyMarkdown(data []byte) string { |
||||||
|
return applyMarkdownSpecial(data, false) |
||||||
|
} |
||||||
|
|
||||||
|
func applyMarkdownSpecial(data []byte, skipNoFollow bool) string { |
||||||
|
mdExtensions := 0 | |
||||||
|
blackfriday.EXTENSION_TABLES | |
||||||
|
blackfriday.EXTENSION_FENCED_CODE | |
||||||
|
blackfriday.EXTENSION_AUTOLINK | |
||||||
|
blackfriday.EXTENSION_STRIKETHROUGH | |
||||||
|
blackfriday.EXTENSION_SPACE_HEADERS | |
||||||
|
blackfriday.EXTENSION_AUTO_HEADER_IDS |
||||||
|
htmlFlags := 0 | |
||||||
|
blackfriday.HTML_USE_SMARTYPANTS | |
||||||
|
blackfriday.HTML_SMARTYPANTS_DASHES |
||||||
|
|
||||||
|
// Generate Markdown
|
||||||
|
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) |
||||||
|
// Strip out bad HTML
|
||||||
|
policy := getSanitizationPolicy() |
||||||
|
policy.RequireNoFollowOnLinks(!skipNoFollow) |
||||||
|
outHTML := string(policy.SanitizeBytes(md)) |
||||||
|
// Strip newlines on certain block elements that render with them
|
||||||
|
outHTML = blockReg.ReplaceAllString(outHTML, "<$1>") |
||||||
|
outHTML = endBlockReg.ReplaceAllString(outHTML, "</$1></$2>") |
||||||
|
// Remove all query parameters on YouTube embed links
|
||||||
|
// TODO: make this more specific. Taking the nuclear approach here to strip ?autoplay=1
|
||||||
|
outHTML = youtubeReg.ReplaceAllString(outHTML, "$1") |
||||||
|
|
||||||
|
return outHTML |
||||||
|
} |
||||||
|
|
||||||
|
func applyBasicMarkdown(data []byte) string { |
||||||
|
mdExtensions := 0 | |
||||||
|
blackfriday.EXTENSION_STRIKETHROUGH | |
||||||
|
blackfriday.EXTENSION_SPACE_HEADERS | |
||||||
|
blackfriday.EXTENSION_HEADER_IDS |
||||||
|
htmlFlags := 0 | |
||||||
|
blackfriday.HTML_SKIP_HTML | |
||||||
|
blackfriday.HTML_USE_SMARTYPANTS | |
||||||
|
blackfriday.HTML_SMARTYPANTS_DASHES |
||||||
|
|
||||||
|
// Generate Markdown
|
||||||
|
md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) |
||||||
|
// Strip out bad HTML
|
||||||
|
policy := bluemonday.UGCPolicy() |
||||||
|
policy.AllowAttrs("class", "id").Globally() |
||||||
|
outHTML := string(policy.SanitizeBytes(md)) |
||||||
|
outHTML = markeddownReg.ReplaceAllString(outHTML, "$1") |
||||||
|
outHTML = strings.TrimRightFunc(outHTML, unicode.IsSpace) |
||||||
|
|
||||||
|
return outHTML |
||||||
|
} |
||||||
|
|
||||||
|
func postTitle(content, friendlyId string) string { |
||||||
|
const maxTitleLen = 80 |
||||||
|
|
||||||
|
// Strip HTML tags with bluemonday's StrictPolicy, then unescape the HTML
|
||||||
|
// entities added in by sanitizing the content.
|
||||||
|
content = html.UnescapeString(bluemonday.StrictPolicy().Sanitize(content)) |
||||||
|
|
||||||
|
content = strings.TrimLeftFunc(stripmd.Strip(content), unicode.IsSpace) |
||||||
|
eol := strings.IndexRune(content, '\n') |
||||||
|
blankLine := strings.Index(content, "\n\n") |
||||||
|
if blankLine != -1 && blankLine <= eol && blankLine <= assumedTitleLen { |
||||||
|
return strings.TrimSpace(content[:blankLine]) |
||||||
|
} else if utf8.RuneCountInString(content) <= maxTitleLen { |
||||||
|
return content |
||||||
|
} |
||||||
|
return friendlyId |
||||||
|
} |
||||||
|
|
||||||
|
func getSanitizationPolicy() *bluemonday.Policy { |
||||||
|
policy := bluemonday.UGCPolicy() |
||||||
|
policy.AllowAttrs("src", "style").OnElements("iframe", "video") |
||||||
|
policy.AllowAttrs("frameborder", "width", "height").Matching(bluemonday.Integer).OnElements("iframe") |
||||||
|
policy.AllowAttrs("allowfullscreen").OnElements("iframe") |
||||||
|
policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") |
||||||
|
policy.AllowAttrs("target").OnElements("a") |
||||||
|
policy.AllowAttrs("style", "class", "id").Globally() |
||||||
|
policy.AllowURLSchemes("http", "https", "mailto", "xmpp") |
||||||
|
return policy |
||||||
|
} |
@ -0,0 +1,178 @@ |
|||||||
|
package writefreely |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/guregu/null" |
||||||
|
"github.com/guregu/null/zero" |
||||||
|
"github.com/kylemcc/twitter-text-go/extract" |
||||||
|
"github.com/writeas/monday" |
||||||
|
"github.com/writeas/slug" |
||||||
|
"github.com/writeas/web-core/converter" |
||||||
|
"github.com/writeas/web-core/parse" |
||||||
|
"github.com/writeas/web-core/tags" |
||||||
|
"html/template" |
||||||
|
"regexp" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// Post ID length bounds
|
||||||
|
minIDLen = 10 |
||||||
|
maxIDLen = 10 |
||||||
|
userPostIDLen = 10 |
||||||
|
postIDLen = 10 |
||||||
|
|
||||||
|
postMetaDateFormat = "2006-01-02 15:04:05" |
||||||
|
) |
||||||
|
|
||||||
|
type ( |
||||||
|
AuthenticatedPost struct { |
||||||
|
ID string `json:"id" schema:"id"` |
||||||
|
*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"` |
||||||
|
|
||||||
|
// [{ "medium": "ev" }, { "twitter": "ilikebeans" }]
|
||||||
|
Crosspost []map[string]string `json:"crosspost" schema:"crosspost"` |
||||||
|
} |
||||||
|
|
||||||
|
// 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:"-"` |
||||||
|
EmbedViewCount int64 `db:"embed_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"` |
||||||
|
|
||||||
|
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:"-"` |
||||||
|
Domain string `json:"-"` |
||||||
|
DisplayDate string `json:"-"` |
||||||
|
Views int64 `json:"views"` |
||||||
|
Owner *PublicUser `json:"-"` |
||||||
|
IsOwner bool `json:"-"` |
||||||
|
Collection *CollectionObj `json:"collection,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
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"` |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
func (p *Post) processPost() PublicPost { |
||||||
|
res := &PublicPost{Post: p, Views: 0} |
||||||
|
res.Views = p.ViewCount |
||||||
|
// TODO: move to own function
|
||||||
|
loc := monday.FuzzyLocale(p.Language.String) |
||||||
|
res.DisplayDate = monday.Format(p.Created, monday.LongFormatsByLocale[loc], loc) |
||||||
|
|
||||||
|
return *res |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: merge this into getSlugFromPost or phase it out
|
||||||
|
func getSlug(title, lang string) string { |
||||||
|
return getSlugFromPost("", title, lang) |
||||||
|
} |
||||||
|
|
||||||
|
func getSlugFromPost(title, body, lang string) string { |
||||||
|
if title == "" { |
||||||
|
title = postTitle(body, body) |
||||||
|
} |
||||||
|
title = parse.PostLede(title, false) |
||||||
|
// Truncate lede if needed
|
||||||
|
title, _ = parse.TruncToWord(title, 80) |
||||||
|
if lang != "" && len(lang) == 2 { |
||||||
|
return slug.MakeLang(title, lang) |
||||||
|
} |
||||||
|
return slug.Make(title) |
||||||
|
} |
||||||
|
|
||||||
|
// isFontValid returns whether or not the submitted post's appearance is valid.
|
||||||
|
func (p *SubmittedPost) isFontValid() bool { |
||||||
|
validFonts := map[string]bool{ |
||||||
|
"norm": true, |
||||||
|
"sans": true, |
||||||
|
"mono": true, |
||||||
|
"wrap": true, |
||||||
|
"code": true, |
||||||
|
} |
||||||
|
|
||||||
|
if _, valid := validFonts[p.Font]; valid { |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func (p *Post) extractData() { |
||||||
|
p.Tags = tags.Extract(p.Content) |
||||||
|
p.extractImages() |
||||||
|
} |
||||||
|
|
||||||
|
var imageURLRegex = regexp.MustCompile(`(?i)^https?:\/\/[^ ]*\.(gif|png|jpg|jpeg)$`) |
||||||
|
|
||||||
|
func (p *Post) extractImages() { |
||||||
|
matches := extract.ExtractUrls(p.Content) |
||||||
|
urls := map[string]bool{} |
||||||
|
for i := range matches { |
||||||
|
u := matches[i].Text |
||||||
|
if !imageURLRegex.MatchString(u) { |
||||||
|
continue |
||||||
|
} |
||||||
|
urls[u] = true |
||||||
|
} |
||||||
|
|
||||||
|
resURLs := make([]string, 0) |
||||||
|
for k := range urls { |
||||||
|
resURLs = append(resURLs, k) |
||||||
|
} |
||||||
|
p.Images = resURLs |
||||||
|
} |
Loading…
Reference in new issue