588 lines
14 KiB
C
588 lines
14 KiB
C
/* $Id$ */
|
|
/*
|
|
* Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
#include <inttypes.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
#include <kcgi.h>
|
|
#include <kcgijson.h>
|
|
|
|
#include "extern.h"
|
|
|
|
/*
|
|
* Start with five pages.
|
|
* As you add more pages, you'll start by giving them an identifier key
|
|
* in this enum.
|
|
*/
|
|
enum page {
|
|
PAGE_INDEX,
|
|
PAGE_BOOKS,
|
|
PAGE_AUTHORS,
|
|
PAGE_SERIES,
|
|
PAGE__MAX
|
|
};
|
|
|
|
enum key {
|
|
KEY_PAGE,
|
|
KEY_PERPAGE,
|
|
KEY_SORT,
|
|
KEY_ORDER,
|
|
KEY_TERM,
|
|
KEY__MAX
|
|
};
|
|
|
|
static const char *const pages[PAGE__MAX] = {
|
|
"index", /* PAGE_INDEX */
|
|
"books", /* PAGE_BOOKS */
|
|
"authors", /* PAGE_AUTHORS */
|
|
"series", /* PAGE_SERIES */
|
|
};
|
|
|
|
static const struct kvalid keys[KEY__MAX] = {
|
|
{ kvalid_int, "page" }, /* KEY_PAGE */
|
|
{ kvalid_int, "perpage" }, /* KEY_PERPAGE */
|
|
{ kvalid_string, "sort" }, /* KEY_SORT */
|
|
{ kvalid_string, "order" }, /* KEY_ORDER */
|
|
{ kvalid_stringne, "term" }, /* KEY_TERM */
|
|
};
|
|
|
|
/*
|
|
* Fill out all HTTP secure headers.
|
|
* Use the existing document's MIME type.
|
|
*/
|
|
static void
|
|
http_alloc(struct kreq *r, enum khttp code)
|
|
{
|
|
|
|
khttp_head(r, kresps[KRESP_STATUS],
|
|
"%s", khttps[code]);
|
|
khttp_head(r, kresps[KRESP_CONTENT_TYPE],
|
|
"%s", kmimetypes[r->mime]);
|
|
khttp_head(r, "X-Content-Type-Options", "nosniff");
|
|
khttp_head(r, "X-Frame-Options", "DENY");
|
|
khttp_head(r, "X-XSS-Protection", "1; mode=block");
|
|
}
|
|
|
|
/*
|
|
* Fill out all headers with http_alloc() then start the HTTP document
|
|
* body (no more headers after this point!)
|
|
*/
|
|
static void
|
|
http_open(struct kreq *r, enum khttp code)
|
|
{
|
|
|
|
http_alloc(r, code);
|
|
khttp_body(r);
|
|
}
|
|
|
|
static void
|
|
puterror(struct kjsonreq *req, char *message)
|
|
{
|
|
kjson_obj_open(req);
|
|
kjson_putstringp(req, "msg", message);
|
|
kjson_obj_close(req);
|
|
}
|
|
|
|
/*
|
|
* Append author object to json (NOT CLOSED).
|
|
*/
|
|
static void
|
|
putauthor(struct kjsonreq *req, Author *a)
|
|
{
|
|
kjson_obj_open(req);
|
|
kjson_putintp(req, "id", a->id);
|
|
kjson_putstringp(req, "name", a->name);
|
|
}
|
|
|
|
/*
|
|
* Append book object to json (NOT CLOSED).
|
|
*/
|
|
static void
|
|
putbook(struct kjsonreq *req, Book *b)
|
|
{
|
|
kjson_obj_open(req);
|
|
kjson_putintp(req, "id", b->id);
|
|
kjson_putstringp(req, "title", b->title);
|
|
if (b->s.id >= 0) {
|
|
kjson_objp_open(req, "series");
|
|
kjson_putintp(req, "id", b->s.id);
|
|
kjson_putstringp(req, "name", b->s.name);
|
|
kjson_putdoublep(req, "idx", b->s_idx);
|
|
kjson_obj_close(req);
|
|
}
|
|
}
|
|
|
|
static void
|
|
puttags(struct kjsonreq *req, BookAdv *b)
|
|
{
|
|
if (b->tsize > 0) {
|
|
kjson_arrayp_open(req, "tags");
|
|
for (size_t i=0; i<b->tsize;i++) {
|
|
kjson_putstring(req, b->tags[i]);
|
|
}
|
|
kjson_array_close(req);
|
|
}
|
|
}
|
|
|
|
static void
|
|
putauthors(struct kjsonreq *req, BookAdv *b)
|
|
{
|
|
if (b->asize > 0) {
|
|
kjson_arrayp_open(req, "authors");
|
|
for (size_t j=0; j<b->asize; j++) {
|
|
putauthor(req, b->a[j]);
|
|
kjson_obj_close(req);
|
|
}
|
|
kjson_array_close(req);
|
|
}
|
|
}
|
|
|
|
static void
|
|
putbookadv(struct kjsonreq *req, BookAdv *ba)
|
|
{
|
|
putbook(req, &ba->b);
|
|
putauthors(req, ba);
|
|
puttags(req, ba);
|
|
kjson_obj_close(req);
|
|
db_book_adv_free(ba);
|
|
}
|
|
|
|
static void
|
|
putbookdata(struct kjsonreq *req, BookFull *b)
|
|
{
|
|
if (b->dsize > 0) {
|
|
kjson_arrayp_open(req, "data");
|
|
for (size_t i=0;i<b->dsize;i++) {
|
|
kjson_obj_open(req);
|
|
kjson_putstringp(req, "name", b->data[i]->name);
|
|
kjson_putstringp(req, "format", b->data[i]->format);
|
|
kjson_putintp(req, "size", b->data[i]->size);
|
|
kjson_obj_close(req);
|
|
}
|
|
kjson_array_close(req);
|
|
}
|
|
}
|
|
|
|
static void
|
|
putbookfull(struct kjsonreq *req, BookFull *b)
|
|
{
|
|
char tsstr[30];
|
|
|
|
putbook(req, &b->ba.b);
|
|
kutil_epoch2str(b->timestamp, tsstr, sizeof(tsstr));
|
|
kjson_putstringp(req, "timestamp", tsstr);
|
|
kjson_putintp(req, "pubdate", b->pubdate);
|
|
kjson_putstringp(req, "isbn", b->isbn);
|
|
kjson_putstringp(req, "lccn", b->lccn);
|
|
kjson_putstringp(req, "path", b->path);
|
|
kjson_putstringp(req, "uuid", b->uuid);
|
|
kjson_putboolp(req, "has_cover", b->has_cover);
|
|
kjson_putstringp(req, "lang", b->lang);
|
|
if (NULL != b->publisher)
|
|
kjson_putstringp(req, "publisher", b->publisher);
|
|
}
|
|
|
|
static const char*
|
|
initsort(struct kreq *r)
|
|
{
|
|
if (NULL != r->fieldmap[KEY_SORT])
|
|
return r->fieldmap[KEY_SORT]->parsed.s;
|
|
return NULL;
|
|
}
|
|
|
|
static const char*
|
|
initorder(struct kreq *r)
|
|
{
|
|
if (NULL != r->fieldmap[KEY_ORDER])
|
|
return r->fieldmap[KEY_ORDER]->parsed.s;
|
|
return NULL;
|
|
}
|
|
|
|
static int
|
|
initpage(struct kreq *r)
|
|
{
|
|
if (NULL != r->fieldmap[KEY_PAGE])
|
|
return r->fieldmap[KEY_PAGE]->parsed.i;
|
|
return 1;
|
|
}
|
|
|
|
static size_t
|
|
inittermz(struct kreq *r)
|
|
{
|
|
size_t termz = 0;
|
|
for (size_t j=0; j<r->fieldsz; j++) {
|
|
struct kpair field = r->fields[j];
|
|
if (KPAIR_VALID == field.state && 0 == strcmp(keys[KEY_TERM].name, field.key))
|
|
termz++;
|
|
}
|
|
return termz;
|
|
}
|
|
|
|
static void
|
|
initterms(struct kreq *r, const char **terms)
|
|
{
|
|
int i = 0;
|
|
for (size_t j=0; j<r->fieldsz; j++) {
|
|
struct kpair field = r->fields[j];
|
|
if (KPAIR_VALID == field.state && 0 == strcmp(keys[KEY_TERM].name, field.key))
|
|
terms[i++] = field.parsed.s;
|
|
}
|
|
}
|
|
|
|
static int64_t
|
|
idfrompath(struct kreq *r)
|
|
{
|
|
const char *errid;
|
|
if (r->path[0] != '\0')
|
|
return strtonum(r->path, INT64_MIN, INT64_MAX, &errid);
|
|
return -1;
|
|
}
|
|
|
|
static int
|
|
initperpage(struct kreq *r)
|
|
{
|
|
if (NULL != r->fieldmap[KEY_PERPAGE])
|
|
return r->fieldmap[KEY_PERPAGE]->parsed.i;
|
|
return 20;
|
|
}
|
|
|
|
static void
|
|
sendbooks(struct kreq *r)
|
|
{
|
|
struct kjsonreq req;
|
|
BookFull *b = NULL;
|
|
int res, i = 0;
|
|
|
|
debug(r, "sendbooks");
|
|
int page = initpage(r);
|
|
int per = initperpage(r);
|
|
const char* sort = initsort(r);
|
|
const char* order = initorder(r);
|
|
size_t termz = inittermz(r);
|
|
int64_t id = idfrompath(r);
|
|
|
|
|
|
if (id >= 0) {
|
|
b = db_book_load(r, id);
|
|
}
|
|
http_open(r, (id > 0 && NULL == b) ? KHTTP_404 : KHTTP_200);
|
|
kjson_open(&req, r);
|
|
if (id >= 0 && NULL == b) {
|
|
puterror(&req, "Unknown book");
|
|
} else if (NULL != b) {
|
|
putbookfull(&req, b);
|
|
putauthors(&req, &b->ba);
|
|
puttags(&req, &b->ba);
|
|
putbookdata(&req, b);
|
|
kjson_obj_close(&req);
|
|
db_book_full_free(b);
|
|
} else if (termz > 0) {
|
|
const char **terms = kcalloc(termz, sizeof(char *));
|
|
initterms(r, terms);
|
|
Book **books = kcalloc(per, sizeof(Book));
|
|
int c = db_books_search(r, per, books, terms, termz, 0);
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "count", c);
|
|
kjson_arrayp_open(&req, "books");
|
|
while (i < per && i < c) {
|
|
putbook(&req, books[i]);
|
|
kjson_obj_close(&req);
|
|
i++;
|
|
}
|
|
kjson_array_close(&req);
|
|
kjson_obj_close(&req);
|
|
free(books);
|
|
free(terms);
|
|
} else {
|
|
kjson_array_open(&req);
|
|
BookAdv **books = kcalloc(per, sizeof(BookAdv));
|
|
res = db_books_load(r, books, sort, order, per, per * (page-1));
|
|
while (i < res) {
|
|
putbookadv(&req, books[i]);
|
|
i++;
|
|
}
|
|
free(books);
|
|
|
|
kjson_array_close(&req);
|
|
}
|
|
kjson_close(&req);
|
|
}
|
|
|
|
static void
|
|
sendauthors(struct kreq *r)
|
|
{
|
|
struct kjsonreq req;
|
|
int res, i = 0;
|
|
AuthorFull *a = NULL;
|
|
|
|
debug(r, "sendauthors");
|
|
int page = initpage(r);
|
|
int per = initperpage(r);
|
|
const char* sort = initsort(r);
|
|
const char* order = initorder(r);
|
|
size_t termz = inittermz(r);
|
|
int64_t id = idfrompath(r);
|
|
|
|
http_open(r, KHTTP_200);
|
|
kjson_open(&req, r);
|
|
|
|
if (id >= 0)
|
|
a = db_author_load(r, id);
|
|
if (id >= 0 && NULL == a) {
|
|
puterror(&req, "Unknown author");
|
|
} else if (id >= 0) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", a->a.id);
|
|
kjson_putstringp(&req, "name", a->a.name);
|
|
if (a->bsize>0) {
|
|
kjson_arrayp_open(&req, "books");
|
|
for (size_t j = 0; j<a->bsize; j++) {
|
|
putbookadv(&req, a->books[j]);
|
|
}
|
|
kjson_array_close(&req);
|
|
}
|
|
kjson_obj_close(&req);
|
|
} else if (termz > 0) {
|
|
const char **terms = kcalloc(termz, sizeof(char *));
|
|
initterms(r, terms);
|
|
Author **authors = kcalloc(per, sizeof(Author));
|
|
int c = db_authors_search(r, per, authors, terms, termz, 0);
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "count", c);
|
|
kjson_arrayp_open(&req, "authors");
|
|
while (i < per && i < c) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", authors[i]->id);
|
|
kjson_putstringp(&req, "name", authors[i]->name);
|
|
kjson_obj_close(&req);
|
|
i++;
|
|
}
|
|
kjson_array_close(&req);
|
|
kjson_obj_close(&req);
|
|
free(authors);
|
|
free(terms);
|
|
} else {
|
|
kjson_array_open(&req);
|
|
AuthorAdv **authors = kcalloc(per, sizeof(AuthorAdv));
|
|
res = db_authors_load(r, authors, sort, order, per, per * (page-1));
|
|
while (i < res) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", authors[i]->a.id);
|
|
kjson_putstringp(&req, "name", authors[i]->a.name);
|
|
kjson_putintp(&req, "count", authors[i]->count);
|
|
kjson_obj_close(&req);
|
|
db_author_adv_free(authors[i]);
|
|
i++;
|
|
}
|
|
free(authors);
|
|
kjson_array_close(&req);
|
|
}
|
|
|
|
kjson_close(&req);
|
|
}
|
|
|
|
static void
|
|
sendseries(struct kreq *r)
|
|
{
|
|
struct kjsonreq req;
|
|
int res, i = 0;
|
|
SeriesFull *s = NULL;
|
|
|
|
debug(r, "sendseries");
|
|
int page = initpage(r);
|
|
int per = initperpage(r);
|
|
size_t termz = inittermz(r);
|
|
const char* sort = initsort(r);
|
|
const char* order = initorder(r);
|
|
int64_t id = idfrompath(r);
|
|
|
|
http_open(r, KHTTP_200);
|
|
kjson_open(&req, r);
|
|
|
|
if (id >= 0)
|
|
s = db_serie_load(r, id);
|
|
if (id >= 0 && NULL == s) {
|
|
puterror(&req, "Unknown series");
|
|
} else if (id >= 0) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", s->s.s.id);
|
|
kjson_putstringp(&req, "name", s->s.s.name);
|
|
if (s->s.books>0) {
|
|
kjson_arrayp_open(&req, "books");
|
|
for (int64_t j = 0; j<s->s.books; j++) {
|
|
putbook(&req, s->b[j]);
|
|
kjson_obj_close(&req);
|
|
}
|
|
kjson_array_close(&req);
|
|
}
|
|
if (s->s.asize > 0) {
|
|
kjson_arrayp_open(&req, "authors");
|
|
for (size_t j=0; j<s->s.asize; j++) {
|
|
putauthor(&req, s->s.a[j]);
|
|
kjson_obj_close(&req);
|
|
}
|
|
kjson_array_close(&req);
|
|
}
|
|
kjson_obj_close(&req);
|
|
} else if (termz > 0) {
|
|
const char **terms = kcalloc(termz, sizeof(char *));
|
|
initterms(r, terms);
|
|
Series **series = kcalloc(per, sizeof(Series));
|
|
int c = db_series_search(r, per, series, terms, termz, 0);
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "count", c);
|
|
kjson_arrayp_open(&req, "series");
|
|
while (i < per && i < c) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", series[i]->id);
|
|
kjson_putstringp(&req, "name", series[i]->name);
|
|
kjson_obj_close(&req);
|
|
i++;
|
|
}
|
|
kjson_array_close(&req);
|
|
kjson_obj_close(&req);
|
|
free(series);
|
|
free(terms);
|
|
} else {
|
|
kjson_array_open(&req);
|
|
SeriesAdv **series = kcalloc(per, sizeof(SeriesAdv));
|
|
res = db_series_load(r, series, sort, order, per, per * (page-1));
|
|
while (i < res) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", series[i]->s.id);
|
|
kjson_putstringp(&req, "name", series[i]->s.name);
|
|
kjson_putintp(&req, "count", series[i]->books);
|
|
if (series[i]->asize > 0) {
|
|
kjson_arrayp_open(&req, "authors");
|
|
for (size_t j=0; j<series[i]->asize; j++) {
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "id", series[i]->a[j]->id);
|
|
kjson_putstringp(&req, "name", series[i]->a[j]->name);
|
|
kjson_obj_close(&req);
|
|
}
|
|
kjson_array_close(&req);
|
|
}
|
|
kjson_obj_close(&req);
|
|
db_series_adv_free(series[i]);
|
|
i++;
|
|
}
|
|
free(series);
|
|
kjson_array_close(&req);
|
|
}
|
|
|
|
kjson_close(&req);
|
|
}
|
|
|
|
static void
|
|
sendindex(struct kreq *r)
|
|
{
|
|
struct kjsonreq req;
|
|
|
|
debug(r, "sendindex");
|
|
http_open(r, KHTTP_200);
|
|
kjson_open(&req, r);
|
|
kjson_obj_open(&req);
|
|
kjson_putintp(&req, "count", db_books_count(r));
|
|
kjson_obj_close(&req);
|
|
kjson_close(&req);
|
|
}
|
|
|
|
int
|
|
main(void)
|
|
{
|
|
struct kreq r;
|
|
enum kcgi_err er;
|
|
|
|
kutil_openlog(LOGFILE);
|
|
|
|
/* Actually parse HTTP document. */
|
|
|
|
er = khttp_parsex(&r, ksuffixmap,
|
|
kmimetypes, KMIME__MAX, keys, KEY__MAX,
|
|
pages, PAGE__MAX, KMIME_APP_JSON,
|
|
PAGE_INDEX, NULL, NULL, 0, NULL);
|
|
|
|
if (KCGI_OK != er) {
|
|
fprintf(stderr, "HTTP parse error: %d\n", er);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
#ifdef __OpenBSD__
|
|
if (-1 == pledge("stdio rpath cpath wpath flock fattr", NULL)) {
|
|
fputs("pledge", stderr);
|
|
khttp_free(&r);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
#endif
|
|
|
|
|
|
/*
|
|
* Front line of defence: make sure we're a proper method, make
|
|
* sure we're a page, make sure we're a JSON file.
|
|
*/
|
|
|
|
if (KMETHOD_GET != r.method &&
|
|
KMETHOD_POST != r.method) {
|
|
http_open(&r, KHTTP_405);
|
|
khttp_free(&r);
|
|
return(EXIT_SUCCESS);
|
|
} else if (PAGE__MAX == r.page ||
|
|
KMIME_APP_JSON != r.mime) {
|
|
http_open(&r, KHTTP_404);
|
|
khttp_puts(&r, "Page not found.");
|
|
khttp_free(&r);
|
|
return(EXIT_SUCCESS);
|
|
}
|
|
|
|
debug(&r, "Opening database at %s/%s", DATADIR, DATABASE);
|
|
if ( ! db_open(&r, DATADIR "/" DATABASE)) {
|
|
kutil_warn(&r, NULL, "Error opening database at %s/%s", DATADIR, DATABASE);
|
|
http_open(&r, KHTTP_500);
|
|
json_emptydoc(&r);
|
|
khttp_free(&r);
|
|
return(EXIT_SUCCESS);
|
|
}
|
|
debug(&r, "Database ready %s/%s", DATADIR, DATABASE);
|
|
|
|
switch (r.page) {
|
|
case (PAGE_INDEX):
|
|
sendindex(&r);
|
|
break;
|
|
case (PAGE_BOOKS):
|
|
sendbooks(&r);
|
|
break;
|
|
case (PAGE_AUTHORS):
|
|
sendauthors(&r);
|
|
break;
|
|
case (PAGE_SERIES):
|
|
sendseries(&r);
|
|
break;
|
|
default:
|
|
abort();
|
|
}
|
|
|
|
db_close(&r);
|
|
khttp_free(&r);
|
|
return(EXIT_SUCCESS);
|
|
}
|