Commit e5abe17c authored by Libor Peltan's avatar Libor Peltan Committed by Daniel Salzman

utils: add kjournalprint utility

parent 138c9b8a
......@@ -72,6 +72,7 @@
/src/kdig
/src/keymgr
/src/khost
/src/kjournalprint
/src/knot1to2
/src/knotc
/src/knotd
......
......@@ -484,6 +484,7 @@ src/utils/keymgr/options.h
src/utils/khost/khost_main.c
src/utils/khost/khost_params.c
src/utils/khost/khost_params.h
src/utils/kjournalprint/main.c
src/utils/knot1to2/cf-lex.c
src/utils/knot1to2/cf-lex.l
src/utils/knot1to2/cf-parse.tab.c
......
......@@ -4,6 +4,7 @@
/man/kdig.1
/man/keymgr.8
/man/khost.1
/man/kjournalprint.1
/man/knot.conf.5
/man/knot1to2.1
/man/knotc.8
......
MANPAGES_IN = man/knot.conf.5in man/knotc.8in man/knotd.8in man/kdig.1in man/khost.1in man/knsupdate.1in man/knot1to2.1in man/knsec3hash.1in man/keymgr.8in man/kzonecheck.1in
MANPAGES_RST = reference.rst man_knotc.rst man_knotd.rst man_kdig.rst man_khost.rst man_knsupdate.rst man_knot1to2.rst man_knsec3hash.rst man_keymgr.rst man_kzonecheck.rst
MANPAGES_IN = man/knot.conf.5in man/knotc.8in man/knotd.8in man/kdig.1in man/khost.1in man/kjournalprint.1in man/knsupdate.1in man/knot1to2.1in man/knsec3hash.1in man/keymgr.8in man/kzonecheck.1in
MANPAGES_RST = reference.rst man_knotc.rst man_knotd.rst man_kdig.rst man_khost.rst man_kjournalprint.rst man_knsupdate.rst man_knot1to2.rst man_knsec3hash.rst man_keymgr.rst man_kzonecheck.rst
EXTRA_DIST = \
conf.py \
......@@ -62,7 +62,7 @@ man_MANS += man/knot.conf.5 man/knotc.8 man/knotd.8
endif # HAVE_DAEMON
if HAVE_UTILS
man_MANS += man/kdig.1 man/khost.1 man/knsupdate.1 man/knot1to2.1 man/knsec3hash.1 man/keymgr.8 man/kzonecheck.1
man_MANS += man/kdig.1 man/khost.1 man/kjournalprint.1 man/knsupdate.1 man/knot1to2.1 man/knsec3hash.1 man/keymgr.8 man/kzonecheck.1
endif # HAVE_UTILS
man/knot.conf.5: man/knot.conf.5in
......@@ -70,6 +70,7 @@ man/knotc.8: man/knotc.8in
man/knotd.8: man/knotd.8in
man/kdig.1: man/kdig.1in
man/khost.1: man/khost.1in
man/kjournalprint.1: man/kjournalprint.1in
man/knsupdate.1: man/knsupdate.1in
man/knot1to2.1: man/knot1to2.1in
man/knsec3hash.1: man/knsec3hash.1in
......
......@@ -220,6 +220,7 @@ man_pages = [
('man_kdig', 'kdig', 'Advanced DNS lookup utility', author, 1),
('man_keymgr', 'keymgr', ' DNSSEC key management utility', author, 8),
('man_khost', 'khost', 'Simple DNS lookup utility', author, 1),
('man_kjournalprint', 'kjournalprint', 'Knot DNS journal print utility', author, 1),
('man_knot1to2', 'knot1to2', 'Knot DNS configuration conversion utility', author, 1),
('man_knotc', 'knotc', 'Knot DNS control utility', author, 8),
('man_knotd', 'knotd', 'Knot DNS server daemon', author, 8),
......
.\" Man page generated from reStructuredText.
.
.TH "KJOURNALPRINT" "1" "@RELEASE_DATE@" "@VERSION@" "Knot DNS"
.SH NAME
kjournalprint \- Knot DNS journal print utility
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.SH SYNOPSIS
.sp
\fBkjournalprint\fP [\fIparameters\fP] \fIjournal\fP \fIzone_name\fP
.SH DESCRIPTION
.sp
Program requires journal. As default, changes are colored for terminal.
.SS Parameters
.INDENT 0.0
.TP
\fB\-n\fP, \fB\-\-no\-color\fP
Removes changes coloring.
.TP
\fB\-l\fP, \fB\-\-limit\fP \fIlimit\fP
Limits the number of displayed changes.
.TP
\fB\-h\fP, \fB\-\-help\fP
Print the program help.
.TP
\fB\-V\fP, \fB\-\-version\fP
Print the program version.
.UNINDENT
.SS Journal
.sp
Requires journal in the form of path/zone\-name.db
.SS Zone name
.sp
Requires name of the zone contained in the journal.
.SH EXAMPLES
.SS Last (\fImost recent\fP) 5 changes without colors
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
$ kjournalprint \-nl 5 example.com.db example.com.
.ft P
.fi
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fIknotd(8)\fP, \fIknot.conf(5)\fP\&.
.SH AUTHOR
CZ.NIC Labs <http://www.knot-dns.cz>
.SH COPYRIGHT
Copyright 2010–2016, CZ.NIC, z.s.p.o.
.\" Generated by docutils manpage writer.
.
.. highlight:: console
kjournalprint – Knot DNS journal print utility
==============================================
Synopsis
--------
:program:`kjournalprint` [*parameters*] *journal* *zone_name*
Description
-----------
Program requires journal. As default, changes are colored for terminal.
Parameters
..........
**-n**, **--no-color**
Removes changes coloring.
**-l**, **--limit** *limit*
Limits the number of displayed changes.
**-h**, **--help**
Print the program help.
**-V**, **--version**
Print the program version.
Journal
.......
Requires journal in the form of path/zone-name.db
Zone name
.........
Requires name of the zone contained in the journal.
Examples
--------
Last (*most recent*) 5 changes without colors
.............................................
::
$ kjournalprint -nl 5 example.com.db example.com.
See Also
--------
:manpage:`knotd(8)`, :manpage:`knot.conf(5)`.
......@@ -13,6 +13,7 @@ the server. This section collects manual pages for all provided binaries:
man_kdig
man_keymgr
man_khost
man_kjournalprint
man_knot1to2
man_knotc
man_knotd
......
......@@ -482,7 +482,7 @@ endif # HAVE_DAEMON
if HAVE_UTILS
bin_PROGRAMS = kdig khost knsec3hash knsupdate kzonecheck
bin_PROGRAMS = kdig khost knsec3hash knsupdate kzonecheck kjournalprint
if !HAVE_DAEMON
noinst_LTLIBRARIES += libknotd.la
endif
......@@ -519,16 +519,21 @@ kzonecheck_SOURCES = \
utils/kzonecheck/zone_check.c \
utils/kzonecheck/zone_check.h
kjournalprint_SOURCES = \
utils/kjournalprint/main.c
# bin programs
kdig_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
kdig_LDADD = $(libidn_LIBS) libknotus.la
khost_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
khost_LDADD = $(libidn_LIBS) libknotus.la
knsupdate_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
knsupdate_LDADD = zscanner/libzscanner.la libknotus.la
knsec3hash_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir)/dnssec/lib/dnssec -I$(srcdir)/dnssec
knsec3hash_LDADD = dnssec/libdnssec.la dnssec/libshared.la
kzonecheck_LDADD = libknotd.la
kdig_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
kdig_LDADD = $(libidn_LIBS) libknotus.la
khost_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
khost_LDADD = $(libidn_LIBS) libknotus.la
knsupdate_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
knsupdate_LDADD = zscanner/libzscanner.la libknotus.la
knsec3hash_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir)/dnssec/lib/dnssec -I$(srcdir)/dnssec
knsec3hash_LDADD = dnssec/libdnssec.la dnssec/libshared.la
kzonecheck_LDADD = libknotd.la
kjournalprint_CPPFLAGS = $(AM_CPPFLAGS) $(gnutls_CFLAGS)
kjournalprint_LDADD = $(libidn_LIBS) libknotd.la
#######################################
# Optional Knot DNS Utilities modules #
......
......@@ -177,7 +177,7 @@ static int ixfr_load_chsets(list_t *chgsets, const zone_t *zone,
char *path = conf_journalfile(conf(), zone->name);
pthread_mutex_lock((pthread_mutex_t *)&zone->journal_lock);
ret = journal_load_changesets(path, zone, chgsets, serial_from, serial_to);
ret = journal_load_changesets(path, zone->name, chgsets, serial_from, serial_to);
pthread_mutex_unlock((pthread_mutex_t *)&zone->journal_lock);
free(path);
......
......@@ -700,7 +700,7 @@ bool journal_exists(const char *path)
}
/*! \brief No doc here. Moved from zones.h (@mvavrusa) */
static int changesets_unpack(changeset_t *chs)
int changesets_unpack(changeset_t *chs)
{
/* Read changeset flags. */
......@@ -870,9 +870,9 @@ static int changeset_pack(const changeset_t *chs, journal_t *j)
}
/*! \brief Helper for iterating journal (this is temporary until #80) */
typedef int (*journal_apply_t)(journal_t *, journal_node_t *, const zone_t *, list_t *);
typedef int (*journal_apply_t)(journal_t *, journal_node_t *, const knot_dname_t *, list_t *);
static int journal_walk(const char *fn, uint32_t from, uint32_t to,
journal_apply_t cb, const zone_t *zone, list_t *chgs)
journal_apply_t cb, const knot_dname_t *zone, list_t *chgs)
{
/* Open journal for reading. */
journal_t *journal = NULL;
......@@ -917,9 +917,9 @@ finish:
return ret;
}
static int load_changeset(journal_t *journal, journal_node_t *n, const zone_t *zone, list_t *chgs)
int load_changeset(journal_t *journal, journal_node_t *n, const knot_dname_t *zone, list_t *chgs)
{
changeset_t *ch = changeset_new(zone->name);
changeset_t *ch = changeset_new(zone);
if (ch == NULL) {
return KNOT_ENOMEM;
}
......@@ -945,7 +945,7 @@ static int load_changeset(journal_t *journal, journal_node_t *n, const zone_t *z
return KNOT_EOK;
}
int journal_load_changesets(const char *path, const zone_t *zone, list_t *dst,
int journal_load_changesets(const char *path, const knot_dname_t *zone, list_t *dst,
uint32_t from, uint32_t to)
{
int ret = journal_walk(path, from, to, &load_changeset, zone, dst);
......
......@@ -180,9 +180,13 @@ bool journal_exists(const char *path);
* \retval KNOT_ERANGE if given entry was not found.
* \return < KNOT_EOK on error.
*/
int journal_load_changesets(const char *path, const struct zone *zone, list_t *dst,
int journal_load_changesets(const char *path, const knot_dname_t *zone, list_t *dst,
uint32_t from, uint32_t to);
// TODO: :-/
int load_changeset(journal_t *journal, journal_node_t *n, const knot_dname_t *zone, list_t *chgs);
int changesets_unpack(changeset_t *chs);
/*!
* \brief Store changesets in journal.
*
......
/* Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
/* Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -136,7 +136,7 @@ static int node_dump_text(zone_node_t *node, void *data)
return KNOT_EOK;
}
int zone_dump_text(zone_contents_t *zone, FILE *file)
int zone_dump_text(zone_contents_t *zone, FILE *file, bool comments)
{
if (zone == NULL || file == NULL) {
return KNOT_EINVAL;
......@@ -148,7 +148,9 @@ int zone_dump_text(zone_contents_t *zone, FILE *file)
return KNOT_ENOMEM;
}
fprintf(file, ";; Zone dump (Knot DNS %s)\n", PACKAGE_VERSION);
if (comments) {
fprintf(file, ";; Zone dump (Knot DNS %s)\n", PACKAGE_VERSION);
}
// Set structure with parameters.
zone_node_t *apex = zone->apex;
......@@ -172,7 +174,7 @@ int zone_dump_text(zone_contents_t *zone, FILE *file)
// Dump RRSIG records if available.
params.dump_rrsig = true;
params.dump_nsec = false;
params.first_comment = ";; DNSSEC signatures\n";
params.first_comment = comments ? ";; DNSSEC signatures\n" : NULL;
ret = zone_contents_apply(zone, node_dump_text, &params);
if (ret != KNOT_EOK) {
return ret;
......@@ -181,7 +183,7 @@ int zone_dump_text(zone_contents_t *zone, FILE *file)
// Dump NSEC chain if available.
params.dump_rrsig = false;
params.dump_nsec = true;
params.first_comment = ";; DNSSEC NSEC chain\n";
params.first_comment = comments ? ";; DNSSEC NSEC chain\n" : NULL;
ret = zone_contents_apply(zone, node_dump_text, &params);
if (ret != KNOT_EOK) {
return ret;
......@@ -190,7 +192,7 @@ int zone_dump_text(zone_contents_t *zone, FILE *file)
// Dump NSEC3 chain if available.
params.dump_rrsig = false;
params.dump_nsec = true;
params.first_comment = ";; DNSSEC NSEC3 chain\n";
params.first_comment = comments ? ";; DNSSEC NSEC3 chain\n" : NULL;
ret = zone_contents_nsec3_apply(zone, node_dump_text, &params);
if (ret != KNOT_EOK) {
return ret;
......@@ -198,23 +200,25 @@ int zone_dump_text(zone_contents_t *zone, FILE *file)
params.dump_rrsig = true;
params.dump_nsec = false;
params.first_comment = ";; DNSSEC NSEC3 signatures\n";
params.first_comment = comments ? ";; DNSSEC NSEC3 signatures\n" : NULL;
ret = zone_contents_nsec3_apply(zone, node_dump_text, &params);
if (ret != KNOT_EOK) {
return ret;
}
// Create formatted date-time string.
time_t now = time(NULL);
struct tm tm;
localtime_r(&now, &tm);
char date[64];
strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S %Z", &tm);
// Dump trailing statistics.
fprintf(file, ";; Written %"PRIu64" records\n"
";; Time %s\n",
params.rr_count, date);
if (comments) {
// Create formatted date-time string.
time_t now = time(NULL);
struct tm tm;
localtime_r(&now, &tm);
char date[64];
strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S %Z", &tm);
// Dump trailing statistics.
fprintf(file, ";; Written %"PRIu64" records\n"
";; Time %s\n",
params.rr_count, date);
}
free(params.buf); // params.buf may be != buf because of knot_rrset_txt_dump_dynamic()
......
/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
/* Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
......@@ -29,12 +29,13 @@
/*!
* \brief Dumps given zone to text file.
*
* \param zone Zone to be saved.
* \param file File to write to.
* \param zone Zone to be saved.
* \param file File to write to.
* \param comments Add separating comments indicator.
*
* \retval KNOT_EOK on success.
* \retval < 0 if error.
*/
int zone_dump_text(zone_contents_t *zone, FILE *file);
int zone_dump_text(zone_contents_t *zone, FILE *file, bool comments);
/*! @} */
......@@ -112,7 +112,7 @@ int zone_load_journal(conf_t *conf, zone_t *zone, zone_contents_t *contents)
init_list(&chgs);
pthread_mutex_lock(&zone->journal_lock);
int ret = journal_load_changesets(journal_name, zone, &chgs, serial,
int ret = journal_load_changesets(journal_name, zone->name, &chgs, serial,
serial - 1);
pthread_mutex_unlock(&zone->journal_lock);
free(journal_name);
......
......@@ -316,7 +316,7 @@ int zonefile_write(const char *path, zone_contents_t *zone)
return ret;
}
ret = zone_dump_text(zone, file);
ret = zone_dump_text(zone, file, true);
fclose(file);
if (ret != KNOT_EOK) {
unlink(tmp_name);
......
/* Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <getopt.h>
#include "libknot/libknot.h"
#include "knot/server/serialization.h"
#include "knot/zone/zone-dump.h"
#include "utils/common/exec.h"
#include "contrib/strtonum.h"
#define PROGRAM_NAME "kjournalprint"
#define RED "\x1B[31m"
#define GRN "\x1B[32m"
#define YLW "\x1B[93m"
#define RESET "\x1B[0m"
static void print_help(void)
{
printf("Usage: %s [parameter] <journal> <zone_name>\n"
"\n"
"Parameters:\n"
" -n, --no-color Get output without terminal coloring.\n"
" -l, --limit <Limit> Read only x newest changes.\n",
PROGRAM_NAME);
}
static inline char *get_rrset(knot_rrset_t *rrset, char **buff, size_t *len)
{
int ret = knot_rrset_txt_dump(rrset, buff, len, &KNOT_DUMP_STYLE_DEFAULT);
return (ret > 0) ? *buff : "Corrupted or missing!\n";
}
int print_journal(char *path, knot_dname_t *name, uint32_t limit, bool color)
{
list_t db;
init_list(&db);
size_t buflen = 8192;
char *buff = malloc(buflen);
if (buff == NULL) {
return KNOT_ENOMEM;
}
// Open journal for reading.
journal_t *journal = NULL;
int ret = journal_open(&journal, path, ~((size_t)0));
if (ret != KNOT_EOK) {
return ret;
}
// Load changesets from journal.
if (journal->qtail == journal->qhead) {
journal_close(journal);
return KNOT_ENOENT;
}
size_t i = (limit && journal->qtail - limit) ?
journal->qtail - limit : journal->qhead;
for (; i < journal->qtail; i = (i + 1) % journal->max_nodes) {
// Skip invalid nodes.
journal_node_t *n = journal->nodes + i;
if (!(n->flags & JOURNAL_VALID)) {
printf("%zu. node invalid\n", i);
continue;
}
load_changeset(journal, n, name, &db);
}
// Unpack and print changsets.
changeset_t *chs = NULL;
for (chs = (void *)(db.head); (node_t *)((node_t *)chs)->next; chs = (void *)((node_t *) chs)->next) {
ret = changesets_unpack(chs);
if (ret != KNOT_EOK) {
break;
}
printf(color ? YLW : "");
printf(";; Changes between zone versions: %u -> %u\n",
knot_soa_serial(&chs->soa_from->rrs),
knot_soa_serial(&chs->soa_to->rrs));
// Removed.
printf(color ? RED : "");
printf(";; Removed\n");
printf("%s", get_rrset(chs->soa_from, &buff, &buflen));
zone_dump_text(chs->remove, stdout, false);
// Added.
printf(color ? GRN : "");
printf(";; Added\n");
printf("%s", get_rrset(chs->soa_to, &buff, &buflen));
zone_dump_text(chs->add, stdout, false);
printf(color ? RESET : "");
}
free(buff);
changesets_free(&db);
journal_close(journal);
return ret;
}
int main(int argc, char *argv[])
{
uint32_t limit = 0;
bool color = true;
struct option opts[] = {
{ "limit", required_argument, NULL, 'l' },
{ "no-color", no_argument, NULL, 'n' },
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ NULL }
};
int opt = 0;
while ((opt = getopt_long(argc, argv, "l:nhV", opts, NULL)) != -1) {
switch (opt) {
case 'l':
if (str_to_u32(optarg, &limit) != KNOT_EOK) {
print_help();
return EXIT_FAILURE;
}
break;
case 'n':
color = false;
break;
case 'h':
print_help();
return EXIT_SUCCESS;
case 'V':
print_version(PROGRAM_NAME);
return EXIT_SUCCESS;
default:
print_help();
return EXIT_FAILURE;
}
}
char *db = NULL;
knot_dname_t *name = NULL;
switch (argc - optind) {
case 2:
name = knot_dname_from_str_alloc(argv[optind + 1]);
// FALLTHROUGH
case 1:
db = argv[optind];
break;
default:
print_help();
return EXIT_FAILURE;
}
if (db == NULL) {
fprintf(stderr, "Journal file not specified\n");
return EXIT_FAILURE;
}
if (name == NULL) {
fprintf(stderr, "Zone not specified\n");
return EXIT_FAILURE;
}
int ret = print_journal(db, name, limit, color);
free(name);
switch (ret) {
case KNOT_ENOENT:
printf("The journal is empty.\n");
break;
case KNOT_EOUTOFZONE:
fprintf(stderr, "The specified journal DB does not contain the specified zone.\n");
return EXIT_FAILURE;
case KNOT_EOK:
break;
default:
fprintf(stderr, "Failed to load changesets (%s).\n", knot_strerror(ret));
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
......@@ -229,7 +229,7 @@ static void test_store_load(const char *jfilename)
ok(ret == KNOT_EOK, "journal: store changeset");
list_t l;
init_list(&l);
ret = journal_load_changesets(jfilename, &z, &l, 0, 1);
ret = journal_load_changesets(jfilename, z.name, &l, 0, 1);
ok(ret == KNOT_EOK && changesets_eq(TAIL(l), &ch), "journal: load changeset");
changeset_clear(&ch);
changesets_free(&l);
......@@ -247,7 +247,7 @@ static void test_store_load(const char *jfilename)
/* Load all changesets stored until now. */
serial--;
ret = journal_load_changesets(jfilename, &z, &l, 0, serial);
ret = journal_load_changesets(jfilename, z.name, &l, 0, serial);
changesets_free(&l);
ok(ret == KNOT_EOK, "journal: load changesets");
......@@ -263,7 +263,7 @@ static void test_store_load(const char *jfilename)
/* Load all changesets, except the first one that got evicted. */
init_list(&l);
ret = journal_load_changesets(jfilename, &z, &l, 1, serial + 1);
ret = journal_load_changesets(jfilename, z.name, &l, 1, serial + 1);
changesets_free(&l);
ok(ret == KNOT_EOK, "journal: load changesets after flush");
}
......
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