Commit 36fcac79 authored by Dennis Pehlke's avatar Dennis Pehlke
Browse files

Merged master into syntaxDiff

parents 11bf6004 445bc218
......@@ -3,6 +3,15 @@ function doSearch()
document.location = "spicey.cgi?Search/search/" + $("#searchfield")[0].value;
}
// get value of cells for sorting
const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent;
// compare two cells, either numeric values or strings
const comparer =
(idx, asc) => (a, b) =>
((v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2)) (getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
document.addEventListener("DOMContentLoaded", function(event) {
$("#searchfield").keydown(function(e){
if (e.keyCode == 13) {
......@@ -15,5 +24,29 @@ document.addEventListener("DOMContentLoaded", function(event) {
$("#menu-toggle").click( function (e){
e.preventDefault();
$("#wrapper").toggleClass("menuDisplayed");
});
});
$('th').click(function(){
var table = $(this).parents('table').eq(0)
var rows = table.find('tr:gt(0)').toArray().sort(comparer($(this).index()))
this.asc = !this.asc
if (!this.asc){
rows = rows.reverse()
}
for (var i = 0; i < rows.length; i++){
table.append(rows[i])
}
$('.sort-asc').removeClass('sort-asc')
$('.sort-desc').removeClass('sort-desc')
$(this).addClass(this.asc ? 'sort-asc' : 'sort-desc')
})
});
// add listener to sort any HTML tables by clicking the headers
/*document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => {
const table = th.closest('table');
Array.from(table.querySelectorAll('tr:not(:first-child)'))
.sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc = !this.asc))
.forEach(tr => table.appendChild(tr) );
}))); */
module Config.Config where
--- The name of the SQLite database file.
sqliteDBFile :: String
sqliteDBFile = "../Masala.db"
--- The complete root URL where the deployed system can be reached.
--- This can be used in various places, e.g. to provide full URLs in email notifications.
--- It must include the preferred protocol (http, https) and must not include a
--- trailing forward slash.
rootUrl :: String
rootUrl = "http://127.0.0.1/Masala"
--- This is the system-specific Key (a.k.a. "pepper") used in password hash generation,
--- in concatenation with the user Password and randomly generated Salt.
--- This key should be changed to a different random string of sufficient length
--- (12 characters is considered good) before the system is compiled for
--- deployment in production.
--- It must not be changed thereafter, for this would cause logins to previously created accounts to fail.
--- The key used in production should be kept secret. A leak or accidental disclosure will
--- nullify the additional account security it provides in the event of a database leak.
--- Specifically, an attacker then has the ability to efficiently run dictionary and brute-force
--- attacks against individual user account hashes, using the known Salt and systemkey.
--- In the event of a leak of this key or the database file, it is advised to :
--- (1) Temporarily disable logins to the system
--- (2) Identify and close the leak
--- (3) Implement a second, different systemkey that is used in all subsequent account
--- creations and password changes. User accounts should be tagged if they use the new key.
--- (4) Force all untagged users to change their password on the next login,
--- causing their password hash to be created from the new secret systemkey.
--- The old systemkey must still be stored to be able to verify the password of old accounts.
--- (5) Send an email to all users to inform them of the security breach, that their passwords might be
--- endangered, and that they should change their password immediately.
systemkey :: String
systemkey = "p9-2xK!81$W7"
-- The default mail profile. You can easily change this to reference one of those declared below.
mailProfDefault :: (String, ([String] -> [String]))
mailProfDefault = mailProfMailx
-- The sender email address to use for admin emails
adminEmail :: String
adminEmail = "noreply@informatik.uni-kiel.de"
-- Send mail with mailx ignoring the system configuration (send directly to recipient's mailserver)
-- This may work when inside the Uni Kiel network and sending mail to an IfI recipient.
-- Most mail providers will reject such mail from home IPs though, for spam protection.
mailProfRawMailx :: (String, ([String] -> [String]))
mailProfRawMailx = ("mailx", (\[subject, to] -> ["-n", "-a", "From: " ++ adminEmail, "-s", subject, to]))
-- Send mail with mailx using the system configuration
mailProfMailx :: (String, ([String] -> [String]))
mailProfMailx = ("mailx", (\[subject, to] -> ["-s", subject, to]))
-- Simply write mail content to local file 'mail.log' (for testing)
mailProfLogFile :: (String, ([String] -> [String]))
mailProfLogFile = ("sed", (\_ -> ["-n", "w mail.log"]))
-- Remote URL of the MasalaJob /api/job endpoint, use 'Nothing' if there is no remote server used
masalaJobServer :: Maybe String
masalaJobServer = Nothing
--------------------------------------------------------------------------
\ No newline at end of file
......@@ -2,6 +2,7 @@ module Config.RoutesData where
import System.Authentication
import System.SessionInfo
import System.Authorization
data ControllerReference = ProcessListController
| LoginController
......@@ -20,7 +21,7 @@ data UrlMatch = Exact String
| Matcher (String -> Bool)
| Always
type Route = (String,UrlMatch,ControllerReference, (Bool -> Bool))
type Route = (String,UrlMatch,ControllerReference, (UserSessionInfo -> Bool))
--- This constant specifies the association of URLs to controllers.
......@@ -31,25 +32,23 @@ type Route = (String,UrlMatch,ControllerReference, (Bool -> Bool))
getRoutes :: IO [Route]
getRoutes =
do login <- getSessionLogin
return $ maybe [] (\ld ->
[("Settings", Prefix "User" (ldName ld ++ "/settings"), UserController, id)]) login
++
[("Search", Prefix "Search" "search",SearchController, const False)
let loginName = maybe "" ldName login
return
[("Settings", Prefix "User" (loginName ++ "/settings"), UserController, isLoggedIn)
,("Search", Prefix "Search" "search",SearchController, const False)
,("Users", Exact "Users",UserController, const True)
,("New User",Prefix "User" "new",UserController, const True)
,("List User",Prefix "User" "list",UserController, const True)
,("Sign up",Exact "Signup",UserController, not)
,("Sign up",Exact "Signup",UserController, (not . isLoggedIn))
,("Email Confirmation",Exact "Confirm",UserController, const False)
,("Password Reset",Exact "Passreset",UserController, const False)
,("List Package",Prefix "Package" "list",PackageController, const True)
,("List Category",Prefix "Category" "list",CategoryController, const True)
,("New Category",Prefix "Category" "new",CategoryController, const True)
,("List CurryModule",Prefix "CurryModule" "list",CurryModuleController, const True)
,("New CurryModule",Prefix "CurryModule" "new",CurryModuleController, const True)
,("Upload",Exact "Upload",UploadController, id)
,("Packages",Prefix "Package" "list",PackageController, const True)
,("Categories",Prefix "Category" "list",CategoryController, const True)
,("New Category",Prefix "Category" "new",CategoryController, isAdmin)
,("CurryModules",Prefix "CurryModule" "list",CurryModuleController, const True)
,("New CurryModule",Prefix "CurryModule" "new",CurryModuleController, isAdmin)
,("Upload",Exact "Upload",UploadController, isLoggedIn)
,("Login",Exact "Login",LoginController, const False)
,("Logout",Exact "Logout",LogoutController, const False)
,("Admin", Exact "Admin", AdminController, id)
,("diff", Exact "diff", DiffController, const True)
,("diff", Exact "diffapi", DiffController, const True)
,("DiffAPI", Exact "diffapi", DiffController, const True)
,("Admin", Exact "Admin", AdminController, isAdmin)
,("default", Always, PackageController, const True)]
......@@ -6,12 +6,14 @@ import Time
import Masala
import View.Category
import Maybe
import Controller.Shared
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
import Config.UserProcesses
import View.MasalaEntitiesToHtml
import Database.CDBI.Connection
import Controller.Shared
--- Choose the controller for a Category entity according to the URL parameter.
mainCategoryController :: Controller
......@@ -125,4 +127,4 @@ addCategorizes versions category =
removeCategorizes :: [Version] -> Category -> DBAction ()
removeCategorizes versions category =
mapM_ (\t -> deleteCategorizes (categoryKey category) (versionKey t))
versions
\ No newline at end of file
versions
......@@ -6,6 +6,7 @@ import Time
import Masala
import View.CurryModule
import Maybe
import Controller.Shared
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
......
......@@ -6,6 +6,7 @@ import Time
import Masala
import View.Package
import Maybe
import Controller.Shared
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
......@@ -21,8 +22,8 @@ adminPackageController :: Controller
adminPackageController =
do args <- getControllerParams
case args of
["Package"] -> listPackageController
["Package", "list"] -> listPackageController
["Package"] -> listPackageControllerAdmin
["Package", "list"] -> listPackageControllerAdmin
["Package", "new"] -> newPackageController
["Package", "edit",s] ->
applyControllerOn (readPackageKey s) (runJustT . getPackage)
......@@ -101,14 +102,20 @@ deletePackageT package = deletePackage package
--- Lists all Package entities with buttons to show, delete,
--- or edit an entity.
listPackageController :: Controller
listPackageController =
listPackageControllerAdmin :: Controller
listPackageControllerAdmin =
checkAuthorization (packageOperationAllowed ListEntities)
$ (\sinfo ->
do packages <- runQ queryAllPackages
return (listPackageView sinfo packages))
return (listPackageViewAdmin sinfo packages))
--- Shows a Package entity.
listPackageController :: Controller
listPackageController = do
packages <- runQ queryAllPackages
return (listPackageView packages)
--- Shows a Package entity.
showPackageController :: Package -> Controller
showPackageController package =
checkAuthorization (packageOperationAllowed (ShowEntity package))
......@@ -154,4 +161,4 @@ showPackageByNameController pack ver = do
case thisVersion of
[version] -> showPackagePV package version versions
_ -> fail "Unknown version"
_ -> fail "Unknown package"
\ No newline at end of file
_ -> fail "Unknown package"
......@@ -9,6 +9,7 @@ import View.Package
import View.User
import Maybe
import List
import Controller.Shared
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
......@@ -100,7 +101,7 @@ listSearchController =
checkAuthorization (\_ -> return AccessGranted)
$ (\sinfo ->
do packets <- runQ $ queryCondPackage (\x -> True )
return (listPackageView sinfo packets))
return (listPackageViewAdmin sinfo packets))
------- Utility functions
......@@ -111,4 +112,4 @@ caseInsensitiveCompare A comparator B =
a = map toLower A
b = map toLower B
in
a `comparator` b
\ No newline at end of file
a `comparator` b
module Controller.Shared (checkAuthorization) where
import System.Authorization
import System.Spicey(Controller, displayError)
import System.SessionInfo
--- Checks the results of an authoriation access.
--- If the access is granted, we proceed with the given controller
--- to which the current user session information is passed,
--- otherwise we display the access error message.
checkAuthorization :: (UserSessionInfo -> IO AccessResult)
-> (UserSessionInfo -> Controller)
-> Controller
checkAuthorization getaccess controller = do
sinfo <- getUserSessionInfo
accresult <- getaccess sinfo
case accresult of
AccessGranted -> controller sinfo
AccessDenied reason -> displayError reason
......@@ -20,6 +20,7 @@ import System.SessionInfo
import System.Authentication
import View.SpiceySystem
import View.Shared
import Controller.Shared
import Controller.DefaultController
-----------------------------------------------------------------------------
......
module Controller.Upload ( mainUploadController ) where
import System.Spicey
import HTML.Base
import Time
import Masala
import View.Upload
import Maybe
import Controller.Shared
import System.PackageUpload (handleUploadNewPackageTarGz)
import System.Spicey
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
......@@ -19,7 +21,6 @@ import IOExts
import Controller.DefaultController
import Directory
import qualified CPM.Package as CPM
import System.PackageUpload (handleUploadNewPackageTarGz)
--- Choose the controller for a Category entity according to the URL parameter.
mainUploadController :: Controller
......
module Controller.User ( mainUserController, adminUserController ) where
import System.Spicey
import HTML.Base
import Time
import Masala
import View.User
import View.Shared
import Maybe
import List
import Char
import Controller.DefaultController
import System.SessionInfo
import Database.CDBI.Connection
import HTML.Base
import Masala
import Config.Config
--import Config.UserProcesses
import Controller.Shared
import System.Authentication
import System.Authorization
import System.AuthorizedActions
import Config.UserProcesses
import Config.Config
import View.MasalaEntitiesToHtml
import Database.CDBI.Connection
import System.Crypto
import System.Mail
import System.Spicey
import System.SessionInfo
import View.MasalaEntitiesToHtml
import View.User
import View.Shared
adminUserController :: Controller
adminUserController = do
(url,args) <- getControllerURL
(_, args) <- getControllerURL
case args of
["User"] -> listUserController -- DB Admin's user entity list
["User", "list"] -> listUserController -- DB Admin's user entity list
......@@ -47,6 +49,8 @@ mainUserController = do
("Signup", []) -> signupController -- public signup
("Confirm", [s]) -> mailConfirmController s -- public confirm controller
("Passreset", []) -> passResetController -- public passreset controller
("User", [s]) -> userProfileController (urlencoded2string s) -- public user profile
("User", [s,"settings"]) -> userSettingsController (urlencoded2string s) -- user profile settings
_ -> displayError "Illegal URL"
getUserByName :: String -> IO (Maybe User)
......@@ -234,8 +238,7 @@ mailConfirmController ccode = do
cTime <- getClockTime
case mbUserAndTokens of
Nothing -> -- no user in the DB has a token with this confirmation code
(setPageMessage "Email confirmation failed! Unknown confirmation code.") >>
defaultController
displayError "Email confirmation failed! Unknown confirmation code."
Just (dbUser, mailTokens, mt@(UserT expiry _)) -> -- a user has the supplied confirmation code
if (clockTimeToInt cTime) > expiry
then
......@@ -263,8 +266,7 @@ mailConfirmController ccode = do
then
-- code has expired, just remove the token
(runT (updateUser (setUserToken dbUser (show (delete mt mailTokens))))) >>
(setPageMessage "This confirmation code has expired.") >>
defaultController
displayError "This confirmation code has expired."
else -- the ccode is valid, the user can now create a new password!
return $ passResetView (userName dbUser) (passResetHandler (userKey dbUser) ccode mt)
Just (dbUser, mailTokens, mt@(MailT expiry _ newMail)) ->
......@@ -272,8 +274,7 @@ mailConfirmController ccode = do
then
-- code has expired, just remove the token
(runT (updateUser (setUserToken dbUser (show (delete mt mailTokens))))) >>
(setPageMessage "This confirmation code has expired.") >>
defaultController
displayError "This confirmation code has expired."
else do -- the ccode is valid, the new email address can be set!
let modifiedUser = setUserEmail dbUser newMail
runT $ updateUser modifiedUser
......@@ -639,4 +640,4 @@ readMailTokens tokenField = maybe [] id (readMaybe tokenField)
splitMailToken :: String -> (UserID, String)
splitMailToken token =
let (suid, dashCode) = break (=='-') token
in (UserID (maybe (-1) id (readMaybe suid)), dropWhile (=='-') dashCode)
\ No newline at end of file
in (UserID (maybe (-1) id (readMaybe suid)), dropWhile (=='-') dashCode)
......@@ -6,6 +6,7 @@ import Time
import Masala
import View.Version
import Maybe
import Controller.Shared
import System.SessionInfo
import System.Authorization
import System.AuthorizedActions
......
......@@ -12,7 +12,7 @@ cabal-version: >=1.10
executable spicey.cgi
main-is: Main.hs
build-depends: base >=4.8 && <5,
process,
process >= 1.3,
directory >= 1.2.2 && < 1.4,
bytestring,
parsec,
......
{-# LANGUAGE OverloadedStrings #-}
import System.IO
import System.IO.Temp
import System.Process
......@@ -14,11 +15,20 @@ main = do
content_type <- lookupEnv "CONTENT_TYPE"
case content_type of
(Just x) -> do
if "multipart/form-data;" `isInfixOf` x
then handleUpload x
else forwardToSpicey
request_uri <- lookupEnv "QUERY_STRING"
case request_uri of
(Just uri) -> case uri of
"jobupload" -> do
mpart <- handleUpload x
handleJobUpload mpart
_ -> if "multipart/form-data;" `isInfixOf` x
then do
mpart <- handleUpload x
forwardToSpiceyWithFile mpart
else forwardToSpicey
_ -> forwardToSpicey
_ -> forwardToSpicey
--- Forward stdin/stdout to spicey directly
forwardToSpicey :: IO ()
forwardToSpicey = do
......@@ -26,8 +36,27 @@ forwardToSpicey = do
waitForProcess handle
return ()
--- Handle a upload from the job runner
handleJobUpload :: [MultipartResult] -> IO ()
handleJobUpload multipart = do
let (MultipartString _ package) = getPart "Package"
let (MultipartString _ version) = getPart "Version"
let (MultipartString _ secret) = getPart "Secret"
let (MultipartFile _ path) = getPart "File"
(_, _, _, handle) <- createProcess (proc "tar" ["xzf", "../../../" ++ BS.unpack path, "--overwrite"]){ std_out = NoStream, std_err = NoStream, cwd = Just $ "packages/" ++ (BS.unpack package) ++ "/" ++ (BS.unpack version) ++ "/" }
waitForProcess handle
print "Content-type: text/plain\n\nok\n"
hClose stdin
return ()
where
getPart name =
let [matchingValue] = filter (filterKey name) multipart
in matchingValue
filterKey name (MultipartFile key _) = key == name
filterKey name (MultipartString key _) = key == name
--- Handle a multipart upload
handleUpload :: String -> IO ()
handleUpload :: String -> IO [MultipartResult]
handleUpload content_type = do
-- Setup handles
hSetBinaryMode stdin True
......@@ -36,7 +65,10 @@ handleUpload content_type = do
-- Transform multipart to a regular field-based post request
let boundary = drop 30 content_type -- Todo: Use a regex?
(MultiPart multipart) <- hGetMultipartBody boundary stdin
mpb <- mapM showMultipartBody multipart
mapM showMultipartBody multipart
forwardToSpiceyWithFile :: [MultipartResult] -> IO ()
forwardToSpiceyWithFile mpb = do
parts <- mapM showMultipartResult mpb
-- Run spicey and pass arguments via stdin
(Just hin, _, _, handle) <- createProcess (proc "./run.cgi" []){ std_out = UseHandle stdout, std_in = CreatePipe}
......
This diff is collapsed.
This diff is collapsed.
......@@ -1679,6 +1679,18 @@ getCurryModuleOrCreate name = do
let [m] = ms
return $ curryModuleKey m
--- Gets or creates a new `Category` entity
getCategoryOrCreate :: String -> Database.CDBI.Connection.DBAction CategoryID
getCategoryOrCreate name = do
ms <- queryCondCategory (\x -> categoryName x == name)
if null ms
then do
m <- newCategory name ""
return $ categoryKey m
else do
let [m] = ms
return $ categoryKey m
--- Inserts a new `CurryModule` entity.
newCurryModule :: String -> Database.CDBI.Connection.DBAction CurryModule
newCurryModule name_p =
......
--- This module specifies the access authorization to web pages.
module System.Authorization (
AccessType(..), AccessResult(..), checkAuthorization,
AccessType(..), AccessResult(..),
effectivePermission, isLoggedIn, isAdmin
) where
import HTML.Base
import System.Spicey(Controller, displayError)
import System.SessionInfo
import Maybe
......@@ -28,17 +27,3 @@ isAdmin sinfo = (effectivePermission sinfo) >= ADMIN
--- Get the effective user permission level from a session info
effectivePermission :: UserSessionInfo -> URole
effectivePermission = (maybe GUEST ldRole) . userLoginOfSession
--- Checks the results of an authoriation access.
--- If the access is granted, we proceed with the given controller
--- to which the current user session information is passed,
--- otherwise we display the access error message.
checkAuthorization :: (UserSessionInfo -> IO AccessResult)
-> (UserSessionInfo -> Controller)
-> Controller
checkAuthorization getaccess controller = do
sinfo <- getUserSessionInfo
accresult <- getaccess sinfo
case accresult of
AccessGranted -> controller sinfo
AccessDenied reason -> displayError reason
This diff is collapsed.
......@@ -7,14 +7,15 @@ module System.Routes
( getControllerReference, getRouteMenu )
where
import List
import Maybe
import HTML.Base
import Config.RoutesData
import System.Authentication
import System.Authorization
import System.SessionInfo
--generated in RoutesData
--type Route = (String, UrlMatch, ControllerReference)
--- Gets the reference of a controller corresponding to a given URL
--- according to the definition of all routes specified in
--- module RoutesData.
......@@ -42,13 +43,13 @@ getControllerReference url = getRoutes >>= return . findControllerReference
--- `Exact` in the module RoutesData, except for "login",
--- are taken as menu entries.
getRouteMenu :: (String, [String]) -> IO [HtmlExp]
getRouteMenu controllerUrl@(url, parameters) = do
getRouteMenu controllerUrl = do
routes <- getRoutes
isLoggedIn <- isUserLoggedIn
return $ getNavbar isLoggedIn routes
sinfo <- getUserSessionInfo
return $ getNavbar sinfo routes
where
getNavbar :: Bool -> [Route] -> [HtmlExp]
getNavbar isLoggedIn routes@(_:_) =
getNavbar :: UserSessionInfo -> [Route] -> [HtmlExp]
getNavbar session routes@(_:_) =
-- Curryversion of navbar and sidebar of Home.html
-- Navbar (with div wrapper)
[
......@@ -60,7 +61,7 @@ getRouteMenu controllerUrl@(url, parameters) = do
, HtmlStruct "div" [("class", "collapse navbar-collapse"), ("id", "navbarSupportedContent")]
[ HtmlStruct "ul" [("class", "navbar-nav mr-auto")]
[ HtmlStruct "li" [("class", "nav-item active")]
[ HtmlStruct "div" [("id", "breadcrumbs"), ("class", "ml-xs-0 ml-md-5 mr-auto")] [getBreadCrumbs controllerUrl]
[ HtmlStruct "div" [("id", "breadcrumbs"), ("class", "ml-xs-0 ml-md-5 mr-auto")] (getBreadCrumbs controllerUrl)
]
]
-- SearchField + Button
......@@ -70,7 +71,7 @@ getRouteMenu controllerUrl@(url, parameters) = do
]
--- Login ---
, HtmlStruct "div" [("class", "md-2 mr-5 my-2")] -- div2
[ HtmlStruct "a" [("href", if isLoggedIn then "?Logout" else "?Login"), ("style", "text-decoration: underline; color: white;")] [HtmlText (if isLoggedIn then "Logout" else "Login")]]
[ HtmlStruct "a" [("href", (if (isLoggedIn session) then "?Logout" else "?Login")), ("style", "text-decoration: underline; color: white;")] [HtmlText (if (isLoggedIn session) then "Logout" else "Login")]]
]
-- Sidebar-toggle-button
, HtmlStruct "a" [("href", "#"), ("class", "btn bg-secondary"), ("style", "border-width: 1px; border-color: darkgrey"), ("id", "menu-toggle")]
......@@ -79,7 +80,7 @@ getRouteMenu controllerUrl@(url, parameters) = do
] -- closing tag of nav
-- Sidebar
, HtmlStruct "div" [("id", "sidebar-wrapper")]
[ HtmlStruct "ul" [("class", "h-100 sidebar-nav")] (getLinks isLoggedIn routes)] -- closing tag of div-sidebar
[ HtmlStruct "ul" [("class", "h-100 sidebar-nav")] (getLinks session routes)] -- closing tag of div-sidebar
]
]
getNavbar _ [] = []
......@@ -87,66 +88,61 @@ getRouteMenu controllerUrl@(url, parameters) = do
--- returns all links of the RoutesList Parameter as [[HtmlStruct "li" [] [HtmlExp "href" ...]]]
--- the ouput should be something like this:
--- [HtmlStruct "li" [] [HtmlStruct "a" [("href", "?spiceyProcesses")] [HtmlText "Processes"]], [Html Struct "li" ...]
getLinks :: Bool -> [Route] -> [HtmlExp]
getLinks isLoggedIn ((name, matcher, _, loginCheck):restroutes) =
if not (loginCheck isLoggedIn)
then getLinks isLoggedIn restroutes
getLinks :: UserSessionInfo -> [Route] -> [HtmlExp]
getLinks session ((name, matcher, _, loginCheck):restroutes) =
if not (loginCheck session)
then getLinks session restroutes
else case matcher of