<|start_filename|>templates/admin/settings/snippet.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui padded segment">
        {{with $success := $.Request.FormValue "success"}}
            <div class="ui info message">
                <div class="header">
                    {{if eq $success "changes"}}
                        Changes saved successfully
                    {{else if eq $success "todo"}}
                        TODO
                    {{end}}
                </div>
                <a class="close-link" href="/admin/settings/snippet"><i class="close icon"></i></a>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}
        {{with $error := $.Request.FormValue "error"}}
            <div class="ui large negative message">
                <div class="content">
                    <div class="header">
                        {{if eq $error "invalid"}}
                            Invalid. Please try again.
                        {{else}}
                            {{$error}}
                        {{end}}
                    </div>
                    <a class="close-link" href="/admin/settings/snippet"><i class="close icon"></i></a>
                </div>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}


        <form class="ui large form" action="/admin/settings/snippet" method="POST" novalidate autocomplete="off" enctype="multipart/form-data">
            <div class="ui blue dividing header">Analytics tracking code</div>
            <div class="ui message">
                <i class="info circle grey icon"></i>Add your web analytics tracking code or other HTML snippets.
            </div>
            <div class="ui hidden divider"></div>
            <div class="field">
                <textarea name="snippet" {{if $.Info.Snippet}}rows="9"}}{{else}}rows="6"{{end}} placeholder="&lt;script&gt;...&lt;/script&gt;">{{$.Info.Snippet}}</textarea>
            </div>
            <div class="equal width fields">
                <div class="field mobile hidden">&nbsp;</div>
                <div class="field">
                    <div class="two ui buttons">
                        <a href="/" class="ui large button">Cancel</a>
                        <button type="submit" class="ui large blue button">Save</button>
                    </div>
                </div>
            </div>
        </form>
    </div>

</div>

{{template "footer.html" .}}



<|start_filename|>templates/admin/settings/password.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui padded segment">
        {{with $success := $.Request.FormValue "success"}}
            <div class="ui info message">
                <div class="header">
                    {{if eq $success "changes"}}
                        Changes saved successfully
                    {{else if eq $success "todo"}}
                        TODO
                    {{end}}
                </div>
                <a class="close-link" href="/admin/settings/password"><i class="close icon"></i></a>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}
        {{with $error := $.Request.FormValue "error"}}
            <div class="ui large negative message">
                <div class="content">
                    <div class="header">
                        {{if eq $error "invalid"}}
                            Invalid. Please try again.
                        {{else}}
                            {{$error}}
                        {{end}}
                    </div>
                    <a class="close-link" href="/admin/settings/password"><i class="close icon"></i></a>
                </div>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}

        <form class="ui large form" action="/admin/settings/password" method="POST" novalidate autocomplete="off" enctype="multipart/form-data">
            <div class="ui blue dividing header">Change admin password</div>
            <div class="ui message">
                <i class="info circle grey icon"></i>Update your password. Must be 8+ characters.
            </div>
            <div class="ui hidden divider"></div>

            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Current Password</div>
                    <input name="current_password" type="password" placeholder="<PASSWORD>" value="">
                </div>
                <div class="field">
                    <div class="ui small header">New Password</div>
                    <input name="new_password" type="password" placeholder="<PASSWORD>" value="">
                </div>
            </div>

            <div class="equal width fields">
                <div class="field mobile hidden">&nbsp;</div>
                <div class="field">
                    <div class="two ui buttons">
                        <a href="/" class="ui large button">Cancel</a>
                        <button type="submit" class="ui large blue button">Save</button>
                    </div>
                </div>
            </div>
        </form>
    </div>

</div>

{{template "footer.html" .}}



<|start_filename|>utils.go<|end_filename|>
package main

import (
	"crypto/rand"
	"encoding/base32"
	"io/ioutil"
	"os"
	"path/filepath"
)

func overwrite(filename string, data []byte, perm os.FileMode) error {
	f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
	if err != nil {
		return err
	}
	if _, err := f.Write(data); err != nil {
		return err
	}
	if err := f.Sync(); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	if err := os.Chmod(f.Name(), perm); err != nil {
		return err
	}
	return os.Rename(f.Name(), filename)
}

func randomString(n int) string {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	return base32.StdEncoding.EncodeToString(b)[:n]
}


<|start_filename|>static/style.css<|end_filename|>
body {
    padding-bottom: 100px;
    color: #222;
    background-color: #fdfdfd;
}

a {
    color: #0d71bb;
}

a:hover, a:active {
    color: #999;
}

.show-profile.card {
    background-color: #fdfdfd !important;
}

.block.header {
    background-color: #fdfdfd !important;
}

.center-aligned {
    text-align: center;
}

.right-aligned {
    text-align: right;
}

.left-aligned {
    text-align: left;
}

.close-link {
    color: #000 !important;
    position: absolute;
    right: 5px;
    top: 5px;
}

.copylink {
    color: yellow !important;
    background-color: #444 !important;
}

.copylink::selection {
    color: #fff !important;
}

.break-word {
    overflow-wrap: break-word !important;
}

.borderless.menu {
    border-radius: 0 !important;
}

.navmenu img {
    max-width: 22px;
}

.centered-column {
    max-width: 350px !important;
}

.page-text {
    margin-top: 0.5em;
    font-size: 1.4em;
    line-height: 1.6em;
    color: #555;
    overflow-wrap: break-word;
}

pre.code {
    color: #00b5ad;
    background-color: #f9f9f9;
    padding: 1em;
    border: 5px dashed #ddd;
    border-radius: 5px;
    overflow-x: auto;
    font-size: 1.3em;
    line-height: 1.6em;
}

.post-header {
    margin-top: 0.5em;
    font-size: 1.3em;
    font-weight: bold;
    line-height: 1.4em;
    color: #333;
    overflow-wrap: break-word;
}

.post-header > span, .post-header > a {
    color: #999 !important;
}

.post-header > img {
    width: 24px !important;
    border-radius: 50%;
    height: auto;
    float: left;
    margin-right: 0.5em;
}

.post-body {
    margin-top: 0.5em;
    margin-bottom: 0.5em;
    font-size: 1.4em;
    line-height: 1.6em;
    color: #555;
    overflow-wrap: break-word;
}

.post + .post {
    margin-top: 1.5em;
    padding-top: 1.5em;
    border-top: 1px solid #ddd;
}

@media only screen and (max-width: 767px) {
    [class*="mobile hidden"],
    [class*="tablet only"]:not(.mobile),
    [class*="computer only"]:not(.mobile),
    [class*="large monitor only"]:not(.mobile),
    [class*="widescreen monitor only"]:not(.mobile),
    [class*="or lower hidden"] {
        display: none !important;
    }
}

.post-form-body {
    box-shadow: 0px 0px 10px rgba(0, 119, 255, 0.33) !important;
}

.ui.brand.button {
    background-color: #389eee !important;
    color: #f9f9f9 !important;
}

.brand.icon {
    color: #389eee !important;
}

.profile-image {
    width: 144px !important;
    max-width: 100%;
    border-radius: 50%;
    height: auto;
    margin: auto;
    border: 3px solid #ddd;
}

.readonly-input {
    background-color: #f1f1f1 !important;
}


<|start_filename|>templates/admin/settings/sso.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui padded segment">
        {{with $success := $.Request.FormValue "success"}}
            <div class="ui info message">
                <div class="header">
                    {{if eq $success "changes"}}
                        Changes saved successfully
                    {{else if eq $success "domain"}}
                        Domain changed. Please re-configuring SAML for SSO.
                    {{else if eq $success "todo"}}
                        TODO
                    {{end}}
                </div>
                <a class="close-link" href="/admin/settings/sso"><i class="close icon"></i></a>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}
        {{with $error := $.Request.FormValue "error"}}
            <div class="ui large negative message">
                <div class="content">
                    <div class="header">
                        {{if eq $error "invalid"}}
                            Invalid. Please try again.
                        {{else if eq $error "saml"}}
                            Failed to configure SAML.
                        {{else}}
                            {{$error}}
                        {{end}}
                    </div>
                    <a class="close-link" href="/admin/settings/sso"><i class="close icon"></i></a>
                </div>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}

        <form class="ui large form" action="/admin/settings/sso" method="POST" novalidate autocomplete="off">
            <div class="ui blue dividing header">Single Sign-On (SAML)</div>
            <div class="ui message">
                <i class="info circle grey icon"></i>
                To enable Single Sign-On using SAML add a custom SAML app in your identity provider's admin panel, then copy/paste the IDP metadata XML below. If you want to use a custom domain for your blog, <a href="/admin/settings">configure</a> that first.
                <div class="large ui list">
                    <a class="item" target="_blank" href="https://support.google.com/a/answer/6087519?hl=en"><i class="right triangle icon"></i>Instructions for G Suite users</a>
                    <a class="item" target="_blank" href="https://developer.okta.com/standards/SAML/setting_up_a_saml_application_in_okta/"><i class="right triangle icon"></i>Instructions for Okta users</a>
                    <a class="item" target="_blank" href="https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-single-sign-on-non-gallery-applications"><i class="right triangle icon"></i>Instructions for Azure AD users</a>
                    
                </div>
            </div>
            <div class="ui hidden divider"></div>

            {{with $domain := $.Info.Domain}}
                <div class="field">
                    <div class="ui small header">Your ACS URL</div>
                    <input class="readonly-input" type="text" value="https://{{$domain}}/saml/acs" readonly>
                </div>
                <div class="field">
                    <div class="ui small header">Your Entity ID</div>
                    <input class="readonly-input" type="text" value="https://{{$domain}}/saml/metadata" readonly>
                </div>
            {{else}}
                <div class="field">
                    <div class="ui small header">Your ACS URL</div>
                    <input class="readonly-input" type="text" value="https://{{$.HTTPHost}}/saml/acs" readonly>
                </div>
                <div class="field">
                    <div class="ui small header">Your Entity ID</div>
                    <input class="readonly-input" type="text" value="https://{{$.HTTPHost}}/saml/metadata" readonly>
                </div>
            {{end}}
            <div class="field">
                <div class="ui small header">IDP Metadata</div>
                <textarea rows="10" name="saml_metadata" placeholder="Enable single sign-on by entering the SAML metadata from your SSO provider.">{{$.Info.SAML.IDPMetadata}}</textarea>
            </div>
            <div class="ui hidden divider"></div>
            <div class="equal width fields">
                <div class="field mobile hidden">&nbsp;</div>
                <div class="field">
                    <div class="two ui buttons">
                        <a href="/" class="ui large button">Cancel</a>
                        <button type="submit" class="ui large blue button">Save</button>
                    </div>
                </div>
            </div>

        </form>

    </div>
</div>

{{template "footer.html" .}}



<|start_filename|>templates/index.html<|end_filename|>
{{template "header.html" .}}

{{$query := $.Request.FormValue "q"}}

<div class="ui container">
    <div class="ui mobile reversed stackable grid">

        <div class="four wide column">
            {{template "sidebar.html" .}}
        </div>

        <div class="twelve wide column">
            <div class="ui padded segment">
                {{if $query}}
                    <a href="/" class="ui large basic button"><i class="long arrow alternate left icon"></i>Search results for &quot;{{$query}}&quot;</a>
                    <div class="ui hidden divider"></div>

                    {{if not $.Posts}}
                        <div class="ui center aligned grey header">No posts found</div>
                    {{end}}

                {{end}}

                {{if not $query}}
                    {{if $.Admin}}
                        <form id="post-form" class="ui large form post-form" action="/admin/posts/add" method="POST">
                            <div class="field">
                                <textarea name="body" class="post-form-body" rows="5" maxlength="280" placeholder="Add a post"></textarea>
                            </div>
                            <div class="equal width fields">
                                <div class="field mobile hidden"></div>
                                <div class="field mobile hidden"></div>
                                <div class="field right-aligned">
                                    <a title="Add another update" class="add-body-button ui large basic icon button"><i class="ui brand plus circle icon"></i></a>
                                    <button type="submit" class="ui large brand button">Post</button>
                                </div>
                            </div>
                        </form>
                        <div class="ui hidden divider"></div>
                    {{end}}
                {{end}}

                {{if $.Posts}}
                    {{range $post := $.Posts}}
                        <!-- post -->
                        <div class="post">
                            <div class="ui large right floated horizontal divided link list">
                                {{if $post.Tweet}}
                                    <a class="item" href="https://twitter.com/{{$.Info.Twitter.ID}}/status/{{$post.Tweet}}"><i class="brand twitter square icon"></i></a>
                                {{end}}
                            </div>
                            <a href="/posts/{{$post.ID}}" class="post-header">
                                <img src="/profile">
                                {{$.Info.Name}} <span title="{{date $post.Created}}">&nbsp; {{dateshort $post.Created}}</span>
                            </a>
                            <div class="post-body">
                                {{enhance $post.Body}}
                            </div>

                            {{if $post.Thread}}
                                <a href="/posts/{{$post.ID}}" class="ui basic button">Show this thread</a>
                            {{end}}
                            <!-- post -->
                        </div>
                    {{end}}

                    {{if not $query}}
                        {{$showall := $.Request.FormValue "all"}}
                        <div class="center-aligned">
                            <div class="ui hidden section divider"></div>
                            {{if eq $showall "yes"}}
                                <a href="/" class="ui small basic button">Show threaded</a>
                            {{else}}
                                <a href="/?all=yes" class="ui small basic button">Show all</a>
                            {{end}}
                        </div>
                    {{end}}

                {{else}}
                    <div class="ui center aligned grey header">There are no posts yet.</div>
                {{end}}
            </div>
        </div>
    </div>


</div>


{{if 0}}
    <!--div class="right-aligned">
    <span id="countdown">280</span>
    </div-->

    <!--script>
        function updateCountdown() {
            var remaining = 280 - $('#body').val().length;
            $('#countdown').text(remaining);
        }
        $(document).ready(function() {
            $('#body').change(updateCountdown);
            $('#body').keyup(updateCountdown);
        });
    </script-->
{{end}}

{{template "footer.html" .}}


<|start_filename|>config.go<|end_filename|>
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io/ioutil"
	"math/big"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

var (
	ErrPostNotFound    = errors.New("post not found")
	ErrPostDuplicateID = errors.New("duplicate post ID")

	ErrDraftNotFound    = errors.New("draft not found")
	ErrDraftDuplicateID = errors.New("duplicate draft ID")
)

type Info struct {
	Email          string        `json:"email"`
	Password       []byte        `json:"password"`
	Secret         string        `json:"secret"`
	Configured     bool          `json:"configure"`
	Name           string        `json:"name"`
	Description    string        `json:"description"`
	Domain         string        `json:"domain"`
	Headerlink1    string        `json:"headerlink1"`
	Headerlink1URL string        `json:"headerlink1_url"`
	Headerlink2    string        `json:"headerlink2"`
	Headerlink2URL string        `json:"headerlink2_url"`
	Headerlink3    string        `json:"headerlink3"`
	Headerlink3URL string        `json:"headerlink3_url"`
	Snippet        string        `json:"snippet"`
	Schedule       time.Duration `json:"schedule"`
	ScheduleHour   int           `json:"schedule_hour"`
	HashKey        string        `json:"hash_key"`
	BlockKey       string        `json:"block_key"`
	Location       string        `json:"location"`
	SAML           struct {
		IDPMetadata string `json:"idp_metadata"`
		PrivateKey  []byte `json:"private_key"`
		Certificate []byte `json:"certificate"`
	} `json:"saml"`
	Mail struct {
		From     string `json:"from"`
		Server   string `json:"server"`
		Port     int    `json:"port"`
		Username string `json:"username"`
		Password string `json:"password"`
	} `json:"mail"`
	Twitter struct {
		ConsumerKey       string `json:"consumer_key"`
		ConsumerSecret    string `json:"consumer_secret"`
		AccessToken       string `json:"acess_token"`
		AccessTokenSecret string `json:"access_token_secret"`

		ID         int64  `json:"id"`
		ScreenName string `json:"screen_name"`
	} `json:"twitter"`
}

type Config struct {
	mu       sync.RWMutex
	filename string

	Info   *Info    `json:"info"`
	Posts  []*Post  `json:"posts"`
	Drafts []*Draft `json:"drafts"`

	Modified time.Time `json:"modified"`
}

func NewConfig(filename string) (*Config, error) {
	filename = filepath.Join(datadir, filename)
	c := &Config{filename: filename}
	b, err := ioutil.ReadFile(filename)

	// Create new config with defaults
	if os.IsNotExist(err) {
		c.Info = &Info{
			HashKey:      randomString(32),
			BlockKey:     randomString(32),
			Schedule:     24 * time.Hour,
			ScheduleHour: 9,
			Location:     "UTC",
		}
		return c, c.generateSAMLKeyPair()
	}
	if err != nil {
		return nil, err
	}

	// Open existing config
	if err := json.Unmarshal(b, c); err != nil {
		return nil, fmt.Errorf("invalid config %q: %s", filename, err)
	}

	// Set defaults for upgrades.
	if c.Info.Schedule == 0 {
		c.Info.Schedule = 24 * time.Hour
	}
	if c.Info.ScheduleHour == 0 {
		c.Info.ScheduleHour = 9
	}
	if c.Info.Location == "" {
		c.Info.Location = "UTC"
	}
	if len(c.Info.SAML.PrivateKey) == 0 || len(c.Info.SAML.Certificate) == 0 {
		if err := c.generateSAMLKeyPair(); err != nil {
			return nil, err
		}
	}

	return c, nil
}

func (c *Config) Lock(loc string) {
	c.mu.Lock()
}

func (c *Config) Unlock(loc string) {
	c.mu.Unlock()
}

func (c *Config) RLock(loc string) {
	c.mu.RLock()
}

func (c *Config) RUnlock(loc string) {
	c.mu.RUnlock()
}

func (c *Config) FindInfo() Info {
	c.RLock("FindInfo")
	defer c.RUnlock("FindInfo")
	return *c.Info
}

func (c *Config) UpdateInfo(fn func(*Info) error) error {
	c.Lock("UpdateInfo")
	defer c.Unlock("UpdateInfo")
	if err := fn(c.Info); err != nil {
		return err
	}
	return c.save()
}

func (c *Config) save() error {
	b, err := json.MarshalIndent(c, "", "    ")
	if err != nil {
		return err
	}
	return overwrite(c.filename, b, 0644)
}

//
// Draft
//

type Draft struct {
	ID      string    `json:"id"`
	Body    string    `json:"body"`
	Created time.Time `json:"created"`
}

func (c *Config) ListDrafts() []Draft {
	c.RLock("ListDrafts")
	defer c.RUnlock("ListDrafts")
	var drafts []Draft
	for _, d := range c.Drafts {
		drafts = append(drafts, *d)
	}
	return drafts
}

func (c *Config) AddDraft(body string) (Draft, error) {
	c.Lock("AddDraft")
	defer c.Unlock("AddDraft")

	draftID := randomString(15)
	if _, err := c.findDraft(draftID); err != ErrDraftNotFound {
		return Draft{}, ErrDraftDuplicateID
	}

	draft := Draft{
		ID:      draftID,
		Body:    body,
		Created: time.Now().In(getTimezone()),
	}

	c.Drafts = append(c.Drafts, &draft)
	return draft, c.save()
}

func (c *Config) DeleteDraft(id string) error {
	c.Lock("DeleteDraft")
	defer c.Unlock("DeleteDraft")
	var drafts []*Draft
	for _, d := range c.Drafts {
		if d.ID == id {
			continue
		}
		drafts = append(drafts, d)
	}
	c.Drafts = drafts
	return c.save()
}

func (c *Config) FindDraft(id string) (Draft, error) {
	c.RLock("FindDraft")
	defer c.RUnlock("FindDraft")
	d, err := c.findDraft(id)
	if err != nil {
		return Draft{}, err
	}
	return *d, c.save()
}

func (c *Config) findDraft(id string) (*Draft, error) {
	for _, d := range c.Drafts {
		if d.ID == id {
			return d, nil
		}
	}
	return nil, ErrDraftNotFound
}

//
// Post
//

type Post struct {
	ID        string    `json:"id"`
	Body      string    `json:"body"`
	Tweet     int64     `json:"tweet"`
	ParentID  string    `json:"parent"`
	Scheduled bool      `json:"scheduled"`
	Created   time.Time `json:"created"`

	Thread []Post `json:"-"`
}

func (c *Config) SearchPosts(query string) []Post {
	c.RLock("SearchPosts")
	defer c.RUnlock("SearchPosts")
	if len(c.Posts) == 0 {
		return nil
	}

	keywords := []string{}
	for _, kw := range strings.Fields(query) {
		keywords = append(keywords, strings.ToLower(kw))
	}

	var posts []Post
	for i := len(c.Posts) - 1; i >= 0; i-- {
		p := c.Posts[i]
		match := false
		for _, kw := range keywords {
			if strings.Contains(strings.ToLower(p.Body), kw) {
				match = true
				break
			}
		}
		if match {
			posts = append(posts, *p)
		}
	}
	return posts
}

func (c *Config) ListParentPosts() []Post {
	c.RLock("ListParentPosts")
	defer c.RUnlock("ListParentPosts")
	if len(c.Posts) == 0 {
		return nil
	}

	var posts []Post
	for i := len(c.Posts) - 1; i >= 0; i-- {
		post := *c.Posts[i]
		if post.ParentID != "" {
			continue
		}

		parentID := post.ID
		for _, p := range c.Posts {
			if p.ParentID != parentID {
				continue
			}
			post.Thread = append(post.Thread, *p)
			parentID = p.ID
		}
		posts = append(posts, post)
	}
	return posts
}

func (c *Config) MostRecentScheduledPost() (Post, error) {
	c.RLock("MostRecentScheduledPost")
	defer c.RUnlock("MostRecentScheduledPost")
	if len(c.Posts) == 0 {
		return Post{}, ErrPostNotFound
	}

	for i := len(c.Posts) - 1; i >= 0; i-- {
		p := c.Posts[i]
		if !p.Scheduled {
			continue
		}
		return *p, nil
	}
	return Post{}, ErrPostNotFound
}

func (c *Config) ListPosts() []Post {
	c.RLock("ListPosts")
	defer c.RUnlock("ListPosts")
	if len(c.Posts) == 0 {
		return nil
	}

	var posts []Post
	for i := len(c.Posts) - 1; i >= 0; i-- {
		p := c.Posts[i]
		posts = append(posts, *p)
	}
	return posts
}

func (c *Config) AddPost(body, parentID string, scheduled bool) (Post, error) {
	c.Lock("AddPost")
	defer c.Unlock("AddPost")

	postID := randomString(15)
	if _, err := c.findPost(postID); err != ErrPostNotFound {
		return Post{}, ErrPostDuplicateID
	}

	post := Post{
		ID:        postID,
		Body:      body,
		ParentID:  parentID,
		Scheduled: scheduled,
		Created:   time.Now().In(getTimezone()),
	}

	c.Posts = append(c.Posts, &post)
	return post, c.save()
}

func (c *Config) DeletePost(id string) error {
	c.Lock("DeletePost")
	defer c.Unlock("DeletePost")
	var posts []*Post
	for _, p := range c.Posts {
		if p.ID == id {
			continue
		}
		posts = append(posts, p)
	}
	c.Posts = posts
	return c.save()
}

func (c *Config) FindPost(id string) (Post, error) {
	c.RLock("FindPost")
	defer c.RUnlock("FindPost")
	p, err := c.findPost(id)
	if err != nil {
		return Post{}, err
	}
	post := *p

	parentID := post.ID
	for _, p := range c.Posts {
		if p.ParentID != parentID {
			continue
		}
		post.Thread = append(post.Thread, *p)
		parentID = p.ID
	}
	return post, c.save()
}

func (c *Config) findPost(id string) (*Post, error) {
	for _, p := range c.Posts {
		if p.ID == id {
			return p, nil
		}
	}
	return nil, ErrPostNotFound
}

func (c *Config) UpdatePost(id string, fn func(*Post) error) error {
	c.Lock("UpdatePost")
	defer c.Unlock("UpdatePost")
	t, err := c.findPost(id)
	if err != nil {
		return err
	}
	if err := fn(t); err != nil {
		return err
	}
	return c.save()
}

func (c *Config) generateSAMLKeyPair() error {
	// Generate private key.
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return err
	}

	// Generate the certificate.
	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
	if err != nil {
		return err
	}

	tmpl := x509.Certificate{
		NotBefore:    time.Now(),
		NotAfter:     time.Now().AddDate(5, 0, 0),
		SerialNumber: serialNumber,
		Subject: pkix.Name{
			CommonName:   httpHost,
			Organization: []string{"Nanoblog"},
		},
		BasicConstraintsValid: true,
	}

	cert, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key)
	if err != nil {
		return err
	}

	// Generate private key PEM block.
	c.Info.SAML.PrivateKey = pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(key),
	})

	// Generate certificate PEM block.
	c.Info.SAML.Certificate = pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: cert,
	})
	return c.save()
}


<|start_filename|>templates/admin/index.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui mobile reversed stackable grid">
        <div class="four wide column">
            <div class="ui padded segment">
                {{with $success := $.Request.FormValue "success"}}
                    <div class="ui info message">
                        <div class="header">
                            {{if eq $success "settings"}}
                                Settings saved successfully
                            {{else if eq $success "removed"}}
                                Removed successfully
                            {{end}}
                        </div>
                        <a class="close-link" href="/"><i class="close icon"></i></a>
                    </div>
                {{end}}
                {{with $error := $.Request.FormValue "error"}}
                    <div class="ui large negative message">
                        <div class="content">
                            <div class="header">
                                {{if eq $error "adding"}}
                                    Adding property failed
                                {{else if eq $error "removing"}}
                                    Removing property failed
                                {{else}}
                                    {{$error}}
                                {{end}}
                            </div>
                            <a class="close-link" href="/"><i class="close icon"></i></a>
                        </div>
                    </div>
                    <div class="ui hidden divider"></div>
                {{end}}

                <!--div class="ui large message">
                    <i class="info circle grey icon"></i>A buffered post will be automatically published every {{if gt $.Info.Schedule.Hours 24.0}}{{days $.Info.Schedule}} days{{else}}day{{end}} at {{hourstamp $.Info.ScheduleHour}} {{$.Info.Location}}.
                </div-->

                <div class="ui large blue dividing header">Posting schedule</div>
                <form class="ui large form" method="POST" action="/admin/schedule">
                    <div class="field">
                        <select name="schedule">
                            <option value="24" {{if eq $.Info.Schedule.Hours 24.0}}selected{{end}}>Every day</option>
                            <option value="48" {{if eq $.Info.Schedule.Hours 48.0}}selected{{end}}>Every 2 days</option>
                            <option value="72" {{if eq $.Info.Schedule.Hours 72.0}}selected{{end}}>Every 3 days</option>
                            <option value="168" {{if eq $.Info.Schedule.Hours 168.0}}selected{{end}}>Every 7 days</option>
                            <option value="240" {{if eq $.Info.Schedule.Hours 240.0}}selected{{end}}>Every 10 days</option>
                            <option value="336" {{if eq $.Info.Schedule.Hours 336.0}}selected{{end}}>Every 14 days</option>
                        </select>
                    </div>
                    <div class="field">
                        <select name="hour">
                            <option value="6" {{if eq $.Info.ScheduleHour 6}}selected{{end}}>at 6:00 AM</option>
                            <option value="7" {{if eq $.Info.ScheduleHour 7}}selected{{end}}>at 7:00 AM</option>
                            <option value="8" {{if eq $.Info.ScheduleHour 8}}selected{{end}}>at 8:00 AM</option>
                            <option value="9" {{if eq $.Info.ScheduleHour 9}}selected{{end}}>at 9:00 AM</option>
                            <option value="10" {{if eq $.Info.ScheduleHour 10}}selected{{end}}>at 10:00 AM</option>
                            <option value="11" {{if eq $.Info.ScheduleHour 11}}selected{{end}}>at 11:00 AM</option>
                            <option value="12" {{if eq $.Info.ScheduleHour 12}}selected{{end}}>at 12:00 PM</option>
                            <option value="13" {{if eq $.Info.ScheduleHour 13}}selected{{end}}>at 1:00 PM</option>
                            <option value="14" {{if eq $.Info.ScheduleHour 14}}selected{{end}}>at 2:00 PM</option>
                            <option value="15" {{if eq $.Info.ScheduleHour 15}}selected{{end}}>at 3:00 PM</option>
                            <option value="16" {{if eq $.Info.ScheduleHour 16}}selected{{end}}>at 4:00 PM</option>
                            <option value="17" {{if eq $.Info.ScheduleHour 17}}selected{{end}}>at 5:00 PM</option>
                            <option value="18" {{if eq $.Info.ScheduleHour 18}}selected{{end}}>at 6:00 PM</option>
                            <option value="19" {{if eq $.Info.ScheduleHour 19}}selected{{end}}>at 7:00 PM</option>
                            <option value="20" {{if eq $.Info.ScheduleHour 20}}selected{{end}}>at 8:00 PM</option>
                            <option value="21" {{if eq $.Info.ScheduleHour 21}}selected{{end}}>at 9:00 PM</option>
                            <option value="22" {{if eq $.Info.ScheduleHour 22}}selected{{end}}>at 10:00 PM</option>
                            <option value="23" {{if eq $.Info.ScheduleHour 23}}selected{{end}}>at 11:00 PM</option>
                            <option value="0" {{if eq $.Info.ScheduleHour 0}}selected{{end}}>at 12:00 AM</option>
                            <option value="1" {{if eq $.Info.ScheduleHour 1}}selected{{end}}>at 1:00 AM</option>
                            <option value="2" {{if eq $.Info.ScheduleHour 2}}selected{{end}}>at 2:00 AM</option>
                            <option value="3" {{if eq $.Info.ScheduleHour 3}}selected{{end}}>at 3:00 AM</option>
                            <option value="4" {{if eq $.Info.ScheduleHour 4}}selected{{end}}>at 4:00 AM</option>
                            <option value="5" {{if eq $.Info.ScheduleHour 5}}selected{{end}}>at 5:00 AM</option>
                        </select>
                    </div>
                    <div class="field">
                        <button type="submit" class="ui blue fluid button">Save</button>
                    </div>
                </form>
            </div>
        </div>

        <div class="twelve wide column">
            <div class="ui padded segment">
                <div class="ui large blue dividing header">Queued posts</div>
                <div class="ui large message">
                    <i class="info circle grey icon"></i>Queued posts are automatically published according to the selected schedule.
                </div>
                <div class="ui hidden divider"></div>

                <form class="ui large form post-form" action="/admin/posts/add" method="POST">
                    <div class="field">
                        <textarea id="body" name="body" class="post-form-body" rows="5" maxlength="280" autofocus></textarea>
                    </div>
                    <div class="equal width fields">
                        <div class="field mobile hidden"></div>
                        <div class="field mobile hidden"></div>
                        <div class="field right-aligned">
                            <button type="submit" name="queue" value="yes" class="ui large blue button">Queue post</button>
                        </div>
                    </div>
                </form>
                {{if $.Drafts}}
                    <div class="ui section divider"></div>

                    {{range $draft := $.Drafts}}
                        <div class="post">
                            <div class="ui large right floated horizontal divided link list">
                                <a href="/admin/drafts/delete/{{$draft.ID}}" class="item confirm" data-prompt="Delete?"><i class="grey trash icon"></i></a>
                            </div>
                            <a class="post-header">
                                <img src="/profile">
                                {{$.Info.Name}}
                            </a>
                            <div class="post-body">
                                {{enhance $draft.Body}}
                            </div>
                        </div>
                    {{end}}
                {{else}}
                    <div class="ui hidden divider"></div>
                    <div class="ui center aligned grey header">No queued posts yet</div>
                {{end}}

            </div>
        </div>
    </div>

</div>
{{template "footer.html" .}}


<|start_filename|>templates/posts/view.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui mobile reversed stackable grid">
        <div class="four wide column">
            {{template "sidebar.html" .}}
        </div>
        <div class="twelve wide column">
            <div class="ui padded segment">
                <a href="/" class="ui large basic button"><i class="long arrow alternate left icon"></i>{{$.Info.Name}}</a>
                <div class="ui hidden divider"></div>
                {{range $post := $.Posts}}
                    <!-- post -->
                    <div class="post">
                        <div class="ui large right floated horizontal divided link list">
                            {{if $post.Tweet}}
                                <a class="item" href="https://twitter.com/{{$.Info.Twitter.ID}}/status/{{$post.Tweet}}"><i class="brand twitter square icon"></i></a>
                            {{end}}
                            {{if $.Admin}}
                                {{if eq $.Post.ID $post.ID}}
                                    <a href="/admin/posts/delete/{{$post.ID}}?f={{$.Post.ID}}" class="item confirm" data-prompt="Delete post?"><i class="grey trash icon"></i></a>
                                {{end}}
                            {{end}}
                        </div>
                        <a href="/posts/{{$post.ID}}" class="post-header">
                            <img src="/profile">
                            {{$.Info.Name}} <span title="{{date $post.Created}}">&nbsp; {{dateshort $post.Created}}</span>
                        </a>
                        <div class="post-body">
                            {{enhance $post.Body}}
                        </div>
                    </div>
                    <!-- post -->
                {{end}}

                {{if $.Admin}}
                    <div class="ui hidden section divider"></div>
                    <form id="post-form" class="ui large form post-form" action="/admin/posts/add" method="POST">
                        <input type="hidden" name="in_reply_to_status" value="{{$.PostParent.Tweet}}">
                        <input type="hidden" name="parent" value="{{$.PostParent.ID}}">
                        <div class="field">
                            <textarea name="body" class="post-form-body" rows="5" maxlength="280" placeholder="Add another post"></textarea>
                        </div>
                        <div class="equal width fields">
                            <div class="field mobile hidden"></div>
                            <div class="field mobile hidden"></div>
                            <div class="field right-aligned">
                                <a title="Add another update" class="add-body-button ui large basic icon button"><i class="ui brand plus circle icon"></i></a>
                                <button type="submit" class="ui large brand button">Post</button>
                            </div>
                        </div>
                    </form>
                    <div class="ui hidden divider"></div>
                {{end}}

            </div>
        </div>
    </div>

</div>

{{template "footer.html" .}}


<|start_filename|>templates/admin/settings/api.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui padded segment">
        {{with $success := $.Request.FormValue "success"}}
            <div class="ui info message">
                <div class="header">
                    {{if eq $success "changes"}}
                        Changes saved successfully
                    {{else if eq $success "todo"}}
                        TODO
                    {{end}}
                </div>
                <a class="close-link" href="/admin/settings/api"><i class="close icon"></i></a>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}
        {{with $error := $.Request.FormValue "error"}}
            <div class="ui large negative message">
                <div class="content">
                    <div class="header">
                        {{if eq $error "invalid"}}
                            Invalid. Please try again.
                        {{else if eq $error "twitter"}}
                            Linking Twitter failed. Please verify API credentials.
                        {{else}}
                            {{$error}}
                        {{end}}
                    </div>
                    <a class="close-link" href="/admin/settings/api"><i class="close icon"></i></a>
                </div>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}


        <form class="ui large form" action="/admin/settings/api" method="POST" novalidate autocomplete="off" enctype="multipart/form-data">
            <div class="ui blue dividing header">Twitter API</div>
            <div class="ui message">
                <i class="info circle grey icon"></i>Enter Twitter Developer API credentials to enable Twitter crossposting.
                <div class="ui middle aligned divided list">
                    <div class="item">
                        1. Create a <a href="https://developer.twitter.com" target="_blank">Twitter developer account</a> using your existing Twitter account.
                    </div>
                    <div class="item">
                        2. Create an app for your blog on the <b>Developer &gt; Apps</b> page.
                    </div>
                    <div class="item">
                        3. From the <b>App details &gt; Keys and tokens</b> page, copy your consumer key, consumer secret, and generate your access token &amp; access token secret.
                    </div>
                </div>
            </div>
            <div class="ui hidden divider"></div>

            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Consumer Key</div>
                    <input name="twitter_consumer_key" type="text" placeholder="Consumer Key" value="{{$.Info.Twitter.ConsumerKey}}">
                </div>
                <div class="field">
                    <div class="ui small header">Consumer Secret</div>
                    <input name="twitter_consumer_secret" type="text" placeholder="Consumer Secret" value="{{$.Info.Twitter.ConsumerSecret}}">
                </div>
            </div>
            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Access Token</div>
                    <input name="twitter_access_token" type="text" placeholder="Access Token" value="{{$.Info.Twitter.AccessToken}}">
                </div>
                <div class="field">
                    <div class="ui small header">Access Token Secret</div>
                    <input name="twitter_access_token_secret" type="text" placeholder="Access Token Secret" value="{{$.Info.Twitter.AccessTokenSecret}}">
                </div>
            </div>

            <div class="equal width fields">
                <div class="field mobile hidden">&nbsp;</div>
                <div class="field">
                    <div class="two ui buttons">
                        <a href="/" class="ui large button">Cancel</a>
                        <button type="submit" class="ui large blue button">Save</button>
                    </div>
                </div>
            </div>
        </form>
    </div>

</div>

{{template "footer.html" .}}



<|start_filename|>dman.go<|end_filename|>
package main

import (
	"net/url"
	"time"
)

var dmanInterval = 30 * time.Second

func dman() {
	logger.Infof("dman> init")

	for {
		time.Sleep(dmanInterval)
		logger.Debugf("dman> running checks")

		now := time.Now().In(getTimezone())
		drafts := config.ListDrafts()
		info := config.FindInfo()

		// Skip because there are no drafts.
		if len(drafts) == 0 {
			logger.Debugf("dman> skipping because there are no drafts")
			continue
		}

		// Skip if not the correct hour.
		if now.Hour() != info.ScheduleHour {
			logger.Debugf("dman> skipping because now %d is not the scheduled hour %d", now.Hour(), info.ScheduleHour)
			continue
		}

		// Skip if most recently posted.
		lastPost, err := config.MostRecentScheduledPost()
		if err == nil {
			lastPostAge := now.Sub(lastPost.Created)
			if info.Schedule > lastPostAge {
				logger.Debugf("dman> skipping draft because schedule %s > post %s", info.Schedule, lastPostAge)
				continue
			}
		}

		draft := drafts[0]
		post, err := config.AddPost(draft.Body, "", true)
		if err != nil {
			logger.Errorf("dman> add post failed: %s", err)
			continue
		}
		config.DeleteDraft(draft.ID)
		logger.Infof("dman> added post %s from draft %s", post.ID, draft.ID)

		if twitterAPI != nil {
			tweet, err := twitterAPI.PostTweet(post.Body, url.Values{})
			if err == nil {
				config.UpdatePost(post.ID, func(p *Post) error {
					p.Tweet = tweet.Id
					return nil
				})
			}
		}
	}
}


<|start_filename|>web.go<|end_filename|>
package main

import (
	"bytes"
	"encoding/gob"
	"fmt"
	"html/template"
	"net"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/ChimeraCoder/anaconda"
	"github.com/crewjam/saml"
	"github.com/crewjam/saml/samlsp"
	humanize "github.com/dustin/go-humanize"
	"github.com/julienschmidt/httprouter"
	"golang.org/x/net/publicsuffix"
	xurls "mvdan.cc/xurls"
)

var (
	SessionCookieName    = "__nanoblog_session"
	SessionCookieNameSSO = "__nanoblog_sso_session"

	urlRegexp       = xurls.Relaxed()
	linebreakRegexp = regexp.MustCompile(`\r?\n`)
	hashtagRegexp   = regexp.MustCompile(`#(\S+)`)
	imageRegexp     = regexp.MustCompile(`\.(?i:jpeg|jpg|png|gif)`)
)

type Session struct {
	Admin     bool
	NotBefore time.Time
	NotAfter  time.Time
}

type Web struct {
	// Internal
	w        http.ResponseWriter
	r        *http.Request
	ps       httprouter.Params
	template string

	// Default
	HTTPHost string
	Admin    bool
	Backlink string
	Version  string
	Request  *http.Request
	Section  string
	Time     time.Time
	Info     Info
	SAML     *samlsp.Middleware
	Email    string

	// Paging
	Page int

	// Additional
	Twitter *anaconda.TwitterApi

	Post       Post
	Posts      []Post
	PostParent Post

	Draft  Draft
	Drafts []Draft
}

func init() {
	gob.Register(Session{})
}

func Error(w http.ResponseWriter, err error) {
	logger.Error(err)

	w.WriteHeader(http.StatusInternalServerError)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, errorPageHTML+"\n")
}

func (w *Web) HTML() {
	t := template.New(w.template).Funcs(template.FuncMap{
		"hasprefix": strings.HasPrefix,
		"hassuffix": strings.HasSuffix,
		"add": func(a, b int) int {
			return a + b
		},
		"sub": func(a, b int) int {
			return a - b
		},
		"bytes": func(n int64) string {
			return fmt.Sprintf("%.2f GB", float64(n)/1024/1024/1024)
		},
		"hourstamp": func(hour int) string {
			t := time.Date(0, 0, 0, hour, 0, 0, 0, time.UTC)
			return t.Format(time.Kitchen)
		},
		"days": func(d time.Duration) string {
			return fmt.Sprintf("%0.f", d.Hours()/float64(24))
		},
		"date": func(t time.Time) string {
			return t.Format(time.UnixDate)
		},
		"dateshort": func(t time.Time) string {
			now := time.Now().In(t.Location())
			diff := now.Sub(t)

			if diff < (59 * time.Second) {
				return fmt.Sprintf("%.0fs", diff.Seconds())
			} else if diff < (59 * time.Minute) {
				return fmt.Sprintf("%.0fm", diff.Minutes())
			} else if diff < (24 * time.Hour) {
				return fmt.Sprintf("%.0fh", diff.Hours())
			} else if t.Year() == now.Year() {
				return t.Format("3:04 PM · Jan 2")
			}
			return t.Format("3:04 PM · Jan 2 2006")
		},
		"time": humanize.Time,
		"jsfloat64": func(n float64) template.JS {
			return template.JS(fmt.Sprintf("%.0f", n))
		},
		"jsint": func(n int) template.JS {
			return template.JS(fmt.Sprintf("%d", n))
		},
		"jsint64": func(n int) template.JS {
			return template.JS(fmt.Sprintf("%d", n))
		},
		"safehtml": func(s string) template.HTML {
			return template.HTML(s)
		},
		"enhance": func(s string) template.HTML {
			// Linkify URLs and display inline media.
			matches := urlRegexp.FindAllString(s, -1)
			for _, match := range matches {
				tmpurl := match

				if !strings.HasPrefix(match, "http") {
					tmpurl = "https://" + match
				}
				u, err := url.Parse(tmpurl)
				if err != nil {
					continue
				}
				if u.Scheme != "http" && u.Scheme != "https" {
					continue
				}
				if u.Host == "" {
					continue
				}
				if u.Path == "" {
					u.Path = "/"
				}

				var link string

				if imageRegexp.MatchString(u.Path) {
					// Display image inline.
					link = fmt.Sprintf(`<a href="%s" target="_blank"><img src="%s" class="ui fluid rounded bordered image"></a>`, u.String(), u.String())
				} else {
					// Linkify URL
					host := strings.TrimPrefix(u.Host, "www.")
					link = fmt.Sprintf(`<a href="%s" title="%s">%s%s</a>`, u.String(), u.String(), host, u.Path)
				}
				s = strings.ReplaceAll(s, match, link)
			}

			// Create linebreaks.
			s = linebreakRegexp.ReplaceAllString(s, "<br>\n")

			// Linkify hashtags.
			s = hashtagRegexp.ReplaceAllString(s, `<a href="/?q=%23${1}">#${1}</a>`)

			return template.HTML(s)
		},
		"truncate": func(s string, n int) string {
			if len(s) > n {
				s = s[:n-3] + "..."
			}
			return s
		},
		"timestamp": func(n int64) string {
			t := time.Unix(n, 0).Local()
			return t.Format("2006/01/02")
		},
		"linebreaks": func(s string) template.HTML {
			return template.HTML(strings.Replace(s, "\n", "\n<br>\n", -1))
		},
		"ssoprovider": func() string {
			if samlSP == nil {
				return ""
			}
			redirect, err := url.Parse(samlSP.ServiceProvider.GetSSOBindingLocation(saml.HTTPRedirectBinding))
			if err != nil {
				logger.Warnf("SSO redirect invalid URL: %s", err)
				return "unknown"
			}
			domain, err := publicsuffix.EffectiveTLDPlusOne(redirect.Host)
			if err != nil {
				logger.Warnf("SSO redirect invalid URL domain: %s", err)
				return "unknown"
			}
			suffix, icann := publicsuffix.PublicSuffix(domain)
			if icann {
				suffix = "." + suffix
			}
			return strings.Title(strings.TrimSuffix(domain, suffix))
		},
	})

	for _, filename := range AssetNames() {
		if !strings.HasPrefix(filename, "templates/") {
			continue
		}
		name := strings.TrimPrefix(filename, "templates/")
		b, err := Asset(filename)
		if err != nil {
			Error(w.w, err)
			return
		}

		var tmpl *template.Template
		if name == t.Name() {
			tmpl = t
		} else {
			tmpl = t.New(name)
		}
		if _, err := tmpl.Parse(string(b)); err != nil {
			Error(w.w, err)
			return
		}
	}

	w.w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := t.Execute(w.w, w); err != nil {
		Error(w.w, err)
		return
	}
}

func (w *Web) NotFound() {
	http.NotFound(w.w, w.r)
}

func (w *Web) Redirect(format string, a ...interface{}) {
	location := fmt.Sprintf(format, a...)
	http.Redirect(w.w, w.r, location, http.StatusFound)
}

func WebHandler(h func(*Web), section string) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		web := &Web{
			w:        w,
			r:        r,
			ps:       ps,
			template: section + ".html",

			HTTPHost: httpHost,
			Backlink: backlink,
			Time:     time.Now().In(getTimezone()),
			Version:  version,
			Request:  r,
			Section:  section,
			Info:     config.FindInfo(),

			Twitter: twitterAPI,
			SAML:    samlSP,
		}

		var public = map[string]bool{
			"signin":     true,
			"forgot":     true,
			"index":      true,
			"configure":  true,
			"profile":    true,
			"banner":     true,
			"posts/view": true,
			"domain":     true,
		}

		if section == "signout" {
			h(web)
			return
		}

		// Send user to custom domain URL if one is configured.
		// Without forcing them, so they can avoid it if there's a DNS issue, etc.
		if domain := config.FindInfo().Domain; domain != "" {
			if r.Host != domain {
				if section == "index" {
					web.Redirect("/domain")
					return
				}
			}
		}

		// Has a valid session.
		if session, _ := ValidateSession(r); session != nil {
			logger.Infof("session exists admin is %t", session.Admin)
			web.Admin = session.Admin
		} else if samlSP != nil {
			// SAML auth.
			if token := samlSP.GetAuthorizationToken(r); token != nil {
				r = r.WithContext(samlsp.WithToken(r.Context(), token))

				email := token.StandardClaims.Subject
				if email == "" {
					Error(w, fmt.Errorf("SAML token missing email"))
					return
				}

				web.Email = email
				web.Admin = true

				logger.Debugf("valid SSO token, signing in session")
				if err := web.SigninSession(true); err != nil {
					Error(web.w, err)
					return
				}
			}
		}

		if web.Admin || public[section] {
			h(web)
			return
		}

		if !config.FindInfo().Configured {
			web.Redirect("/configure")
			return
		}

		logger.Warnf("auth: sign in required")
		web.Redirect("/signin")
	}
}

func Log(h httprouter.Handle) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		start := time.Now()
		h(w, r, ps)
		rang := r.Header.Get("Range")
		logger.Infof("%d %q %s %q %d ms", start.Unix(), rang, r.Method, r.RequestURI, int64(time.Since(start)/time.Millisecond))
	}
}

func staticHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	serveAsset(w, r, ps.ByName("path"))
}

func serveAsset(w http.ResponseWriter, r *http.Request, filename string) {
	path := "static" + filename

	b, err := Asset(path)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	fi, err := AssetInfo(path)
	if err != nil {
		Error(w, err)
		return
	}
	http.ServeContent(w, r, path, fi.ModTime(), bytes.NewReader(b))
}

func ValidateSession(r *http.Request) (*Session, error) {
	cookie, err := r.Cookie(SessionCookieName)
	if err != nil {
		return nil, fmt.Errorf("auth: missing cookie")
	}
	session := &Session{}
	if err := securetoken.Decode(SessionCookieName, cookie.Value, session); err != nil {
		return nil, err
	}
	if time.Now().Before(session.NotBefore) {
		return nil, fmt.Errorf("invalid session (before valid)")
	}
	if time.Now().After(session.NotAfter) {
		return nil, fmt.Errorf("invalid session (expired session.NotAfter is %s and now is %s)", session.NotAfter, time.Now())
	}
	return session, nil
}

func (w *Web) SignoutSession() {
	domain, _, err := net.SplitHostPort(w.r.Host)
	if err != nil {
		logger.Warnf("parsing Host header failed: %s", err)
	}
	http.SetCookie(w.w, &http.Cookie{
		Name:     SessionCookieNameSSO,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		Secure:   !httpInsecure,
		Domain:   domain,
		MaxAge:   -1,
		Expires:  time.Unix(1, 0),
	})
	http.SetCookie(w.w, &http.Cookie{
		Name:     SessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		Secure:   !httpInsecure,
		Domain:   domain,
		MaxAge:   -1,
		Expires:  time.Unix(1, 0),
	})
}

func (w *Web) SigninSession(admin bool) error {
	expires := time.Now().Add(12 * time.Hour)

	encoded, err := securetoken.Encode(SessionCookieName, Session{
		Admin:     admin,
		NotBefore: time.Now(),
		NotAfter:  expires,
	})
	if err != nil {
		return fmt.Errorf("auth: encoding error: %s", err)
	}
	domain, _, err := net.SplitHostPort(w.r.Host)
	if err != nil {
		logger.Warnf("parsing Host header failed: %s", err)
	}
	logger.Infof("HTTP INSECURE IS %t (so Secure: %t)", httpInsecure, !httpInsecure)
	http.SetCookie(w.w, &http.Cookie{
		Name:     SessionCookieName,
		Value:    encoded,
		Path:     "/",
		Domain:   domain,
		HttpOnly: true,
		Secure:   !httpInsecure,
		Expires:  expires,
	})
	return nil
}


<|start_filename|>templates/header.html<|end_filename|>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
        <meta name="referrer" content="origin">
        <link rel="icon" href="/profile?v={{$.Version}}">
        <link rel="apple-touch-icon" href="/profile?v={{$.Version}}">

        <title>{{if $.Info.Name}}{{$.Info.Name}}{{else}}Nanoblog{{end}}</title>

        <link rel="stylesheet" type="text/css" href="/static/roboto.css?v={{$.Version}}">
        <link rel="stylesheet" type="text/css" href="/static/semantic/semantic.min.css?v={{$.Version}}">
        <link rel="stylesheet" type="text/css" href="/static/style.css?v={{$.Version}}">

        <script src="/static/jquery.min.js?v={{$.Version}}"></script>
        <script src="/static/semantic/semantic.min.js?v={{$.Version}}"></script>
    </head>
    <body>
        {{if ne $.Section "domain"}}
            <div class="borderless ui large stackable menu">
                <div class="ui container">
                    {{if and $.Backlink $.Admin}}
                        <a class="item" href="{{.Backlink}}"><i class="home icon"></i></a>
                    {{end}}

                    {{if and $.Info.Headerlink1 $.Info.Headerlink1URL}}
                        <a href="{{$.Info.Headerlink1URL}}" class="{{if eq $.Info.Headerlink1URL "/"}}active{{end}} item">{{$.Info.Headerlink1}}</a>
                    {{end}}
                    {{if and $.Info.Headerlink2 $.Info.Headerlink2URL}}
                        <a href="{{$.Info.Headerlink2URL}}" class="{{if eq $.Info.Headerlink2URL "/"}}active{{end}} item">{{$.Info.Headerlink2}}</a>
                    {{end}}
                    {{if and $.Info.Headerlink3 $.Info.Headerlink3URL}}
                        <a href="{{$.Info.Headerlink3URL}}" class="{{if eq $.Info.Headerlink3URL "/"}}active{{end}} item">{{$.Info.Headerlink3}}</a>
                    {{end}}

                    <div class="right menu">
                        {{if $.Admin}}
                            <a href="/admin" class="{{if eq $.Section "admin/index"}}active{{end}} item">Queue</a>
                            <a href="/admin/settings" class="{{if hasprefix $.Section "admin/settings"}}active{{end}} item">Settings</a>
                            <a class="item confirm" data-prompt="Sign out?" href="/signout" title="Sign out">Sign Out</a>
                        {{else}}
                            <a class="item" href="/signin" title="Sign in">Sign In</a>
                        {{end}}
                    </div>
                </div>
            </div>

            {{if hasprefix $.Section "admin/settings"}}
                <div class="ui container">
                    <div class="ui large secondary pointing stackable menu">
                        <div class="ui container">
                            <a class="{{if eq $.Section "admin/settings/index"}}active{{end}} item" href="/admin/settings">General Settings</a>
                            <a class="{{if eq $.Section "admin/settings/menu"}}active{{end}} item" href="/admin/settings/menu">Menu Links</a>
                            <a class="{{if eq $.Section "admin/settings/sso"}}active{{end}} item" href="/admin/settings/sso">SSO</a>
                            <a class="{{if eq $.Section "admin/settings/api"}}active{{end}} item" href="/admin/settings/api">Twitter API</a>
                            <a class="{{if eq $.Section "admin/settings/snippet"}}active{{end}} item" href="/admin/settings/snippet">Analytics</a>
                            <a class="{{if eq $.Section "admin/settings/password"}}active{{end}} item" href="/admin/settings/password">Change Password</a>
                        </div>
                    </div>
                </div>
            {{end}}
        {{end}}

        <div class="ui hidden divider"></div>


<|start_filename|>templates/admin/settings/menu.html<|end_filename|>
{{template "header.html" .}}

<div class="ui container">
    <div class="ui padded segment">
        {{with $success := $.Request.FormValue "success"}}
            <div class="ui info message">
                <div class="header">
                    {{if eq $success "changes"}}
                        Changes saved successfully
                    {{else if eq $success "todo"}}
                        TODO
                    {{end}}
                </div>
                <a class="close-link" href="/admin/settings/menu"><i class="close icon"></i></a>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}
        {{with $error := $.Request.FormValue "error"}}
            <div class="ui large negative message">
                <div class="content">
                    <div class="header">
                        {{if eq $error "invalid"}}
                            Invalid. Please try again.
                        {{else}}
                            {{$error}}
                        {{end}}
                    </div>
                    <a class="close-link" href="/admin/settings/menu"><i class="close icon"></i></a>
                </div>
            </div>
            <div class="ui hidden divider"></div>
        {{end}}

        <form class="ui large form" action="/admin/settings/menu" method="POST" novalidate autocomplete="off" enctype="multipart/form-data">
            <div class="ui blue dividing header">Menu links</div>
            <div class="ui message">
                <i class="info circle grey icon"></i>Menu links are displayed in the header of your blog.
            </div>
            <div class="ui hidden divider"></div>

            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Menu Link 1</div>
                    <input name="headerlink1" type="text" placeholder="e.g. Homepage" value="{{$.Info.Headerlink1}}">
                </div>
                <div class="field">
                    <div class="ui small header">Menu Link 1 URL</div>
                    <input name="headerlink1_url" type="text" placeholder="e.g. https://example.com/" value="{{$.Info.Headerlink1URL}}">
                </div>
            </div>

            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Menu Link 2</div>
                    <input name="headerlink2" type="text" placeholder="e.g. Homepage" value="{{$.Info.Headerlink2}}">
                </div>
                <div class="field">
                    <div class="ui small header">Menu Link 2 URL</div>
                    <input name="headerlink2_url" type="text" placeholder="e.g. https://example.com/" value="{{$.Info.Headerlink2URL}}">
                </div>
            </div>

            <div class="equal width fields">
                <div class="field">
                    <div class="ui small header">Menu Link 3</div>
                    <input name="headerlink3" type="text" placeholder="e.g. Homepage" value="{{$.Info.Headerlink3}}">
                </div>
                <div class="field">
                    <div class="ui small header">Menu Link URL 3</div>
                    <input name="headerlink3_url" type="text" placeholder="e.g. https://example.com/" value="{{$.Info.Headerlink3URL}}">
                </div>
            </div>

            <div class="equal width fields">
                <div class="field mobile hidden">&nbsp;</div>
                <div class="field">
                    <div class="two ui buttons">
                        <a href="/" class="ui large button">Cancel</a>
                        <button type="submit" class="ui large blue button">Save</button>
                    </div>
                </div>
            </div>
        </form>
    </div>

</div>

{{template "footer.html" .}}



<|start_filename|>handlers.go<|end_filename|>
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/julienschmidt/httprouter"
	"golang.org/x/crypto/bcrypt"
)

var (
	validEmail    = regexp.MustCompile(`^[ -~]+@[ -~]+$`)
	validPassword = regexp.MustCompile(`^[ -~]{8,200}$`)
	validString   = regexp.MustCompile(`^[ -~]{1,200}$`)
)

func ssoHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	if token := samlSP.GetAuthorizationToken(r); token != nil {
		http.Redirect(w, r, "/", http.StatusFound)
		return
	}
	logger.Debugf("SSO: require account handler")
	samlSP.RequireAccountHandler(w, r)
	return
}

func samlHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	if samlSP == nil {
		Error(w, fmt.Errorf("SAML is not configured"))
		return
	}
	logger.Debugf("SSO: samlSP.ServeHTTP")
	samlSP.ServeHTTP(w, r)
}

func configureHandler(w *Web) {
	if config.FindInfo().Configured {
		w.Redirect("/?error=configured")
		return
	}

	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email")))
	emailConfirm := strings.ToLower(strings.TrimSpace(w.r.FormValue("email_confirm")))
	password := w.r.FormValue("password")
	name := strings.TrimSpace(w.r.FormValue("name"))
	description := strings.TrimSpace(w.r.FormValue("description"))
	domain := strings.TrimSpace(w.r.FormValue("domain"))

	if name == "" {
		name = "Unnamed Blog"
	}

	// Domain
	domain = strings.TrimPrefix(domain, "http://")
	domain = strings.TrimPrefix(domain, "https://")
	domain = strings.TrimRight(domain, "/")
	domain = strings.TrimRight(domain, ".")
	domain = strings.TrimLeft(domain, ".")

	if !validEmail.MatchString(email) || !validPassword.MatchString(password) || email != emailConfirm {
		w.Redirect("/configure?error=invalid")
		return
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		w.Redirect("/forgot?error=bcrypt")
		return
	}
	config.UpdateInfo(func(i *Info) error {
		i.Email = email
		i.Name = name
		i.Description = description
		i.Domain = domain
		i.Password = <PASSWORD>
		i.Configured = true
		i.Headerlink1 = name
		i.Headerlink1URL = "/"
		return nil
	})

	if err := w.SigninSession(true); err != nil {
		Error(w.w, err)
		return
	}

	w.Redirect("/")
	return
}

func forgotHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email")))
	secret := w.r.FormValue("secret")
	password := w.r.FormValue("password")

	if email != "" && !validEmail.MatchString(email) {
		w.Redirect("/forgot?error=invalid")
		return
	}
	if secret != "" && !validString.MatchString(secret) {
		w.Redirect("/forgot?error=invalid")
		return
	}
	if email != "" && secret != "" && !validPassword.MatchString(password) {
		w.Redirect("/forgot?error=invalid&email=%s&secret=%s", email, secret)
		return
	}

	if email != config.FindInfo().Email {
		w.Redirect("/forgot?error=invalid")
		return
	}

	if secret == "" {
		secret = config.FindInfo().Secret
		if secret == "" {
			secret = randomString(32)
			config.UpdateInfo(func(i *Info) error {
				if i.Secret == "" {
					i.Secret = secret
				}
				return nil
			})
		}

		go func() {
			if err := mailer.Forgot(email, secret); err != nil {
				logger.Error(err)
			}
		}()

		w.Redirect("/forgot?success=forgot")
		return
	}

	if secret != config.FindInfo().Secret {
		w.Redirect("/forgot?error=invalid")
		return
	}

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		w.Redirect("/forgot?error=bcrypt")
		return
	}
	config.UpdateInfo(func(i *Info) error {
		i.Password = <PASSWORD>
		i.Secret = ""
		return nil
	})

	if err := w.SigninSession(true); err != nil {
		Error(w.w, err)
		return
	}
	w.Redirect("/")
	return
}

func signoutHandler(w *Web) {
	w.SignoutSession()
	w.Redirect("/")
}

func signinHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email")))
	password := w.r.FormValue("password")

	if email != config.FindInfo().Email {
		w.Redirect("/signin?error=invalid")
		return
	}

	if err := bcrypt.CompareHashAndPassword(config.FindInfo().Password, []byte(password)); err != nil {
		w.Redirect("/signin?error=invalid")
		return
	}
	if err := w.SigninSession(true); err != nil {
		Error(w.w, err)
		return
	}

	w.Redirect("/admin/settings")
}

func domainHandler(w *Web) {
	w.HTML()
}

func indexHandler(w *Web) {
	if !config.FindInfo().Configured {
		w.Redirect("/configure")
		return
	}

	var posts []Post

	if query := w.r.FormValue("q"); query != "" {
		posts = config.SearchPosts(query)
	} else if w.r.FormValue("all") == "yes" {
		posts = config.ListPosts()
	} else {
		posts = config.ListParentPosts()
	}

	w.Posts = posts
	w.HTML()
}

func postsViewHandler(w *Web) {
	post, err := config.FindPost(w.ps.ByName("post"))
	if err != nil {
		w.NotFound()
		return
	}

	var posts []Post
	posts = append(posts, post)
	posts = append(posts, post.Thread...)

	// Last post becomes parent to replies.
	for _, p := range posts {
		w.PostParent = p
	}

	w.Post = post
	w.Posts = posts
	w.HTML()
}

func profileHandler(w *Web) {
	if _, err := os.Stat(profileFilename); err != nil {
		w.Redirect("/static/favicon.png")
		return
	}
	http.ServeFile(w.w, w.r, profileFilename)
}

/*
func bannerHandler(w *Web) {
	if _, err := os.Stat(bannerFilename); err != nil {
		w.Redirect("/static/banner.jpg")
		return
	}
	http.ServeFile(w.w, w.r, bannerFilename)
}
*/
func adminScheduleHourHandler(w *Web) {
	w.Redirect("/admin")
}

func adminScheduleHandler(w *Web) {
	scheduleInterval, _ := strconv.Atoi(w.r.FormValue("schedule"))
	schedule := time.Duration(scheduleInterval) * time.Hour

	scheduleHour, _ := strconv.Atoi(w.r.FormValue("hour"))
	if scheduleHour < 0 {
		scheduleHour = 0
	}
	if scheduleHour > 23 {
		scheduleHour = 23
	}

	config.UpdateInfo(func(i *Info) error {
		i.Schedule = schedule
		i.ScheduleHour = scheduleHour
		return nil
	})

	w.Redirect("/admin")
}

func adminSettingsIndexHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email")))
	name := strings.TrimSpace(w.r.FormValue("name"))
	description := strings.TrimSpace(w.r.FormValue("description"))
	location := strings.TrimSpace(w.r.FormValue("location"))
	domain := strings.TrimSpace(w.r.FormValue("domain"))

	// Email
	if !validEmail.MatchString(email) {
		w.Redirect("/admin/settings?error=invalid")
		return
	}

	// Timezone
	if location != "" {
		if err := setTimezone(location); err != nil {
			Error(w.w, err)
			return
		}
	}

	// Name
	if name == "" {
		name = "<NAME>"
	}

	// Domain
	domain = strings.TrimPrefix(domain, "http://")
	domain = strings.TrimPrefix(domain, "https://")
	domain = strings.TrimRight(domain, "/")
	domain = strings.TrimRight(domain, ".")
	domain = strings.TrimLeft(domain, ".")

	// Profile
	if file, fileHeader, err := w.r.FormFile("profile"); fileHeader != nil && err == nil {
		defer file.Close()

		profile, err := os.Create(profileFilename)
		if err != nil {
			w.Redirect("/admin/settings?error=profile")
			return
		}
		if _, err := io.Copy(profile, file); err != nil {
			w.Redirect("/admin/settings?error=profile")
			return
		}
		if err := profile.Close(); err != nil {
			w.Redirect("/admin/settings?error=profile")
			return
		}
	}

	// If domain changes we need to re-configure SAML.
	// This updates the cookie domain.
	if config.FindInfo().Domain != domain {
		if err := configureSAML(); err != nil {
			logger.Warnf("configuring SAML failed: %s", err)
			w.Redirect("/admin/settings/sso?error=saml")
			return
		}
		return
	}

	config.UpdateInfo(func(i *Info) error {
		i.Email = email
		i.Name = name
		i.Description = description
		i.Location = location
		i.Domain = domain
		return nil
	})

	w.Redirect("/admin/settings?success=changes")
}

func adminSettingsMenuHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	headerlink1 := strings.TrimSpace(w.r.FormValue("headerlink1"))
	headerlink1URL := strings.TrimSpace(w.r.FormValue("headerlink1_url"))
	headerlink2 := strings.TrimSpace(w.r.FormValue("headerlink2"))
	headerlink2URL := strings.TrimSpace(w.r.FormValue("headerlink2_url"))
	headerlink3 := strings.TrimSpace(w.r.FormValue("headerlink3"))
	headerlink3URL := strings.TrimSpace(w.r.FormValue("headerlink3_url"))

	config.UpdateInfo(func(i *Info) error {
		if headerlink1 == "" {
			i.Headerlink1 = i.Name
			i.Headerlink1URL = "/"
		} else {
			i.Headerlink1 = headerlink1
			i.Headerlink1URL = headerlink1URL
		}

		i.Headerlink2 = headerlink2
		i.Headerlink2URL = headerlink2URL
		i.Headerlink3 = headerlink3
		i.Headerlink3URL = headerlink3URL
		return nil
	})

	w.Redirect("/admin/settings/menu?success=changes")
}

func adminSettingsSSOHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	samlMetadata := strings.TrimSpace(w.r.FormValue("saml_metadata"))

	config.UpdateInfo(func(i *Info) error {
		i.SAML.IDPMetadata = samlMetadata
		return nil
	})

	// Configure SAML if metadata is present.
	if len(samlMetadata) > 0 {
		if err := configureSAML(); err != nil {
			logger.Warnf("configuring SAML failed: %s", err)
			w.Redirect("/admin/settings/sso?error=saml")
			return
		}
	} else {
		samlSP = nil
	}

	w.Redirect("/admin/settings/sso?success=changes")
}

func adminSettingsAPIHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	twitterConsumerKey := strings.TrimSpace(w.r.FormValue("twitter_consumer_key"))
	twitterConsumerSecret := strings.TrimSpace(w.r.FormValue("twitter_consumer_secret"))
	twitterAccessToken := strings.TrimSpace(w.r.FormValue("twitter_access_token"))
	twitterAccessTokenSecret := strings.TrimSpace(w.r.FormValue("twitter_access_token_secret"))

	config.UpdateInfo(func(i *Info) error {
		i.Twitter.ConsumerKey = twitterConsumerKey
		i.Twitter.ConsumerSecret = twitterConsumerSecret
		i.Twitter.AccessToken = twitterAccessToken
		i.Twitter.AccessTokenSecret = twitterAccessTokenSecret
		return nil
	})

	if err := updateTwitterAPI(); err != nil {
		w.Redirect("/admin/settings/api?error=twitter")
		return
	}

	w.Redirect("/admin/settings/api?success=changes")
}

func adminSettingsSnippetHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	snippet := strings.TrimSpace(w.r.FormValue("snippet"))

	config.UpdateInfo(func(i *Info) error {
		i.Snippet = snippet
		return nil
	})

	w.Redirect("/admin/settings/snippet?success=changes")
}

func adminSettingsPasswordHandler(w *Web) {
	if w.r.Method == "GET" {
		w.HTML()
		return
	}

	currentPassword := w.r.FormValue("current_password")
	newPassword := w.r.FormValue("new_password")

	if currentPassword != "" || newPassword != "" {
		if !validPassword.MatchString(newPassword) {
			w.Redirect("/admin/settings?error=invalid")
			return
		}

		if err := bcrypt.CompareHashAndPassword(config.FindInfo().Password, []byte(currentPassword)); err != nil {
			w.Redirect("/admin/settings?error=invalid")
			return
		}

		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(<PASSWORD>), bcrypt.DefaultCost)
		if err != nil {
			w.Redirect("/admin/settings?error=bcrypt")
			return
		}

		config.UpdateInfo(func(i *Info) error {
			i.Password = <PASSWORD>
			return nil
		})
	}

	w.Redirect("/admin/settings/password?success=changes")
}

func helpHandler(w *Web) {
	w.HTML()
}

func adminIndexHandler(w *Web) {
	w.Drafts = config.ListDrafts()
	w.HTML()
}

func draftsDeleteHandler(w *Web) {
	draft, err := config.FindDraft(w.ps.ByName("draft"))
	if err != nil {
		w.Redirect("/?error=notfound")
		return
	}
	config.DeleteDraft(draft.ID)
	w.Redirect("/admin")
}

func postsAddHandler(w *Web) {
	w.r.ParseForm()
	queue := w.r.FormValue("queue") == "yes"

	inReplyToStatusId, _ := strconv.ParseInt(w.r.FormValue("in_reply_to_status"), 10, 64)
	parentID := w.r.FormValue("parent")

	for _, body := range w.r.PostForm["body"] {
		body = strings.TrimSpace(body)
		if body == "" {
			continue
		}
		if queue {
			if _, err := config.AddDraft(body); err != nil {
				w.Redirect("/?error=database")
				return
			}
		} else {
			post, err := config.AddPost(body, parentID, false)
			if err != nil {
				w.Redirect("/?error=database")
				return
			}
			parentID = post.ID

			if twitterAPI != nil {
				params := url.Values{}
				if inReplyToStatusId > 0 {
					params.Add("in_reply_to_status_id", fmt.Sprintf("%d", inReplyToStatusId))
				}
				logger.Infof("tweeting post %s %s", post.ID, post.Body)
				tweet, err := twitterAPI.PostTweet(post.Body, params)
				if err == nil {
					config.UpdatePost(post.ID, func(p *Post) error {
						p.Tweet = tweet.Id
						return nil
					})
					inReplyToStatusId = tweet.Id
				} else {
					logger.Warnf("twitter tweet post %s failed: %s", post.ID, err)
				}
			}

		}
	}

	if queue {
		w.Redirect("/admin")
		return
	}

	w.Redirect("/")
}

func postsDeleteHandler(w *Web) {
	from := w.r.FormValue("f")

	post, err := config.FindPost(w.ps.ByName("post"))
	if err != nil {
		w.Redirect("/?error=notfound")
		return
	}
	config.DeletePost(post.ID)

	if twitterAPI != nil && post.Tweet > 0 {
		if _, err := twitterAPI.DeleteTweet(post.Tweet, false); err != nil {
			logger.Warnf("twitter delete tweet post %s tweet %d", post.ID, post.Tweet)
		}
	}
	if from != "" && from != post.ID {
		if _, err := config.FindPost(from); err == nil {
			w.Redirect("/posts/%s", from)
			return
		}
	}
	w.Redirect("/")
}


<|start_filename|>Dockerfile<|end_filename|>
FROM phusion/baseimage:0.11
MAINTAINER github.com/nanoblog/nanoblog

COPY ./nanoblog-linux-amd64 /usr/bin/nanoblog
COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update && apt-get install -y tzdata

RUN chmod +x /usr/bin/nanoblog /usr/local/bin/entrypoint.sh

ENTRYPOINT [ "/usr/local/bin/entrypoint.sh" ]

CMD [ "/sbin/my_init" ]
