File Coverage

blib/lib/LaTeXML/Plugin/LtxMojo.pm
Criterion Covered Total %
statement 34 36 94.4
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 46 48 95.8


line stmt bran cond sub pod time code
1             package LaTeXML::Plugin::LtxMojo;
2 1     1   1156 use Mojo::Base 'Mojolicious';
  1         2  
  1         7  
3 1     1   196117 use Mojo::JSON;
  1         3  
  1         44  
4 1     1   18 use Mojo::IOLoop;
  1         2  
  1         11  
5 1     1   22 use Mojo::ByteStream qw(b);
  1         2  
  1         49  
6              
7 1     1   5 use File::Basename 'dirname';
  1         2  
  1         42  
8 1     1   5 use File::Spec::Functions qw(catdir catfile);
  1         1  
  1         46  
9 1     1   1180 use File::Temp qw(tempdir);
  1         11513  
  1         100  
10 1     1   11 use File::Path qw(remove_tree);
  1         2  
  1         58  
11              
12 1     1   1053 use Archive::Zip qw(:CONSTANTS :ERROR_CODES);
  1         44936  
  1         225  
13 1     1   851 use IO::String;
  1         3053  
  1         58  
14 1     1   8 use Encode;
  1         3  
  1         142  
15              
16 1     1   479 use LaTeXML::Common::Config;
  0            
  0            
17             use LaTeXML::Util::Pathname qw(pathname_protocol);
18             use LaTeXML;
19             use LaTeXML::Plugin::LtxMojo::Startup;
20              
21             our $dbfile = '.LaTeXML_Mojo.cache';
22              
23             # Every CPAN module needs a version
24             our $VERSION = '0.4';
25              
26             # This method will run once at server start
27             sub startup {
28             my $app = shift;
29             # Switch to installable home directory
30             $app->home->parse(catdir(dirname(__FILE__), 'LtxMojo'));
31              
32             # Switch to installable "public" directory
33             $app->static->paths->[0] = $app->home->rel_dir('public');
34             # Switch to installable "templates" directory
35             $app->renderer->paths->[0] = $app->home->rel_dir('templates');
36              
37             $ENV{MOJO_MAX_MESSAGE_SIZE} = 107374182; # close to 100 MB file upload limit
38             $ENV{MOJO_REQUEST_TIMEOUT} = 600;# 10 minutes;
39             $ENV{MOJO_CONNECT_TIMEOUT} = 120; # 2 minutes
40             $ENV{MOJO_INACTIVITY_TIMEOUT} = 600; # 10 minutes;
41              
42             # Make signed cookies secure
43             $app->secrets(['LaTeXML is the way to go!']);
44              
45             #Prep a LaTeXML Startup instance
46             my $startup = LaTeXML::Plugin::LtxMojo::Startup->new(dbfile => catfile($app->home,$dbfile));
47              
48             # Do a one-time check for admin, add if none:
49             $startup->modify_user('admin', 'admin', 'admin')
50             unless $startup->exists_user('admin');
51              
52             $app->helper(convert_zip => sub {
53             my ($self) = @_;
54             # Make sure we point to the actual source directory
55             my $name = $self->req->headers->header('x-file-name');
56             $name =~ s/\.zip$//;
57             # HTTP GET parameters hold the conversion options
58             my @all_params = @{ $self->req->url->query->params || [] };
59             my $opts=[];
60             # Ugh, disallow 'null' as a value!!! (TODO: Smarter fix??)
61             while (my ($key,$value) = splice(@all_params,0,2)) {
62             if ($key=~/^(?:local|path|destination|directory)$/) {
63             # You don't get to specify harddrive info in the web service
64             next; }
65             $value = '' if ($value && ($value eq 'null'));
66             push @$opts, ($key,$value); }
67              
68             my $config = LaTeXML::Common::Config->new();
69             $config->read_keyvals($opts);
70             my @latexml_inputs = ('.',grep {defined} split(':',($ENV{LATEXMLINPUTS}||'')));
71             $config->set('paths',\@latexml_inputs);
72             $config->set('whatsin','archive');
73             $config->set('whatsout','archive');
74             $config->set('log',"$name.log");
75             $config->set('local',($self->tx->remote_address eq '127.0.0.1'));
76             # Only HTML5 for now.
77             $config->set('format','html5');
78             # Prepare and convert
79             my $converter = LaTeXML->get_converter($config);
80             $converter->prepare_session($config);
81             my $source = $self->req->body;
82             $source = "literal:".$source if ($source && (pathname_protocol($source) eq 'file'));
83             my $response = $converter->convert($source);
84             # Catch errors
85             $self->render(text=>'Fatal: Internal Conversion Error, please contact the administrator.') unless
86             (defined $response && ($response->{result}));
87             # Return
88             my $headers = Mojo::Headers->new;
89             $headers->add('Content-Type',"application/zip;name=$name.zip");
90             $headers->add('Content-Disposition',"attachment;filename=$name.zip");
91             $self->res->content->headers($headers);
92             return $self->render(data=>$response->{result});
93             });
94              
95             # TODO: Maybe reintegrate IF we support username-based profiles
96             # if (!defined $opt->{profile}) {
97             # if (defined $opt->{user}
98             # && $startup->verify_user($opt->{user}, $opt->{password}))
99             # {
100             # $opt->{profile} =
101             # $startup->lookup_user_property($opt->{user}, 'default') || 'custom';
102             # }
103             # else {
104             # $opt->{profile} = 'custom';
105             # }
106             # }
107              
108             $app->helper(convert_string => sub {
109             my ($self) = @_;
110             my ($source,$is_jsonp);
111             my $get_params = $self->req->url->query->params || [];
112             my $post_params = $self->req->body_params->params || [];
113             if (scalar(@$post_params) == 1) {
114             $source = $post_params->[0];
115             $post_params=[];
116             } elsif ((scalar(@$post_params) == 2) && ($post_params->[0] !~ /^(?:tex|source)$/)) {
117             $source = $post_params->[0].$post_params->[1];
118             $post_params=[];
119             }
120             # We need to be careful to preserve the parameter order, so use arrayrefs
121             my @all_params = (@$get_params, @$post_params);
122             my $opts = [];
123             # Ugh, disallow 'null' as a value!!! (TODO: Smarter fix??)
124             while (my ($key,$value) = splice(@all_params,0,2)) {
125             # JSONP ?
126             if ($key eq 'jsonp') {
127             $is_jsonp = $value;
128             next;
129             } elsif ($key =~ /^(?:tex|source)$/) {
130             # TeX is data, separate
131             $source = $value unless defined $source;
132             next;
133             } elsif ($key=~/^(?:local|path|destination|directory)$/) {
134             # You don't get to specify harddrive info in the web service
135             next;
136             } elsif ($key=~/^(?:preamble|postamble)$/) {
137             $value = "literal:".$value if ($value && (pathname_protocol($value) eq 'file'));
138             }
139             $value = '' if ($value && ($value eq 'null'));
140             push @$opts, ($key,$value);
141             }
142             my $config = LaTeXML::Common::Config->new();
143             $config->read_keyvals($opts);
144             # We now have a LaTeXML config object - $config.
145             my @latexml_inputs = grep {defined} split(':',($ENV{LATEXMLINPUTS}||''));
146             $config->set('paths',\@latexml_inputs);
147             my $converter = LaTeXML->get_converter($config);
148              
149             #Override/extend with session-specific options in $opt:
150             $converter->prepare_session($config);
151             # If there are no protocols, use literal: as default:
152             if ((! defined $source) || (length($source)<1)) {
153             $self->render(json => {result => '', status => "Fatal:input:empty No TeX provided on input", status_code=>3,
154             log => "Status:conversion:3\nFatal:input:empty No TeX provided on input"});
155             } else {
156             $source = "literal:".$source if ($source && (pathname_protocol($source) eq 'file'));
157             #Send a request:
158             my $response = $converter->convert($source);
159             my ($result, $status, $status_code, $log);
160             if (defined $response) {
161             ($result, $status, $status_code, $log) = map { $response->{$_} } qw(result status status_code log);
162             }
163             # Delete converter if Fatal occurred
164             undef $converter unless defined $result;
165             # TODO: This decode business is fishy... very fishy!
166             if ($is_jsonp) {
167             my $json_result = $self->render(
168             json => {result => $result,
169             status => $status, status_code=>$status_code, log => $log, partial=>1});
170             $self->render(data => "$is_jsonp($json_result)", format => 'js');
171             } elsif ($config->get('whatsout') eq 'archive') { # Archive conversion returns a ZIP
172             $self->render(data => $result);
173             } else {
174             $self->render(json => {result => $result, status => $status, status_code=>$status_code, log => $log});
175             }
176             }
177             });
178              
179              
180             ################################################
181             ## ##
182             ## ROUTES ##
183             ## ##
184             ################################################
185             my $r = $app->routes;
186             $r->post('/convert' => sub {
187             my $self = shift;
188             my $type = $self->req->headers->header('x-file-type');
189             if ($type && $type =~ 'zip' && ($self->req->headers->header('content-type') eq 'multipart/form-data')) {
190             $self->convert_zip;
191             } else {
192             $self->convert_string;
193             }
194             });
195              
196             $r->websocket('/convert' => sub {
197             my $self = shift;
198             my $json = Mojo::JSON->new;
199             # Connected
200             $self->app->log->debug('WebSocket connected.');
201             # Increase inactivity timeout for connection a bit
202             Mojo::IOLoop->stream($self->tx->connection)->timeout(300);
203             $self->on('message' => sub {
204             my ($tx, $bytes) = @_;
205             #TODO: We want the options in the right order, is this Decode safe in this respect?
206             my $opts = $json->decode($bytes);
207             my $source = $opts->{source}; delete $opts->{source};
208             $source = $opts->{tex} unless defined $opts->{source}; delete $opts->{tex};
209             my $config = LaTeXML::Common::Config->new();
210             $config->read_keyvals([%$opts]);
211             # We now have a LaTeXML options object - $opt.
212             my $converter = LaTeXML->get_converter($config);
213             #Override/extend with session-specific options in $opt:
214             $converter->prepare_session($config);
215             #Send a request:
216             my $response = $converter->convert($source);
217             my ($result, $status, $log);
218             if (defined $response) {
219             if (! defined $response->{result}) {
220             # Delete converter if Fatal occurred
221             undef $converter;
222             } else {
223             #$response->{result} = decode('UTF-8',$response->{result});
224             }
225             }
226             $tx->send({text=>$json->encode($response)});
227             });
228             # Disconnected
229             $self->on('finish' => sub {
230             my $self = shift;
231             $self->app->log->debug('WebSocket disconnected.');
232             });
233             });
234              
235             $r->get('/login' => sub {
236             my $self = shift;
237             my $name = $self->param('name') || '';
238             my $pass = $self->param('pass') || '';
239             return $self->render
240             unless ($startup->verify_user($name, $pass) eq 'admin');
241             $self->session(name => $name);
242             $self->flash(message => "Thanks for logging in $name!");
243             $self->redirect_to('admin');
244             } => 'login');
245              
246             $r->get('/about' => sub {
247             my $self = shift;
248             my $headers = Mojo::Headers->new;
249             $headers->add('Content-Type', 'application/xhtml+xml');
250             $self->res->content->headers($headers);
251             $self->render();
252             } => 'about');
253              
254             $r->get('/demo' => sub {
255             my $self = shift;
256             } => 'demo');
257              
258             $r->get('/editor' => sub {
259             my $self = shift;
260             my $headers = Mojo::Headers->new;
261             $headers->add('Content-Type', 'application/xhtml+xml');
262             $self->res->content->headers($headers);
263             $self->render();
264             } => 'editor');
265              
266             $r->get('/editor5' => sub {
267             my $self = shift;
268             my $headers = Mojo::Headers->new;
269             $headers->add('Content-Type', 'text/html');
270             $self->res->content->headers($headers);
271             $self->render();
272             } => 'editor5');
273              
274             $r->get('/ws-editor' => sub {
275             my $self = shift;
276             my $headers = Mojo::Headers->new;
277             $headers->add('Content-Type', 'application/xhtml+xml');
278             $self->res->content->headers($headers);
279             $self->render();
280             } => 'ws-editor');
281              
282              
283             $r->get('/' => sub {
284             my $self = shift;
285             return $self->redirect_to('about');
286             });
287              
288             $r->get('/logout' => sub {
289             my $self = shift;
290             $self->session(expires => 1);
291             $self->flash(message => "Successfully logged out!");
292             $self->redirect_to('login');
293             });
294              
295             $r->get('/admin' => sub {
296             my $self = shift;
297             return $self->redirect_to('login') unless $self->session('name');
298             $self->stash(startup => $startup);
299             $self->render;
300             } => 'admin');
301              
302             $r->get('/help' => sub {
303             my $self = shift;
304             $self->render;
305             } => 'help');
306              
307              
308             $r->get('/upload' => sub {
309             my $self = shift;
310             $self->render;
311             } => 'upload');
312              
313             $r->post('/upload' => sub {
314             my $self = shift;
315             # TODO: Need a session?
316             my $type = $self->req->headers->header('x-file-type');
317             if ($type && $type =~ 'zip' && ($self->req->headers->header('content-type') eq 'multipart/form-data')) {
318             $self->convert_zip;
319             } else {
320             #.tex , .sty , .jpg and so on - write to filesystem (when are we done?)
321             $self->render(text=>"Uploaded, but ignored!");
322             }
323             });
324              
325             $r->any('/ajax' => sub {
326             my $self = shift;
327             return $self->redirect_to('login') unless $self->session('name');
328             my $header = $self->req->headers->header('X-Requested-With');
329             if ($header && $header eq 'XMLHttpRequest') {
330              
331             # Users API:
332             my $user_action = $self->param('user_action');
333             if ($user_action) {
334             my $name = $self->param('name');
335             my $message = 'This request was empty, please resend with Name set!';
336            
337             if ($user_action eq 'modify') {
338             if ($name) {
339             my $pass = $self->param('pass');
340             my $role = $self->param('role');
341             my $default = $self->param('default_profile');
342             $message = $startup->modify_user($name, $pass, $role, $default);
343             }
344             }
345             elsif ($user_action eq 'add') {
346             if ($name) {
347             my $pass = $self->param('pass');
348             my $role = $self->param('role');
349             my $default = $self->param('default_profile');
350             $message = $startup->modify_user($name, $pass, $role, $default); }
351             }
352             elsif ($user_action eq 'delete') { $message = $startup->delete_user($name) if $name; }
353             elsif ($user_action eq 'startup_users') {
354             $self->render(
355             json => {
356             users => $startup->users
357             }
358             );}
359             elsif ($user_action eq 'overview_users') {
360             my $users = $startup->users;
361             my $summary = [];
362             push @$summary, $startup->summary_user($_) foreach (@$users);
363             $self->render(json => {users => $users, summary => $summary});
364             }
365             else { $message = "Unrecognized Profile Action!" }
366             $self->render(json => {message => $message});
367             }
368              
369             # Profiles API:
370             my $profile_action = $self->param('profile_action');
371             if ($profile_action) {
372             my $message =
373             'This request was empty, please resend with profile_action set!';
374             if ($profile_action eq 'startup_profiles') {
375             $self->render(
376             json => {
377             profiles => [@{$startup->profiles}]
378             }
379             );
380             }
381             elsif ($profile_action eq 'select') {
382             my $pname = $self->param('profile_name');
383             $self->render(json => {message => 'Please provide a profile name!'})
384             unless $pname;
385             my $form = $startup->summary_profile($pname);
386             my $lines = 0;
387             $lines++ while ($form =~ /<[tb]r/g);
388             my $minh = "min-height: " . ($lines * 5) . "px;";
389             my $message = "Selected profile: " . $pname;
390             my $json = Mojo::JSON->new;
391             open TMP, ">", "/tmp/json.txt";
392             print TMP $json->encode(
393             {form => $form, style => $minh, message => $message});
394             close TMP;
395             $self->render(
396             text => $json->encode(
397             {form => $form, style => $minh, message => $message}
398             )
399             );
400             }
401             else {$self->render(json => {message => "Unrecognized Profile Action!"});}
402             $self->render(json => {message => $message});
403             }
404             # General Actions API:
405             }
406             else {
407             $self->render(text => "Only AJAX request are acceptexd at this route!\n");
408             }
409             });
410              
411             }
412             1;
413              
414             __END__