├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TERMS_OF_USE.txt ├── css └── go.css ├── go.cfg ├── go.py ├── go_test.py ├── html ├── base.html ├── check.html ├── dumplist.html ├── editlink.html ├── help.html ├── index.html ├── list.html ├── listinc.html ├── notfound.html ├── special.html ├── toplinks.html ├── variables.html ├── vartable.html └── verify_special.html ├── js └── go.js ├── newterms.txt ├── requirements.txt └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pickle 3 | tmp/* 4 | venv/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | # command to install dependencies for test purposes. 5 | # Pytest is only needed for development so install seperately 6 | # from the requirements. 7 | install: 8 | - "pip3 install nose" 9 | - "pip3 install -r requirements.txt" 10 | script: "nosetests go_test.py" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015, F5 Networks, Inc. 4 | Copyright (c) 2019, Lucas Messenger 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # The F5 Go Redirector 3 | 4 | [![Build Status](https://travis-ci.org/f5devcentral/f5go.svg?branch=master)](https://travis-ci.org/f5devcentral/f5go) 5 | 6 | *A simple service for redirecting mnemonic terms to destination urls.* 7 | 8 | Features include: 9 | 10 | - anyone can add terms easily 11 | - regex parsing for "special cases" (using regular expressions) 12 | - automatically appends everything after the second slash to the destination url 13 | - tracks and displays term usage frequency on frontpage with fontsize 14 | - variables allow destination URLs to change en masse (e.g. project name) 15 | 16 | ## Required Packages 17 | 18 | python-cherrypy3 19 | python-jinja2 20 | 21 | ## Tips 22 | 23 | To run, execute go.py and go to localhost:8080 in a browser. 24 | 25 | backup go database regularly 26 | 27 | $ ./go.py export 28 | $ backup newterms.txt 29 | 30 | --- 31 | contributed by Saul Pwanson 32 | 33 | -------------------------------------------------------------------------------- /TERMS_OF_USE.txt: -------------------------------------------------------------------------------- 1 | THIS LICENSE AGREEMENT IS ENTERED INTO BETWEEN THE SUBMITTING PARTY AND F5 NETWORKS, INC. AND THE SUBMITTING PARTY AGREES TO BE BOUND BY THE TERMS OF THIS AGREEMENT BY SUBMITTING, POSTING, DOWNLOADING, COPYING, MODIFYING, INPUTTING, INSTALLATION, UPLOAD OR OTHER USE OF F5 MATERIALS AND THE SUBMISSIONS. IF YOU DO NOT AGREE TO THE FOREGOING, DO NOT POST THE SUBISSIONS OR USE THE F5 MATERIALS. 2 | (1) F5 does not claim ownership of the materials you provide to F5 (including feedback and suggestions) or post, upload, input or submit to any F5 GitHub repository (collectively "Submissions"). However, by posting, uploading, inputting, providing or submitting your Submission you grant F5, its affiliated companies and necessary sub-licensees a full, complete, irrevocable copyright license to use your Submission including, without limitation, the rights to: copy, distribute, transmit, publicly display, publicly perform, reproduce, edit, translate and reformat your Submission; and to publish your name in connection with your Submission. In addition, you agree that your submission will be subject to the terms of the MIT License (F5 MIT License). 3 | (2) By posting, uploading, inputting, providing or submitting your Submission you warrant and represent that you own, are approved by your employer, or otherwise control all of the rights to your Submission as described including, without limitation, all the rights necessary for you to provide, post, upload, input or submit the Submissions. 4 | (3) Infringement Indemnification. Submitting party will defend and indemnify F5 against a claim that any information, design, specification, instruction, software, data, or material furnished by the submitting party under this license infringes a trademark, copyright, or patent. F5 will notify the submitting party promptly of such claim and will give sole control of defense and all related settlement negotiations to submitting party. F5 will provide reasonable assistance, information, and authority necessary to perform these obligations. Reasonable out-of-pocket expenses incurred by F5 for providing such assistance will be reimbursed by the submitting party. 5 | (4) THE MATERIALS AND SERVICES MADE AVAILABLE AT AND THROUGH THIS SITE ARE PROVIDED BY F5 ON AN "AS IS" BASIS. F5 MAKES NO REPRESENTATIONS, WARRANTIES OR GUARANTIES OF ANY KIND, EXPRESS OR IMPLIED, AS TO THE OPERATION OF THIS SITE, ITS CONTENT, OR ANY PRODUCTS OR SERVICES DESCRIBED OR OFFERED BY THIS SITE. TO THE FULL EXTENT PERMISSIBLE BY APPLICABLE LAW, F5 DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY, INCLUDING MERCHANTABILITY OF COMPUTER PROGRAMS AND INFORMATIONAL CONTENT, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, TITLE, OR THAT THE SITE CONTENT IS RELIABLE, ACCURATE, OR TIMELY. F5 WILL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING FROM THE USE OF THIS SITE, INCLUDING, BUT NOT LIMITED TO DIRECT, INDIRECT, INCIDENTAL, PUNITIVE, SPECIAL, CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF USE, DATA OR PROFITS, ARISING OUT OF OR IN ANY WAY CONNECTED WITH THE USE OR PERFORMANCE OF THE WEB SITE, WITH THE DELAY OR INABILITY TO USE THE WEB SITE OR RELATED SERVICES, THE PROVISION OF OR FAILURE TO PROVIDE SERVICES, OR FOR ANY INFORMATION, SOFTWARE, PRODUCTS, SERVICES AND RELATED GRAPHICS OBTAINED THROUGH THE WEB SITE, OR OTHERWISE ARISING OUT OF THE USE OF THE WEB SITE, WHETHER BASED ON CONTRACT, TORT, NEGLIGENCE, STRICT LIABILITY OR OTHERWISE, EVEN IF F5 OR ANY OF ITS SUPPLIERS HAS BEEN ADVISED OF THE POSSIBILITY OF DAMAGES. BECAUSE SOME STATES/JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. WHILE THIS SITE MAY PROVIDE LINKS TO THIRD PARTY SITES, F5 DOES NOT CONTROL OR ENDORSE ANY THIRD PARTY SITE AND DISCLAIMS ANY RESPONSIBILITY FOR ITS FUNCTIONALITY OR CONTENT. THESE DISCLAIMERS AND LIMITATIONS ARE MADE IN ADDITION TO THOSE MADE IN AND APPLICABLE TO VARIOUS PAGES OR SECTIONS OF THIS SITE. 6 | -------------------------------------------------------------------------------- /css/go.css: -------------------------------------------------------------------------------- 1 | a { text-decoration: none; } 2 | a:hover { 3 | color: red; 4 | text-decoration: none; 5 | opacity: 1.0 !important; 6 | } 7 | 8 | 9 | .link { 10 | overflow: hidden; 11 | } 12 | .cloudli { 13 | float: left; 14 | margin-left: .5em; 15 | margin-top: .25em; 16 | } 17 | 18 | .center { text-align: center; } 19 | .fineprint { 20 | font-size: 75%; 21 | } 22 | 23 | table.table { margin-bottom: 10px; } 24 | td { 25 | white-space: normal !important; 26 | } 27 | 28 | table.shallow td { padding: 4px; }*/ 29 | 30 | .link div.shallow { min-height: 0px } 31 | 32 | .linkedit td:first-child { 33 | font-weight: bold; 34 | } 35 | 36 | .column .inner { 37 | margin: .5em; 38 | } 39 | 40 | .linkedit td input[name="example"], .linkedit td input[name="otherlists"] { 41 | width: 12em; 42 | } 43 | 44 | .linkedit input[type="text"], .linkedit textarea { 45 | width: 85%; 46 | } 47 | 48 | .linkedit input[type="checkbox"] { 49 | margin-right: .5em; 50 | vertical-align: top; 51 | } 52 | 53 | .help tr.topic > td:first-child { 54 | text-align: center; 55 | width: 10%; 56 | } 57 | 58 | .help tr.topic > td { 59 | padding: 1em; 60 | 61 | } 62 | 63 | table td .red { 64 | background-color: red 65 | } 66 | -------------------------------------------------------------------------------- /go.cfg: -------------------------------------------------------------------------------- 1 | # constants to be used within go.py 2 | 3 | [goconfig] 4 | 5 | # The name of the serialized data file 6 | cfg_fnDatabase: godb.pickle 7 | 8 | # F5's favicon for use in client browsers 9 | cfg_urlFavicon: https://www.f5.com/favicon.ico 10 | 11 | # FQDN where go.py will run 12 | cfg_hostname: localhost 13 | 14 | # Port to use for the web service 15 | cfg_port: 8080 16 | 17 | # (optional) Auth URL for redirecting users when editing links 18 | cfg_urlSSO: None 19 | 20 | # (optional) Use SSL? If enabled this will make cfg_sslCertificate and cfg_sslPrivateKey required 21 | cfg_sslEnabled: false 22 | 23 | # The path to the SSL certificate 24 | cfg_sslCertificate: go.crt 25 | 26 | # The path to the SSL private key 27 | cfg_sslPrivateKey: go.key 28 | 29 | # The email address of a person to contact if there is a question/problem/etc. which will show up at the bottom of pages if set. 30 | cfg_contactEmail: None 31 | 32 | # The name of a person to contact if there is a question/problem/etc. which will show up at the bottom of pages if set. 33 | cfg_contactName: None 34 | 35 | # A URL to internal documentation written for your Go redirector instance which will show up at the bottom of pages if set 36 | cfg_customDocs: None 37 | -------------------------------------------------------------------------------- /go.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """This is the Go Redirector. It uses short mnemonics as redirects to otherwise 5 | long URLs. Few remember how to write in cursive, most people don't remember 6 | common phone numbers, and just about everyone needs a way around bookmarks. 7 | """ 8 | 9 | __author__ = "Saul Pwanson " 10 | __credits__ = "Bill Booth, Bryce Bockman, treebird, Sean Smith, layertwo" 11 | 12 | import base64 13 | import datetime 14 | import os 15 | import pickle 16 | import random 17 | import re 18 | import string 19 | import sys 20 | import time 21 | import urllib.request 22 | import urllib.error 23 | import urllib.parse 24 | import configparser 25 | import cherrypy 26 | import jinja2 27 | import shutil 28 | import html 29 | 30 | 31 | config = configparser.ConfigParser() 32 | config.read('go.cfg') 33 | 34 | cfg_fnDatabase = config.get('goconfig', 'cfg_fnDatabase') 35 | cfg_urlFavicon = config.get('goconfig', 'cfg_urlFavicon') 36 | cfg_hostname = config.get('goconfig', 'cfg_hostname') 37 | cfg_port = config.getint('goconfig', 'cfg_port') 38 | cfg_urlSSO = config.get('goconfig', 'cfg_urlSSO') 39 | cfg_urlEditBase = "https://" + cfg_hostname 40 | cfg_sslEnabled = False # default to False 41 | try: 42 | cfg_sslEnabled = config.getboolean('goconfig', 'cfg_sslEnabled') 43 | except: 44 | # just preventing from crashing if the cfg option doesn't exist since technically it's optional 45 | pass 46 | cfg_sslCertificate = config.get('goconfig', 'cfg_sslCertificate') 47 | cfg_sslPrivateKey = config.get('goconfig', 'cfg_sslPrivateKey') 48 | cfg_contactEmail = config.get('goconfig', 'cfg_contactEmail') 49 | cfg_contactName = config.get('goconfig', 'cfg_contactName') 50 | cfg_customDocs = config.get('goconfig', 'cfg_customDocs') 51 | 52 | class MyGlobals(object): 53 | def __init__(self): 54 | self.db_hnd = None 55 | 56 | def __repr__(self): 57 | return '%s(hnd=%s)' % (self.__class__.__name__, self.db_hnd) 58 | 59 | 60 | def set_handle(self, hnd): 61 | self.db_hnd = hnd 62 | 63 | MYGLOBALS = MyGlobals() 64 | 65 | 66 | class Error(Exception): 67 | """base error exception class for go, never raised""" 68 | pass 69 | 70 | 71 | class InvalidKeyword(Error): 72 | """Error raised when a keyword fails the sanity check""" 73 | pass 74 | 75 | 76 | def deampify(s): 77 | """Replace '&'' with '&'.""" 78 | return s.replace("&", "&") 79 | 80 | 81 | def escapeascii(s): 82 | return html.escape(s) 83 | 84 | 85 | def randomlink(): 86 | return random.choice([x for x in list(g_db.linksById.values()) if not x.isGenerative() and x.usage()]) 87 | 88 | 89 | def today(): 90 | return datetime.date.today().toordinal() 91 | 92 | 93 | def escapekeyword(kw): 94 | return urllib.parse.quote_plus(kw, safe="/") 95 | 96 | 97 | def prettyday(d): 98 | if d < 10: 99 | return 'never' 100 | 101 | s = today() - d 102 | if s < 1: 103 | return 'today' 104 | elif s < 2: 105 | return 'yesterday' 106 | elif s < 60: 107 | return '%d days ago' % s 108 | else: 109 | return '%d months ago' % (s / 30) 110 | 111 | 112 | def prettytime(t): 113 | if t < 100000: 114 | return 'never' 115 | 116 | dt = time.time() - t 117 | if dt < 24*3600: 118 | return 'today' 119 | elif dt < 2 * 24*3600: 120 | return 'yesterday' 121 | elif dt < 60 * 24*3600: 122 | return '%d days ago' % (dt / (24 * 3600)) 123 | else: 124 | return '%d months ago' % (dt / (30 * 24*3600)) 125 | 126 | 127 | def makeList(s): 128 | if isinstance(s, str): 129 | return [s] 130 | elif isinstance(s, list): 131 | return s 132 | else: 133 | return list(s) 134 | 135 | 136 | def canonicalUrl(url): 137 | if url: 138 | m = re.search(r'href="(.*)"', jinja2.utils.urlize(url)) 139 | if m: 140 | return m.group(1) 141 | 142 | return url 143 | 144 | 145 | def getDictFromCookie(cookiename): 146 | if cookiename not in cherrypy.request.cookie: 147 | return {} 148 | 149 | return dict(urllib.parse.parse_qsl(cherrypy.request.cookie[cookiename].value)) 150 | 151 | 152 | sanechars = string.ascii_lowercase + string.digits + "-." 153 | 154 | 155 | def sanitary(s): 156 | s = s.lower() 157 | for a in s[:-1]: 158 | if a not in sanechars: 159 | return None 160 | 161 | if s[-1] not in sanechars and s[-1] != "/": 162 | return None 163 | 164 | return s 165 | 166 | 167 | def byClicks(links): 168 | return sorted(links, key=lambda L: (-L.recentClicks, -L.totalClicks)) 169 | 170 | 171 | def getCurrentEditableUrl(): 172 | redurl = cfg_urlEditBase + cherrypy.request.path_info 173 | if cherrypy.request.query_string: 174 | redurl += "?" + cherrypy.request.query_string 175 | 176 | return redurl 177 | 178 | 179 | def getCurrentEditableUrlQuoted(): 180 | return urllib.parse.quote(getCurrentEditableUrl(), safe=":/") 181 | 182 | 183 | def getSSOUsername(redirect=True): 184 | """ 185 | If no SSO URL is specified then the 'testuser' is returned, otherwise returns an SSO username 186 | (or redirects to SSO to get it) 187 | :param redirect: 188 | :return: the SSO username 189 | """ 190 | if cfg_urlSSO is None or cfg_urlSSO == 'None': 191 | return 'testuser' 192 | 193 | if cherrypy.request.base != cfg_urlEditBase: 194 | if not redirect: 195 | return None 196 | if redirect is True: 197 | redirect = getCurrentEditableUrl() 198 | elif redirect is False: 199 | raise cherrypy.HTTPRedirect(redirect) 200 | 201 | if "issosession" not in cherrypy.request.cookie: 202 | if not redirect: 203 | return None 204 | if redirect is True: 205 | redirect = cherrypy.url(qs=cherrypy.request.query_string) 206 | 207 | raise cherrypy.HTTPRedirect(cfg_urlSSO + urllib.parse.quote(redirect, safe=":/")) 208 | 209 | sso = urllib.parse.unquote(cherrypy.request.cookie["issosession"].value) 210 | session = list(map(base64.b64decode, string.split(sso, "-"))) 211 | return session[0] 212 | 213 | 214 | class Clickable: 215 | def __init__(self): 216 | self.archivedClicks = 0 217 | self.clickData = {} 218 | 219 | def __repr__(self): 220 | return '%s(archivedClicks=%s, clickData=%s)' % (self.__class__.__name__, 221 | self.archivedClicks, 222 | self.clickData) 223 | 224 | def clickinfo(self): 225 | return "%s recent clicks (%s total); last visited %s" % (self.recentClicks, self.totalClicks, prettyday(self.lastClickDay)) 226 | 227 | def __getattr__(self, attrname): 228 | if attrname == "totalClicks": 229 | return self.archivedClicks + sum(self.clickData.values()) 230 | elif attrname == "recentClicks": 231 | return sum(self.clickData.values()) 232 | elif attrname == "lastClickTime": 233 | if not self.clickData: 234 | return 0 235 | maxk = max(self.clickData.keys()) 236 | return time.mktime(datetime.date.fromordinal(maxk).timetuple()) 237 | elif attrname == "lastClickDay": 238 | if not self.clickData: 239 | return 0 240 | return max(self.clickData.keys()) 241 | else: 242 | raise AttributeError(attrname) 243 | 244 | def clicked(self, n=1): 245 | """ 246 | :param n: The number of clicks to record 247 | :return: 248 | """ 249 | todayord = today() 250 | if todayord not in self.clickData: 251 | # partition clickdata around 30 days ago 252 | archival = [] 253 | recent = [] 254 | for od, nclicks in list(self.clickData.items()): 255 | if todayord - 30 > od: 256 | archival.append((od, nclicks)) 257 | else: 258 | recent.append((od, nclicks)) 259 | 260 | # archive older samples 261 | if archival: 262 | self.archivedClicks += sum(nclicks for od, nclicks in archival) 263 | 264 | # recent will have at least one sample if it was ever clicked 265 | recent.append((todayord, n)) 266 | self.clickData = dict(recent) 267 | else: 268 | self.clickData[todayord] += n 269 | 270 | def _export(self): 271 | return "%d,%s" % (self.archivedClicks, "".join(str(self.clickData).split())) 272 | 273 | def _import(self, s): 274 | archivedClicks, clickdict = s.split(",", 1) 275 | self.archivedClicks = int(archivedClicks) 276 | self.clickData = eval(clickdict) 277 | return self 278 | 279 | 280 | class Link(Clickable): 281 | def __init__(self, linkid=0, url="", title=""): 282 | Clickable.__init__(self) 283 | 284 | self.linkid = linkid 285 | self._url = canonicalUrl(url) 286 | self.title = title 287 | 288 | self.edits = [] # (edittime, editorname); [-1] is most recent 289 | self.lists = [] # List() instances 290 | 291 | def __repr__(self): 292 | return '%s(linkid=%s, url=%s, title=%s, edits=%s, lists=%s)' % (self.__class__.__name__, 293 | self.linkid, self._url, 294 | self.title, self.edits, 295 | self.lists) 296 | 297 | def isGenerative(self): 298 | return any([K.isGenerative() for K in self.lists]) 299 | 300 | def listnames(self): 301 | return [x.name for x in self.lists] 302 | 303 | def _export(self): 304 | a = "+".join(self._url.split()) 305 | b = "||".join([x.name for x in self.lists]) or "None" 306 | c = Clickable._export(self) 307 | d = ",".join(["%d/%s" % x for x in self.edits]) or "None" 308 | e = self.title 309 | 310 | return "link %s %s %s %s %s" % (a, b, c, d, e) 311 | 312 | def _dump(self): 313 | a = "|".join([x.name for x in self.lists]) or "None" 314 | b = self.title 315 | c = self._url 316 | 317 | return "%s\t%s\t%s" % (a, b, c) 318 | 319 | def _import(self, line): 320 | self._url, lists, clickdata, edits, title = line.split(" ", 4) 321 | print(">>", line) 322 | print(self._url) 323 | if self._url in g_db.linksByUrl: 324 | self._url = g_db.linksByUrl[self._url].linkid 325 | print("XYZ", self._url) 326 | 327 | if lists != "None": 328 | for listname in lists.split("||"): 329 | if "{*}" in self._url: 330 | if listname[-1] != "/": 331 | listname += "/" 332 | g_db.getList(listname, create=True).addLink(self) 333 | 334 | self.title = title.strip() 335 | 336 | Clickable._import(self, clickdata) 337 | 338 | if edits != "None": 339 | edits = [x.split("/") for x in edits.split(",")] 340 | self.edits = [(float(x[0]), x[1]) for x in edits] 341 | 342 | def editedBy(self, editor): 343 | self.edits.append((time.time(), editor)) 344 | 345 | def lastEdit(self): 346 | if not self.edits: 347 | return (0, "") 348 | 349 | return self.edits[-1] 350 | 351 | def href(self): 352 | if self.isGenerative(): 353 | kw = self.mainKeyword() 354 | if kw: 355 | return "/.%s" % escapekeyword(kw.name) 356 | else: 357 | return "" 358 | else: 359 | if self.linkid > 0: 360 | return "/_link_/%s" % self.linkid 361 | else: 362 | return self._url 363 | 364 | def url(self, keyword=None, args=None): 365 | remainingPath = (keyword or cherrypy.request.path_info).split("/")[2:] 366 | d = {"*": "/".join(remainingPath), "0": keyword} 367 | d.update(g_db.variables) 368 | d.update(getDictFromCookie("variables")) 369 | 370 | while True: 371 | try: 372 | return string.Formatter().vformat(self._url, args or remainingPath, d) 373 | except KeyError as e: 374 | missingKey = e.args[0] 375 | d[missingKey] = "{%s}" % missingKey 376 | except IndexError: 377 | return None 378 | 379 | def mainKeyword(self): 380 | goesStraightThere = [LL for LL in self.lists if LL.goesDirectlyTo(self)] 381 | 382 | if not goesStraightThere: 383 | return None 384 | 385 | return byClicks(goesStraightThere)[0] 386 | 387 | def usage(self): 388 | kw = self.mainKeyword() 389 | if kw is None: 390 | return "" 391 | return kw.usage() 392 | 393 | def opacity(self, todayord): 394 | """goes from 1.0 (today) to 0.2 (a month ago)""" 395 | dtDays = todayord - self.lastClickDay 396 | c = min(1.0, max(0.2, (30.0 - dtDays) / 30)) 397 | return "%.02f" % c 398 | 399 | 400 | class ListOfLinks(Link): 401 | # for convenience, inherits from Link. most things that apply 402 | # to Link applies to a ListOfLinks too 403 | def __init__(self, linkid=0, name="", redirect="freshest"): 404 | Link.__init__(self, linkid) 405 | self.name = name 406 | self._url = redirect # list | freshest | top | random 407 | self.links = [] 408 | 409 | def __repr__(self): 410 | return '%s(linkid=%s, name=%s, redirect=%s, links=%s)' % (self.__class__.__name__, 411 | self.linkid, self.name, 412 | self._url, self.links) 413 | 414 | 415 | def isGenerative(self): 416 | return self.name[-1] == "/" 417 | 418 | def usage(self): 419 | if self.isGenerative(): # any([ L.isGenerative() for L in self.links ]): 420 | return "%s..." % self.name 421 | 422 | return self.name 423 | 424 | def addLink(self, link): 425 | if link not in self.links: 426 | self.links.insert(0, link) 427 | link.lists.append(self) 428 | 429 | def removeLink(self, link): 430 | if link in self.links: 431 | self.links.remove(link) 432 | if self in link.lists: 433 | link.lists.remove(self) 434 | 435 | def getRecentLinks(self): 436 | return self.links 437 | 438 | def getPopularLinks(self): 439 | return byClicks(self.links) 440 | 441 | def getLinks(self, nDaysOfRecentEdits=1): 442 | earliestRecentEdit = time.time() - nDaysOfRecentEdits * 24 * 3600 443 | 444 | recent = [x for x in self.links if x.lastEdit()[0] > earliestRecentEdit] 445 | popular = self.getPopularLinks() 446 | 447 | for L in recent: 448 | popular.remove(L) 449 | 450 | return recent, popular 451 | 452 | def getDefaultLink(self): 453 | if not self._url or self._url == "list": 454 | return None 455 | elif self._url == "top": 456 | return self.getPopularLinks()[0] 457 | elif self._url == "random": 458 | return random.choice(self.links) 459 | elif self._url == "freshest": 460 | return self.getRecentLinks()[0] 461 | else: 462 | return g_db.getLink(self._url) 463 | 464 | def url(self, keyword=None, args=None): 465 | if not self._url or self._url == "list": 466 | return None 467 | elif self._url == "top": 468 | return self.getPopularLinks()[0].url(keyword, args) 469 | elif self._url == "random": 470 | return random.choice(self.links).url(keyword, args) 471 | elif self._url == "freshest": 472 | return self.getRecentLinks()[0].url(keyword, args) 473 | else: # should be a linkid 474 | return "/_link_/" + self._url 475 | 476 | def goesDirectlyTo(self, link): 477 | return self._url == str(link.linkid) or self.url() == link.url() 478 | 479 | def _export(self): 480 | if isinstance(self._url, int): # linkid needs to be converted for export 481 | L = g_db.getLink(self._url) 482 | if L and L in self.links: 483 | print(L) 484 | self._url = L._url 485 | else: 486 | print("fixing unknown dest linkid for", self.name) 487 | self._url = "list" 488 | 489 | return ("list %s " % self.name) + Link._export(self) 490 | 491 | def _import(self, line): 492 | self.name, _, rest = line.split(" ", 2) 493 | assert _ == "link" 494 | g_db._addList(self) 495 | Link._import(self, rest) 496 | 497 | 498 | class RegexList(ListOfLinks): 499 | def __init__(self, linkid=0, regex=""): 500 | ListOfLinks.__init__(self, linkid, regex) 501 | 502 | self.regex = regex 503 | 504 | def __repr__(self): 505 | return '%s(linkid=%s, regex=%s)' % (self.__class__.__name__, 506 | self.linkid, self.regex) 507 | 508 | def usage(self): 509 | return self.regex 510 | 511 | def isGenerative(self): 512 | return True 513 | 514 | def matches(self, kw=None): 515 | if kw is None: 516 | kw = cherrypy.request.path_info.split("/")[1] 517 | 518 | ret = [] 519 | 520 | m = re.match(self.regex, kw, re.IGNORECASE) 521 | if m: 522 | deflink = self.getDefaultLink() 523 | for L in deflink and [deflink] or self.links: 524 | url = L.url(keyword=kw, args=(m.group(0), ) + m.groups()) 525 | ret.append((L, Link(0, url, L.title))) 526 | 527 | return ret 528 | 529 | def url(self, kw=None): 530 | 531 | if kw is None: 532 | kw = cherrypy.request.path_info.split("/")[1] 533 | 534 | m = re.match(self.regex, kw, re.IGNORECASE) 535 | if not m: 536 | return None 537 | 538 | return ListOfLinks.url(self, keyword=kw, args=(m.group(0), ) + m.groups()) 539 | 540 | def _export(self): 541 | return ("regex %s " % self.regex) + ListOfLinks._export(self) 542 | 543 | def _import(self, line): 544 | self.regex, _, rest = line.split(" ", 2) 545 | assert _ == "list" 546 | ListOfLinks._import(self, rest) 547 | 548 | 549 | class LinkDatabase: 550 | def __init__(self): 551 | self.regexes = {} # regex -> RegexList 552 | self.lists = {} # listname -> ListOfLinks 553 | self.variables = {} # varname -> value 554 | self.linksById = {} # link.linkid -> Link 555 | self.linksByUrl = {} # link._url -> Link 556 | self._nextlinkid = 1 557 | 558 | def __repr__(self): 559 | return '%s(regexes=%s, lists=%s, vars=%s, byId=%s, byUrl=%s)' % (self.__class__.__name__, 560 | self.regexes, self.lists, 561 | self.variables, 562 | self.linksById, 563 | self.linksByUrl) 564 | 565 | 566 | @staticmethod 567 | def load(db=cfg_fnDatabase): 568 | """Attempt to load the database defined at cfg_fnDatabase. Create a 569 | new one if the database doesn't already exist. 570 | """ 571 | try: 572 | print("Loading DB from %s" % db) 573 | return pickle.load(open(db, 'rb')) 574 | except IOError: 575 | print(sys.exc_info()[1]) 576 | print("Creating new database...") 577 | return LinkDatabase() 578 | 579 | def save(self): 580 | #TODO: Make this get saved to a database, this is a temporary solution to prevent corruption 581 | tmpfile = cfg_fnDatabase + '.tmp' 582 | pickle.dump(self, open(tmpfile, "wb")) 583 | shutil.copyfile(tmpfile, cfg_fnDatabase) 584 | os.remove(tmpfile) 585 | 586 | def nextlinkid(self): 587 | r = self._nextlinkid 588 | self._nextlinkid += 1 589 | return r 590 | 591 | def addRegexList(self, regex=None, url=None, desc=None, owner=""): 592 | r = RegexList(self.nextlinkid(), regex) 593 | r._url = url 594 | self._addRegexList(r, owner) 595 | 596 | def _addRegexList(self, r, owner): 597 | self.regexes[r.regex] = r 598 | self._addList(r) # add to all indexes 599 | 600 | def addLink(self, lists, url, title, owner=""): 601 | if url in self.linksByUrl: 602 | raise RuntimeError("existing url") 603 | 604 | if type(lists) == str: 605 | lists = lists.split() 606 | 607 | link = Link(self.nextlinkid(), url, title) 608 | 609 | for kw in lists: 610 | self.getList(kw, create=True).addLink(link) 611 | 612 | self._addLink(link, owner) 613 | 614 | return link 615 | 616 | def _addLink(self, link, editor=None): 617 | if editor: 618 | link.editedBy(editor) 619 | 620 | self.linksById[link.linkid] = link 621 | self.linksByUrl[link._url] = link 622 | 623 | def _changeLinkUrl(self, link, newurl): 624 | if link._url in self.linksByUrl: 625 | del self.linksByUrl[link._url] 626 | link._url = newurl 627 | self.linksByUrl[newurl] = link 628 | 629 | def _addList(self, LL): 630 | self.lists[LL.name] = LL 631 | 632 | def deleteLink(self, link): 633 | for LL in list(link.lists): 634 | LL.removeLink(link) 635 | if not LL.links: # auto-delete lists with no links 636 | self.deleteList(LL) 637 | 638 | self._removeLinkFromUrls(link._url) 639 | 640 | if link.linkid in self.linksById: 641 | del self.linksById[link.linkid] 642 | 643 | if isinstance(link, RegexList): 644 | del self.regexes[link.regex] 645 | 646 | return "deleted go/%s" % link.linkid 647 | 648 | def _removeLinkFromUrls(self, url): 649 | if url in self.linksByUrl: 650 | del self.linksByUrl[url] 651 | 652 | def deleteList(self, LL): 653 | for link in list(LL.links): 654 | LL.removeLink(link) 655 | 656 | del self.lists[LL.name] 657 | self.deleteLink(LL) 658 | return "deleted go/%s" % LL.name 659 | 660 | def getLink(self, linkid): 661 | return self.linksById.get(int(linkid), None) 662 | 663 | def getAllLists(self): 664 | return byClicks(list(self.lists.values())) 665 | 666 | def getSpecialLinks(self): 667 | links = set() 668 | for R in list(g_db.regexes.values()): 669 | links.update(R.links) 670 | 671 | links.update(self.getFolders()) 672 | 673 | return list(links) 674 | 675 | def getFolders(self): 676 | return [x for x in list(self.linksById.values()) if x.isGenerative()] 677 | 678 | def getNonFolders(self): 679 | return [x for x in list(self.linksById.values()) if not x.isGenerative()] 680 | 681 | def getList(self, listname, create=False): 682 | if "\\" in listname: # is a regex 683 | return self.getRegex(listname, create) 684 | 685 | sanelistname = sanitary(listname) 686 | 687 | if not sanelistname: 688 | raise InvalidKeyword("keyword '%s' not sanitary" % listname) 689 | 690 | if sanelistname not in self.lists: 691 | if not create: 692 | return None 693 | self._addList(ListOfLinks(self.nextlinkid(), sanelistname, redirect="freshest")) 694 | 695 | return self.lists[sanelistname] 696 | 697 | def getRegex(self, listname, create=False): 698 | try: 699 | re.compile(listname) 700 | except: 701 | raise InvalidKeyword(listname) 702 | 703 | if listname not in self.regexes: 704 | if not create: 705 | return None 706 | self._addRegexList(RegexList(self.nextlinkid(), listname), "") 707 | 708 | return self.regexes[listname] 709 | 710 | def renameList(self, LL, newname): 711 | assert newname not in self.lists 712 | oldname = LL.name 713 | self.lists[newname] = self.lists[oldname] 714 | del self.lists[oldname] 715 | LL.name = newname 716 | return "renamed go/%s to go/%s" % (oldname, LL.name) 717 | 718 | def _export(self, fn): 719 | print("exporting to %s" % fn) 720 | with open(fn, "w") as f: 721 | for k, v in list(self.variables.items()): 722 | f.write("variable %s %s\n" % (k, v)) 723 | 724 | for L in list(self.linksById.values()): 725 | f.write(L._export() + "\n") 726 | 727 | for LL in list(self.lists.values()): 728 | f.write(LL._export() + "\n") 729 | 730 | # for the tsv dumper 731 | def _dump(self, fh): 732 | for link in list(self.linksById.values()): 733 | fh.write(link._dump() + "\n") 734 | 735 | def _import(self, fn): 736 | print("importing from %s" % fn) 737 | with open(fn, "r") as f: 738 | for l in f.readlines(): 739 | if not l.strip(): continue 740 | print(l.strip()) 741 | a, b = string.split(l, " ", 1) 742 | if a == "regex": 743 | R = RegexList(self.nextlinkid()) 744 | R._import(b) 745 | elif a == "link": 746 | L = Link(self.nextlinkid()) 747 | L._import(b) 748 | self._addLink(L) 749 | elif a == "list": 750 | listname, rest = string.split(b, " ", 1) 751 | if listname in self.lists: 752 | LL = self.lists[listname] 753 | else: 754 | LL = ListOfLinks(self.nextlinkid()) 755 | LL._import(b) 756 | elif a == "variable": 757 | k, v = b.split(" ", 1) 758 | self.variables[k] = v.strip() 759 | 760 | assert self._nextlinkid == max(self.linksById.keys()) + 1 761 | 762 | self.save() 763 | 764 | 765 | class Root: 766 | def redirect(self, url, status=307): 767 | cherrypy.response.status = status 768 | cherrypy.response.headers["Location"] = url 769 | 770 | def undirect(self): 771 | raise cherrypy.HTTPRedirect(cherrypy.request.headers.get("Referer", "/")) 772 | 773 | def notfound(self, msg): 774 | return env.get_template("notfound.html").render(message=msg) 775 | 776 | def redirectIfNotFullHostname(self, scheme=None): 777 | if scheme is None: 778 | scheme = cherrypy.request.scheme 779 | 780 | # redirect to our full hostname to get the user's cookies 781 | if cherrypy.request.scheme != scheme or cherrypy.request.base.find(cfg_hostname) < 0: 782 | fqurl = scheme + "://" + cfg_hostname 783 | fqurl += cherrypy.request.path_info 784 | if cherrypy.request.query_string: 785 | fqurl += "?" + cherrypy.request.query_string 786 | raise cherrypy.HTTPRedirect(fqurl) 787 | 788 | def redirectToEditLink(self, **kwargs): 789 | if "linkid" in kwargs: 790 | url = "/_edit_/%s" % kwargs["linkid"] 791 | del kwargs["linkid"] 792 | else: 793 | url = "/_add_" 794 | 795 | return self.redirect(url + "?" + urllib.parse.urlencode(kwargs)) 796 | 797 | def redirectToEditList(self, listname, **kwargs): 798 | baseurl = "/_editlist_/%s?" % escapekeyword(listname) 799 | return self.redirect(baseurl + urllib.parse.urlencode(kwargs)) 800 | 801 | @cherrypy.expose 802 | def robots_txt(self): 803 | # Specifically for the internal GSA 804 | return open("robots.txt").read() 805 | 806 | @cherrypy.expose 807 | def favicon_ico(self): 808 | cherrypy.response.headers["Cache-control"] = "max-age=172800" 809 | return self.redirect(cfg_urlFavicon, status=301) 810 | 811 | @cherrypy.expose 812 | def lucky(self): 813 | luckylink = random.choice(g_db.getNonFolders()) 814 | luckylink.clicked() 815 | return self.redirect(deampify(luckylink.url())) 816 | 817 | @cherrypy.expose 818 | def index(self, **kwargs): 819 | self.redirectIfNotFullHostname() 820 | 821 | if "keyword" in kwargs: 822 | return self.redirect("/" + kwargs["keyword"]) 823 | 824 | return env.get_template('index.html').render(now=today()) 825 | 826 | @cherrypy.expose 827 | def default(self, *rest, **kwargs): 828 | self.redirectIfNotFullHostname() 829 | 830 | keyword = rest[0] 831 | rest = rest[1:] 832 | 833 | forceListDisplay = False 834 | 835 | if keyword[0] == ".": # force list page instead of redirect 836 | if keyword == ".me": 837 | username = getSSOUsername() 838 | self.redirect("." + username) 839 | forceListDisplay = True 840 | keyword = keyword[1:] 841 | 842 | if rest: 843 | keyword += "/" 844 | elif forceListDisplay and cherrypy.request.path_info[-1] == "/": 845 | # allow go/keyword/ to redirect to go/keyword but go/.keyword/ 846 | # to go to the keyword/ index 847 | keyword += "/" 848 | 849 | # try it as a list 850 | try: 851 | ll = g_db.getList(keyword, create=False) 852 | except InvalidKeyword as e: 853 | return self.notfound(str(e)) 854 | 855 | if not ll: # nonexistent list 856 | # check against all special cases 857 | matches = [] 858 | for R in list(g_db.regexes.values()): 859 | matches.extend([(R, L, genL) for L, genL in R.matches(keyword)]) 860 | 861 | if not matches: 862 | kw = sanitary(keyword) 863 | if not kw: 864 | return self.notfound("No match found for '%s'" % keyword) 865 | 866 | # serve up empty fake list 867 | return env.get_template('list.html').render(L=ListOfLinks(0), keyword=kw) 868 | elif len(matches) == 1: 869 | R, L, genL = matches[0] # actual regex, generated link 870 | R.clicked() 871 | L.clicked() 872 | return self.redirect(deampify(genL.url())) 873 | else: # len(matches) > 1 874 | LL = ListOfLinks(-1) # -1 means non-editable 875 | LL.links = [genL for R, L, genL in matches] 876 | return env.get_template('list.html').render(L=LL, keyword=keyword) 877 | 878 | listtarget = ll.getDefaultLink() 879 | 880 | if listtarget and not forceListDisplay: 881 | ll.clicked() 882 | listtarget.clicked() 883 | return self.redirect(deampify(listtarget.url())) 884 | 885 | tmplList = env.get_template('list.html') 886 | return tmplList.render(L=ll, keyword=keyword) 887 | 888 | @cherrypy.expose 889 | def special(self): 890 | LL = ListOfLinks(-1) 891 | LL.name = "Smart Keywords" 892 | LL.links = g_db.getSpecialLinks() 893 | 894 | env.globals['g_db'] = g_db 895 | return env.get_template('list.html').render(L=LL, keyword="special") 896 | 897 | @cherrypy.expose 898 | def _login_(self, redirect=""): 899 | if redirect: 900 | return self.redirect(redirect) 901 | return self.undirect() 902 | 903 | @cherrypy.expose 904 | def me(self): 905 | username = getSSOUsername() 906 | return self.redirect(username) 907 | 908 | @cherrypy.expose 909 | def _link_(self, linkid): 910 | link = g_db.getLink(linkid) 911 | if link: 912 | link.clicked() 913 | return self.redirect(link.url(), status=301) 914 | 915 | cherrypy.response.status = 404 916 | return self.notfound("Link %s does not exist" % linkid) 917 | 918 | @cherrypy.expose 919 | def _add_(self, *args, **kwargs): 920 | # _add_/tag1/tag2/tag3 921 | link = Link() 922 | link.lists = [g_db.getList(listname, create=False) or ListOfLinks(0, listname) for listname in args] 923 | return env.get_template("editlink.html").render(L=link, returnto=(args and args[0] or None), **kwargs) 924 | 925 | @cherrypy.expose 926 | def _edit_(self, linkid, **kwargs): 927 | link = g_db.getLink(linkid) 928 | if link: 929 | return env.get_template("editlink.html").render(L=link, **kwargs) 930 | 931 | # edit new link 932 | return env.get_template("editlink.html").render(L=Link(), **kwargs) 933 | 934 | @cherrypy.expose 935 | def _editlist_(self, keyword, **kwargs): 936 | K = g_db.getList(keyword, create=False) 937 | if not K: 938 | K = ListOfLinks() 939 | return env.get_template("list.html").render(L=K, keyword=keyword) 940 | 941 | @cherrypy.expose 942 | def _setbehavior_(self, keyword, **kwargs): 943 | K = g_db.getList(keyword, create=False) 944 | 945 | if "behavior" in kwargs: 946 | K._url = kwargs["behavior"] 947 | 948 | return self.redirectToEditList(keyword) 949 | 950 | @cherrypy.expose 951 | def _delete_(self, linkid, returnto=""): 952 | 953 | g_db.deleteLink(g_db.getLink(linkid)) 954 | 955 | return self.redirect("/." + returnto) 956 | 957 | @cherrypy.expose 958 | @cherrypy.tools.allow(methods=['POST']) 959 | def _modify_(self, **kwargs): 960 | username = getSSOUsername() 961 | 962 | linkid = kwargs.get("linkid", "") 963 | title = escapeascii(kwargs.get("title", "")) 964 | lists = kwargs.get("lists", []) 965 | url = kwargs.get("url", "") 966 | otherlists = kwargs.get("otherlists", "") 967 | 968 | returnto = kwargs.get("returnto", "") 969 | 970 | # remove any whitespace/newlines in url 971 | url = "".join(url.split()) 972 | 973 | if type(lists) not in [tuple, list]: 974 | lists = [lists] 975 | 976 | lists.extend(otherlists.split()) 977 | 978 | if linkid: 979 | link = g_db.getLink(linkid) 980 | if link._url != url: 981 | g_db._changeLinkUrl(link, url) 982 | link.title = title 983 | 984 | newlistset = [] 985 | for listname in lists: 986 | if "{*}" in url: 987 | if listname[-1] != "/": 988 | listname += "/" 989 | try: 990 | newlistset.append(g_db.getList(listname, create=True)) 991 | except: 992 | return self.redirectToEditLink(error="invalid keyword '%s'" % listname, **kwargs) 993 | 994 | for LL in newlistset: 995 | if LL not in link.lists: 996 | LL.addLink(link) 997 | 998 | for LL in [x for x in link.lists]: 999 | if LL not in newlistset: 1000 | LL.removeLink(link) 1001 | if not LL.links: 1002 | g_db.deleteList(LL) 1003 | 1004 | link.lists = newlistset 1005 | 1006 | link.editedBy(username) 1007 | 1008 | g_db.save() 1009 | 1010 | return self.redirect("/." + returnto) 1011 | 1012 | if not lists: 1013 | return self.redirectToEditLink(error="delete links that have no lists", **kwargs) 1014 | 1015 | if not url: 1016 | return self.redirectToEditLink(error="URL required", **kwargs) 1017 | 1018 | # if url already exists, redirect to that link's edit page 1019 | if url in g_db.linksByUrl: 1020 | link = g_db.linksByUrl[url] 1021 | 1022 | # only modify lists; other fields will only be set if there 1023 | # is no original 1024 | 1025 | combinedlists = set([x.name for x in link.lists]) | set(lists) 1026 | 1027 | fields = {'title': link.title or title, 1028 | 'lists': " ".join(combinedlists), 1029 | 'linkid': str(link.linkid) 1030 | } 1031 | 1032 | return self.redirectToEditLink(error="found identical existing URL; confirm changes and re-submit", **fields) 1033 | 1034 | link = g_db.addLink(lists, url, title, username) 1035 | 1036 | g_db.save() 1037 | return self.redirect("/." + returnto) 1038 | 1039 | @cherrypy.expose 1040 | def _internal_(self, *args, **kwargs): 1041 | # check, toplinks, special, dumplist 1042 | return env.get_template(args[0] + ".html").render(**kwargs) 1043 | 1044 | @cherrypy.expose 1045 | def toplinks(self, n="100"): 1046 | return env.get_template("toplinks.html").render(n=int(n)) 1047 | 1048 | @cherrypy.expose 1049 | def variables(self): 1050 | return env.get_template("variables.html").render() 1051 | 1052 | @cherrypy.expose 1053 | def help(self): 1054 | return env.get_template("help.html").render() 1055 | 1056 | @cherrypy.expose 1057 | def _override_vars_(self, **kwargs): 1058 | cherrypy.response.cookie["variables"] = urllib.parse.urlencode(kwargs) 1059 | cherrypy.response.cookie["variables"]["max-age"] = 10 * 365 * 24 * 3600 1060 | 1061 | return self.redirect("variables") 1062 | 1063 | @cherrypy.expose 1064 | def _set_variable_(self, varname="", value=""): 1065 | if varname and value: 1066 | g_db.variables[varname] = value 1067 | g_db.save() 1068 | 1069 | return self.redirect("/variables") 1070 | 1071 | 1072 | env = jinja2.Environment(loader=jinja2.FileSystemLoader("./html")) 1073 | 1074 | 1075 | def main(): 1076 | cherrypy.config.update({'server.socket_host': '::', 1077 | 'server.socket_port': cfg_port, 1078 | 'request.query_string_encoding': "latin1", 1079 | }) 1080 | 1081 | cherrypy.https = s = cherrypy._cpserver.Server() 1082 | if cfg_sslEnabled: 1083 | s.socket_host = '::' 1084 | s.socket_port = 443 1085 | s.ssl_certificate = cfg_sslCertificate 1086 | s.ssl_private_key = cfg_sslPrivateKey 1087 | s.subscribe() 1088 | 1089 | # checkpoint the database every 60 seconds 1090 | cherrypy.process.plugins.BackgroundTask(60, lambda: g_db.save()).start() 1091 | 1092 | file_path = os.getcwd().replace("\\", "/") 1093 | conf = {'/images': {"tools.staticdir.on": True, "tools.staticdir.dir": file_path + "/images"}, 1094 | '/css': {"tools.staticdir.on": True, "tools.staticdir.dir": file_path + "/css"}, 1095 | '/js': {"tools.staticdir.on": True, "tools.staticdir.dir": file_path + "/js"}} 1096 | print("Cherrypy conf: %s" % conf) 1097 | cherrypy.quickstart(Root(), "/", config=conf) 1098 | 1099 | 1100 | if __name__ == "__main__": 1101 | 1102 | g_db = LinkDatabase.load() 1103 | 1104 | if "import" in sys.argv: 1105 | g_db._import("newterms.txt") 1106 | 1107 | elif "export" in sys.argv: 1108 | g_db._export("newterms.txt") 1109 | 1110 | elif "dump" in sys.argv: 1111 | g_db._dump(sys.stdout) 1112 | 1113 | else: 1114 | env = jinja2.Environment(loader=jinja2.FileSystemLoader("./html")) 1115 | env.filters['time_t'] = prettytime 1116 | env.filters['int'] = int 1117 | env.filters['escapekeyword'] = escapekeyword 1118 | 1119 | env.globals["enumerate"] = enumerate 1120 | env.globals["sample"] = random.sample 1121 | env.globals["len"] = len 1122 | env.globals["min"] = min 1123 | env.globals["str"] = str 1124 | env.globals["list"] = makeList 1125 | env.globals.update(globals()) 1126 | main() 1127 | -------------------------------------------------------------------------------- /go_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import datetime 5 | import unittest 6 | import time 7 | 8 | import go 9 | 10 | 11 | class GeneralTestCases(unittest.TestCase): 12 | def test_deampify_url(self): 13 | """ 14 | Verify that ampersands are turned from '&' to '&' 15 | :return: 16 | """ 17 | input_string = 'https://www.example.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8' 18 | expected = 'https://www.example.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8' 19 | self.assertEqual(expected, go.deampify(input_string)) 20 | 21 | def test_prettyday_should_return_never(self): 22 | """ 23 | Verify cases where the prettyday function should return the string 'never' 24 | :return: 25 | """ 26 | self.assertEqual('never', go.prettyday(0)) 27 | self.assertEqual('never', go.prettyday(-1)) 28 | 29 | def test_prettyday_should_return_today(self): 30 | """ 31 | Verify case where the prettyday function should return the string 'today' 32 | :return: 33 | """ 34 | _today = go.today() 35 | self.assertEqual('today', go.prettyday(_today)) 36 | 37 | def test_prettyday_should_return_yesterday(self): 38 | """ 39 | Verify case where the prettyday function should return the string 'yesterday' 40 | :return: 41 | """ 42 | yesterday = go.today() - 1 43 | self.assertEqual('yesterday', go.prettyday(yesterday)) 44 | 45 | def test_prettyday_should_return_num_of_days(self): 46 | """ 47 | Verify cases where the prettyday function should return the string for the number of days 48 | :return: 49 | """ 50 | today = go.today() 51 | month_ago = today - 30 52 | self.assertEqual('30 days ago', go.prettyday(month_ago)) 53 | 54 | def test_prettyday_should_return_num_of_months(self): 55 | """ 56 | Verify cases where the prettyday function should return the string for the number of months 57 | :return: 58 | """ 59 | today = go.today() 60 | months_ago = today - 95 61 | self.assertEqual('3 months ago', go.prettyday(months_ago)) 62 | 63 | def test_prettytime_should_return_never(self): 64 | """ 65 | Verify cases where the prettytime function should return the string 'never' 66 | :return: 67 | """ 68 | self.assertEqual('never', go.prettytime(420)) 69 | self.assertEqual('never', go.prettytime(-1)) 70 | 71 | def test_prettytime_should_return_today(self): 72 | """ 73 | Verify case where the prettytime function should return the string 'today' 74 | :return: 75 | """ 76 | timestamp = time.time() 77 | self.assertEqual('today', go.prettytime(timestamp)) 78 | 79 | def test_prettytime_should_return_yesterday(self): 80 | """ 81 | Verify case where the prettytime function should return the string 'yesterday' 82 | :return: 83 | """ 84 | timestamp = time.time() - (24 * 3600) 85 | self.assertEqual('yesterday', go.prettytime(timestamp)) 86 | 87 | def test_prettytime_should_return_num_of_days(self): 88 | """ 89 | Verify case where the prettytime function should return the string number of days 90 | :return: 91 | """ 92 | timestamp = time.time() - (6 * 24 * 3600) 93 | self.assertEqual('6 days ago', go.prettytime(timestamp)) 94 | 95 | def test_prettytime_should_return_num_of_months(self): 96 | """ 97 | Verify case where the prettytime function should return the string number of months 98 | :return: 99 | """ 100 | timestamp = time.time() - (95 * 24 * 3600) 101 | self.assertEqual('3 months ago', go.prettytime(timestamp)) 102 | 103 | def test_makeList_should_return_list(self): 104 | """ 105 | Verify that the makeList function returns a list of the items passed in using various data structures 106 | :return: 107 | """ 108 | _list = [1, 2, 3] 109 | # Leave as an explicit set call to support python 2.6 110 | _num_set = set([42, 35]) 111 | _string = 'foo' 112 | 113 | self.assertTrue(isinstance(go.makeList(_list), list)) 114 | self.assertTrue(isinstance(go.makeList(_string), list)) 115 | self.assertTrue(isinstance(go.makeList(_num_set), list)) 116 | 117 | self.assertEqual(go.makeList(_list), _list) 118 | self.assertNotEqual(go.makeList(_string), _string) 119 | self.assertNotEqual(go.makeList(_num_set), _num_set) 120 | 121 | def test_escapekeyword_should_replace_singlequote(self): 122 | """ 123 | %27 (as seen in expected) is the character used in web 124 | applications for a single quote 125 | """ 126 | keyword = "\'test\'" 127 | expected = "%27test%27" 128 | self.assertEqual(expected, go.escapekeyword(keyword)) 129 | 130 | def test_escapeascii(self): 131 | """ 132 | convert &, <, and > to html safe sequences 133 | see https://docs.python.org/3/library/html.html 134 | """ 135 | keyword = "&<>foobar" 136 | expected = "&<>foobar" 137 | self.assertEqual(expected, go.escapeascii(keyword)) 138 | 139 | 140 | def test_canonicalUrl_should_return_none(self): 141 | """ 142 | When None is passed in None should be returned 143 | :return: 144 | """ 145 | self.assertEqual(None, go.canonicalUrl(None)) 146 | 147 | def test_canonicalUrl_should_return_correct_url(self): 148 | url = "https://www.google.com" 149 | self.assertEqual(url, go.canonicalUrl(url)) 150 | 151 | # Validates that jinja2.utils.urlize works the way we expect 152 | url = "(https://www.google.com....." 153 | self.assertEqual("https://www.google.com", go.canonicalUrl(url)) 154 | 155 | def test_sanitary_should_return_None(self): 156 | """ 157 | Verify that underscores are marked as unsanitary charachters 158 | :return: 159 | """ 160 | # While in theory underscores are fine, we block them as unsanitary 161 | url = "this_is_an_invalid_name" 162 | self.assertEqual(None, go.sanitary(url)) 163 | 164 | # There's a branch that specifically checks the last character is valid or / 165 | url = "this-is-an-invalid-name_" 166 | self.assertEqual(None, go.sanitary(url)) 167 | 168 | def test_sanitary_should_return_url(self): 169 | """ 170 | Verify that dashes and / are considered sanitary characters 171 | :return: 172 | """ 173 | url = "this-is-a-valid-name" 174 | self.assertEqual(url, go.sanitary(url)) 175 | 176 | url = "this-is-a-valid-name/" 177 | self.assertEqual(url, go.sanitary(url)) 178 | 179 | def test_getSSOUsername_should_return_testuser(self): 180 | # TODO: Will have to be changed if/when SSO is made generic 181 | self.assertEqual("testuser", go.getSSOUsername()) 182 | 183 | 184 | class LinkTestCases(unittest.TestCase): 185 | def test_create_link(self): 186 | link = go.Link(url='www.example.com', title='example site') 187 | self.assertEqual(0, link.linkid) 188 | self.assertEqual('example site', link.title) 189 | 190 | def test_edit_link(self): 191 | """ 192 | Validate the last edit user starts off blank and then adds a users when edited 193 | :return: 194 | """ 195 | link = go.Link(url='example.com', title='example site') 196 | (last_edit_time, last_edit_name) = link.lastEdit() 197 | self.assertEqual(0, last_edit_time) 198 | self.assertEqual('', last_edit_name) 199 | 200 | link.editedBy('testuser') 201 | (_, last_edit_name) = link.lastEdit() 202 | self.assertEqual('testuser', last_edit_name) 203 | 204 | def test_opacity_never_clicked(self): 205 | """ 206 | By default the opacity is 0.2 207 | :return: 208 | """ 209 | link = go.Link(url='example.com', title='example site') 210 | today = datetime.date.today() 211 | date = datetime.date.toordinal(today) 212 | self.assertEqual('0.20', link.opacity(date)) 213 | 214 | def test_opacity_clicked_today(self): 215 | """ 216 | By default the opacity is 0.2, by "clicking" today it's set to 1.0 217 | :return: 218 | """ 219 | link = go.Link(url='example.com', title='example site') 220 | today = datetime.date.today() 221 | date = datetime.date.toordinal(today) 222 | link.clicked() 223 | self.assertEqual('1.00', link.opacity(date)) 224 | 225 | def test_usage_not_exists(self): 226 | link = go.Link(url='example.com', title='example site') 227 | self.assertEqual('', link.usage()) 228 | 229 | if __name__ == '__main__': 230 | unittest.main() 231 | -------------------------------------------------------------------------------- /html/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}go/{% block titlekeyword %}{% endblock titlekeyword %}{% endblock title %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 |

go/{% block keyword %}{% endblock %}

16 |
17 | 18 |
19 | go/help 20 |
21 |
22 | 23 | {% if username %} 24 | logged in as {{ username }} 25 | {% else %} 26 | login 27 | {% endif %} 28 |
29 |
30 | 31 |
32 | {% block body %} 33 | 34 | {% endblock body %} 35 | 36 |
37 | {# If cfg_contactEmail exists and is not "None" then display it #} 38 | {% if cfg_contactEmail is defined and cfg_contactEmail != None and cfg_contactEmail != "None" %} 39 |

Problems, questions, suggestions? email {{ cfg_contactName }}.

40 | {% endif %} 41 | 42 | {# If cfg_customDocs exists and is not "None" then display it #} 43 | {% if cfg_customDocs is defined and cfg_customDocs != None and cfg_customDocs != "None" %} 44 |
45 | GO redirector information page 46 |
47 | {% endif %} 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /html/check.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set username = getSSOUsername() %} 4 | 5 | {% block keyword %}_check_{% endblock keyword %} 6 | 7 | {% block body %} 8 | 9 | {% for _, LL in g_db.lists.items() %} 10 | {% if not LL.regex and len(LL.links) == 0 %} 11 |
  • 12 | {{ LL.name }} has 0 links 13 | {% if vacwm %} 14 | {{ g_db.deleteList(LL) }} 15 | {% else %} 16 | and will be deleted 17 | {% endif %} 18 |
  • 19 | {% endif %} 20 | 21 | {% if isinstance(LL.name, int) %} 22 |
  • 23 | {{ LL.name }} has integer name, 24 | {% if vacwm %} 25 | {{ g_db.renameList(LL, "n" + LL.name) }} 26 | (renamed to n{{ LL.name }}) 27 | {% else %} 28 | will be renamed to n{{ LL.name }} 29 | {% endif %} 30 |
  • 31 | {% endif %} 32 | 33 | {% for L in LL.links %} 34 | {% if L._url not in g_db.linksByUrl %} 35 |
  • 36 | lost link #{{ L.linkid }}: {{ L._url }} not in linksByUrl 37 |
  • 38 | {% if vacwm %} 39 | {{ g_db._addLink(L) }} 40 | (added to linksByUrl) 41 | {% else %} 42 | (will be added to linksByUrl) 43 | {% endif %} 44 | {% endif %} 45 | 46 | {% if L.linkid not in g_db.linksById %} 47 |
  • 48 | lost link #{{ L.linkid }}: not in linksById 49 | {% if vacwm %} 50 | {{ g_db._addLink(L) }} 51 | (added to linksById) 52 | {% else %} 53 | (will be added to linksById) 54 | {% endif %} 55 |
  • 56 | {% endif %} 57 | 58 | {% for A in L.lists %} 59 | {% if L not in A.links %} 60 |
  • 61 | link #{{ L.linkid }} has {{ A }} in its lists but not the reverse. 62 |
  • 63 | {% endif %} 64 | {% endfor %} 65 | {% endfor %} 66 | {% endfor %} 67 | 68 | {% for url, L in g_db.linksByUrl.items() %} 69 | {% if url != L._url %} 70 |
  • 71 | link #{{ L.linkid }} url ({{ L._url }}) doesn't match byUrl {{ url }} 72 | {% if vacwm %} 73 | {{ g_db._removeLinkFromUrls(url) }} (removed from byUrl) 74 | {% else %} 75 | (will be removed from byUrl) 76 | {% endif %} 77 |
  • 78 | {% endif %} 79 | 80 | {% endfor %} 81 | 82 | {% for L in g_db.linksById.values() %} 83 | {% if L._url is none %} 84 |
  • 85 | link #{{ L.linkid }} has invalid url: {{ L.url }} 86 | {% if vacwm %} 87 | {{ g_db.deleteLink(L) }} 88 | {% else %} 89 | (will be deleted) 90 | {% endif %} 91 |
  • 92 | {% endif %} 93 | {% endfor %} 94 | 95 | {% if not vacwm %} 96 | Cleanup 97 | {% endif %} 98 | 99 | {% endblock body %} 100 | -------------------------------------------------------------------------------- /html/dumplist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block keyword %}:dumplist{% endblock %} 4 | {% block body %} 5 |
    6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for L in g_db.getList(listname).links: %} 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | {% endfor %} 27 |
    L.linkidL.urlL.titleL.editsL.lists
    {{ L.linkid }}{{ L._url }}{{ L.title }}{% for lastEditTime, lastEditor in L.edits %} 20 | {{ lastEditTime|time_t }} by {{ lastEditor }} 21 |
    22 | {% endfor %} 23 |
    {% for x in L.lists %}{{ x.name }} {% endfor %}
    28 |
    29 | 30 | {% endblock %} 31 | 32 | -------------------------------------------------------------------------------- /html/editlink.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% from "listinc.html" import clickstats %} 3 | 4 | 5 | 6 | {% block title %}{% if L.linkid == 0 %}Add Link{% else %}Edit go link #{{ L.linkid }}{% endif %}{% endblock title %} 7 | {% block keyword %}{% if L.linkid %}_edit_/{{ L.linkid }}{% else %}_add_{% endif %}{% endblock keyword %} 8 | 9 | {% block body %} 10 |
    11 | 12 |
    13 |
    14 | 15 | {% if L.linkid != 0 %} 16 | 17 | {% endif %} 18 | 19 |
    20 |
    21 | {% if returnto %} 22 | 23 | {% endif %} 24 | 25 | {% if L.linkid != 0 %} 26 | 27 | {% endif %} 28 | 29 | 30 | {% if error %} 31 | 32 | 33 | 34 | {% endif %} 35 | 36 | 37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | 52 | 53 | 68 | 69 | 70 | 71 | 75 | 76 |

    {{ error }}

    Title 39 | 40 |
    URL 47 | 48 |
    Lists 54 | {% set listnames = L.listnames() or list(lists) %} 55 | {% for t in listnames %} 56 | {% set K = g_db.getList(t, create=False) %} 57 | {% set flDirect = K and K.goesDirectlyTo(L) %} 58 | 59 | 60 | {% if flDirect %}{% else %}{% endif %} 61 | {{ t }} 62 | {% if flDirect %}{% else %}{% endif %} 63 | 64 |
    65 | {% endfor %} 66 | add to: (space-separated) 67 |
    72 | 73 | 74 |
    77 |
    78 |
    79 |
    80 |
    {# end inner #} 81 | 82 |
    83 |
    84 |
    85 | {% include "vartable.html" %} 86 |

    Also see Python Format Syntax for variable formatting 87 | and Python Regex Syntax for regexes 88 |

    89 |
    90 |
    91 | 92 |
    93 | 94 | {{ clickstats(L) }} 95 | 96 |
    edits: 97 | {% for editTime, editor in L.edits %} 98 |
    — {{ editTime|time_t }} by {{ editor }} 99 | {% endfor %} 100 | 101 | 102 | {% endblock body %} 103 | -------------------------------------------------------------------------------- /html/help.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% from "listinc.html" import editlink %} 4 | 5 | {% block title %}go/help{% endblock title %} 6 | 7 | 8 | {% macro badge(n) -%} 9 | {{ n }} 10 | {%- endmacro %} 11 | 12 | {% set L = randomlink() %} 13 | {% set idx = 0 %} 14 | {% set usage = L.usage() %} 15 | 16 | {% block body %} 17 | 18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | 43 | 46 | 63 | 64 | 65 | 66 | 69 | 92 | 93 | 94 | 95 | 98 | 99 | 105 | 106 | 107 | 110 | 119 | 120 | 121 | 124 | 193 | 194 | 195 | 196 | 199 | 264 | 265 |
    25 |

    The go/ redirector is a mnemonic URL shortener and a link database.

    26 |

    Motivation

    31 |

    Instead of having to publicize a long and winding URL like 32 | {{ L.url()|truncate(140, "...") }}, 33 | 34 | 35 | you can simply create a go keyword like 36 | go/{{ usage }} 37 | 38 | that will automatically redirect straight to that webpage.

    39 |
    44 |

    Plain Keywords

    45 |
    47 | 48 |

    49 | 50 | go/keyword might redirect to a list of its links, a 51 | specific link, or a random link, depending on how the keyword is configured. 52 | By default, a plain keyword with only one link will redirect to that link. 53 |

    54 | 55 |

    go/.keyword ("dot-keyword") will always go to the 56 | index page for that keyword, where links can be added and removed, and the 57 | redirect behavior of the keyword itself can be changed.

    58 | 59 |

    Plain keywords can only have letters, numbers, dots, and dashes. No other 60 | characters are allowed in a plain keyword.

    61 | 62 |
    67 |

    Searchable Keywords

    68 |
    70 | 71 |

    Some keywords are effectively search queries; everything following 72 | the keyword is injected into the URL.

    73 | 74 |

    For example, go/ogle/ffiv does a Google search for "ffiv". 75 | 76 | The link for this keyword has the URL https://www.google.com/search?q={*}. The {*} is replaced with the search query, in this case "ffiv". 77 |

    78 | 79 |

    Searchable keywords end in a forward slash. 80 | 81 | If you try to add a plain keyword to a link with {*} in the URL, 82 | the keyword will be converted to a searchable keyword by appending a slash. 83 | 84 |

    The index page for searchable keywords is the same as for plain keywords, as 85 | long as the keyword ends with its slash. For example, go/.ogle/ goes to the index page for 87 | the ogle/ searchable keyword.

    88 | 89 |

    Replace whitespace in search queries with a plus symbol (+). For example, go/ogle/F5+networks+rules.

    90 | 91 |
    96 |

    Smart Keywords

    97 |
    100 |

    XXX: TODO

    101 | 102 |

    A list of all searchable keywords and regexes is at go/special.

    103 | 104 |
    108 |

    Variables

    109 |
    111 | 112 |

    All link URLs can have {VARIABLES} in them, which expand to a 113 | system-wide default which can be overridden per browser (via cookie).

    114 | 115 |

    go/variables allows you to 116 | add new variables and change the system values of existing ones. The "Save 117 | Local Overrides" button will store a cookie in your browser with your local 118 | variable overrides.

    122 |

    Link Summary

    123 |
    125 | 126 | 127 | 158 | 172 | 173 |
    128 | 129 | 130 | #{% if idx > 0 %}{{ idx }}{% else %} {% endif %} 131 | 132 | 157 | 159 |
    160 | {{ badge(4) }} 161 | 169 |
    170 | 171 |
    174 | 175 |

    The link summary appears on the index page, the keyword index page, and the 176 | toplinks page.

    177 | 178 |

    The main keyword {{ badge(1) }} will redirect immediately to the link.

    179 | 180 |

    The Title {{ badge(2) }} gives an indication of where the link goes; hover 181 | over the Title to see the full URL.

    182 | 183 |

    Additional keywords {{ badge(3) }} associated with this link (if any) are shown below 184 | the main keyword. bold keywords redirect immediately to the link; italic keywords do not.

    185 | 186 | 187 |

    Click the ⚒ (hammer and pick) in the upper right {{ badge(4) }} to edit the 188 | link.

    189 | 190 | 191 |

    Hover over the octothorpe (#) to see click info.

    192 |
    197 |

    Editing Links

    198 |
    200 |
    201 | {{ badge(6) }} DELETE 202 | 203 |
    204 | 205 | 206 | 207 | 210 | 211 | 212 | 213 | 214 | 215 | 218 | 219 | 220 | 221 | 222 | 238 | 239 | 240 | 241 | 245 | 246 |
    Title {{ badge(1) }} 208 | 209 |
    URL {{ badge(2) }} 216 | 217 |
    Lists 223 | {% set listnames = L.listnames() or list(lists) %} 224 | {% for idx, t in enumerate(listnames) %} 225 | {% set K = g_db.getList(t, create=False) %} 226 | {% set flDirect = K and K.goesDirectlyTo(L) %} 227 | {% if idx == 0 %}{{ badge(3) }}{% endif %} 228 | 229 | 230 | {% if flDirect %}{% else %}{% endif %} 231 | {{ t }} 232 | {% if flDirect %}{% else %}{% endif %} 233 | 234 |
    235 | {% endfor %} 236 | add to: {{ badge(4) }} (space-separated) 237 |
    242 | 243 | {{ badge(5) }} 244 |
    247 |
    248 |
    249 | {# end inner #} 250 | 251 |
    252 | 253 |

    Links have an optional Title {{ badge(1) }} which 254 | is displayed instead of the URL {{ badge(2) }} in the link summary.

    255 | 256 |

    257 | The link can be removed from a keyword by unchecking the checkbox {{ badge(3) }} next to the keyword, and it can be added to keywords by typing them in the "add to" input box {{ badge(4) }}). These will be updated when the Submit Link button {{ badge(5) }} is pressed.

    258 | 259 |

    Clicking on the DELETE {{ badge(6) }} in the upper right will immediately remove the 260 | link from all keywords and obliterate it from the system. Delete links with 261 | caution!

    262 | 263 |
    266 |
    267 |
    268 |
    269 | 270 | {% endblock body %} 271 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set username = getSSOUsername(False) %} 4 | {% set topLinks = byClicks(g_db.getNonFolders()) %} 5 | {% set folderLinks = byClicks(g_db.getSpecialLinks()) %} 6 | 7 | {% from "listinc.html" import renderlink %} 8 | 9 | {% block keyword %}
    {% endblock %} 10 | 11 | {% block body %} 12 |
    13 |
    14 |
    15 | 16 | 17 | 18 | {% for idx, link in enumerate(folderLinks[:15]): %} 19 | {{ renderlink(idx + 1, link, username) }} 20 | {% endfor %} 21 |

    Special Cases

    22 |
    23 |
    24 | 25 |
    26 |
    27 | 28 | 29 | 30 | 31 | {% if topLinks %} 32 | {% for idx, link in enumerate(topLinks[:8]): %} 33 | {{ renderlink(idx+1, link, username) }} 34 | {% endfor %} 35 | {% for idx in sample(range(0, len(topLinks)), 1): %} 36 | {{ renderlink(idx+1, topLinks[idx], username) }} 37 | {% endfor %} 38 | {% endif %} 39 |

    Recent Top Links

    40 |
    41 |
    42 | 43 |
    44 | 45 |

    Lists used in last 30 days

    46 |
    47 | {% for LL in g_db.getAllLists() %} 48 | 53 | {% endfor %} 54 |
    55 | 56 | {% endblock body %} 57 | -------------------------------------------------------------------------------- /html/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set popularLinks = L.getPopularLinks() %} 4 | 5 | 6 | {% from "listinc.html" import renderlink, clickstats with context %} 7 | 8 | {% block titlekeyword %}{{ keyword }}{% endblock %} 9 | {% block keyword %}{{ keyword }}{% endblock %} 10 | 11 | {% block body %} 12 | 13 |
    14 | 15 |

    {{ L.name }}

    16 |
    17 |
    18 | 19 | 20 | {% if L.linkid > 0 %} 21 | 22 | 40 | 41 | {% endif %} 42 | {% if L.linkid != -1 %} 43 | 44 | 53 | 54 | {% endif %} 55 | 56 | {% for idx, link in enumerate(popularLinks): %} 57 | {{ renderlink(idx+1, link, username) }} 58 | {% else %} 59 | 60 | 62 | 63 | {% endfor %} 64 |
    23 |
    24 | 25 | go/{{ keyword }} redirects to 26 | 35 | {% if username %} 36 | 37 | {% endif %} 38 |
    39 |
    45 |

    46 | {% if username %} 47 | Add new link 48 | {% else %} 49 | Login to edit this list 50 | {% endif %} 51 |

    52 |

    No links for this keyword.

    61 |
    65 |
    66 |
    67 | 68 |
    69 | 70 | {{ clickstats(L) }} 71 | {% endblock body %} 72 | -------------------------------------------------------------------------------- /html/listinc.html: -------------------------------------------------------------------------------- 1 | {% macro editlink(link, username) -%} 2 |
    3 | 11 |
    12 | {%- endmacro %} 13 | 14 | {% macro clickstats(L) -%} 15 | {% if L.clicks %} 16 |
    17 |

    {{ L.clickinfo() }}

    18 |
    19 | {% endif %} 20 | {%- endmacro %} 21 | 22 | {% macro renderlink(idx, link, username) -%} 23 | 24 | 25 | 26 | #{% if idx > 0 %}{{ idx }}{% else %} {% endif %} 27 | 28 | 53 | 54 | {{ editlink(link, username) }} 55 | 56 | {%- endmacro %} 57 | 58 | -------------------------------------------------------------------------------- /html/notfound.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Not Found{% endblock title %} 4 | {# block keyword %}{{ keyword }}{% endblock keyword #} 5 | 6 | {% block body %} 7 | 8 |

    {{ message | escape }}

    9 | 10 |

    The URL 11 | 12 | {{ getCurrentEditableUrl() | escape }} 13 | 14 | was not found. 15 |

    16 | 17 | {% endblock body %} 18 | -------------------------------------------------------------------------------- /html/special.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% from "listinc.html" import editlink %} 4 | 5 | {% set username = getSSOUsername() %} 6 | 7 | {% block keyword %}:regex{% endblock %} 8 | 9 | {% block body %} 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | {% for R in g_db.getSpecialLinks() %} 20 | 21 | 22 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 | {# 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | #} 43 | 44 |
    14 | Python Regex of keywordHelp StringDestination URL Format String
    {{ editlink(R, username) }}{% for K in R.lists %} 23 | 24 | {{ K.usage() }} 25 | 26 |
    27 | {% endfor %} 28 |
    {{ R.title }}{{ R._url }}
    45 | 46 | {% endblock body %} 47 | 48 | -------------------------------------------------------------------------------- /html/toplinks.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% from "listinc.html" import renderlink with context %} 4 | {% set username = getSSOUsername(False) %} 5 | 6 | {% block keyword %}toplinks{% endblock keyword %} 7 | {% block title %}Top {{ n }} go/ Links{% endblock title %} 8 | 9 | {% block body %} 10 | 11 |
    12 |

    Top {{ n }} Links

    13 |
    14 |
    15 | 16 | {% for idx, link in enumerate(byClicks(g_db.getNonFolders())[:n|int]): %} 17 | {{ renderlink(idx+1, link, username) }} 18 | {% endfor %} 19 |
    20 |
    21 |
    22 |
    23 | {% endblock body %} 24 | 25 | -------------------------------------------------------------------------------- /html/variables.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | {% set username = getSSOUsername(False) %} 4 | 5 | {% block keyword %}variables{% endblock keyword %} 6 | {% block body %} 7 |
    8 |
    9 |
    10 |
    11 | {% include "vartable.html" %} 12 | {% if username %} 13 |
    14 | 15 |

    Add Variable

    16 | This has been disabled for the time being. -Bill 17 | {% endif %} 18 |
    19 |
    20 |
    21 |
    22 | {% endblock body %} 23 | -------------------------------------------------------------------------------- /html/vartable.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {% if regexedit == "regex" %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% endif %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for k, v in g_db.variables.items() %} 30 | 31 | 32 | 33 | 36 | 37 | {% endfor %} 38 | 39 |
    VariablesReplace with
    {0}full regex match
    {n}regex match group <n>
    {*}everything following go/keyword/
    {{ "{" ~ k ~ "}" }}{{ v }} 34 | 35 |
    40 |
    41 | -------------------------------------------------------------------------------- /html/verify_special.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block keyword %}{{ regex }}{% endblock %} 3 | {% block body %} 4 | 5 | {% set exampleurl = RegexList(0, regex, url, example, title).url(example) %} 6 | 7 | {% if exampleurl %} 8 |

    Test Your Regex

    9 |

    Try out your example in another window: go/{{ example }}

    10 |
    11 |

    Did it go to the right place? 12 | 13 |
    14 | 15 |

    16 | 17 | 18 | 19 | 20 | 21 |
    22 | 23 | {% else %} 24 | 25 |

    The regex '{{ regex }}' didn't even match the example '{{ example }}'!

    26 | {% endif %} 27 | 28 |
    29 | 30 | 31 | 32 | 33 | 34 |
    35 | 36 | 37 | 38 | {% endblock body %} 39 | -------------------------------------------------------------------------------- /js/go.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Sean Smith on 10/11/16. 3 | */ 4 | 5 | document.getElementById('delete').onclick = function () { 6 | var lists = document.getElementsByName('lists'); 7 | var message = 'Doing this will delete the link from '; 8 | if (lists.length == 1) { 9 | message += 'the \'' + lists[0].value + '\' keyword'; 10 | } 11 | else if (lists.length == 2) { 12 | message += 'the following keywords: \'' + lists[0].value + '\' and \'' + lists[1].value + '\''; 13 | } else { 14 | message += 'the following keywords: '; 15 | for (var i = 0; i < lists.length; i++) { 16 | if (i + 1 != lists.length) { 17 | message += '\'' + lists[i].value + '\', '; 18 | } 19 | else { 20 | message += 'and \'' + lists[i].value + '\''; 21 | } 22 | } 23 | } 24 | if (lists.length > 1) { 25 | message += '\n\nIf you just want to disassociate it from a keyword uncheck it and submit'; 26 | } 27 | return confirm(message); 28 | }; -------------------------------------------------------------------------------- /newterms.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5go/1b7455dae4509b28eb9cdc6617d4ab4220e67cae/newterms.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jinja2==2.11.3 2 | cherrypy==18.1.1 3 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /variables 3 | 4 | User-agent: * 5 | Disallow: /check.html 6 | 7 | User-agent: * 8 | Disallow: /dumplist.html 9 | 10 | User-agent: * 11 | Disallow: /editlink.html 12 | 13 | User-agent: * 14 | Disallow: /vartable.html 15 | 16 | User-agent: * 17 | Disallow: /verify_special.html 18 | 19 | User-agent: * 20 | Disallow: /_* 21 | 22 | User-agent: * 23 | Allow: /.* 24 | --------------------------------------------------------------------------------