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	type pageData struct {
157		Table string // HTML Table inserted to home page
158		Start string // Start
159		End   string
160	}
161
162	var homePage pageData
163
164	// And ranging through the keys to pull ordered values from the map
165	for _, k := range linksKeys {
166		v := links[k]
167		homePage.Table = homePage.Table + fmt.Sprintf(`<tr>
168	<td><p>%s</p></td>
169	<td><p>%s</p></td>
170	<td><a class="button" href="/?action=edit&name=%s&url=%s">Edit</a><a class="button" href="/?action=delete&name=%s">Delete</a></td>
171</tr>`, k, v, k, url.QueryEscape(string(v)), k)
172	}
173
174	homePage.Start = linksKeys[0]
175	homePage.End = linksKeys[len(linksKeys)-1]
176
177	page := new(strings.Builder)
178	err = tmpl.Execute(page, homePage)
179	if err != nil {
180		log.Println(err)
181	}
182	return page.String()
183}
184
185type EditFields struct {
186	URL  string
187	Name string
188}
189
190// update serves the editing interface to the user
191func (m model) edit(name string, url string) string {
192	fields := EditFields{url, name}
193	editPage, err := templates.ReadFile("templates/edit.html")
194	if err != nil {
195		log.Fatalln(err)
196	}
197	tmpl, err := template.New("editPage").Parse(string(editPage))
198	if err != nil {
199		log.Println(err)
200	}
201	page := new(strings.Builder)
202	err = tmpl.Execute(page, fields)
203	if err != nil {
204		log.Println(err)
205	}
206	return page.String()
207}