#!/usr/bin/perl use strict; use FCGI; use JSON; use DBI; use Data::Dumper; use warnings; $Data::Dumper::Sortkeys = 1; my $conf = load_conf("../etc/autodoc.json"); my $dbh = sqlconnect($conf->{sql}); my %map = ( api_v1_POST_documents => \&api_v1_POST_documents, api_v1_POST_documents_id_data => \&api_v1_POST_documents_id_data, api_v1_GET_documents_id_image => \&api_v1_GET_documents_id_image, api_v1_GET_pages_image => \&api_v1_GET_pages_image, api_v1_GET_documents => \&api_v1_GET_documents, api_v1_GET_documents_id => \&api_v1_GET_documents_id, api_v1_GET_pages_id => \&api_v1_GET_pages_id, api_v1_PATCH_documents_id => \&api_v1_PATCH_documents_id, ); my $request = FCGI::Request(); while($request->Accept() >= 0) { my $user = $ENV{REMOTE_USER} || 'undefined'; my $qs = parse_querystring($ENV{QUERY_STRING}); my $method = $ENV{REQUEST_METHOD}; my $path = [ split(/\//,$ENV{SCRIPT_NAME}) ] if exists $ENV{SCRIPT_NAME}; shift(@{$path}); my $post = parse_post( \*STDIN, exists $ENV{CONTENT_LENGTH} ? $ENV{CONTENT_LENGTH} : 0, exists $ENV{CONTENT_TYPE} ? $ENV{CONTENT_TYPE} : 0 ); my($code, $type, $data) = process_query($method, $path, $qs, $post, $user); if ( defined $type ) { if ( $type eq 'application/json' ) { $data = to_json($data, { utf8 => 1, pretty => 1 }); } } send_response($code, $type, $data); } sub send_response { my ($code, $type, $data) = @_; printf("Status: %s\r\n", $code); printf("Content-type: %s\r\n", $type) if defined $type; printf("Content-length: %i\r\n", length($data)) if defined $data; printf("\r\n"); print $data if defined $data; } sub fatal_api_error { my($code,$type,$body)=api_error(@_); send_response($code, $type, $body); exit; } sub load_conf { my($file) = @_; my $x=''; open(F,"$file") || fatal_api_error(500,"Failed to load configuration file"); while() { $x.=$_; } close(F); return from_json($x); } sub process_query { my($method, $path, $qs, $post, $user) = @_; my ($api_version, $path_main, $path_id, $path_sub) = @{$path}; return api_error(404, "Unknown API version") if !defined $api_version; return api_error(405, "Unknown METHOD") if !defined $method; return api_error(404, "Unknown API function") if !defined $path_main; my $func = 'api_' . $api_version . '_' . $method . '_' . $path_main; $func .= '_id' if defined $path_id; $func .= '_'.$path_sub if defined $path_sub; print STDERR "FUNC=$map{$func}\n"; return $map{$func}->($path_id, $qs, $post, $user) if exists $map{$func}; return api_error(404, "Invalid path/method combination"); } sub db_get_document_object { my($id) = @_; my $document; my @pages; my @pageids; my @tags; my $q = sqlquery($dbh, "SELECT * FROM documents WHERE id = ?", $id); while(my $hash = $q->fetchrow_hashref()) { $document = $hash; } $q = sqlquery($dbh, "SELECT * FROM pages WHERE documentId = ?", $id); while(my $hash = $q->fetchrow_hashref()) { push @pages, $hash; push @pageids, $hash->{id}; } $q = sqlquery($dbh, " SELECT tags.tag AS tag FROM documents_tags LEFT JOIN tags ON documents_tags.tagId = tags.id WHERE documentId = ? ORDER BY tag", $id); while(my ($tag) = $q->fetchrow_array()) { push @tags, $tag; } my %lang; foreach my $page ( @pages ) { $q = sqlquery($dbh, "SELECT * FROM pages_lang WHERE pageId = ?", $page->{id}); while(my $hash = $q->fetchrow_hashref()) { $lang{$hash->{language}}++; } } if ( defined $document ) { my $out = { id => $document->{id}, pageId => [ @pageids ], name => $document->{name}, created => $document->{created}, owner => $document->{owner}, status => $document->{status}, languages => [ keys %lang ], tags => [ @tags ], }; return (200, "application/json", $out); } return (404, "application/json", ""); } sub db_get_page_object { my($id) = @_; my $out; my %lang; my $q = sqlquery($dbh, "SELECT * FROM pages_lang WHERE pageId = ?", $id); while(my $hash = $q->fetchrow_hashref()) { $lang{$hash->{language}}++; } $q = sqlquery($dbh, "SELECT * FROM pages WHERE documentId = ?", $id); while(my $hash = $q->fetchrow_hashref()) { $out = { id => $id, documentId => $hash->{documentId}, name => $hash->{name}, created => $hash->{created}, owner => $hash->{owner}, status => $hash->{status}, language => [ keys %lang ], }; } if ( exists $out->{id} ) { return (200, "application/json", $out); } return (404, "application/json", ""); } # create an empty document object. sub api_v1_POST_documents { my($id, $qs, $post, $user) = @_; my $q = sqlquery($dbh, " INSERT INTO documents SET owner = ?, status = 'nodata' ", $user); $q = sqlquery($dbh, "SELECT LAST_INSERT_ID()"); while(my($lastid) = $q->fetchrow_array()) { $id = $lastid; } return db_get_document_object($id); } sub api_v1_POST_documents_id_data { return api_error(501,"Not yet implemented"); } sub api_v1_GET_documents_id_image { return api_error(501,"Not yet implemented"); } sub api_v1_GET_pages_image { return api_error(501,"Not yet implemented"); } # get a list of document objects sub api_v1_GET_documents { my($id, $qs, $post, $user) = @_; $qs->{pageSize} = $conf->{query}{pageSize} if !exists $qs->{pageSize}; $qs->{pageIndex} = $conf->{query}{pageIndex} if !exists $qs->{pageIndex}; my @out; my $q = sqlquery($dbh, "SELECT id FROM documents LIMIT ?,?", $qs->{pageSize} * $qs->{pageIndex}, $qs->{pageSize}); while(my ($id) = $q->fetchrow_array()) { my ($code, $ct, $body) = db_get_document_object($id); push @out, $body; } return ( 200, "application/json", \@out ); } # get a single document object sub api_v1_GET_documents_id { my($id, $qs, $post, $user) = @_; return db_get_document_object($id); } # get a specific page sub api_v1_GET_pages_id { my($id, $qs, $post, $user) = @_; return db_get_page_object($id); } # change document properties sub api_v1_PATCH_documents_id { my($id, $qs, $post, $user) = @_; if ( exists $qs->{addTags} ) { my $tags = get_array($qs->{addTags}); foreach my $tag ( @{$tags} ) { sqlquery($dbh, "CALL tag_add(?,?)", $id, $tag); } } if ( exists $qs->{deleteTags} ) { my $tags = get_array($qs->{deleteTags}); foreach my $tag ( @{$tags} ) { sqlquery($dbh, "CALL tag_del(?,?)", $id, $tag); } } if ( exists $qs->{name} ) { sqlquery($dbh, "UPDATE documents SET name = ? WHERE id = ?", $qs->{name}, $id); } return (200); } sub get_array { my($x) = @_; my @arr; if ( ref $x eq 'ARRAY' ) { @arr = @{$x}; } else { @arr = [ $x ]; } return \@arr; } sub api_error { my($code, $text)=@_; my %deftext = ( "000" => "An error produced an internal error, cascading errors over errors", "404" => "No such API path" ); $code = "000" if !defined $code; $text = $deftext{$code} if ( !defined $text || $code eq "000" ); return ( $code, "text/plain", $text . "\r\n" ); } sub parse_querystring { my($in) = @_; my %out; if ( defined $in && length $in ) { foreach(split(/&/,$in)) { my($name,$value) = split(/=/); $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; # handle arrays if ( exists $out{$name} ) { if ( ! ref $out{$name} ) { my $old = $out{$name}; $out{$name} = [ $old ]; } push @{$out{$name}}, $value; } else { $out{$name}=$value; } } } return \%out; } sub parse_post { my($fd, $len, $ct) = @_; my $data = ''; while ( $len > 0 ) { my $buf; my $read = read($fd, $buf, $len); $len -= $read; $data .= $buf; } if ( $ct eq 'application/json' ) { my $tmp = from_json($data); $data = $tmp; } return $data; } sub sqlconnect { my($sql) = @_; my $dsn = "DBI:mysql:database=$sql->{base};host=$sql->{host}"; my $dbh = DBI->connect($dsn, $sql->{user}, $sql->{pass}) || \\ fatal_api_error(500,"Failed to connect to database"); return $dbh; } sub sqlquery { my $dbh = shift; my $query = shift; my @args = @_; my $sth = $dbh->prepare($query) || fatal_api_error(500,"Failed to execute SQL query"); $sth->execute(@args) || fatal_api_error(500,"Failed to execute SQL query"); return $sth; }