#!/usr/bin/wapptclsh
#
# This script implements a simple URL shortener backed by an SQLite database.
#
# For administrative access, this script must be invoked via the
# Fossil CGI extension mechanism:
#
# https://fossil-scm.org/home/doc/trunk/www/serverext.wiki
#
# That is because administrative access uses the FOSSIL_CAPABILITIES and
# FOSSIL_USER environment variables. However, for ordinary shortener
# redirects, this routine can be run as an ordinary CGI. So the intended
# use case is:
#
# 1. Set up a CGI (perhaps called just "/u") on the website to act
# as the normal redirect site.
#
# 2. Set up a separate CGI (at something like "/fossil/ext/urlshortener")
# uses the same database as (1) and serving as the administration
# point for creating, editing, and deleting short URLs.
#
#
########
#
# Setup:
set BASEURL https://sqlite.org/u
set ADMIN_CAPABILITY i
set DATABASE /url-shortener.db
#
########
package require wapp
# Common header for all pages that do no redirect
#
proc common-header {} {
wapp-trim {
<div class='fossil-doc' data-title='URL Shortener'>
}
}
proc common-footer {} {
wapp-trim {
</div>
}
}
# Generate a submenu of hyperlinks.
#
proc sub-menu {args} {
wapp-trim {
<p><table border="1" cellpadding="10" cellspacing="0"><tr>
}
foreach {name url} $args {
wapp-trim {
<td><a href="%url($url)">%html($name)</a></td>
}
}
wapp-trim {
</tr></table></p>
}
}
# Open the shortener database. Initialize it if necessary.
#
proc open-database {} {
global DATABASE
sqlite3 db $DATABASE
db eval {
CREATE TABLE IF NOT EXISTS url(
name TEXT PRIMARY KEY,
redir TEXT,
user TEXT,
ctime DATE,
mtime DATE,
notes TEXT
) WITHOUT ROWID;
}
}
# Debugging: show the environment
proc wapp-page-env {} {
wapp-allow-xorigin-params
common-header
set base [wapp-param BASE_URL]
sub-menu {Create New URL} $base/ee {Search} $base/sx \
{Recent Changes} $base/lr
wapp-trim {
<h1>Wapp Environment</h1>
<pre>%html([wapp-debug-env])</pre>
}
common-footer
}
# Issue the "Not Found" page
proc wapp-page-nf {} {
wapp-reply-code "404 Not Found"
wapp-trim {
<h1>Not Found</h1>
<p>No such resource:
"%html%([wapp-param BASE_URL])%%html%([wapp-param PATH_INFO])%"</p>
}
}
# Do a redirect
proc wapp-default {} {
set x [wapp-param PATH_HEAD {}]
if {$x==""} {
wapp-page-sx
return
}
open-database
set redir [db one {SELECT redir FROM url WHERE name=$x}]
if {$redir==""} {
wapp-page-nf
return
}
wapp-redirect $redir
}
# Check credentials. Return true to abort because credentials
# are invalid.
#
proc check-credentials {} {
global ADMIN_CAPABILITY env
if {![info exists env(FOSSIL_CAPABILITIES)]} {
wapp-page-nf
return 1
}
return 0
}
# Search for an existing entry
#
proc wapp-page-sx {} {
if {[check-credentials]} return
common-header
set base [wapp-param BASE_URL]
sub-menu {Create New URL} $base/ee {Recents Changes} $base/lr \
{CGI Environment} $base/env
wapp-trim {
<form>
Search: <input type="text" size="60" name="s">
<input type="submit" name="s1" value="Go">
</form>
}
if {[wapp-param-exists s]} {
set s [wapp-param s]
wapp-trim {
<h2>Search results for %html($s):</h2><ol>
}
open-database
set pattern %$s%
set cnt 0
db eval {
SELECT name, redir, user, mtime FROM url
WHERE name LIKE $pattern OR redir LIKE $pattern
} {
wapp-trim {
<li><a href="%url($base)/ee/%url($name)">%html($name)</a>
→ %html($redir) (by %html($user) on %html($mtime))
}
incr cnt
}
wapp-trim {</ol>}
if {$cnt==0} {
wapp-trim {
<p>No matches found</p>
}
}
}
common-footer
}
# Show recent modified URL shorteners pages
#
proc wapp-page-lr {} {
if {[check-credentials]} return
common-header
set base [wapp-param BASE_URL]
sub-menu {Create New URL} $base/ee {Search} $base/sx \
{CGI Environment} $base/env
wapp-trim {
<ol>
}
open-database
set cnt 0
db eval {
SELECT name, redir, user, mtime FROM url
ORDER BY mtime DESC LIMIT 50
} {
wapp-trim {
<li><a href="%url($base)/ee/%url($name)">%html($name)</a>
→ %html($redir) (by %html($user) on %html($mtime))
}
incr cnt
}
wapp-trim {</ol>}
if {$cnt==0} {
wapp-trim {
<p>No entries</p>
}
}
common-footer
}
# Create or edit a new URL shortener entry.
#
# Name of the entry to be editted is in PATH_TAIL. Or if PATH_TAIL is
# blank, create a new entry.
#
proc wapp-page-ee {} {
global BASEURL
wapp-content-security-policy {
default-src 'self' data:;
style-src 'self' 'unsafe-inline';
}
if {[check-credentials]} return
common-header
if {[wapp-param-exists cancel]} {
wapp-redirect [wapp-param BASE_URL]/sx
return
}
open-database
set hardname [wapp-param PATH_TAIL {}]
set name [wapp-param name $hardname]
set redir [wapp-param redir {}]
set notes [wapp-param notes {}]
set errmsg {}
if {[wapp-param-exists s1]} {
if {$hardname==""} {
set e [db one {SELECT count(*) FROM url WHERE name=$name}]
if {$e==1} {
set errmsg "Shortened URL name \"$name\" already exists.\
Pick a different name."
} elseif {[string length $name]<4} {
set errmsg "Shortened URL name \"$name\" too short.\
Must be at least 4 characters."
} elseif {[regexp {[^a-z0-9]} $name]} {
set errmsg "Shortened URL name \"$name\" contains invalid characters. \
The name may contain only lower-case ASCII letters and digits."
} elseif {[lsearch [info procs] wapp-page-$name]>=0} {
set errmsg "Shortened URL name \"$name\" already exists.\
Pick a different name."
} else {
set user $::env(FOSSIL_USER)
db eval {
INSERT INTO url(name,redir,user,notes,ctime,mtime)
VALUES($name,$redir,$user,$notes,datetime(),datetime())
}
set hardname $name
}
} else {
db eval {
UPDATE url SET redir=$redir, notes=$notes, mtime=datetime()
WHERE name=$name
}
}
}
set ctime {}
set user {}
set mtime {}
if {$hardname==""} {
wapp-trim {
<h1>Create A New Shortened URL</h1>
}
if {$errmsg!=""} {
wapp-trim {
<p style='color:red;' >%html($errmsg)</p>
}
}
wapp-trim {
<form method="POST">
<table border="0" cellpadding="0" cellspacing="10">
<tr>
<td align="right">Shortened-URL:</td>
<td>%html($BASEURL)/<input type="text" name="name" value="%html($name)" size="10">
</tr>
}
} else {
wapp-trim {
<h1>Edit Shortened URL: %html($BASEURL)/%html($hardname)</h1>
<form method="POST">
<input type="hidden" name="name" value="%html($hardname)">
<table border="0" cellpadding="0" cellspacing="10">
<tr>
<td align="right">Shortened-URL:</td>
<td>%html($BASEURL)/%html($hardname)
</tr>
}
db eval {
SELECT ctime, mtime, user, redir, notes FROM url WHERE name=$name
} break
set redir [wapp-param redir $redir]
set notes [wapp-param notes $notes]
}
wapp-trim {
<tr>
<td align="right">Redirect-To:</td>
<td><input type="text" name="redir" value="%html($redir)" size="70"></td>
</tr>
}
if {$ctime!=""} {
if {$user==""} {set user ???}
wapp-trim {
<tr><td align="right">Created:</td>
<td>%html($ctime) by %html($user)</td></tr>
}
}
if {$mtime!="" && $mtime!=$ctime} {
wapp-trim {
<tr><td align="right">Modified:</td><td>%html($mtime)</td></tr>
}
}
wapp-trim {
<tr>
<td align="right">Notes:</td>
<td><input type="text" name="notes" value="%html($notes)" size="70"></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" name="s1" value="Submit">
<input type="submit" name="cancel" value="Cancel">
</tr>
</table>
</form>
}
common-footer
}
wapp-start $argv