Commit 290d71b1 authored by Ted Trask's avatar Ted Trask Committed by Natanael Copa

initial commit

based on weblog.tar.gz ncopa got by email.
parents
APP_NAME=weblog
PACKAGE=acf-$(APP_NAME)
VERSION=0.1.0
APP_DIST=\
weblog* \
EXTRA_DIST=README Makefile config.mk
DISTFILES=$(APP_DIST) $(EXTRA_DIST)
TAR=tar
P=$(PACKAGE)-$(VERSION)
tarball=$(P).tar.bz2
install_dir=$(DESTDIR)/$(appdir)/$(APP_NAME)
all:
clean:
rm -rf $(tarball) $(P)
dist: $(tarball)
install:
mkdir -p "$(install_dir)"
cp -a $(APP_DIST) "$(install_dir)"
$(tarball): $(DISTFILES)
rm -rf $(P)
mkdir -p $(P)
cp -a $(DISTFILES) $(P)
$(TAR) -jcf $@ $(P)
rm -rf $(P)
# target that creates a tar package, unpacks is and install from package
dist-install: $(tarball)
$(TAR) -jxf $(tarball)
$(MAKE) -C $(P) install DESTDIR=$(DESTDIR)
rm -rf $(P)
include config.mk
.PHONY: all clean dist install dist-install
ACF for weblog
prefix=/usr
datadir=${prefix}/share
sysconfdir=${prefix}/etc
localstatedir=${prefix}/var
acfdir=${datadir}/acf
wwwdir=${acfdir}/www
cgibindir=${acfdir}/cgi-bin
appdir=${acfdir}/app
acflibdir=${acfdir}/lib
sessionsdir=${localstatedir}/lib/acf/sessions
<% local form, viewlibrary, page_info = ...
require("viewfunctions")
%>
<% if form.value.result then %>
<H1><%= html.html_escape(form.value.result.label) %></H1>
<% require("html") %>
<%= html.cfe_unpack(form.value.result.value) %>
<form action="/cgi-bin/acf/weblog/weblog/downloadadhocquery" method="POST">
<input class="hidden" type="hidden" name="query" value="<%= html.html_escape(form.value.query.value) %>" >
<DL>
<DT>Download query result</DT><DD><input class="submit" type="submit" name="Download" value="Download"></DD>
</DL>
</FORM>
<% end %>
<H1><%= html.html_escape(form.label) %></H1>
<% displayformstart(form, page_info) %>
<DL>
<% displayformitem(form.value.query, "query") %>
</DL>
<% displayformend(form) %>
This form accepts a Postgresql SELECT statement and displays the results. Examples:
<ul>
<li><pre>SELECT clientuserid, sum(bytes) AS total FROM weblog GROUP BY clientuserid ORDER BY total DESC</pre>
<li><pre>SELECT extract(hour from date) AS hour, sum(numrequest) AS numrequest, sum(numblock) AS numblock FROM usagestat GROUP BY extract(hour from date) ORDER BY hour</pre>
</ul>
The available database tables and descriptions are as follows:<br>
<H3>PubWeblog and PubBlocklog</H3>
These tables contain the combined squid access log and dansguardian log for every access and blocked accesses respectively. The definition of the table is as follows:
<pre>
(
sourcename character varying(40),
clientip inet NOT NULL,
clientuserid character varying(64) NOT NULL,
logdatetime timestamp(3) without time zone NOT NULL,
uri text NOT NULL,
bytes integer NOT NULL,
reason text,
score integer
)
</pre>
<H3>dbHistLog</H3>
This table contains the database history, including such information as which log files were loaded and how many entries they contained. The definition of the table is as follows:
<pre>
(
logdatetime timestamp(0) without time zone NOT NULL,
msgtext text
)
</pre>
<H3>Source</H3>
This table contains the list of log file sources. The definition of the table is as follows:
<pre>
(
sourcename character varying(40) NOT NULL,
method character varying(100) NOT NULL,
userid character varying(32),
passwd character varying(255),
source character varying(255) NOT NULL,
tzislocal boolean,
enabled boolean
)
</pre>
<H3>Usagestat</H3>
This table contains a historical record of pages requested and blocked by hour. The definition of the table is as follows:
<pre>
(
sourcename character varying(40) NOT NULL,
date timestamp(0) without time zone NOT NULL,
numrequest integer,
numblock integer
)
</pre>
<H3>Watchlist</H3>
This table contains the user watch list. The definition of the table is as follows:
<pre>
(
clientuserid character varying(64) NOT NULL,
expiredatetime timestamp(0) without time zone NOT NULL
)
</pre>
<% local form, viewlibrary, page_info = ...
require("viewfunctions")
%>
<H1><%= html.html_escape(form.label) %></H1>
<%
local order = {"auditstart", "auditend", "historydays", "watchdays", "purgedays", "window"}
displayform(form, order, nil, page_info)
%>
module(..., package.seeall)
-- Load libraries
require("controllerfunctions")
default_action = "viewauditstats"
function config(self)
return controllerfunctions.handle_form(self, self.model.getconfig, self.model.updateconfig, self.clientdata, "Save", "Edit Configuration", "Configuration Saved")
end
function listsources(self)
return self.model.getsourcelist()
end
function createsource(self)
return controllerfunctions.handle_form(self, self.model.getnewsource, self.model.createsource, self.clientdata, "Create", "Create new source", "New source created")
end
function deletesource(self)
return self:redirect_to_referrer(self.model.deletesource(self.clientdata.sourcename))
end
function editsource(self)
return controllerfunctions.handle_form(self, function() return self.model.getsource(self.clientdata.sourcename) end, self.model.updatesource, self.clientdata, "Save", "Edit Source", "Source Saved")
end
function testsource(self)
return self:redirect_to_referrer(self.model.testsource(self.clientdata.sourcename))
end
function importlogs(self)
return self:redirect_to_referrer(self.model.importlogs())
end
function viewactivitylog(self)
return self.model.getactivitylog()
end
function viewwatchlist(self)
return self.model.getwatchlist()
end
function createwatchlistentry(self)
return controllerfunctions.handle_form(self, self.model.getnewwatchlistentry, self.model.createwatchlistentry, self.clientdata, "Create", "Create new watchlist entry", "New watchlist entry created")
end
function deletewatchlistentry(self)
return self:redirect_to_referrer(self.model.deletewatchlistent(self.clientdata.clientuserid))
end
function viewweblog(self)
return self.model.getweblog(self.clientdata.clientuserid, self.clientdata.starttime, self.clientdata.endtime, self.clientdata.clientip, clientdata.focus)
end
function downloadweblog(self)
self.conf.viewtype = "stream"
local retval = viewweblog(self)
local file = cfe({ type="longtext", value="", label=retval.value.clientuserid.value .. ".log" })
local content = {"sourcename\tclientip\tclientuserid\tlogdatetime\turi\tbytes\treason\tscore"}
for i,log in ipairs(retval.value.log.value) do
content[#content+1] = string.format("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t",
log.sourcename, log.clientip, log.clientuserid, log.logdatetime,
log.uri, log.bytes, log.reason or "", log.score or "0")
end
file.value = table.concat(content, "\n")
return file
end
function viewblocklog(self)
return self.model.getblocklog(self.clientdata.clientuserid, self.clientdata.starttime, self.clientdata.endtime, self.clientdata.clientip, clientdata.focus)
end
function viewusagestats(self)
return self.model.getusagestats()
end
function viewauditstats(self)
return self.model.getauditstats()
end
function completeaudit(self)
return self:redirect_to_referrer(self.model.completeaudit(self.clientdata.auditend))
end
function adhocquery(self)
return controllerfunctions.handle_form(self, self.model.getnewadhocquery, self.model.adhocquery, self.clientdata, "Submit", "Submit ad-hoc query")
end
function downloadadhocquery(self)
self.conf.viewtype = "stream"
local retval = self.model.getnewadhocquery()
controllerfunctions.handle_clientdata(retval, self.clientdata)
retval = self.model.adhocquery(retval)
local file = cfe({ type="longtext", value="", label="query" })
if retval.value.result and #retval.value.result.value > 0 then
local columns = {}
for name,val in pairs(retval.value.result.value[1]) do
columns[#columns+1] = name
end
local content = {table.concat(columns, "\t")}
for i,entry in ipairs(retval.value.result.value) do
local line = {}
for i,name in ipairs(columns) do
line[#line+1] = entry[name] or ""
end
content[#content+1] = table.concat(line, "\t")
end
file.value = table.concat(content, "\n")
end
return file
end
function status(self)
return self.model.testdatabase()
end
function createdatabase(self)
return controllerfunctions.handle_form(self, self.model.getnewdatabase, self.model.create_database, self.clientdata, "Create", "Create New Database", "Database Created")
end
<% local form, viewlibrary, page_info = ... %>
<% require("viewfunctions") %>
<H1><%= html.html_escape(form.label) %></H1>
<%
form.value.password.type = "password"
form.value.password_confirm.type = "password"
local order = { "password", "password_confirm" }
displayform(form, order, nil, page_info)
%>
weblog-editsource-html.lsp
\ No newline at end of file
<% local form, viewlibrary, page_info = ...
require("viewfunctions")
%>
<H1><%= html.html_escape(form.label) %></H1>
<%
local order = {"clientuserid", "expiredatetime"}
displayform(form, order, nil, page_info)
%>
<% local form, viewlibrary, page_info = ...
require("viewfunctions")
%>
<H1><%= html.html_escape(form.label) %></H1>
<%
form.action = page_info.script .. page_info.prefix .. page_info.controller .. "/" .. page_info.action
if page_info.action == "editsource" then
form.value.sourcename.readonly = true
end
local order = {"sourcename", "enabled", "source", "method", "userid", "passwd"}
displayform(form, order)
%>
<% local data, viewlibrary, page_info, session = ...
require("viewfunctions")
%>
<% displaycommandresults({"deletesource", "editsource", "testsource", "createsource", "importlogs"}, session) %>
<h1><%= html.html_escape(data.label) %></h1>
<TABLE>
<TR style="background:#eee;font-weight:bold;">
<TD style="padding-right:20px;white-space:nowrap;text-align:left;" class="header">Action</TD>
<TD style="padding-right:20px;white-space:nowrap;text-align:left;" class="header">Name</TD>
<TD style="padding-right:20px;white-space:nowrap;text-align:left;" class="header">Enabled</TD>
<TD style="padding-right:20px;white-space:nowrap;text-align:left;" class="header">Source</TD>
<TD style="white-space:nowrap;text-align:left;" class="header">Method</TD>
</TR>
<% for i,source in ipairs(data.value) do %>
<TR>
<TD style="padding-right:20px;white-space:nowrap;">
<%= html.link{value = "editsource?sourcename=" .. source.sourcename.."&redir="..page_info.orig_action, label="Edit "} %>
<%= html.link{value = "deletesource?sourcename=" .. source.sourcename, label="Delete "} %>
<%= html.link{value = "testsource?sourcename=" .. source.sourcename, label="Test "} %>
</TD>
<TD style="padding-right:20px;white-space:nowrap;"><%= html.html_escape(source.sourcename) %></TD>
<TD style="padding-right:20px;white-space:nowrap;"><%= html.html_escape(tostring(source.enabled)) %></TD>
<TD style="padding-right:20px;white-space:nowrap;"><%= html.html_escape(source.source) %></TD>
<TD style="white-space:nowrap;" width="90%"><%= html.html_escape(source.method) %></TD>
</TR>
<% end %>
</TABLE>
<% if data.errtxt then %>
<p class='error'><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value == 0 then %>
<p>No sources found</p>
<% end %>
<DL>
<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller .. "/createsource") %>">
<DT>Create New Source</DT>
<input class="hidden" type="hidden" name="redir" value="<%= html.html_escape(page_info.orig_action) %>" >
<DD><input class="submit" type="submit" value="Create"></DD>
</form>
</DL>
<DL>
<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller .. "/importlogs") %>">
<DT>Import Logs</DT>
<DD><input class="submit" type="submit" value="Import"></DD>
</form>
</DL>
This diff is collapsed.
<% local data, viewlibrary, page_info, session = ...
require("viewfunctions")
%>
<% displaycommandresults({"createdatabase"}, session, true) %>
<H1>Weblog Database Status</H1>
<% local status
if viewlibrary and viewlibrary.dispatch_component then
if session.permissions.postgresql.status then
status = viewlibrary.dispatch_component("postgresql/postgresql/status", nil, true)
end
end
if status then %>
<p>Database is <%= status.value.status.value %>
<% else %>
<p>Database status unknown
<% end %>
<% if data.value then %>
<p>Weblog Database present
<% else %>
<p>Weblog Database missing
<% if viewlibrary and viewlibrary.dispatch_component then
viewlibrary.dispatch_component("createdatabase")
end %>
<% end %>
<% local data, viewlibrary, page_info, session = ... %>
<H1><%= html.html_escape(data.label) %></H1>
<TABLE>
<TR style="background:#eee;font-weight:bold;">
<TD style="padding-right:20px;white-space:nowrap;" class="header">Date</TD>
<TD style="white-space:nowrap;" WIDTH="90%" class="header">Message</TD>
</TR>
<% for i,log in ipairs(data.value) do %>
<TR>
<TD><%= html.html_escape(log.logdatetime) %></TD>
<TD><%= html.html_escape(log.msgtext) %></TD>
</TR>
<% end %>
</TABLE>
<% if data.errtxt then %>
<p class='error'><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value == 0 then %>
<p>No history found</p>
<% end %>
<% local data, viewlibrary, page_info, session = ... %>
<% require("viewfunctions") %>
<link rel="stylesheet" type="text/css" href="/js/style.css">
<script type="text/javascript" src="/js/jquery-latest.js"></script>
<script type="text/javascript" src="/js/jquery.tablesorter.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#audit").tablesorter();
});
</script>
<% displaycommandresults({"completeaudit"}, session) %>
<H1>Audit Parameters</H1>
<DL>
<% displayitem(data.value.auditstart) %>
<% displayitem(data.value.auditend) %>
</DL>
<H1><%= html.html_escape(data.label) %></H1>
<TABLE id="audit" class="tablesorter"><THEAD>
<TR style="background:#eee;font-weight:bold;">
<TH>User ID</TH>
<TH>Blocks</TH>
<TH>Maximum Score</TH>
</TR>
</THEAD><TBODY>
<% for i,stat in ipairs(data.value.stats.value) do %>
<TR><TD><%= html.link{value = "viewblocklog?clientuserid="..stat.clientuserid, label=stat.clientuserid} %></TD>
<TD><%= html.html_escape(stat.numblock) %></TD>
<TD><%= html.html_escape(stat.maxscore) %></TD></TR>
<% end %>
</TBODY></TABLE>
<% if data.errtxt then %>
<p class='error'><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value.stats.value == 0 then %>
<p>No blocks, try adjusting the audit dates</p>
<% end %>
<DL>
<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller .. "/completeaudit") %>">
<DT>Complete Audit</DT>
<DD><input class="submit" type="submit" value="Complete"></DD>
</form>
</DL>
<% local data, viewlibrary, page_info, session = ... %>
<% if data.value.focus.value ~= "" then %>
<script type="text/javascript" src="/js/jquery-latest.js"></script>
<script type="text/javascript">
$(function(){
if ($("#focus").length) {
var top = $("#focus").offset().top;
$("html,body").scrollTop(top);
}
});
</script>
<% end %>
<H1>Search Parameters</H1>
<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller .. "/" .. page_info.action) %>" method="POST">
<DL>
<DT>Start Time</DT>
<DD><%= html.html_escape(data.value.starttime.value) %>
<input class="text" type="text" name="starttime" value="<%= html.html_escape(data.value.starttime.value) %>" >
</DD>
<DT>User ID</DT>
<DD><%= html.html_escape(data.value.clientuserid.value) %>
<input class="text" type="text" name="clientuserid" value="<%= html.html_escape(data.value.clientuserid.value) %>" >
</DD>
<DT>Client IP</DT>
<DD><%= html.html_escape(data.value.clientip.value) %>
<input class="text" type="text" name="clientip" value="<%= html.html_escape(data.value.clientip.value) %>" >
</DD>
<DT>End Time</DT>
<DD><%= html.html_escape(data.value.endtime.value) %>
<input class="text" type="text" name="endtime" value="<%= html.html_escape(data.value.endtime.value) %>" >
</DD>
<DT></DT><DD><input class="submit" type="submit" name="Update" value="Update"></DD>
</DL>
</FORM>
<H1><%= html.html_escape(data.label) %></H1>
<TABLE>
<TR style="background:#eee;font-weight:bold;">
<TD style="padding-right:20px;white-space:nowrap;" class="header" colspan=2>Timestamp</TD>
<TD style="padding-right:20px;white-space:nowrap;" class="header">Client IP</TD>
<TD style="padding-right:20px;white-space:nowrap;" class="header">User ID</TD>
<TD style="white-space:nowrap;" WIDTH="90%" class="header">Size</TD>
</TR>
<% for i,watch in ipairs(data.value.log.value) do %>
<% local time = {}
time.year, time.month, time.day, time.hour, time.min, time.sec =
string.match(watch.logdatetime, "(%d+)%-(%d+)-(%d+)%s+(%d+):(%d+):(%d+)")
time = os.time(time) %>
<% if data.value.focus and data.value.focus.value == watch.logdatetime then %>
<TR style="background:#ff0" id="focus">
<% else %>
<TR style="background:#eee">
<% end %>
<TD colspan=2><%= html.link{value = "viewweblog?clientuserid="..watch.clientuserid..
"&starttime="..os.date("%Y-%m-%d %H:%M:%S", time - 60*(tonumber(data.value.window.value)))..
"&endtime="..os.date("%Y-%m-%d %H:%M:%S", time + 60*(tonumber(data.value.window.value)))..
"&focus="..watch.logdatetime,
label=watch.logdatetime} %></TD>
<TD><%= html.html_escape(watch.clientip) %></TD>
<TD><%= html.html_escape(watch.clientuserid) %></TD>
<TD><%= html.html_escape(watch.bytes) %></TD>
</TR>
<TR><TD></TD><TD colspan=4><%= html.link{value = watch.uri, label=watch.uri} %></TD></TR>
<% if watch.reason and watch.reason ~= "" then %>
<TR><TD></TD><TD style="background:#f33" colspan=4><%= html.html_escape(watch.reason) %></TD></TR>
<% end %>
<% end %>
</TABLE>
<% if data.errtxt then %>
<p class="error"><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value.log.value == 0 then %>
<p>No results, try adjusting search parameters</p>
<% end %>
<% if page_info.action == "viewweblog" then %>
<form action="<%= html.html_escape(page_info.script .. page_info.prefix .. page_info.controller .. "/downloadweblog") %>" method="POST">
<input type="hidden" name="starttime" value="<%= html.html_escape(data.value.starttime.value) %>" >
<input type="hidden" name="clientuserid" value="<%= html.html_escape(data.value.clientuserid.value) %>" >
<input type="hidden" name="clientip" value="<%= html.html_escape(data.value.clientip.value) %>" >
<input type="hidden" name="endtime" value="<%= html.html_escape(data.value.endtime.value) %>" >
<DL>
<DT>Download log</DT><DD><input class="submit" type="submit" name="Download" value="Download"></DD>
</DL>
</FORM>
<% end %>
<% local data, viewlibrary, page_info, session = ... %>
<H1><%= html.html_escape(data.label) %></H1>
<TABLE>
<TR style="background:#eee;font-weight:bold;">
<TD style="padding-right:20px;white-space:nowrap;" class="header">Date</TD>
<TD style="padding-right:20px;white-space:nowrap;" class="header">Source</TD>
<TD style="padding-right:20px;white-space:nowrap;" class="header">Requests</TD>
<TD style="white-space:nowrap;" WIDTH="90%" class="header">Blocks</TD>
</TR>
<% for i,stat in ipairs(data.value) do %>
<TR>
<TD><%= html.html_escape(stat.date) %></TD>
<TD><%= html.html_escape(stat.sourcename) %></TD>
<TD><%= html.html_escape(stat.numrequest) %></TD>
<TD><%= html.html_escape(stat.numblock) %></TD>
</TR>
<% end %>
</TABLE>
<% if data.errtxt then %>
<p class='error'><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value == 0 then %>
<p>No usage stats found</p>
<% end %>
<% local data, viewlibrary, page_info, session = ... %>
<% require("viewfunctions") %>
<link rel="stylesheet" type="text/css" href="/js/style.css">
<script type="text/javascript" src="/js/jquery-latest.js"></script>
<script type="text/javascript" src="/js/jquery.tablesorter.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#watchlist").tablesorter({headers: {0:{sorter: false}}});
});
</script>
<% displaycommandresults({"deletewatchlistentry"}, session) %>
<% displaycommandresults({"createwatchlistentry"}, session, true) %>
<H1><%= html.html_escape(data.label) %></H1>
<TABLE id="watchlist" class="tablesorter"><THEAD>
<TR style="background:#eee;font-weight:bold;">
<TH>Delete</TH>
<TH>User ID</TH>
<TH>Expiration Date</TH>
</TR>
</THEAD><TBODY>
<% for i,watch in ipairs(data.value) do %>
<TR>
<TD><%= html.link{value = "deletewatchlistentry?clientuserid="..watch.clientuserid, label="Delete "} %></TD>
<TD><%= html.link{value = "viewblocklog?clientuserid="..watch.clientuserid, label=watch.clientuserid} %></TD>
<TD><%= html.html_escape(watch.expiredatetime) %></TD>
</TR>
<% end %>
</TBODY></TABLE>
<% if data.errtxt then %>
<p class='error'><%= html.html_escape(data.errtxt) %></p>
<% end %>
<% if #data.value == 0 then %>
<p>No watchlist entries found</p>
<% end %>
<% if viewlibrary and viewlibrary.dispatch_component and session.permissions[page_info.controller].createwatchlistentry then
viewlibrary.dispatch_component("createwatchlistentry")
end %>
weblog-viewblocklog-html.lsp
\ No newline at end of file
#CAT GROUP/DESC TAB ACTION
Applications 41Weblog Audit viewauditstats
Applications 41Weblog Config config
Applications 41Weblog Watch_List viewwatchlist
Applications 41Weblog Sources listsources
Applications 41Weblog Usage viewusagestats
Applications 41Weblog History viewactivitylog
Applications 41Weblog Ad-Hoc_Query adhocquery
Applications 41Weblog Status status
USER=weblog:config,weblog:viewauditstats,weblog:completeaudit,weblog:viewactivitylog,weblog:viewwatchlist,weblog:viewweblog,weblog:downloadweblog,weblog:viewblocklog,weblog:viewusagestats
EXPERT=weblog:listsources,weblog:createsource,weblog:deletesource,weblog:editsource,weblog:testsource,weblog:importlogs,weblog:deletewatchlistentry,weblog:createwatchlistentry,weblog:adhocquery,weblog:downloadadhocquery,weblog:status,weblog:createdatabase
ADMIN=weblog:config,weblog:viewauditstats,weblog:completeaudit,weblog:viewactivitylog,weblog:viewwatchlist,weblog:viewweblog,weblog:downloadweblog,weblog:viewblocklog,weblog:viewusagestats,weblog:listsources,weblog:createsource,weblog:deletesource,weblog:editsource,weblog:testsource,weblog:importlogs,weblog:deletewatchlistentry,weblog:createwatchlistentry,weblog:adhocquery,weblog:downloadadhocquery,weblog:status,weblog:createdatabase
#!/usr/bin/lua
-- Set the path to load libraries and the model
local PATH=package.path
package.path = "/usr/share/acf/app/weblog/?.lua;/usr/share/acf/lib/?.lua;" .. package.path
local t = require("weblog-model")
package.path = PATH
-- create a Configuration Framework Entity (cfe)
-- returns a table with at least "value", "type", and "label"
cfe = function ( optiontable )
optiontable = optiontable or {}
me = { value="",
type="text",
label="" }
for key,value in pairs(optiontable) do
me[key] = value
end
return me
end
_G.cfe = cfe
-- Call the weblog model to import the logs
t.importlogs()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment