diff options
Diffstat (limited to 'lib/Travelynx/Controller/Api.pm')
-rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 591 |
1 files changed, 532 insertions, 59 deletions
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index a442784..687243d 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -1,20 +1,54 @@ package Travelynx::Controller::Api; + +# Copyright (C) 2020-2023 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; -use Travel::Status::DE::IRIS::Stations; +use DateTime; +use List::Util; +use Mojo::JSON qw(encode_json); use UUID::Tiny qw(:std); +# Internal Helpers + sub make_token { return create_uuid_as_string(UUID_V4); } +sub sanitize { + my ( $type, $value ) = @_; + if ( not defined $value ) { + return undef; + } + if ( $type eq '' ) { + return '' . $value; + } + if ( $value =~ m{ ^ [0-9.e]+ $ }x ) { + return 0 + $value; + } + return 0; +} + +# Contollers + sub documentation { my ($self) = @_; - $self->render('api_documentation'); + if ( $self->is_user_authenticated ) { + my $uid = $self->current_user->{id}; + $self->render( + 'api_documentation', + uid => $uid, + api_token => $self->users->get_api_token( uid => $uid ), + ); + } + else { + $self->render('api_documentation'); + } } -sub get_v0 { +sub get_v1 { my ($self) = @_; my $api_action = $self->stash('user_action'); @@ -37,8 +71,21 @@ sub get_v0 { } my $uid = $+{id}; $api_token = $+{token}; - my $token = $self->get_api_token($uid); - if ( $api_token ne $token->{$api_action} ) { + + if ( $uid > 2147483647 ) { + $self->render( + json => { + error => 'Malformed token', + }, + ); + return; + } + + my $token = $self->users->get_api_token( uid => $uid ); + if ( not $api_token + or not $token->{$api_action} + or $api_token ne $token->{$api_action} ) + { $self->render( json => { error => 'Invalid token', @@ -47,75 +94,347 @@ sub get_v0 { return; } if ( $api_action eq 'status' ) { - my $status = $self->get_user_status($uid); + $self->render( json => $self->get_user_status_json_v1( uid => $uid ) ); + } + else { + $self->render( + json => { + error => 'not implemented', + }, + ); + } +} - my @station_descriptions; - my $station_eva = undef; - my $station_lon = undef; - my $station_lat = undef; +sub travel_v1 { + my ($self) = @_; - if ( $status->{arr_ds100} // $status->{dep_ds100} ) { - @station_descriptions - = Travel::Status::DE::IRIS::Stations::get_station( - $status->{arr_ds100} // $status->{dep_ds100} ); - } - if ( @station_descriptions == 1 ) { - ( undef, undef, $station_eva, $station_lon, $station_lat ) - = @{ $station_descriptions[0] }; - } + my $payload = $self->req->json; + + if ( not $payload or ref($payload) ne 'HASH' ) { $self->render( json => { - deprecated => \1, - checked_in => ( - $status->{checked_in} - or $status->{cancelled} - ) ? \1 : \0, - station => { - ds100 => $status->{arr_ds100} // $status->{dep_ds100}, - name => $status->{arr_name} // $status->{dep_name}, - uic => $station_eva, - longitude => $station_lon, - latitude => $station_lat, - }, - train => { - type => $status->{train_type}, - line => $status->{train_line}, - no => $status->{train_no}, - }, - actionTime => $status->{timestamp}->epoch, - scheduledTime => $status->{sched_arrival}->epoch - || $status->{sched_departure}->epoch, - realTime => $status->{real_arrival}->epoch - || $status->{real_departure}->epoch, + success => \0, + deprecated => \0, + error => 'Malformed JSON', }, ); + return; } - else { + + my $api_token = $payload->{token} // ''; + + if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) { $self->render( json => { - error => 'not implemented', + success => \0, + deprecated => \0, + error => 'Malformed token', + }, + ); + return; + } + my $uid = $+{id}; + $api_token = $+{token}; + + if ( $uid > 2147483647 ) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Malformed token', }, ); + return; + } + + my $token = $self->users->get_api_token( uid => $uid ); + if ( not $token->{'travel'} or $api_token ne $token->{'travel'} ) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Invalid token', + }, + ); + return; + } + + if ( not exists $payload->{action} + or $payload->{action} !~ m{^(checkin|checkout|undo)$} ) + { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Missing or invalid action', + status => $self->get_user_status_json_v1( uid => $uid ) + }, + ); + return; + } + + if ( $payload->{action} eq 'checkin' ) { + my $from_station = sanitize( q{}, $payload->{fromStation} ); + my $to_station = sanitize( q{}, $payload->{toStation} ); + my $train_id; + my $hafas = exists $payload->{train}{journeyID} ? 1 : 0; + + if ( + not( + $from_station + and ( ( $payload->{train}{type} and $payload->{train}{no} ) + or $payload->{train}{id} + or $payload->{train}{journeyID} ) + ) + ) + { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Missing fromStation or train data', + status => $self->get_user_status_json_v1( uid => $uid ) + }, + ); + return; + } + + if ( not $hafas and not $self->stations->search($from_station) ) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Unknown fromStation', + status => $self->get_user_status_json_v1( uid => $uid ) + }, + ); + return; + } + + if ( $to_station + and not $hafas + and not $self->stations->search($to_station) ) + { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Unknown toStation', + status => $self->get_user_status_json_v1( uid => $uid ) + }, + ); + return; + } + + my $train_p; + + if ( exists $payload->{train}{journeyID} ) { + $train_p = Mojo::Promise->resolve( + sanitize( q{}, $payload->{train}{journeyID} ) ); + } + elsif ( exists $payload->{train}{id} ) { + $train_p + = Mojo::Promise->resolve( sanitize( 0, $payload->{train}{id} ) ); + } + else { + my $train_type = sanitize( q{}, $payload->{train}{type} ); + my $train_no = sanitize( q{}, $payload->{train}{no} ); + + $train_p = $self->iris->get_departures_p( + station => $from_station, + lookbehind => 140, + lookahead => 40 + )->then( + sub { + my ($status) = @_; + if ( $status->{errstr} ) { + return Mojo::Promise->reject( + 'Error requesting departures from fromStation: ' + . $status->{errstr} ); + } + my ($train) = List::Util::first { + $_->type eq $train_type and $_->train_no eq $train_no + } + @{ $status->{results} }; + if ( not defined $train ) { + return Mojo::Promise->reject( + 'Train not found at fromStation'); + } + return Mojo::Promise->resolve( $train->train_id ); + } + ); + } + + $self->render_later; + + $train_p->then( + sub { + my ($train_id) = @_; + return $self->checkin_p( + station => $from_station, + train_id => $train_id, + uid => $uid + ); + } + )->then( + sub { + my ($train) = @_; + if ( $payload->{comment} ) { + $self->in_transit->update_user_data( + uid => $uid, + user_data => + { comment => sanitize( q{}, $payload->{comment} ) } + ); + } + if ($to_station) { + + # the user may not have provided the correct to_station, so + # request related stations for checkout. + return $self->checkout_p( + station => $to_station, + force => 0, + uid => $uid, + with_related => 1, + ); + } + return Mojo::Promise->resolve; + } + )->then( + sub { + my ( undef, $error ) = @_; + if ($error) { + return Mojo::Promise->reject($error); + } + $self->render( + json => { + success => \1, + deprecated => \0, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + } + )->catch( + sub { + my ($error) = @_; + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Checkin/Checkout error: ' . $error, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + } + )->wait; + } + elsif ( $payload->{action} eq 'checkout' ) { + my $to_station = sanitize( q{}, $payload->{toStation} ); + + if ( not $to_station ) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Missing toStation', + status => $self->get_user_status_json_v1( uid => $uid ) + }, + ); + return; + } + + if ( $payload->{comment} ) { + $self->in_transit->update_user_data( + uid => $uid, + user_data => { comment => sanitize( q{}, $payload->{comment} ) } + ); + } + + $self->render_later; + + # the user may not have provided the correct to_station, so + # request related stations for checkout. + $self->checkout_p( + station => $to_station, + force => $payload->{force} ? 1 : 0, + uid => $uid, + with_related => 1, + )->then( + sub { + my ( $train, $error ) = @_; + if ($error) { + return Mojo::Promise->reject($error); + } + $self->render( + json => { + success => \1, + deprecated => \0, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'Checkout error: ' . $err, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + } + )->wait; + } + elsif ( $payload->{action} eq 'undo' ) { + my $error = $self->undo( 'in_transit', $uid ); + if ($error) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => $error, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + } + else { + $self->render( + json => { + success => \1, + deprecated => \0, + status => $self->get_user_status_json_v1( uid => $uid ) + } + ); + } } } -sub get_v1 { +sub import_v1 { my ($self) = @_; - my $api_action = $self->stash('user_action'); - my $api_token = $self->stash('token'); - if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) { + my $payload = $self->req->json; + + if ( not $payload or ref($payload) ne 'HASH' ) { $self->render( json => { - error => 'Invalid action', + success => \0, + deprecated => \0, + error => 'Malformed JSON', }, ); return; } + + my $api_token = $payload->{token} // ''; + if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) { $self->render( json => { - error => 'Malformed token', + success => \0, + deprecated => \0, + error => 'Malformed token', }, ); return; @@ -126,29 +445,162 @@ sub get_v1 { if ( $uid > 2147483647 ) { $self->render( json => { - error => 'Malformed token', + success => \0, + deprecated => \0, + error => 'Malformed token', }, ); return; } - my $token = $self->get_api_token($uid); - if ( $api_token ne $token->{$api_action} ) { + my $token = $self->users->get_api_token( uid => $uid ); + if ( not $token->{'import'} or $api_token ne $token->{'import'} ) { $self->render( json => { - error => 'Invalid token', + success => \0, + deprecated => \0, + error => 'Invalid token', }, ); return; } - if ( $api_action eq 'status' ) { - $self->render( json => $self->get_user_status_json_v1($uid) ); + + if ( not exists $payload->{fromStation} + or not exists $payload->{toStation} ) + { + $self->render( + json => { + success => \0, + deprecated => \0, + error => 'missing fromStation or toStation', + }, + ); + return; + } + + my %opt; + + eval { + + if ( not $payload->{fromStation}{name} + or not $payload->{toStation}{name} ) + { + die("Missing fromStation/toStation name\n"); + } + if ( not $payload->{train}{type} or not $payload->{train}{no} ) { + die("Missing train data\n"); + } + if ( not $payload->{fromStation}{scheduledTime} + or not $payload->{toStation}{scheduledTime} ) + { + die("Missing fromStation/toStation scheduledTime\n"); + } + + %opt = ( + uid => $uid, + train_type => sanitize( q{}, $payload->{train}{type} ), + train_no => sanitize( q{}, $payload->{train}{no} ), + train_line => sanitize( q{}, $payload->{train}{line} ), + cancelled => $payload->{cancelled} ? 1 : 0, + dep_station => sanitize( q{}, $payload->{fromStation}{name} ), + arr_station => sanitize( q{}, $payload->{toStation}{name} ), + sched_departure => + sanitize( 0, $payload->{fromStation}{scheduledTime} ), + rt_departure => sanitize( + 0, + $payload->{fromStation}{realTime} + // $payload->{fromStation}{scheduledTime} + ), + sched_arrival => + sanitize( 0, $payload->{toStation}{scheduledTime} ), + rt_arrival => sanitize( + 0, + $payload->{toStation}{realTime} + // $payload->{toStation}{scheduledTime} + ), + comment => sanitize( q{}, $payload->{comment} ), + lax => $payload->{lax} ? 1 : 0, + ); + + if ( $payload->{intermediateStops} + and ref( $payload->{intermediateStops} ) eq 'ARRAY' ) + { + $opt{route} + = [ map { sanitize( q{}, $_ ) } + @{ $payload->{intermediateStops} } ]; + } + + for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) + { + $opt{$key} = DateTime->from_epoch( + time_zone => 'Europe/Berlin', + epoch => $opt{$key} + ); + } + }; + if ($@) { + my ($first_line) = split( qr{\n}, $@ ); + $self->render( + json => { + success => \0, + deprecated => \0, + error => $first_line + } + ); + return; + } + + my $db = $self->pg->db; + my $tx = $db->begin; + + $opt{db} = $db; + my ( $journey_id, $error ) = $self->journeys->add(%opt); + my $journey; + + if ( not $error ) { + $journey = $self->journeys->get_single( + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1 + ); + $error + = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 ); + } + + if ($error) { + $self->render( + json => { + success => \0, + deprecated => \0, + error => $error + } + ); + } + elsif ( $payload->{dryRun} ) { + $self->render( + json => { + success => \1, + deprecated => \0, + id => $journey_id, + result => $journey + } + ); } else { + $self->journey_stats_cache->invalidate( + ts => $opt{rt_departure}, + db => $db, + uid => $uid + ); + $tx->commit; $self->render( json => { - error => 'not implemented', - }, + success => \1, + deprecated => \0, + id => $journey_id, + result => $journey + } ); } } @@ -156,11 +608,15 @@ sub get_v1 { sub set_token { my ($self) = @_; if ( $self->validation->csrf_protect->has_error('csrf_token') ) { - $self->render( 'account', invalid => 'csrf' ); + $self->render( + 'bad_request', + csrf => 1, + status => 400 + ); return; } my $token = make_token(); - my $token_id = $self->app->token_type->{ $self->param('token') }; + my $token_id = $self->users->get_token_id( $self->param('token') ); if ( not $token_id ) { $self->redirect_to('account'); @@ -193,4 +649,21 @@ sub set_token { $self->redirect_to('account'); } +sub autocomplete { + my ($self) = @_; + + $self->res->headers->cache_control('max-age=86400, immutable'); + + my $output + = "document.addEventListener('DOMContentLoaded',function(){M.Autocomplete.init(document.querySelectorAll('.autocomplete'),{\n"; + $output .= 'minLength:3,limit:50,data:'; + $output .= encode_json( $self->stations->get_for_autocomplete ); + $output .= "\n});});\n"; + + $self->render( + format => 'js', + data => $output + ); +} + 1; |