1// SPDX-FileCopyrightText: 2022 Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: BSD-2-Clause
  4
  5package main
  6
  7import (
  8	"errors"
  9	"fmt"
 10	"log"
 11	"net/url"
 12	"strings"
 13	"text/template"
 14
 15	"github.com/dgraph-io/badger/v3"
 16	"golang.org/x/exp/maps"
 17	"golang.org/x/exp/slices"
 18)
 19
 20// Create shortens a given URL with an optional name. If a name is provided,
 21// that name will be used. Otherwise, 4-character string will be generated and
 22// used instead.
 23func (m model) create(name string, url string) string {
 24	err := m.database.Update(func(txn *badger.Txn) error {
 25		return txn.Set([]byte(name), []byte(url))
 26	})
 27	if err != nil {
 28		// TODO: return an error instead so it can be handled like an error
 29		return fmt.Sprint(err)
 30	}
 31
 32	log.Println("\"" + url + "\" mapped to \"" + name + "\"")
 33	return fmt.Sprint("URL mapped to ", name, "\n")
 34}
 35
 36// read() accepts a start variable for the first link in a set (implying reverse
 37// iteration), an end variable for the last link in a set (implying forward
 38// iteration), and a count variable for the number of requested entries. The
 39// requested set is returned as JSON.
 40func (m model) read(start string, end string, count int) (map[string]string, error) {
 41	links := make(map[string]string)
 42
 43	if !(count > 0) {
 44		return nil, errors.New("Count parameter is required")
 45	}
 46
 47	err := m.database.View(func(txn *badger.Txn) error {
 48		opts := badger.DefaultIteratorOptions
 49		opts.PrefetchSize = 20
 50		if start != "" && end == "" {
 51			// Start value provided, iterate backwards
 52			opts.Reverse = true
 53		}
 54		iterator := txn.NewIterator(opts)
 55		defer iterator.Close()
 56		if start == "" && end == "" {
 57			iterator.Rewind()
 58		} else if start != "" && end == "" {
 59			// Set position to "start"
 60			iterator.Seek([]byte(start))
 61			// If "start" exists, move to next value
 62			if iterator.Valid() {
 63				iterator.Next()
 64			}
 65		} else if start == "" && end != "" {
 66			// Start value provided, iterate forwards
 67			// Set position to "end"
 68			iterator.Seek([]byte(end))
 69			// If "end" exists, move to next value
 70			if iterator.Valid() {
 71				iterator.Next()
 72			}
 73		} else {
 74			return errors.New("Only provide start OR end parameters, not both")
 75		}
 76		for i := 0; i < count; i++ {
 77			if iterator.Valid() {
 78				err := iterator.Item().Value(func(v []byte) error {
 79					links[string(iterator.Item().Key())] = string(v)
 80					return nil
 81				})
 82				if err != nil {
 83					return err
 84				}
 85				iterator.Next()
 86			}
 87		}
 88		return nil
 89	})
 90	if err != nil {
 91		return nil, err
 92	}
 93
 94	return links, nil
 95}
 96
 97// Update modifies a shortened link
 98func (m model) update(name string, url string, oldName string) string {
 99	m.create(name, url)
100	if name != oldName {
101		m.delete(oldName)
102	}
103	return fmt.Sprint("\"" + url + "\" mapped to \"" + name + "\"")
104}
105
106// Delete removes a shortened URL from the database given its name.
107func (m model) delete(name string) string {
108	err := m.database.Update(func(txn *badger.Txn) error {
109		return txn.Delete([]byte(name))
110	})
111	if err != nil {
112		return fmt.Sprint(err)
113	}
114
115	log.Println("\"" + name + "\" has been deleted")
116	return fmt.Sprint("\"" + name + "\" has been deleted\n")
117}
118
119// nameExists returns true if the provided name is already stored in the
120// database and false if it's not.
121func (m model) nameExists(name string) bool {
122	err := m.database.View(func(txn *badger.Txn) error {
123		_, err := txn.Get([]byte(name))
124		if err != nil {
125			return err
126		}
127		return nil
128	})
129	return err == nil
130}
131
132// unauthenticated serves the unauthenticated home page to the visitor
133func unauthenticated() string {
134	home, err := templates.ReadFile("templates/home_unauthenticated.html")
135	if err != nil {
136		log.Fatalln(err)
137	}
138	return string(home)
139}
140
141// authenticated serves the authenticated dashboard to the user
142func (m model) authenticated(links map[string]string) string {
143	dash, err := templates.ReadFile("templates/home_authenticated.html")
144	if err != nil {
145		log.Fatalln(err)
146	}
147	tmpl, err := template.New("authenticated").Parse(string(dash))
148	if err != nil {
149		log.Println(err)
150	}
151
152	// Maps have no order so we're loading the keys into a slice and sorting the slice
153	linksKeys := maps.Keys(links)
154	slices.Sort(linksKeys)
155
156	// And ranging through the keys to pull ordered values from the map
157	var table string
158	for _, k := range linksKeys {
159		v := links[k]
160		table = table + fmt.Sprintf(`<tr>
161	<td><p>%s</p></td>
162	<td><p>%s</p></td>
163	<td><a class="button" href="/?action=edit&name=%s&url=%s">Edit</a><a class="button" href="/?action=delete&name=%s">Delete</a></td>
164</tr>`, k, v, k, url.QueryEscape(string(v)), k)
165	}
166
167	page := new(strings.Builder)
168	err = tmpl.Execute(page, table)
169	if err != nil {
170		log.Println(err)
171	}
172	return page.String()
173}
174
175type EditFields struct {
176	URL  string
177	Name string
178}
179
180// update serves the editing interface to the user
181func (m model) edit(name string, url string) string {
182	fields := EditFields{url, name}
183	editPage, err := templates.ReadFile("templates/edit.html")
184	if err != nil {
185		log.Fatalln(err)
186	}
187	tmpl, err := template.New("editPage").Parse(string(editPage))
188	if err != nil {
189		log.Println(err)
190	}
191	page := new(strings.Builder)
192	err = tmpl.Execute(page, fields)
193	if err != nil {
194		log.Println(err)
195	}
196	return page.String()
197}