cgi-bin/loona.lua
author Timm S. Mueller <tmueller@neoscientists.org>
Sun, 11 Mar 2007 13:45:39 +0100
changeset 141 fc50232cf086
parent 137 45904cd06c29
child 143 8495a4fdc479
permissions -rw-r--r--
Added unrolling of multilingual sites
tmueller@0
     1
tmueller@0
     2
--
tmueller@0
     3
--	loona - tiny CMS
tmueller@0
     4
--	Written by Timm S. Mueller <tmueller at neoscientists.org>
tmueller@0
     5
--	See copyright notice in COPYRIGHT
tmueller@0
     6
--
tmueller@0
     7
tmueller@68
     8
local tek = require "tek"
tmueller@68
     9
local cgi = require "tek.cgi"
tmueller@68
    10
local posix = require "tek.posix"
tmueller@0
    11
require "tek.cgi.request"
tmueller@0
    12
require "tek.cgi.request.args"
tmueller@0
    13
require "tek.cgi.session"
tmueller@0
    14
require "tek.web"
tmueller@0
    15
require "tek.web.markup"
tmueller@0
    16
require "tek.util"
tmueller@0
    17
tmueller@23
    18
tmueller@0
    19
local boxed_G = { 
tmueller@0
    20
	string = string, table = table,
tmueller@0
    21
	assert = assert, collectgarbage = collectgarbage, dofile = dofile,
tmueller@0
    22
	error = error, getfenv = getfenv, getmetatable = getmetatable,
tmueller@0
    23
	ipairs = ipairs, load = load, loadfile = loadfile, loadstring = loadstring,
tmueller@0
    24
	next = next, pairs = pairs, pcall = pcall, print = print,
tmueller@0
    25
	rawequal = rawequal, rawget = rawget, rawset = rawset, require = require,
tmueller@0
    26
	select = select, setfenv = setfenv, setmetatable = setmetatable,
tmueller@0
    27
	tonumber = tonumber, tostring = tostring, type = type, unpack = unpack,
tmueller@0
    28
	xpcall = xpcall
tmueller@0
    29
}
tmueller@0
    30
tmueller@68
    31
local table, string, assert, unpack, ipairs, pairs, type, require =
tmueller@68
    32
	table, string, assert, unpack, ipairs, pairs, type, require
tmueller@0
    33
local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv
tmueller@0
    34
local open, remove, rename, getenv, time =
tmueller@0
    35
	io.open, os.remove, os.rename, os.getenv, os.time
tmueller@0
    36
tmueller@0
    37
tmueller@0
    38
module "loona"
tmueller@0
    39
tmueller@0
    40
tmueller@129
    41
_VERSION = 3
tmueller@141
    42
_REVISION = 1
tmueller@0
    43
tmueller@0
    44
tmueller@20
    45
local function lookupname(tab, val)
tmueller@0
    46
	if tab then
tmueller@0
    47
		for i, v in ipairs(tab) do
tmueller@20
    48
			if v.name == val then
tmueller@0
    49
				return i
tmueller@0
    50
			end
tmueller@0
    51
		end
tmueller@0
    52
	end
tmueller@0
    53
end
tmueller@0
    54
tmueller@0
    55
tmueller@129
    56
local atom = { }
tmueller@20
    57
tmueller@129
    58
function atom:new(o)
tmueller@129
    59
	o = o or { }
tmueller@129
    60
	setmetatable(o, self)
tmueller@129
    61
	self.__index = self
tmueller@129
    62
	return o
tmueller@129
    63
end
tmueller@129
    64
tmueller@129
    65
local loona = atom:new(getfenv())
tmueller@129
    66
tmueller@129
    67
tmueller@129
    68
function loona:dbmsg(msg, detail)
tmueller@129
    69
 	return (msg and detail and self.authuser) and
tmueller@129
    70
 		("%s : %s"):format(msg, detail) or msg
tmueller@129
    71
end
tmueller@129
    72
tmueller@129
    73
tmueller@129
    74
function loona:checkprofilename(n)
tmueller@129
    75
	assert(n:match("^%w+$") and n ~= "current",
tmueller@129
    76
		self:dbmsg("Invalid profile name", n))
tmueller@129
    77
	return n
tmueller@129
    78
end
tmueller@129
    79
tmueller@129
    80
tmueller@129
    81
function loona:checkbodyname(s)
tmueller@129
    82
	s = s or "main"
tmueller@129
    83
	assert(s:match("^[%w_]*%w+[%w_]*$"), self:dbmsg("Invalid body name", s))
tmueller@129
    84
	return s
tmueller@129
    85
end
tmueller@129
    86
tmueller@129
    87
tmueller@129
    88
function loona:deleteprofile(p, lang)
tmueller@129
    89
	p = self.config.contentdir .. "/" .. p .. "_" .. (lang or self.lang)
tmueller@129
    90
	for e in tek.util.readdir(p) do
tmueller@129
    91
 		local success, msg = remove(p .. "/" .. e)
tmueller@129
    92
		assert(success, self:dbmsg("Error removing entry in profile", msg))
tmueller@129
    93
	end
tmueller@129
    94
	return remove(p)
tmueller@129
    95
end
tmueller@129
    96
tmueller@129
    97
tmueller@129
    98
function loona:copyprofile(newprofile, srcprofile, lang)
tmueller@129
    99
	srcprofile = srcprofile or self.profile
tmueller@129
   100
	lang = lang or self.lang
tmueller@129
   101
	local contentdir = self.config.contentdir
tmueller@129
   102
	local src = ("%s/%s_%s"):format(contentdir, srcprofile or self.profile, lang)
tmueller@129
   103
	assert(posix.stat(src, "mode") == "directory",
tmueller@129
   104
		self:dbmsg("Not a directory", src))
tmueller@129
   105
	local dst = ("%s/%s_%s"):format(contentdir, newprofile, lang)
tmueller@129
   106
	local success, msg = posix.mkdir(dst)
tmueller@129
   107
	assert(success, self:dbmsg("Error creating profile directory", msg))
tmueller@129
   108
	for e in tek.util.readdir(src) do
tmueller@129
   109
		local ext = e:match("^[^.].*%.([^.]*)$")
tmueller@129
   110
		if ext ~= "LOCK" then
tmueller@129
   111
			local f = src .. "/" .. e
tmueller@129
   112
			if posix.stat(f, "mode") == "file" then
tmueller@129
   113
				success, msg = tek.copyfile(f, dst .. "/" .. e)
tmueller@129
   114
				assert(success, self:dbmsg("Error copying file", msg))
tmueller@129
   115
			end
tmueller@129
   116
		end
tmueller@129
   117
	end
tmueller@129
   118
end
tmueller@129
   119
tmueller@129
   120
tmueller@129
   121
function loona:publishprofile(profile, lang)
tmueller@131
   122
	
tmueller@129
   123
	lang = lang or self.lang
tmueller@129
   124
	local contentdir = self.config.contentdir
tmueller@129
   125
	local newpath = ("%s/current_%s"):format(contentdir, lang)
tmueller@129
   126
	local tmppath = newpath .. "." .. self.session.name
tmueller@129
   127
	local success, msg = posix.symlink(profile .. "_" .. lang, tmppath)
tmueller@129
   128
	assert(success, self:dbmsg("Cannot create symlink", msg))
tmueller@129
   129
	success, msg = rename(tmppath, newpath)
tmueller@129
   130
	assert(success, self:dbmsg("Cannot put symlink in place", msg))
tmueller@129
   131
	
tmueller@141
   132
	-- get languages of the current profile
tmueller@141
   133
	
tmueller@141
   134
	local plangs = { }
tmueller@141
   135
	local lmatch = "^" .. self.profile .. "_(%w+)$"
tmueller@141
   136
	for e in tek.util.readdir(self.config.contentdir) do
tmueller@141
   137
		local l = e:match(lmatch)
tmueller@141
   138
		if l then
tmueller@141
   139
			table.insert(plangs, l)
tmueller@141
   140
		end
tmueller@141
   141
	end
tmueller@141
   142
	
tmueller@129
   143
	-- Unroll site to static HTML
tmueller@129
   144
	
tmueller@141
   145
	for _, lang in ipairs(plangs) do
tmueller@141
   146
		local ext = (#plangs == 1 and ".html") or (".html." .. lang)
tmueller@141
   147
		self:recursesections(self.sections, function(self, s, e, path)
tmueller@141
   148
			path = path and path .. "/" .. e.name or e.name
tmueller@141
   149
			if not e.notvisible then
tmueller@141
   150
				loona:dumphtml { requestpath = path, requestlang = lang, htmlext = ext }
tmueller@141
   151
			end
tmueller@141
   152
			return path
tmueller@141
   153
		end)
tmueller@141
   154
	end
tmueller@131
   155
	
tmueller@131
   156
	-- Update file cache
tmueller@131
   157
tmueller@131
   158
	local htdocs = self.config.htdocsdir
tmueller@131
   159
	local cache = self.config.htmlcachedir
tmueller@131
   160
tmueller@131
   161
	for e in tek.util.readdir(cache) do
tmueller@141
   162
		local f = e:match("^.*%.html%.?(%w*)$")
tmueller@141
   163
		if f and f ~= "tmp" then
tmueller@131
   164
			local success, msg = remove(htdocs .. "/" .. e)
tmueller@131
   165
			success, msg = remove(cache .. "/" .. e)
tmueller@131
   166
 			assert(success, self:dbmsg("Could not purge cached HTML file", msg))
tmueller@131
   167
		end
tmueller@131
   168
	end
tmueller@131
   169
tmueller@131
   170
	for e in tek.util.readdir(cache) do
tmueller@141
   171
		local f = e:match("^(.*%.html%.?%w*)%.tmp$")
tmueller@131
   172
		if f then
tmueller@131
   173
			local success, msg = rename(cache .. "/" .. e, cache .. "/" .. f)
tmueller@131
   174
			assert(success, self:dbmsg("Could not update cached HTML file", msg))
tmueller@131
   175
			success, msg = rename(htdocs .. "/" .. e, htdocs .. "/" .. f)
tmueller@131
   176
			assert(success, self:dbmsg("Could not update cached HTML file", msg))
tmueller@131
   177
		end
tmueller@131
   178
	end
tmueller@131
   179
tmueller@129
   180
end
tmueller@129
   181
tmueller@129
   182
tmueller@129
   183
function loona:recursesections(s, func, ...)
tmueller@0
   184
	for _, e in ipairs(s) do
tmueller@129
   185
		local udata = { func(self, s, e, unpack(arg)) }
tmueller@0
   186
		if e.subs then
tmueller@129
   187
			self:recursesections(e.subs, func, unpack(udata))
tmueller@0
   188
		end
tmueller@129
   189
	end
tmueller@129
   190
end
tmueller@129
   191
tmueller@129
   192
tmueller@129
   193
function loona:indexsections()
tmueller@129
   194
	self:recursesections(self.sections, function(self, s, e)
tmueller@129
   195
		e.notvalid = (not self.secure and e.secure) or 
tmueller@129
   196
			(not self.authuser and e.secret) or nil
tmueller@129
   197
		e.notvisible = e.notvalid or not self.authuser and e.hidden or nil
tmueller@16
   198
		s[e.name] = e
tmueller@129
   199
	end)
tmueller@0
   200
end
tmueller@0
   201
tmueller@0
   202
tmueller@20
   203
--	Decompose section path into a stack of sections, returning only up to
tmueller@0
   204
--	the last valid element in the path. additionally returns the table of
tmueller@0
   205
--	the last section path element (or the default section)
tmueller@0
   206
tmueller@129
   207
function loona:getsection(path)
tmueller@129
   208
	local default = not self.authuser and self.config.defname
tmueller@129
   209
	local tab = { { entries = self.sections, name = default } }
tmueller@129
   210
	local ss = self.sections
tmueller@0
   211
	local sectionpath
tmueller@129
   212
	(path or ""):gsub("(%w+)/?", function(a)
tmueller@27
   213
		if ss then
tmueller@27
   214
			local s = ss[a]
tmueller@20
   215
			if s and not s.notvalid then
tmueller@20
   216
				sectionpath = s
tmueller@0
   217
				tab[#tab].name = a
tmueller@27
   218
				ss = s.subs
tmueller@27
   219
				if ss then
tmueller@27
   220
					table.insert(tab, { entries = ss })
tmueller@0
   221
				end
tmueller@0
   222
			else
tmueller@27
   223
				ss = nil -- stop.
tmueller@0
   224
			end
tmueller@0
   225
		end
tmueller@0
   226
	end)
tmueller@129
   227
	if not self.section and not sectionpath then
tmueller@129
   228
		sectionpath = self.sections[default]
tmueller@0
   229
		if sectionpath then
tmueller@0
   230
			table.insert(tab, { entries = sectionpath.subs })
tmueller@0
   231
		end
tmueller@0
   232
	end
tmueller@0
   233
	return tab, sectionpath
tmueller@0
   234
end
tmueller@0
   235
tmueller@0
   236
tmueller@129
   237
function loona:getpath(delimiter)
tmueller@0
   238
	local t = { }
tmueller@129
   239
	for _, menu in ipairs(self.submenus) do
tmueller@0
   240
		if menu.name then
tmueller@0
   241
			table.insert(t, menu.name)
tmueller@0
   242
		end
tmueller@0
   243
	end
tmueller@0
   244
	return table.concat(t, delimiter or "/")
tmueller@0
   245
end
tmueller@0
   246
tmueller@0
   247
tmueller@129
   248
function loona:deletesection(fname, all_bodies)
tmueller@129
   249
	local fullname = self.contentdir .. "/" .. fname
tmueller@0
   250
	local success, msg = remove(fullname)
tmueller@129
   251
	if all_bodies then
tmueller@0
   252
		local pat = "^" .. 
tmueller@0
   253
			fname:gsub("%^%$%(%)%%%.%[%]%*%+%-%?", "%%%1") .. "%..*$"
tmueller@129
   254
		for e in tek.util.readdir(self.contentdir) do
tmueller@0
   255
			if e:match(pat) then
tmueller@129
   256
				remove(self.contentdir .. "/" .. e)
tmueller@0
   257
			end
tmueller@0
   258
		end
tmueller@0
   259
	end
tmueller@0
   260
	return success, msg
tmueller@0
   261
end
tmueller@0
   262
tmueller@0
   263
tmueller@129
   264
function loona:addpath(path, e)
tmueller@129
   265
	local tab = self.sections
tmueller@0
   266
	path:gsub("(%w+)/?", function(a)
tmueller@20
   267
		if tab then
tmueller@20
   268
			local s = tab[a]
tmueller@20
   269
			if s then
tmueller@20
   270
				if not s.subs then
tmueller@20
   271
					s.subs = { }
tmueller@0
   272
				end
tmueller@20
   273
				tab = s.subs
tmueller@0
   274
			else
tmueller@20
   275
				table.insert(tab, e)
tmueller@20
   276
				tab[a] = e
tmueller@20
   277
 				tab = nil -- stop
tmueller@0
   278
			end
tmueller@0
   279
		end
tmueller@0
   280
	end)
tmueller@129
   281
	return e
tmueller@0
   282
end
tmueller@0
   283
tmueller@0
   284
tmueller@129
   285
function loona:rmpath(path)
tmueller@20
   286
	local parent
tmueller@129
   287
	local tab = self.sections
tmueller@0
   288
	path:gsub("(%w+)/?", function(a)
tmueller@20
   289
		if tab then
tmueller@20
   290
			local idx = lookupname(tab, a)
tmueller@20
   291
			if idx then
tmueller@20
   292
				if tab[idx].subs then
tmueller@20
   293
					parent = tab[idx]
tmueller@20
   294
					tab = tab[idx].subs
tmueller@0
   295
				else
tmueller@20
   296
					table.remove(tab, idx)
tmueller@43
   297
					tab[a] = nil
tmueller@20
   298
					if #tab == 0 and parent then
tmueller@20
   299
						parent.subs = nil
tmueller@0
   300
					end
tmueller@20
   301
					tab = nil
tmueller@0
   302
				end
tmueller@0
   303
			end
tmueller@0
   304
		end
tmueller@0
   305
	end)
tmueller@0
   306
end
tmueller@0
   307
tmueller@0
   308
tmueller@129
   309
function loona:checkpath(path)
tmueller@129
   310
	if path ~= "index" then -- "index" is reserved
tmueller@129
   311
		local res, idx
tmueller@129
   312
		local tab = self.sections
tmueller@129
   313
		path:gsub("(%w+)/?", function(a)
tmueller@129
   314
			if tab then
tmueller@129
   315
				local i = lookupname(tab, a)
tmueller@129
   316
				if i then
tmueller@129
   317
					res, idx = tab, i
tmueller@129
   318
					tab = tab[i].subs
tmueller@129
   319
				else
tmueller@129
   320
					res, idx = nil, nil
tmueller@129
   321
				end
tmueller@129
   322
			end
tmueller@129
   323
		end)
tmueller@129
   324
		return res, idx
tmueller@129
   325
	end
tmueller@8
   326
end
tmueller@8
   327
tmueller@8
   328
tmueller@129
   329
function loona:title()
tmueller@129
   330
	return self.section and (self.section.title or self.section.label or
tmueller@129
   331
		self.section.name) or ""
tmueller@0
   332
end
tmueller@0
   333
tmueller@0
   334
tmueller@0
   335
--	Run a site function snippet, with full error recovery
tmueller@0
   336
--	(also recovers from errors in error handling function)
tmueller@0
   337
tmueller@129
   338
function loona:dosnippet(func, errfunc)
tmueller@0
   339
	local ret = { tek.catch(func) }
tmueller@0
   340
	if ret[1] == 0 or (errfunc and tek.catch(errfunc) == 0) then
tmueller@0
   341
		return unpack(ret)
tmueller@0
   342
	end
tmueller@129
   343
	self:out("<h2>Error</h2>")
tmueller@129
   344
	self:out("<h3>" .. self:encodeform(ret[2] or "") .. "</h3>")
tmueller@129
   345
	if self.authuser then
tmueller@0
   346
		if type(ret[3]) == "string" then
tmueller@129
   347
			self:out("<p>" .. self:encodeform(ret[3]) .. "</p>")
tmueller@0
   348
		end
tmueller@129
   349
		if ret[4] then
tmueller@129
   350
			self:out("<pre>" .. self:encodeform(ret[4]) .. "</pre>")
tmueller@0
   351
		end
tmueller@0
   352
	end
tmueller@0
   353
end	
tmueller@0
   354
tmueller@0
   355
tmueller@129
   356
function loona:lockfile(file)
tmueller@129
   357
	return not self.session and true or 
tmueller@129
   358
		posix.symlink(self.session.filename, file .. ".LOCK")
tmueller@0
   359
end
tmueller@0
   360
tmueller@0
   361
tmueller@129
   362
function loona:unlockfile(file)
tmueller@129
   363
	return not self.session and true or remove(file .. ".LOCK")
tmueller@0
   364
end
tmueller@0
   365
tmueller@0
   366
tmueller@129
   367
function loona:saveindex()
tmueller@129
   368
	local tempname = self.indexfname .. "." .. self.session.name
tmueller@124
   369
	local f, msg = open(tempname, "wb")
tmueller@129
   370
	assert(f, self:dbmsg("Error opening section file for writing", msg))
tmueller@129
   371
	tek.dump(self.sections, function(...)
tmueller@124
   372
		f:write(unpack(arg))
tmueller@124
   373
	end)
tmueller@124
   374
	f:close()
tmueller@129
   375
	local success, msg = rename(tempname, self.indexfname)
tmueller@129
   376
	assert(success, self:dbmsg("Error renaming section file", msg))
tmueller@124
   377
end
tmueller@124
   378
tmueller@124
   379
tmueller@129
   380
function loona:savebody(fname, content)
tmueller@129
   381
	fname = self.contentdir .. "/" .. fname
tmueller@0
   382
	local f, msg = open(fname, "wb")
tmueller@129
   383
	assert(f, self:dbmsg("Could not open file for writing", msg))
tmueller@0
   384
	f:write(content or "")
tmueller@0
   385
	f:close()
tmueller@0
   386
end
tmueller@0
   387
tmueller@23
   388
tmueller@129
   389
function loona:runboxed(func, envitems, ...)
tmueller@129
   390
	local fenv = {
tmueller@129
   391
 		arg = arg,
tmueller@129
   392
 		loona = self
tmueller@129
   393
 	}
tmueller@129
   394
 	if envitems then
tmueller@129
   395
	 	for k, v in pairs(envitems) do
tmueller@129
   396
 			fenv[k] = v
tmueller@129
   397
 		end
tmueller@129
   398
 	end
tmueller@129
   399
 	setmetatable(fenv, { __index = boxed_G }) -- boxed global environment
tmueller@129
   400
	setfenv(func, fenv)
tmueller@129
   401
	return func()
tmueller@129
   402
end
tmueller@0
   403
tmueller@129
   404
tmueller@129
   405
function loona:include(fname, ...)
tmueller@129
   406
	assert(not fname:match("%W"), self:dbmsg("Invalid include name", fname))
tmueller@129
   407
	local fname2 = ("%s/%s.lua"):format(self.config.extdir, fname)
tmueller@0
   408
	local f, msg = open(fname2)
tmueller@129
   409
	assert(f, self:dbmsg("Cannot open file", msg))
tmueller@129
   410
	local parsed, msg = self:loadhtml(f, "loona:out", fname2)
tmueller@129
   411
	assert(parsed, self:dbmsg("Syntax error", msg))
tmueller@129
   412
	return self:runboxed(parsed)
tmueller@0
   413
end
tmueller@0
   414
tmueller@0
   415
tmueller@68
   416
--	produce link target, propagate lang, profile, session
tmueller@68
   417
tmueller@129
   418
function loona:href(section, ...)
tmueller@129
   419
	local target = self:getdocname(section)
tmueller@129
   420
-- 	if self.session or self.profile ~= self.pubprofile then
tmueller@129
   421
	if self.session then
tmueller@68
   422
		return tek.web.gethref(target, { "profile", "session", "lang", 
tmueller@68
   423
			unpack(arg) })
tmueller@0
   424
	end
tmueller@68
   425
	return tek.web.gethref(target, { "lang", unpack(arg) })
tmueller@0
   426
end
tmueller@0
   427
tmueller@129
   428
function loona:ilink(target, text, extra)
tmueller@68
   429
	return ('<a href="%s"%s>%s</a>'):format(target, extra or "", text)
tmueller@0
   430
end
tmueller@0
   431
tmueller@68
   432
--	internal link, propagation of lang, profile, session
tmueller@0
   433
tmueller@129
   434
function loona:link(section, text, ...)
tmueller@129
   435
	return self:ilink(self:href(section, unpack(arg)), text or section)
tmueller@0
   436
end
tmueller@0
   437
tmueller@68
   438
--	external link (opens in a new window), no argument propagation
tmueller@0
   439
tmueller@129
   440
function loona:elink(target, text)
tmueller@129
   441
	return self:ilink(target, text or target, not self.config.extlinksamewindow and
tmueller@83
   442
		' class="extlink" onclick="void(window.open(this.href, \'\', \'\')); return false;"')
tmueller@0
   443
end
tmueller@0
   444
tmueller@68
   445
--	plain link, no argument propagation
tmueller@68
   446
tmueller@129
   447
function loona:plink(section, text, ...)
tmueller@129
   448
	return self:ilink(tek.web.gethref(section, arg), text or section)
tmueller@68
   449
end
tmueller@68
   450
tmueller@68
   451
--	user interface link, propagation of lang, profile, session
tmueller@68
   452
tmueller@129
   453
function loona:uilink(section, text, ...)
tmueller@129
   454
	return self:ilink(self:href(section, unpack(arg)), text or section)
tmueller@68
   455
end
tmueller@68
   456
tmueller@68
   457
--	produce a hidden input value in forms
tmueller@0
   458
tmueller@129
   459
function loona:hidden(name, value)
tmueller@68
   460
	return not value and "" or
tmueller@68
   461
		('<input type="hidden" name="%s" value="%s" />'):format(name, value)
tmueller@0
   462
end
tmueller@0
   463
tmueller@0
   464
tmueller@129
   465
function loona:getprofiles(lang)
tmueller@129
   466
	lang = lang or self.lang
tmueller@129
   467
	local dir = self.config.contentdir
tmueller@0
   468
	local t = { }
tmueller@129
   469
	for f in tek.util.readdir(dir) do
tmueller@129
   470
		if posix.lstat(dir .. "/" .. f, "mode") == "directory" then
tmueller@0
   471
			local e = f:match("^(%w+)_" .. lang .. "$")
tmueller@0
   472
 			if e then
tmueller@0
   473
	 			t[e] = e
tmueller@0
   474
 	 		end
tmueller@0
   475
		end
tmueller@0
   476
	end
tmueller@0
   477
	return t
tmueller@0
   478
end
tmueller@0
   479
tmueller@0
   480
tmueller@80
   481
--	Functions to produce a hierarchical navigation menu
tmueller@80
   482
tmueller@129
   483
local newent = { name = "new", label = "[+]", action="actionnew=true" }
tmueller@124
   484
tmueller@129
   485
function loona:rmenu(level, linkf, path)
tmueller@129
   486
	local sub = (self.authuser and level == #self.submenus + 1) and
tmueller@129
   487
		{ name = "new", entries = { }} or self.submenus[level]
tmueller@124
   488
 	if sub and sub.entries then
tmueller@80
   489
		local visible = { }
tmueller@80
   490
		for _, e in ipairs(sub.entries) do
tmueller@80
   491
			if not e.notvisible then
tmueller@80
   492
				table.insert(visible, e)
tmueller@80
   493
			end
tmueller@80
   494
		end
tmueller@129
   495
		if self.authuser then
tmueller@124
   496
			table.insert(visible, newent)
tmueller@124
   497
		end
tmueller@80
   498
		if #visible > 0 then
tmueller@129
   499
			self:out('<ul id="menulevel' .. level .. '">\n')
tmueller@80
   500
			for _, e in ipairs(visible) do
tmueller@129
   501
				local label = self:encodeform(e.label or e.name)
tmueller@80
   502
				local newpath = path and path .. "/" .. e.name or e.name
tmueller@80
   503
				local active = (e.name == sub.name)
tmueller@129
   504
				self:out('<li>\n')
tmueller@129
   505
				linkf(self, newpath, label, active, e.action)
tmueller@80
   506
				if active then
tmueller@129
   507
					self:rmenu(level + 1, linkf, newpath)
tmueller@80
   508
				end
tmueller@129
   509
				self:out('</li>\n')
tmueller@80
   510
			end
tmueller@129
   511
			self:out('</ul>\n')
tmueller@80
   512
		end
tmueller@80
   513
	end
tmueller@80
   514
end
tmueller@80
   515
tmueller@129
   516
function loona:menulink(path, label, active, ...)
tmueller@129
   517
	self:out(('<a %shref="%s">%s</a>\n'):format(active and 'class="active" ' or "",
tmueller@129
   518
		self:href(path, unpack(arg)), label))
tmueller@80
   519
end
tmueller@80
   520
tmueller@129
   521
function loona:menu(level, linkf)
tmueller@129
   522
	self:rmenu(level or 1, linkf or menulink)
tmueller@80
   523
end
tmueller@80
   524
tmueller@80
   525
tmueller@129
   526
function loona:init()
tmueller@0
   527
	
tmueller@0
   528
	-- get list of languages, in order of preference
tmueller@141
   529
	-- TODO: respect quality parameter, not just order
tmueller@0
   530
	
tmueller@141
   531
	local l = self.requestlang or self.args.lang
tmueller@141
   532
	self.langs = { l and l:match("^%w+$") }
tmueller@129
   533
	if self.config.browserlang then
tmueller@0
   534
		local s = getenv("HTTP_ACCEPT_LANGUAGE")
tmueller@0
   535
		while s do
tmueller@0
   536
			local l, r = s:match("^([%w.=]+)[,;](.*)$")
tmueller@0
   537
			l = l or s
tmueller@0
   538
			s = r
tmueller@0
   539
			if l:match("^%w+$") then
tmueller@129
   540
				table.insert(self.langs, l)
tmueller@0
   541
			end
tmueller@0
   542
		end
tmueller@0
   543
	end
tmueller@129
   544
	table.insert(self.langs, self.config.deflang)
tmueller@0
   545
	
tmueller@0
   546
	-- get list of possible profiles
tmueller@0
   547
	
tmueller@0
   548
	local profiles = { }
tmueller@129
   549
	for e in tek.util.readdir(self.config.contentdir) do
tmueller@0
   550
		profiles[e] = e
tmueller@0
   551
	end
tmueller@0
   552
	
tmueller@0
   553
	-- get pubprofile
tmueller@0
   554
	
tmueller@129
   555
	for _, lang in ipairs(self.langs) do
tmueller@129
   556
		local p = posix.readlink(self.config.contentdir .. "/current_" .. lang)
tmueller@0
   557
		p = p and p:match("^(%w+)_" .. lang .. "$")
tmueller@0
   558
		if p then
tmueller@129
   559
			self.pubprofile = p
tmueller@0
   560
			break
tmueller@0
   561
		end
tmueller@0
   562
	end
tmueller@0
   563
	
tmueller@0
   564
	-- get profile
tmueller@0
   565
	
tmueller@129
   566
	local checkprofile = self.authuser and self.args.profile or self.pubprofile or "default"
tmueller@129
   567
	for _, lang in ipairs(self.langs) do
tmueller@129
   568
		if profiles[checkprofile .. "_" .. lang] then
tmueller@129
   569
			self.profile = checkprofile
tmueller@129
   570
			self.lang = lang
tmueller@0
   571
			break
tmueller@0
   572
		end
tmueller@0
   573
	end
tmueller@0
   574
	
tmueller@129
   575
	assert(self.profile and self.lang, "Invalid profile or language")
tmueller@0
   576
	
tmueller@47
   577
	
tmueller@129
   578
	-- write back language and profile
tmueller@0
   579
tmueller@129
   580
	self.args.lang = self.lang ~= self.config.deflang and self.lang or nil
tmueller@129
   581
	self.args.profile = self.profile
tmueller@0
   582
	
tmueller@47
   583
	
tmueller@0
   584
	-- determine content directory pathname and section filename
tmueller@0
   585
	
tmueller@129
   586
	self.contentdir = ("%s/%s_%s"):format(self.config.contentdir, self.profile, self.lang)
tmueller@129
   587
 	self.indexfname = self.contentdir .. "/.sections"
tmueller@0
   588
	
tmueller@0
   589
	-- load sections
tmueller@0
   590
	
tmueller@129
   591
 	self.sections = tek.source(self.indexfname)
tmueller@0
   592
	
tmueller@20
   593
	-- index sections, determine visibility in menu
tmueller@0
   594
	
tmueller@129
   595
	self:indexsections()
tmueller@0
   596
	
tmueller@129
   597
	-- decompose request path, produce a stack of sections
tmueller@0
   598
	
tmueller@129
   599
	self.submenus, self.section = self:getsection(self.requestpath)
tmueller@0
   600
tmueller@0
   601
	-- handle redirects if not logged on
tmueller@0
   602
	
tmueller@129
   603
	if not self.authuser and self.section and self.section.redirect then
tmueller@129
   604
		self.submenus, self.section = self:getsection(self.section.redirect)
tmueller@0
   605
	end
tmueller@0
   606
			
tmueller@0
   607
	-- section path and document name (refined)
tmueller@0
   608
	
tmueller@129
   609
	self.sectionpath = self:getpath()
tmueller@129
   610
	self.sectionname = self:getpath("_")
tmueller@0
   611
tmueller@0
   612
end
tmueller@0
   613
tmueller@0
   614
tmueller@129
   615
function loona:handlechanges()
tmueller@129
   616
	
tmueller@129
   617
	local save
tmueller@0
   618
tmueller@129
   619
	if self.args.editkey == "main" then
tmueller@23
   620
		
tmueller@23
   621
		-- In main editable section:
tmueller@23
   622
		
tmueller@129
   623
		if self.args.actioncreate then
tmueller@129
   624
			
tmueller@45
   625
			-- Create new section
tmueller@129
   626
			
tmueller@129
   627
			local editname = self.args.editname:lower()
tmueller@20
   628
			assert(not editname:match("%W"),
tmueller@47
   629
				dbmsg("Invalid section name", editname))
tmueller@20
   630
			if not (section and (section.subs or section)[editname]) then
tmueller@20
   631
				local newpath = 
tmueller@129
   632
					(self.sectionpath and (self.sectionpath .. "/")) .. editname
tmueller@129
   633
				local s = self:addpath(newpath, { name = editname,
tmueller@129
   634
					label = self.args.editlabel ~= "" and self.args.editlabel or nil,
tmueller@129
   635
					title = self.args.edittitle ~= "" and self.args.edittitle or nil,
tmueller@129
   636
					redirect = self.args.editredirect ~= "" and self.args.editredirect or nil,
tmueller@129
   637
					hidden = self.args.editvisibility and true,
tmueller@129
   638
					secret = self.args.editsecrecy and true,
tmueller@129
   639
					secure = self.args.editsecure and true,
tmueller@129
   640
					creator = self.authuser,
tmueller@20
   641
					creationdate = time() })
tmueller@23
   642
				save = true
tmueller@0
   643
			end
tmueller@20
   644
		
tmueller@129
   645
		elseif self.args.actionsave then
tmueller@129
   646
			
tmueller@45
   647
			-- Save section
tmueller@129
   648
			
tmueller@129
   649
			self.section.revisiondate = time()
tmueller@129
   650
			self.section.revisioner = self.authuser
tmueller@129
   651
			save = true
tmueller@129
   652
 		
tmueller@129
   653
		elseif self.args.actionsaveprops then
tmueller@129
   654
			
tmueller@129
   655
			-- Save properties
tmueller@129
   656
			
tmueller@129
   657
			self.section.hidden = self.args.editvisibility and true
tmueller@129
   658
			self.section.secret = self.args.editsecrecy and true
tmueller@129
   659
			self.section.secure = self.args.editsecure and true
tmueller@129
   660
			self.section.label = self.args.editlabel ~= "" and self.args.editlabel or nil
tmueller@129
   661
			self.section.title = self.args.edittitle ~= "" and self.args.edittitle or nil
tmueller@129
   662
			self.section.redirect =
tmueller@129
   663
				self.args.editredirect ~= "" and self.args.editredirect or nil
tmueller@23
   664
			save = true
tmueller@20
   665
		
tmueller@129
   666
		elseif self.args.actionup then
tmueller@129
   667
			
tmueller@45
   668
			-- Move section up
tmueller@129
   669
			
tmueller@129
   670
			local t, i = self:checkpath(self.sectionpath)
tmueller@0
   671
			if t and i > 1 then
tmueller@129
   672
				if self.profile == self.pubprofile and not self.args.actionconfirm then
tmueller@115
   673
					useralert = {
tmueller@129
   674
						text = self.locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
tmueller@115
   675
						confirm =
tmueller@115
   676
							'<input type="submit" name="actionup" value="' .. 
tmueller@129
   677
							self.locale.MOVE .. '" /> ' ..
tmueller@129
   678
							self:hidden("actionconfirm", "true")
tmueller@115
   679
					}
tmueller@115
   680
				else
tmueller@115
   681
					local item = table.remove(t, i)
tmueller@115
   682
					table.insert(t, i - 1, item)
tmueller@115
   683
					save = true
tmueller@115
   684
				end
tmueller@0
   685
			end
tmueller@20
   686
		
tmueller@129
   687
		elseif self.args.actiondown then
tmueller@129
   688
			
tmueller@45
   689
			-- Move section down
tmueller@129
   690
			
tmueller@129
   691
			local t, i = self:checkpath(self.sectionpath)
tmueller@0
   692
			if t and i < #t then
tmueller@129
   693
				if self.profile == self.pubprofile and not self.args.actionconfirm then
tmueller@115
   694
					useralert = {
tmueller@129
   695
						text = self.locale.ALERT_MOVE_SECTION_IN_PUBLISHED_PROFILE,
tmueller@115
   696
						confirm =
tmueller@115
   697
							'<input type="submit" name="actiondown" value="' .. 
tmueller@129
   698
							self.locale.MOVE .. '" /> ' ..
tmueller@129
   699
							self:hidden("actionconfirm", "true")
tmueller@115
   700
					}
tmueller@115
   701
				else
tmueller@115
   702
					local item = table.remove(t, i)
tmueller@115
   703
					table.insert(t, i + 1, item)
tmueller@115
   704
					save = true
tmueller@115
   705
				end
tmueller@0
   706
			end
tmueller@20
   707
		
tmueller@129
   708
		elseif self.args.actioncreateprofile and self.args.createprofile then
tmueller@129
   709
			
tmueller@23
   710
			-- Create profile
tmueller@129
   711
			
tmueller@129
   712
			local c = self:checkprofilename(self.args.createprofile:lower())
tmueller@0
   713
			if c == profile then
tmueller@129
   714
				useralert = { text = self.locale.ALERT_CANNOT_COPY_PROFILE_TO_SELF }
tmueller@0
   715
			else
tmueller@129
   716
				local profiles = self:getprofiles()
tmueller@129
   717
				if profiles[c] and not self.args.actionconfirm then
tmueller@20
   718
					useralert = {
tmueller@129
   719
						text = c == self.pubprofile and 
tmueller@129
   720
							self.locale.ALERT_OVERWRITE_PUBLISHED_PROFILE or
tmueller@129
   721
							self.locale.ALERT_OVERWRITE_EXISTING_PROFILE,
tmueller@0
   722
						confirm =
tmueller@20
   723
							'<input type="submit" name="actioncreateprofile" value="' .. 
tmueller@129
   724
							self.locale.OVERWRITE .. '" /> ' ..
tmueller@129
   725
							self:hidden("actionconfirm", "true") .. 
tmueller@129
   726
							self:hidden("createprofile", c)
tmueller@20
   727
					}
tmueller@0
   728
				else
tmueller@0
   729
					if profiles[c] then
tmueller@129
   730
						self:deleteprofile(c)
tmueller@0
   731
					end
tmueller@129
   732
					self:copyprofile(c)
tmueller@0
   733
				end
tmueller@0
   734
			end
tmueller@20
   735
		
tmueller@129
   736
		elseif self.args.actiondeleteprofile and self.args.deleteprofile then
tmueller@129
   737
			
tmueller@23
   738
			-- Delete profile
tmueller@129
   739
			
tmueller@129
   740
			local c = self:checkprofilename(self.args.deleteprofile:lower())
tmueller@129
   741
			assert(c ~= self.pubprofile, self:dbmsg("Cannot delete published profile", c))
tmueller@129
   742
			if self.args.actionconfirm then
tmueller@129
   743
				self:deleteprofile(c)
tmueller@129
   744
				self.profile = nil
tmueller@129
   745
				self.args.profile = nil
tmueller@129
   746
				self:init()
tmueller@23
   747
				save = true
tmueller@0
   748
			else
tmueller@20
   749
				useralert = { 
tmueller@129
   750
					text = self.locale.ALERT_DELETE_PROFILE,
tmueller@20
   751
					confirm = 
tmueller@20
   752
						'<input type="submit" name="actiondeleteprofile" value="' .. 
tmueller@129
   753
						self.locale.DELETE .. '" /> ' ..
tmueller@129
   754
						self:hidden("actionconfirm", "true") ..
tmueller@129
   755
						self:hidden("deleteprofile", c)
tmueller@20
   756
				}
tmueller@0
   757
			end
tmueller@20
   758
		
tmueller@129
   759
		elseif self.args.actionchangeprofile and self.args.changeprofile then
tmueller@129
   760
			
tmueller@23
   761
			-- Change profile
tmueller@129
   762
			
tmueller@129
   763
			local c = self:checkprofilename(self.args.changeprofile:lower())
tmueller@129
   764
			self.profile = c
tmueller@129
   765
			self.args.profile = c
tmueller@23
   766
			save = true
tmueller@20
   767
		
tmueller@129
   768
		elseif self.args.actionpublishprofile and self.args.publishprofile then
tmueller@129
   769
			
tmueller@23
   770
			-- Publish profile
tmueller@129
   771
			
tmueller@129
   772
			local c = self:checkprofilename(self.args.publishprofile:lower())
tmueller@129
   773
			if c ~= self.publicprofile then
tmueller@129
   774
				if self.args.actionconfirm then
tmueller@129
   775
					self:publishprofile(c)
tmueller@23
   776
					save = true
tmueller@0
   777
				else
tmueller@20
   778
					useralert = {
tmueller@129
   779
						text = self.locale.ALERT_PUBLISH_PROFILE,
tmueller@20
   780
						confirm =
tmueller@20
   781
							'<input type="submit" name="actionpublishprofile" value="' ..
tmueller@129
   782
							self.locale.PUBLISH .. '" /> ' ..
tmueller@129
   783
							self:hidden("actionconfirm", "true") ..
tmueller@129
   784
							self:hidden("publishprofile", c)
tmueller@20
   785
					}
tmueller@20
   786
				end
tmueller@20
   787
			end
tmueller@0
   788
		end
tmueller@0
   789
		
tmueller@129
   790
	end
tmueller@0
   791
	
tmueller@129
   792
	if self.args.actiondelete then
tmueller@129
   793
		
tmueller@129
   794
		-- Delete section
tmueller@129
   795
		
tmueller@129
   796
		if not self.args.actionconfirm then
tmueller@129
   797
			useralert = {
tmueller@129
   798
				text = self.profile == self.pubprofile and
tmueller@129
   799
					self.locale.ALERT_DELETE_SECTION_IN_PUBLISHED_PROFILE or
tmueller@129
   800
					self.locale.ALERT_DELETE_SECTION,
tmueller@129
   801
				confirm =
tmueller@129
   802
					'<input type="submit" name="actiondelete" value="' .. 
tmueller@129
   803
					self.locale.DELETE .. '" /> ' ..
tmueller@129
   804
					self:hidden("actionconfirm", "true")
tmueller@129
   805
			}
tmueller@129
   806
		else
tmueller@129
   807
			local key = self.args.editkey
tmueller@129
   808
			if key == "main" and not self.section.subs then
tmueller@129
   809
				self:deletesection(self.sectionname, true) -- all bodies
tmueller@129
   810
				self:rmpath(self.sectionpath) -- and node
tmueller@129
   811
			else
tmueller@129
   812
				local ext = (key == "main" and "") or "." .. key
tmueller@129
   813
				self:deletesection(self.sectionname .. ext) -- only text
tmueller@129
   814
				if self.section.dynamic then
tmueller@129
   815
					self.section.dynamic[key] = nil
tmueller@131
   816
					local n = 0
tmueller@131
   817
					for _ in pairs(self.section.dynamic) do
tmueller@131
   818
						n = n + 1
tmueller@131
   819
					end
tmueller@131
   820
					if n == 0 then
tmueller@131
   821
						self.section.dynamic = nil
tmueller@131
   822
					end
tmueller@129
   823
				end
tmueller@129
   824
			end
tmueller@129
   825
			save = true
tmueller@0
   826
		end
tmueller@0
   827
	end
tmueller@129
   828
		
tmueller@129
   829
	if save then
tmueller@129
   830
		self:saveindex()
tmueller@129
   831
		self:init()
tmueller@129
   832
	end
tmueller@129
   833
	
tmueller@0
   834
end
tmueller@0
   835
tmueller@0
   836
tmueller@129
   837
function loona:encodeform(s)
tmueller@129
   838
	return cgi.encodeform(s)
tmueller@129
   839
end
tmueller@0
   840
tmueller@80
   841
tmueller@129
   842
function loona:loadhtml(src, outfunc, chunkname)
tmueller@129
   843
	return tek.web.include.load(src, outfunc, chunkname)
tmueller@129
   844
end
tmueller@80
   845
tmueller@80
   846
tmueller@129
   847
function loona:domarkup(s)
tmueller@129
   848
	return tek.web.markup.main(s)
tmueller@129
   849
end
tmueller@0
   850
tmueller@0
   851
tmueller@129
   852
function loona:expire(dir, pat, maxage)
tmueller@129
   853
	return tek.util.expire(dir, pat, maxage)
tmueller@129
   854
end
tmueller@129
   855
tmueller@129
   856
--
tmueller@129
   857
--	Get pathname of an existing content file that
tmueller@129
   858
--	the current path is determined by or defaults to
tmueller@129
   859
--
tmueller@129
   860
tmueller@129
   861
function loona:getsectionpath(bodyname, requestpath)
tmueller@129
   862
	local ext = (not bodyname or bodyname == "main") and "" or "." .. bodyname
tmueller@129
   863
	local t, path, section = { }
tmueller@129
   864
	for _, menu in ipairs(self.submenus) do
tmueller@129
   865
		if menu.entries and menu.entries[menu.name] then
tmueller@129
   866
			table.insert(t, menu.name)
tmueller@129
   867
			local fn = table.concat(t, "_")
tmueller@129
   868
			if posix.stat(self.contentdir .. "/" .. fn .. ext, "mode") == "file" then
tmueller@129
   869
				path, section = fn, menu
tmueller@129
   870
			end
tmueller@0
   871
		end
tmueller@0
   872
	end
tmueller@129
   873
	return path, ext, section
tmueller@0
   874
end
tmueller@0
   875
tmueller@20
   876
tmueller@129
   877
function loona:body(name)
tmueller@129
   878
	name = self:checkbodyname(name)
tmueller@129
   879
	local path, ext = self:getsectionpath(name)
tmueller@129
   880
	self:dosnippet(self.editable(name, path and path .. ext, self.sectionname .. ext))
tmueller@0
   881
end
tmueller@0
   882
tmueller@23
   883
tmueller@129
   884
function loona:new(o)
tmueller@0
   885
tmueller@129
   886
	local parsed, msg
tmueller@129
   887
	
tmueller@129
   888
	o = o or { }
tmueller@129
   889
	o = atom.new(self, o)
tmueller@129
   890
	
tmueller@129
   891
	o.out = o.out or function(self, s) tek.web.out(s) end
tmueller@129
   892
	o.setheader = o.setheader or function(self, s) tek.web.setheader(s) end
tmueller@129
   893
	
tmueller@129
   894
	-- Get configuration
tmueller@129
   895
	
tmueller@129
   896
	o.config = o.config or tek.source(o.conffile or "../etc/config.lua") or { }
tmueller@129
   897
	o.config.defname = o.config.defname or "home"
tmueller@129
   898
	o.config.deflang = o.config.deflang or "en"
tmueller@129
   899
	o.config.sessionmaxage = o.config.sessionmaxage or 600
tmueller@129
   900
	o.config.secureport = o.config.secureport or 443
tmueller@129
   901
	o.config.passwdfile = posix.abspath(o.config.passwdfile or "../etc/passwd.lua")
tmueller@129
   902
	o.config.sessiondir = posix.abspath(o.config.sessiondir or "../var/sessions")
tmueller@129
   903
	o.config.extdir = posix.abspath(o.config.extdir or "../extensions")
tmueller@129
   904
	o.config.contentdir = posix.abspath(o.config.contentdir or "../content")
tmueller@129
   905
	o.config.localedir = posix.abspath(o.config.localedir or "../locale")
tmueller@129
   906
	o.config.htdocsdir = posix.abspath(o.config.htdocsdir or "../htdocs")
tmueller@131
   907
	o.config.htmlcachedir = posix.abspath(o.config.htmlcachedir or "../var/htmlcache")
tmueller@129
   908
	
tmueller@129
   909
	-- Create proxy for on-demand loading of locales
tmueller@129
   910
	
tmueller@129
   911
	o.locale = { }
tmueller@129
   912
	local locmt = { }
tmueller@129
   913
	locmt.__index = function(_, key)
tmueller@129
   914
		for _, l in ipairs(o.langs) do
tmueller@129
   915
			locmt.__locale = tek.source(o.config.localedir .. "/" .. l)
tmueller@129
   916
			if locmt.__locale then
tmueller@129
   917
				break
tmueller@129
   918
			end
tmueller@129
   919
		end
tmueller@129
   920
		locmt.__index = function(tab, key)
tmueller@129
   921
			return locmt.__locale[key] or key
tmueller@129
   922
		end
tmueller@129
   923
		return locmt.__locale[key] or key
tmueller@129
   924
	end
tmueller@129
   925
	setmetatable(o.locale, locmt)
tmueller@129
   926
	
tmueller@129
   927
	-- Get request, args, document, script name, request path
tmueller@129
   928
	
tmueller@129
   929
	o.request = o.request or cgi.request
tmueller@129
   930
	o.args = o.args or cgi.request.args
tmueller@129
   931
	o.session = o.session or cgi.session
tmueller@129
   932
 	
tmueller@129
   933
 	o.scriptpath = o.scriptpath or cgi.document.Path
tmueller@129
   934
	o.requesthandler = o.requesthandler or cgi.document.Handler
tmueller@129
   935
 	o.requestdocument = o.requestdocument or cgi.document.Name
tmueller@129
   936
	o.requestpath = o.requestpath or cgi.document.VirtualPath
tmueller@129
   937
tmueller@129
   938
	-- Manage login and establish session
tmueller@129
   939
	
tmueller@129
   940
	o.session.init(o.config.sessiondir, o.args.session, o.config.sessionmaxage)
tmueller@129
   941
	if o.args.login then
tmueller@129
   942
		if o.args.login == "false" then
tmueller@129
   943
			o.session.delete()
tmueller@129
   944
			o.session = nil
tmueller@129
   945
		elseif o.args.password then
tmueller@129
   946
			o.loginfailed = true
tmueller@129
   947
			local pwddb = tek.source(o.config.passwdfile)
tmueller@129
   948
			local pwdentry = pwddb[o.args.login]
tmueller@129
   949
			if pwdentry and pwdentry.password == o.args.password then
tmueller@129
   950
				o.session.data.authuser = pwdentry.username
tmueller@129
   951
				o.session.data.id = o.session.id
tmueller@129
   952
				o.loginfailed = nil
tmueller@129
   953
			end
tmueller@129
   954
		end
tmueller@129
   955
	end
tmueller@129
   956
	
tmueller@129
   957
	o.secure = o.request.Port == o.config.secureport
tmueller@129
   958
	o.authuser = o.session and o.session.data.authuser
tmueller@129
   959
	
tmueller@129
   960
	if o.nologin or not o.authuser then
tmueller@129
   961
		o.authuser = nil
tmueller@129
   962
		o.session = nil
tmueller@129
   963
-- 		o.args.session = nil -- TODO?
tmueller@129
   964
	end
tmueller@129
   965
	
tmueller@129
   966
	-- Get lang, locale, profile, section
tmueller@129
   967
	
tmueller@129
   968
	o:init()
tmueller@129
   969
	if o.authuser then
tmueller@129
   970
		o:handlechanges()
tmueller@129
   971
	end
tmueller@129
   972
	
tmueller@129
   973
	-- Current document
tmueller@129
   974
	
tmueller@129
   975
	o.document = o.requestdocument .. "/" .. o.sectionpath
tmueller@129
   976
	if o.authuser then
tmueller@129
   977
		o.getdocname = function(self, path)
tmueller@129
   978
			return path and self.requestdocument .. "/" .. path or self.requestdocument
tmueller@129
   979
		end
tmueller@129
   980
	else
tmueller@129
   981
		o.getdocname = function(self, path)
tmueller@129
   982
			path = path or self.config.defname
tmueller@129
   983
			if self:isdynamic(path) then
tmueller@129
   984
				return self.requestdocument .. "/" .. path
tmueller@129
   985
			end
tmueller@129
   986
			path = path == self.config.defname and "index" or path
tmueller@129
   987
			return "/" .. path:gsub("/", "_") .. ".html"
tmueller@129
   988
		end
tmueller@129
   989
	end
tmueller@129
   990
	
tmueller@129
   991
	-- Create "editable section" function
tmueller@129
   992
	
tmueller@129
   993
	local func, msg = o:loadhtml(open("loona/editable.lua"),
tmueller@129
   994
		"loona:out", "loona/editable.lua")
tmueller@129
   995
 	assert(func, o:dbmsg("Syntax error", msg))
tmueller@129
   996
	o.editable = o:runboxed(func)
tmueller@129
   997
	
tmueller@129
   998
	-- Save session state
tmueller@129
   999
	
tmueller@129
  1000
	if o.session then
tmueller@129
  1001
		o.session.save()
tmueller@129
  1002
	end
tmueller@129
  1003
tmueller@129
  1004
	return o
tmueller@0
  1005
end
tmueller@0
  1006
tmueller@23
  1007
tmueller@129
  1008
function loona:execute(fname)
tmueller@141
  1009
	self:indexdynamic()
tmueller@129
  1010
	fname = fname or self.requesthandler
tmueller@129
  1011
	local parsed, msg = self:loadhtml(open(fname), "loona:out", fname)
tmueller@129
  1012
	assert(parsed, self:dbmsg("HTML/Lua parsing failed", msg))
tmueller@129
  1013
	self:runboxed(parsed)
tmueller@141
  1014
	
tmueller@141
  1015
	
tmueller@141
  1016
		local fh = open("/tmp/dump", "wb")
tmueller@141
  1017
		tek.dump(self, function(s) fh:write(s) end)
tmueller@141
  1018
	
tmueller@141
  1019
	
tmueller@129
  1020
	return self
tmueller@129
  1021
end
tmueller@80
  1022
tmueller@80
  1023
tmueller@129
  1024
function loona:indexdynamic()
tmueller@129
  1025
	self:recursesections(self.sections, function(self, s, e, path, dynamic)
tmueller@129
  1026
		path = path and path .. "_" .. e.name or e.name
tmueller@129
  1027
		dynamic = dynamic or { }
tmueller@129
  1028
		for k in pairs(e.dynamic or { }) do
tmueller@129
  1029
			dynamic[k] = true
tmueller@129
  1030
		end
tmueller@129
  1031
		for k in pairs(dynamic) do
tmueller@129
  1032
			local ext = (k == "main" and "") or "." .. k
tmueller@129
  1033
			if posix.stat(self.contentdir .. "/" .. path .. ext, "mode") == "file" then
tmueller@129
  1034
				dynamic[k] = e.dynamic and e.dynamic[k]
tmueller@129
  1035
			end
tmueller@129
  1036
		end
tmueller@129
  1037
		local n = 0
tmueller@129
  1038
		for k in pairs(dynamic) do
tmueller@129
  1039
			n = n + 1
tmueller@129
  1040
		end
tmueller@129
  1041
		if n > 0 then
tmueller@129
  1042
			e.dynamic = { }
tmueller@129
  1043
			for k in pairs(dynamic) do
tmueller@129
  1044
				e.dynamic[k] = true
tmueller@129
  1045
			end
tmueller@129
  1046
		else
tmueller@129
  1047
			e.dynamic = nil
tmueller@129
  1048
		end
tmueller@129
  1049
		return path, dynamic
tmueller@129
  1050
	end)
tmueller@129
  1051
end
tmueller@80
  1052
tmueller@0
  1053
tmueller@129
  1054
function loona:isdynamic(path)
tmueller@129
  1055
	path = path or self.sectionpath
tmueller@129
  1056
	local t, i = self:checkpath(path)
tmueller@129
  1057
	return t and t[i].dynamic
tmueller@129
  1058
end
tmueller@0
  1059
tmueller@0
  1060
tmueller@129
  1061
function loona:dumphtml(o)
tmueller@129
  1062
	local outbuf = { }
tmueller@129
  1063
	o = o or { }
tmueller@129
  1064
	o.nologin = true
tmueller@129
  1065
	o.out = function(self, s) table.insert(outbuf, s) end
tmueller@129
  1066
	o.setheader = function(self, s) end
tmueller@129
  1067
	o = self:new(o):execute()
tmueller@129
  1068
	if not o:isdynamic() then
tmueller@129
  1069
		local path = o.sectionname
tmueller@129
  1070
		path = path == o.config.defname and "index" or path
tmueller@141
  1071
		local srcname = o.config.htdocsdir .. "/" .. path .. o.htmlext
tmueller@141
  1072
		local dstname = o.config.htmlcachedir .. "/" .. path .. o.htmlext .. ".tmp"
tmueller@131
  1073
		local fh, msg = open(srcname .. ".tmp", "wb")
tmueller@131
  1074
		assert(fh, self:dbmsg("Could not write cached HTML", msg))
tmueller@129
  1075
		fh:write(unpack(outbuf))
tmueller@129
  1076
		fh:close()
tmueller@131
  1077
		local success, msg = posix.symlink(srcname, dstname)
tmueller@137
  1078
-- 		assert(success, self:dbmsg("Could not link to cached HTML", msg))
tmueller@0
  1079
	end
tmueller@0
  1080
end