-----------------------------------------------------------------------------
-- Provides functions for collection of basic actions needed by a Sputnik to
-- serve as a wiki.
--
-- (c) 2007, 2008 Yuri Takhteyev (yuri@freewisdom.org)
--
-- License: MIT/X, see http://sputnik.freewisdom.org/en/License
-----------------------------------------------------------------------------
module(..., package.seeall)
require("coxpcall")
require("cosmo")
require("versium.util")
require("saci.sandbox")
local util = require("sputnik.util")
local html_forms = require("sputnik.util.html_forms")
-----------------------------------------------------------------------------
-- Creates the HTML for the navigation bar.
--
-- @param node A node table.
-- @param sputnik The Sputnik object.
-- @return An HTML string.
-----------------------------------------------------------------------------
function get_nav_bar (node, sputnik)
assert(node)
local nav_node = sputnik:get_node(sputnik.config.DEFAULT_NAVIGATION_BAR)
local nav = nav_node.content.NAVIGATION
local cur_node = sputnik:dirify(node.name)
local function matches(id, patterns)
patterns = patterns or {}
for i, pattern in ipairs(patterns) do
if id:match(pattern) then
return true
end
end
end
local function remove_quotes(value)
return value:gsub("'", ""):gsub('"', ''):gsub("%s+", " ")
end
local categories = {}
for i, section in ipairs(nav) do
section.title = section.title or section.id
categories[section.id] = section.title
section.accessibility_title = remove_quotes(section.title)
section.id = sputnik:dirify(section.id)
section.link = sputnik:make_link(section.id)
section.class = "back"
section.subsections = section
if section.id == cur_node or section.id == node.category
or matches(node.name, section.patterns) then
section.class = "front"
section.accessibility_title = section.accessibility_title.." (current section)"
nav.current_section = section
end
for j, subsection in ipairs(section) do
subsection.title = subsection.title or subsection.id
categories[subsection.id] = subsection.title
subsection.accessibility_title = remove_quotes(subsection.title)
subsection.id = sputnik:dirify(subsection.id)
subsection.class = "back"
subsection.link = sputnik:make_link(subsection.id)
if subsection.id == cur_node or subsection.id == sputnik:dirify(node.category)
or matches(node.name, subsection.patterns) then
section.class = "front"
nav.current_section = section
subsection.class = "front"
subsection.accessibility_title = subsection.accessibility_title
.." (current subsection)"
end
end
end
local default_navsection = sputnik.config.DEFAULT_NAVSECTION
if not nav.current_section and default_navsection and nav[default_navsection] then
nav[default_navsection].class = "front"
end
return nav, categories
end
-----------------------------------------------------------------------------
-- Checks the post parameters are OK.
-----------------------------------------------------------------------------
function check_post_parameters(node, request, sputnik)
local token = request.params.post_token
local timestamp = request.params.post_timestamp
local timeout = (sputnik.config.POST_TOKEN_TIMEOUT or 15) * 60
if not token then
return false, "MISSING_POST_TOKEN"
elseif not timestamp then
return false, "MISSING_POST_TIME_STAMP"
elseif (os.time()-tonumber(timestamp)) > timeout then
return false, "YOUR_POST_TOKEN_HAS_EXPIRED"
elseif sputnik.auth:timestamp_token(timestamp) ~= token then
return false, "YOUR_POST_TOKEN_IS_INVALID"
else
return true
end
end
--=========================================================================--
-- Actions - this is what this module is all about --
--=========================================================================--
actions = {}
--=========================================================================--
-- First the post actions - those are a bit tricker --
--=========================================================================--
-----------------------------------------------------------------------------
-- Handles all post actions. All "post" requests are routed through this
-- action ("post"). The reason for this is that we localize button labels,
-- and their values are not predictable for this reason. Instead, we we look
-- at the _name_ the button to infer the action. So, to request node.save via
-- post, we actually request node.post&action_save=foo, where foo could be
-- anything.
--
-- @param node
-- @param request We'll look for request.params.action_* to figure out
-- what we should be actually doing.
-- @return HTML (whatever is returned by the action that it
-- dispatches to).
-----------------------------------------------------------------------------
function actions.post(node, request, sputnik)
for k,v in pairs(request.params) do
local action = string.match(k, "^action_(.*)$")
if action then
function err_msg(err_code, message)
request.try_again = "true"
node:post_error(node.translator.translate_key(err_code)..(message or ""))
end
-- check the validity of the request
local ok, err = check_post_parameters(node, request, sputnik)
if not ok then
err_msg(err)
end
-- test captcha, if configured
if sputnik.captcha and not (request.user or request.params.user) then
local client_ip = request.wsapi_env.REMOTE_ADDR
local captcha_ok, err = sputnik.captcha:verify(request.POST, client_ip)
if not captcha_ok then
err_msg("COULD_NOT_VERIFY_CAPTCHA", err)
end
end
-- check if the user is allowed to do this
if not node:check_permissions(request.user, action) then
err_msg("ACTION_NOT_ALLOWED")
end
return node.actions[action](node, request, sputnik)
end
end
end
-----------------------------------------------------------------------------
-- Saves/updates the node based on query params, then redirects to the new
-- version of the node.
--
-- @param node
-- @param request request.params fields are used to update the node.
-- @param sputnik used to save and reload the node.
-----------------------------------------------------------------------------
function actions.save(node, request, sputnik)
if request.try_again then
return node.actions.edit(node, request, sputnik)
else
local new_node = sputnik:update_node_with_params(node, request.params)
new_node = sputnik:activate_node(new_node)
local extra = {minor=request.params.minor}
if not request.user then
extra.ip=request.wsapi_env.REMOTE_ADDR -- track IPs for anonymous
end
new_node = sputnik:save_node(new_node, request, request.user,
request.params.summary or "", extra)
-- redirect to the newly saved node
request.redirect = sputnik:make_url(new_node.name)
return
end
end
-----------------------------------------------------------------------------
-- Forces a re-initialization of Sputnik.
-----------------------------------------------------------------------------
function actions.reload(node, request, sputnik)
sputnik:init(sputnik.initial_config)
node.inner_html = "Reloading..."
request.redirect = sputnik:make_url(node.name)
return
end
-----------------------------------------------------------------------------
-- Updates the node with values in query parameters, then calls show_content.
-- This has the effect of showing us what the node would look like if we
-- saved it. Note that this action is, strictly speaking, idempotent and can
-- be called via GET. However, it's simpler to do it with post - for
-- symmetry with "save".
--
-- @param node
-- @param request request.params fields are used to update the node.
-- @param sputnik used to access update functionality.
-----------------------------------------------------------------------------
function actions.preview_content(node, request, sputnik)
local new_node = sputnik:update_node_with_params(node, request.params)
sputnik:activate_node(new_node)
return new_node.actions.show_content(new_node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Returns HTML showing a preview of the node (based on request.params) and
-- also a form to continue editing the node. (The node is _not_ saved.)
--
-- @param request request.params fields are used to update the node.
-- @param sputnik passed to preview_content().
-----------------------------------------------------------------------------
function actions.preview(node, request, sputnik)
request.preview = actions.preview_content(node, request, sputnik)
return actions.edit(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Handles the clicking of the "cancel" button from the edit form. This
-- action is idempotent and can be called via GET, but is submitted via POST,
-- for symmetry with "save".
--
-- @param node
-- @param request not used.
-- @param sputnik not used.
-----------------------------------------------------------------------------
function actions.cancel(node, request, sputnik)
-- Redirect to the show action
request.redirect = sputnik:make_url(node.name)
return
end
--=========================================================================--
-- Now the GET actions - those should all be idempotent --
--=========================================================================--
-----------------------------------------------------------------------------
-- Returns just the content of the node, without the navigation bar, the tool
-- bars, etc.
--
-- @param node
-- @param request not used.
-- @param sputnik not used.
-----------------------------------------------------------------------------
function actions.show_content(node, request, sputnik)
local title = ""
if request.params.show_title then
title = "
"..node.title.."
\n\n"
end
return title..node.markup.transform(node.content or "", node)
end
REDIRECTION_PROTOCOLS = {
http = true,
https = true,
}
function actions.redirect(node, request, sputnik)
local destination = node.redirect_destination
local protocol = destination:match("^[^%:]*")
if REDIRECTION_PROTOCOLS[protocol] or destination:sub(1,1)=="/" then
request.redirect = destination
else
request.redirect = sputnik:make_url(destination)
end
return
end
-----------------------------------------------------------------------------
-- Returns the complete HTML for the node.
--
-- @param node
-- @param request passed to show_content.
-- @param sputnik passed to show_content.
-----------------------------------------------------------------------------
TMPL = [=[
$note
$do_prototypes[[
 |
$name |
]]
]=]
function actions.show(node, request, sputnik)
if node.redirect_destination and node.redirect_destination~="" then
return actions.redirect(node, request, sputnik)
end
if node.is_a_stub then
request.is_indexable = false
--node:post_notice(node.translator.translate_key("PLEASE_PICK_A_TYPE_TO_CREATE_A_NEW_NODE"))
node.inner_html = cosmo.f(TMPL){
note = node.translator.translate_key("PLEASE_PICK_A_TYPE_TO_CREATE_A_NEW_NODE"),
icon_base_url = sputnik.config.ICON_BASE_URL or sputnik.config.NICE_URL,
do_prototypes = function()
local prototypes = sputnik.config.NEW_NODE_PROTOTYPES or {}
for i,v in ipairs(prototypes) do
cosmo.yield{
name =v.title or v[1],
icon = v.icon,
url =sputnik:make_url(node.id, "edit", {prototype=v[1]})
}
end
end
}
else
request.is_indexable = true
node.inner_html = node.actions.show_content(node, request, sputnik)
end
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Returns the history of changes by users who created their accounts only
-- recently.
--
-- @param node
-- @param request passed to complete_history().
-- @param sputnik passed to complete_history().
-----------------------------------------------------------------------------
function actions.edits_by_recent_users(node, request, sputnik)
request.params.recent_users_only = 1
return actions.complete_history(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Given an edit table, retursn either the author (if other than ""), or the
-- IP address of the edit (if defined), or "Anonymous."
-----------------------------------------------------------------------------
local function author_or_ip(edit)
if not edit.author or edit.author == "" then
if edit.ip then
return edit.ip, "User at IP "..edit.ip:gsub("%.", "-")
else
return "Anonymous", "Anonymous User"
end
elseif edit.author:match("@") then
return edit.author:gsub("@.*", "@..."), edit.author:gsub("@", "_at_")
else
return edit.author, edit.author
end
end
-----------------------------------------------------------------------------
-- Returns the history of this node as HTML.
--
-- @param node
-- @param request request.params.date is an optional filter.
-- @param sputnik used to access history.
-----------------------------------------------------------------------------
function actions.history(node, request, sputnik)
local history = sputnik:get_history(node.name, 200, request.params.date)
-- cosmo iterator for revisions
local function do_revisions()
local old_date = ""
local new_date = ""
for i, edit in ipairs(history) do
new_date = sputnik:format_time(edit.timestamp, "%Y/%m/%d")
if new_date ~= old_date then
cosmo.yield {
if_new_date = cosmo.c(true){
date = new_date
},
if_edit = cosmo.c(false){},
}
end
old_date = new_date
local author_display, author_id_for_link = author_or_ip(edit)
if (not request.params.recent_users_only)
or sputnik.auth:user_is_recent(edit.author) then
cosmo.yield{
version_link = sputnik:make_link(node.id, nil, {version = edit.version}),
version = edit.version,
date = sputnik:format_time(edit.timestamp, "%Y/%m/%d"),
time = sputnik:format_time(edit.timestamp, "%H:%M %z"),
if_minor = cosmo.c((edit.minor or ""):len() > 0){},
title = node.name,
author_link = sputnik:make_link(author_id_for_link),
author_icon = sputnik:get_user_icon(edit.author),
author = author_display,
if_summary = cosmo.c(edit.comment:len() > 0){
summary = util.escape(edit.comment)
},
if_new_date = cosmo.c(false){},
if_edit = cosmo.c(true){},
}
end
end
end
node.inner_html = cosmo.f(node.templates.HISTORY){
do_revisions = do_revisions, -- the function defined above
version = node.version,
node_name = node.name,
base_url = sputnik.config.BASE_URL,
}
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Return HTML representing history of edits to the whole wiki.
--
-- @param request request.params.date is an optional filter.
-----------------------------------------------------------------------------
function actions.complete_history(node, request, sputnik)
require("md5")
local edits = sputnik:get_complete_history(sputnik.config.MAX_ITEMS_IN_HISTORY or 200,
request.params.date,
request.params.prefix)
-- figure out which revisions are stale so that we could group them with
-- the later ones.
local latest = {}
local later
local function same_date(t1, t2)
local format = "%Y/%m/%d"
return sputnik:format_time(t1, format)==sputnik:format_time(t2, format)
end
for i, e in ipairs(edits) do
if later then
later.previous = e.version
end
later = e
if not (latest.id == e.id
and same_date(latest.timestamp, e.timestamp)) then
latest = e
latest.repeats = 0
--n.title_style = ""
else
latest.repeats = latest.repeats + 1
e.repeats = 0
e.stale = true -- this is the field we'll be checking later
end
end
-- the cosmo iterator over revisions
local old_date = ""
local new_date = ""
local function do_revisions()
for i, edit in ipairs(edits) do
new_date = sputnik:format_time(edit.timestamp, "%Y/%m/%d")
if new_date ~= old_date then
cosmo.yield {
if_new_date = cosmo.c(true){
date = new_date
},
if_edit = cosmo.c(false){},
}
end
old_date = new_date
if (not request.params.recent_users_only)
or sputnik.auth:user_is_recent(edit.author) then
local author_display, author_ip_for_link = author_or_ip(edit)
local is_minor = (edit.minor or ""):len() > 0
cosmo.yield{
version_link = sputnik:make_link(edit.id, nil, {version = edit.version}),
diff_link = sputnik:make_link(edit.id, "diff", {version=edit.version, other=edit.previous}),
diff_icon = sputnik:make_url("icons/diff", "png"),
history_link = sputnik:make_link(edit.id, "history"),
history_icon = sputnik:make_url("icons/history", "png"),
latest_link = sputnik:make_link(edit.id),
version = edit.version,
if_new_date = cosmo.c(false){},
if_edit = cosmo.c(true){},
time = sputnik:format_time(edit.timestamp, "%H:%M %z"),
if_minor = cosmo.c(is_minor){},
title = edit.id,
author_link = sputnik:make_link(author_ip_for_link),
author_icon = sputnik:get_user_icon(edit.author),
author = author_display,
if_summary = cosmo.c(edit.comment and edit.comment:len() > 0){
summary = edit.comment
},
if_stale = cosmo.c(edit.stale){},
row_span = edit.repeats + 1,
}
end
end
end
node.inner_html = cosmo.f(node.templates.COMPLETE_HISTORY){
do_revisions = do_revisions, -- function defined above
version = node.version,
base_url = sputnik.config.BASE_URL,
node_name = node.name,
}
return node.wrappers.default(node, request, sputnik)
end
--=========================================================================--
-- Now all the actions that return XML --
--=========================================================================--
-----------------------------------------------------------------------------
-- Returns RSS of recent changes to this node or all nodes.
-----------------------------------------------------------------------------
function actions.rss(node, request, sputnik)
local title, history
if request.show_complete_history then
title = "Recent Wiki Edits" --::LOCALIZE::
edits = sputnik:get_complete_history(sputnik.config.MAX_ITEMS_IN_HISTORY or 50,
request.params.date,
request.params.prefix)
else
title = "Recent Edits to '" .. node.title .."'" --::LOCALIZE::--
edits = sputnik:get_history(node.name, 50)
end
local channel_url = "http://"..sputnik.config.DOMAIN
if request.show_complete_history then
channel_url = channel_url .. sputnik.config.HOME_PAGE_URL
else
channel_url = channel_url .. sputnik:make_url(node.id)
end
return cosmo.f(node.templates.RSS){
title = title,
channel_url = channel_url,
items = function()
for i, edit in ipairs(edits) do
local author_display, author_ip_for_link = author_or_ip(edit)
edit.node = edit.node or node
if (not request.params.recent_users_only)
or sputnik.auth:user_is_recent(edit.author) then
cosmo.yield{
link = "http://" .. sputnik.config.DOMAIN ..
sputnik:make_url(
edit.id or node.id,
"show", {version=edit.version}
),
title = sputnik:escape(string.format("%s: %s by %s",
edit.version,
edit.id or "",
author_display or "")),
ispermalink = "false",
guid = (edit.id or node.id).. "/" .. edit.version,
pub_date = sputnik:format_time_RFC822(edit.timestamp),
author = sputnik:escape(author_display),
summary = sputnik:escape(cosmo.f(node.templates.RSS_SUMMARY){
if_summary_exists = cosmo.c(edit.comment:match("%S")){
summary = edit.comment
},
if_no_summary = cosmo.c(not edit.comment:match("%S")){},
history_url = "http://" .. sputnik.config.DOMAIN ..
sputnik:make_url(edit.id, "history", {version = edit.version}),
diff_url = "http://" .. sputnik.config.DOMAIN ..
sputnik:make_url(edit.id, "diff", {version = edit.version}),
})
}
end
end
end,
}, "application/rss+xml"
end
-----------------------------------------------------------------------------
-- Returns RSS for the whole site.
-----------------------------------------------------------------------------
function actions.complete_history_rss(node, request, sputnik)
request.show_complete_history = 1
return actions.rss(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Returns RSS for edits done by recently registered users.
-----------------------------------------------------------------------------
function actions.rss_for_edits_by_recent_users(node, request, sputnik)
request.show_complete_history = 1
request.params.recent_users_only = 1
return actions.rss(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Returns a list of nodes that the specified user is allowed to see.
-----------------------------------------------------------------------------
function get_visible_nodes(sputnik, user, prefix, options)
local nodes = {}
local num_hidden = 0
for i, node in pairs(sputnik.saci:get_nodes_by_prefix(prefix)) do
if not options or not options.lazy then
--sputnik:decorate_node(node)
local ok, err = copcall(sputnik.activate_node, sputnik, node)
--sputnik:activate_node(node)
local ok = true
if not ok then
error("Could not load node '"..node.id.."'!\n"..err)
end
end
if node:check_permissions(user, "show") then
table.insert(nodes, node)
else
num_hidden = num_hidden + 1
end
end
table.sort(nodes, function(x,y) return x.id < y.id end)
return nodes, num_hidden
end
-----------------------------------------------------------------------------
-- Returns a list of nodes.
-----------------------------------------------------------------------------
function actions.list_nodes(node, request, sputnik)
local nodes = get_visible_nodes(sputnik, request.user)
node.inner_html = util.f(node.templates.LIST_OF_ALL_PAGES){
do_nodes = function()
for i, node in ipairs(nodes) do
cosmo.yield {
name = node.id,
url = sputnik.config.NICE_URL..node.id
}
end
end,
}
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Returns an XML sitemap for this wiki.
-----------------------------------------------------------------------------
function actions.show_sitemap_xml(node, request, sputnik)
local nodes = get_visible_nodes(sputnik, request.user)
return cosmo.f(node.templates.SITEMAP_XML){
do_urls = function()
for i, node in ipairs(nodes) do
local priority, url
if node.id == sputnik.config.HOME_PAGE then
url = sputnik.config.HOME_PAGE_URL
priority = "0.9"
else
url = sputnik.config.NICE_URL..node.id
priority = "0.1"
end
cosmo.yield{
url = "http://"..sputnik.config.DOMAIN..url,
lastmod = sputnik:format_time(sputnik.repo:get_node_info(node.id).timestamp,
"%Y-%m-%dT%H:%M:%S+00:00", "+00:00"),
changefreq = "weekly",
priority = priority
}
end
end,
}, "text/xml"
end
--=========================================================================--
-- Now the few remaining things
--=========================================================================--
function actions.configure (node, request, sputnik, etc)
request.admin_edit = true
return node.actions.edit(node, request, sputnik, etc)
end
-----------------------------------------------------------------------------
-- Shows HTML for the standard Edit field.
-----------------------------------------------------------------------------
function actions.edit (node, request, sputnik, etc)
request.is_indexable = false
etc = etc or {} -- additional parameters
-- check if the user is even allowed to edit
local admin = sputnik.auth:get_metadata(request.user, "is_admin")
if (not node:check_permissions(request.user, request.action))
or (node._id==sputnik.config.ROOT_PROTOTYPE and admin == "true") then
local message = etc.message_if_not_allowed
if request.action == "edit" then
message = message or "NOT_ALLOWED_TO_EDIT"
else
message = message or "ACTION_NOT_ALLOWED"
end
node:post_error(node.translator.translate_key(message))
node.inner_html = ""
return node.wrappers.default(node, request, sputnik)
end
-- Add the editpage stylesheet
node:add_javascript_link(sputnik:make_url("sputnik/js/editpage.js"))
-- select the parameters that should be copied
local fields = {}
for field, field_params in pairs(node.fields) do
if not field_params.virtual then
fields[field] = sputnik:escape(request.params[field] or node.raw_values[field])
end
end
fields.page_name = sputnik:dirify(node.name) -- node name cannot be changed
fields.user= request.params.user or ""
fields.password=""
fields.minor=nil
fields.summary= request.params.summary or ""
local honeypots = ""
math.randomseed(os.time())
for i=0, (sputnik.config.NUM_HONEYPOTS_IN_FORMS or 0) do
local field_name = "honey"..tostring(i)
honeypots = honeypots.."\n"..cosmo.f([[$name = {$order, "honeypot"}]]){
order = string.gsub (tostring(math.random()*5), ",", "."),
name = field_name,
}
fields[field_name] = ""
end
local post_timestamp = os.time()
local post_token = sputnik.auth:timestamp_token(post_timestamp)
local edit_ui_field = etc.edit_ui_field
if request.admin_edit then
edit_ui_field = edit_ui_field or "admin_edit_ui"
else
edit_ui_field = edit_ui_field or "edit_ui"
end
sputnik.logger:debug(node[edit_ui_field]..honeypots)
-- Pre-compile the field spec
local cfields, cfield_names = html_forms.compile_field_spec(node[edit_ui_field]..honeypots)
for i, field in ipairs(cfields) do
local editor_classes = {}
if field.editor_modules then
for j, module in ipairs(field.editor_modules) do
local editor_module = require("sputnik.editor." .. module)
editor_module.initialize(node, request, sputnik)
table.insert(editor_classes, "editor_" .. module)
end
local editor_class_txt = table.concat(editor_classes, " ")
if not field.class or #field.class <= 0 then
field.class = editor_class_txt
else
field.class = field.class .. editor_class_txt
end
end
if cfield_names[i] == "category" then
if field[2] == "select" then
local _, category_hash = get_nav_bar(node, sputnik)
local categories = {{display=" ", value=""}}
for id, _ in pairs(category_hash) do
categories[#categories + 1] = {value=id, display=category_hash[id]}
end
table.sort(categories, function(x,y) return x.display < y.display end)
field.options = categories
end
end
end
local form_params = {
field_spec = node[edit_ui_field]..honeypots,
templates = node.templates,
translator = node.translator,
values = fields,
hash_fn = function(field_name)
return sputnik:hash_field_name(field_name, post_token)
end
}
local html_for_fields, field_list = html_forms.make_html_form(form_params, cfields, cfield_names)
local captcha_html = ""
if not request.user and sputnik.captcha then
for _, field in ipairs(sputnik.captcha:get_fields()) do
table.insert(field_list, field)
end
captcha_html = node.translator.translate_key("ANONYMOUS_USERS_MUST_ENTER_CAPTCHA")..sputnik.captcha:get_html()
end
node.inner_html = cosmo.f(node.templates.EDIT){
if_preview = cosmo.c(request.preview){
preview = request.preview,
summary = fields.summary
},
html_for_fields = html_for_fields,
node_name = node.name,
post_fields = table.concat(field_list,","),
post_token = post_token,
post_timestamp = post_timestamp,
action_url = sputnik.config.BASE_URL,
captcha = captcha_html,
}
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Shows HTML of diff between two versions of the node.
-----------------------------------------------------------------------------
function actions.diff(node, request, sputnik)
request.is_indexable = false
local this_node_info = sputnik.saci:get_node_info(node.id, request.params.version)
if not request.params.other then
-- No version was specified for the "other" node, so compare with the
-- immediately previous version
local history = sputnik:get_history(node.id, 200)
for idx,edit in ipairs(history) do
if edit.version == this_node_info.version then
local prior_edit = history[idx+1]
request.params.other = prior_edit and prior_edit.version
end
end
end
local other_node_data, other_node_info, version2_exists
if request.params.other then
other_node_data = sputnik.saci:get_node(node.id, request.params.other)
other_node_info = sputnik.saci:get_node_info(node.id, request.params.other)
version2_exists = true
else
other_node_data = {raw_values = {}}
other_node_info = {timestamp = 0}
version2_exists = false
end
local diff = ""
local diff_table = node:diff(other_node_data)
for i, field in ipairs(node:get_ordered_field_names()) do
local tokens = diff_table[field]
if tokens then
diff = diff..""..node.translator.translate_key("EDIT_FORM_"..field:upper()).."
\n"
..""..tokens:to_html().."
\n"
end
end
node.inner_html = cosmo.f(node.templates.DIFF){
version1 = this_node_info.version,
link1 = sputnik:make_link(node.id, "show", {version=request.params.version}),
author1 = author_or_ip(this_node_info),
time1 = sputnik:format_time(this_node_info.timestamp, "%H:%M %z"),
date1 = sputnik:format_time(this_node_info.timestamp, "%Y/%m/%d"),
if_version2_exists = cosmo.c(version2_exists){},
version2 = other_node_info.version,
link2 = sputnik:make_link(node.id, "show", {version=request.params.other}),
author2 = author_or_ip(other_node_info),
time2 = sputnik:format_time(other_node_info.timestamp, "%H:%M %z"),
date2 = sputnik:format_time(other_node_info.timestamp, "%Y/%m/%d"),
diff = diff,
}
request.is_diff = true
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Shows the raw content of the node with content-type set to text/plain (note that unlike
-- actions.raw, this method only returns the _content_ of the node, not its metadata).
-----------------------------------------------------------------------------
function actions.raw_content(node, request, sputnik)
if node:check_permissions(request.user, request.action) then
return node.raw_values.content, "text/plain"
else
return "-- Access to raw content not allowed", "text/plain"
end
end
-----------------------------------------------------------------------------
-- Shows the underlying string representation of the node as plain text.
-----------------------------------------------------------------------------
function actions.raw(node, request, sputnik)
if node:check_permissions(request.user, request.action) then
return node.data or "No source available.", "text/plain"
else
return "-- Access to raw content not allowed", "text/plain"
end
end
-----------------------------------------------------------------------------
-- Shows the _content_ of the node shown as 'code'.
-----------------------------------------------------------------------------
function actions.show_content_as_code(node, request, sputnik)
local escaped = sputnik:escape(node.raw_values.content)
return ""..escaped.."
"
end
-----------------------------------------------------------------------------
-- Shows the complete page with it's content shown as 'code'.
-----------------------------------------------------------------------------
function actions.code(node, request, sputnik)
node.inner_html = actions.show_content_as_code(node, request, sputnik)
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Shows the content of the node as Lua code, checking whether it parses.
-----------------------------------------------------------------------------
function actions.show_content_as_lua_code(node, request, sputnik)
local DOLLAR_REPLACEMENT = "$"
local escaped = sputnik:escape(node.raw_values.content)
escaped = escaped:gsub("%$", DOLLAR_REPLACEMENT)
escaped = escaped:gsub(" ", " ")
escaped = string.gsub(escaped, "(%-%-[^\n]*)",
function (comment) return ""..comment.."" end)
local f, errors = loadstring(node.raw_values.content)
if errors then
local reg_exp = "^.+%]%:(%d+)%:"
error_line_num = string.match(errors, reg_exp)
errors = string.gsub(errors, reg_exp, "On line %1:")
end
return cosmo.f(node.templates.LUA_CODE){
do_lines = function()
local i = 0
for line in ("\n"..escaped):gmatch"\n([^\n]*)" do
i = i+1
local class = "ok"
if i == tonumber(error_line_num) then
class = "bad"
end
cosmo.yield{
i = i,
line = line,
class=class
}
end
end,
if_ok = cosmo.c(f~=nil){},
if_errors = cosmo.c(errors~=nil){errors=errors},
}
end
-----------------------------------------------------------------------------
-- Shows the HTML for an error message when a non-existent action is requested.
-----------------------------------------------------------------------------
function actions.action_not_found(node, request, sputnik)
node.inner_html = cosmo.f(node.templates.ACTION_NOT_FOUND){
title = node.title,
url = sputnik:make_url(node.id),
action = request.action,
if_custom_actions = cosmo.c(node.raw_values.actions and node.raw_values.actions:len() > 0){
actions = node.raw_values.actions
}
}
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Shows the list of registered users
-----------------------------------------------------------------------------
function actions.show_users(node, request, sputnik)
local TEMPLATE = [[
username | registration time |
$do_users[=[$username | $time |
]=]
]]
local users = {}
for username, record in pairs(node.content.USERS) do
table.insert(users,
{ username = username, link = sputnik:make_link(username),
time = sputnik:format_time(record.creation_time,
"%Y/%m/%d %H:%M %z")
})
end
table.sort(users, function(x,y) return x.time < y.time end)
node.inner_html = cosmo.f(TEMPLATE) {do_users = users}
return node.wrappers.default(node, request, sputnik)
end
function get_login_form(node, request, sputnik)
local post_timestamp = os.time()
local post_token = sputnik.auth:timestamp_token(post_timestamp)
local field_spec = [[
--please_login = {4.0, "note"}
user = {4.1, "text_field", div_class="autofocus"}
password = {4.2, "password"}
]]
if request.params.next then
field_spec = field_spec .. [[
next = {4.3, "hidden", no_label = true, div_class="hidden"}
]]
end
local html_for_fields, field_list = html_forms.make_html_form{
field_spec = field_spec,
templates = node.templates,
translator = node.translator,
values = {
user = "",
password = "",
next = request.params.next,
},
hash_fn = function(field_name)
return sputnik:hash_field_name(field_name, post_token)
end
}
return cosmo.f(node.templates.LOGIN_FORM){
html_for_fields = html_for_fields,
node_name = request.params.next or node.name,
post_fields = "user,password,next",
post_token = post_token,
post_timestamp = post_timestamp,
action_url = sputnik:make_url(sputnik.config.LOGIN_NODE),
register_link = sputnik:make_url(sputnik.config.REGISTRATION_NODE)
}
end
-----------------------------------------------------------------------------
-- Shows login form.
-----------------------------------------------------------------------------
function actions.show_login_form(node, request, sputnik)
request.is_indexable = false
if (request.params.user and request.user) then -- we've just logged in the user
request.redirect = sputnik:make_url(node.id)
return
end
node.inner_html = get_login_form(node, request, sputnik)
return node.wrappers.default(node, request, sputnik)
end
function actions.logout_user(node, request, sputnik)
request.is_indexable = false
request.user = nil
request.redirect = sputnik:make_url(request.params.next)
return
end
-----------------------------------------------------------------------------
-- Shows the version of sputnik.
-----------------------------------------------------------------------------
function actions.sputnik_version(node, request, sputnik)
request.is_indexable = false
local rocks = {}
if luarocks and luarocks.require then
for _, rock in ipairs(sputnik.config.ROCK_LIST_FOR_VERSION or {}) do
local __, version = luarocks.require.get_rock_from_module(rock)
table.insert(rocks, {rock=rock, version=version or "unknown"})
end
end
node.inner_html = cosmo.f(node.templates.VERSION){
installer = sputnik.config.VERSION or "UNKNOWN",
rocks = rocks
}
return node.wrappers.default(node, request, sputnik)
end
-----------------------------------------------------------------------------
-- Validates a chunk of Lua code. Returns "valid" or "invalid" depending
-- on whether the code is ok. (This
-----------------------------------------------------------------------------
function actions.validate_lua(node, request, sputnik)
request.is_indexable = false
local code = request.params.code or ""
local sandbox = saci.sandbox.new(sputnik.config)
local result, err = sandbox:do_lua(code, true)
if result then
return "valid"
else
return "invalid" --tostring(err.err)
end
end
-----------------------------------------------------------------------------
-- Shows the HTML for an error message when a non-existent action is requested.
-----------------------------------------------------------------------------
wrappers = {}
function get_breadcrumbs(node, sputnik)
local breadcrumbs = {}
local path = ""
local not_first
for i, part in ipairs{util.split(node.name, "/")} do
path = path..part
local b_node
if path == node.id then
b_node = node
else
b_node = sputnik:get_node(path)
end
local crumb = part
if b_node.breadcrumb and b_node.breadcrumb:match("%S") then
crumb = b_node.breadcrumb
end
table.insert(breadcrumbs, {
link = sputnik:make_link(path),
title = crumb,
_template = not_first and 2
})
not_first = true
path = path.."/"
end
return breadcrumbs
end
-----------------------------------------------------------------------------
-- Wraps the HTML content in bells and whistles such as the navigation bar, the header, the footer,
-- etc.
-----------------------------------------------------------------------------
function wrappers.default(node, request, sputnik)
if request.params.skip_wrapper then
return node.inner_html
end
if request.auth_message then
node:post_error(node.translator.translate_key(request.auth_message))
end
local is_old = request.params.version
and sputnik.saci:get_node_info(node.id).version ~= request.params.version
and not request.is_diff
local nav_sections, nav_subsections = get_nav_bar(node, sputnik)
local translate = node.translator.translate
local values = {
site_title = sputnik.config.SITE_TITLE or "",
title = sputnik:escape(node.title),
if_no_index = cosmo.c((not request.is_indexable) or is_old){},
do_css_links = node.css_links,
do_css_snippets = node.css_snippets,
do_javascript_links = node.javascript_links,
do_javascript_snippets = node.javascript_snippets,
if_old_version = cosmo.c(is_old){
version = request.params.version,
},
logout_link = sputnik:make_link(sputnik.config.LOGOUT_NODE, nil, {next = node.name},
nil, {do_not_highlight_missing = true}),
login_link = sputnik:make_link(sputnik.config.LOGIN_NODE, nil, {next = node.name},
nil, {do_not_highlight_missing=true}),
register_link = sputnik:make_link(sputnik.config.REGISTRATION_NODE),
if_logged_in = cosmo.c(request.user){ user = sputnik:escape(request.user) },
if_not_logged_in = cosmo.c(not request.user){},
if_search = cosmo.c(sputnik.config.SEARCH_PAGE){},
base_url = sputnik.config.BASE_URL,
search_page = sputnik.config.SEARCH_PAGE,
search_box_content = sputnik.config.SEARCH_CONTENT or "",
content = node.inner_html,
sidebar = "",
do_messages = node.messages,
do_nav_sections = nav_sections,
do_nav_subsections = nav_sections.current_section,
do_breadcrumb = get_breadcrumbs(node, sputnik),
if_multipart_id = cosmo.c(node.id:match("/")){},
-- "links" include "href="
show_link = sputnik:make_link(node.id),
icon_base_url = sputnik.config.ICON_BASE_URL or sputnik.config.NICE_URL,
css_base_url = sputnik.config.CSS_BASE_URL or sputnik.config.NICE_URL,
js_base_url = sputnik.config.JS_BASE_URL or sputnik.config.NICE_URL,
do_toolbar = function(args)
local icons = sputnik.config.TOOLBAR_ICONS
for i, command in ipairs(sputnik.config.TOOLBAR_COMMANDS) do
if node:check_permissions(request.user, command) then
local icon = icons[command]
cosmo.yield{
link = sputnik:make_link(node.id, command),
title = translate("_("..command:upper()..")"),
command = command,
if_icon = cosmo.c(icon){ icon = icon },
if_text = cosmo.c(not icon){},
}
end
end
end,
node_rss_link = sputnik:make_link(node.id, "rss"),
site_rss_link = sputnik:make_link(sputnik.config.HISTORY_PAGE, "rss"),
sputnik_link = "href='http://spu.tnik.org/'",
-- urls are just urls
make_url = function(args)
return sputnik:make_url(unpack(args))
end,
base_url = sputnik.config.BASE_URL, -- for mods
nice_url = sputnik.config.NICE_URL, -- for mods
home_page_url = sputnik.config.HOME_PAGE_URL,
logo_url = sputnik.config.LOGO_URL,
favicon_url = sputnik.config.FAVICON_URL,
-- icons are urls of images
if_title_icon = cosmo.c(node.icon and node.icon~=""){title_icon = sputnik:make_url(node.icon)},
}
for k, v in pairs(node.fields) do
values[k] = values[k] or node[k]
end
local function translate_and_fill(template, values)
return cosmo.fill(node.translator.translate(template), values)
end
values.head = translate_and_fill(node.html_head, values)
values.menu = translate_and_fill(node.html_menu, values)
values.logo = translate_and_fill(node.html_logo, values)
values.search = translate_and_fill(node.html_search, values)
values.page = translate_and_fill(node.html_page, values)
values.sidebar = translate_and_fill(node.html_sidebar, values)
values.header = translate_and_fill(node.html_header, values)
values.footer = translate_and_fill(node.html_footer, values)
values.body = translate_and_fill(node.html_body, values)
return cosmo.fill(node.html_main, values), "text/html"
end
-- vim:ts=3 ss=3 sw=3 expandtab