File Coverage

blib/lib/App/MFILE/WWW.pm
Criterion Covered Total %
statement 26 64 40.6
branch 0 22 0.0
condition 0 7 0.0
subroutine 9 11 81.8
pod 1 1 100.0
total 36 105 34.2


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             # ------------------------
34             # App::MFILE::WWW top-level module
35             # ------------------------
36              
37             package App::MFILE::WWW;
38              
39 1     1   45053 use 5.012;
  1         6  
40 1     1   39 use strict;
  1         3  
  1         24  
41 1     1   4 use warnings;
  1         2  
  1         32  
42              
43 1     1   290 use App::CELL qw( $CELL $log $site $meta );
  1         62785  
  1         95  
44 1     1   8 use Exporter qw( import );
  1         2  
  1         20  
45 1     1   4 use File::ShareDir;
  1         2  
  1         26  
46 1     1   5 use File::Spec;
  1         1  
  1         18  
47 1     1   315 use Log::Any::Adapter;
  1         254  
  1         4  
48 1     1   24 use Params::Validate qw( :all );
  1         2  
  1         636  
49              
50              
51              
52             =head1 NAME
53              
54             App::MFILE::WWW - Web UI development toolkit with prototype demo app
55              
56              
57              
58              
59             =head1 VERSION
60              
61             Version 0.175
62              
63             =cut
64              
65             our $VERSION = '0.175';
66             our @EXPORT_OK = ( '$VERSION' );
67              
68              
69              
70             =head1 LICENSE
71              
72             This software is distributed under the "BSD 3-Clause" license, the text of
73             which can be found in the file named C in the top-level distro
74             directory. The license text is also reprodued at the top of each source file.
75              
76              
77              
78             =head1 SYNOPSIS
79              
80             $ man mfile-www
81             $ mfile-www
82             $ firefox http://localhost:5001
83              
84              
85              
86             =head1 DESCRIPTION
87              
88             This distro contains a foundation/framework/toolkit for developing the "front
89             end" portion of web applications.
90              
91             L is a L application that provides a HTTP
92             request-response handler (based on L), CSS and HTML source code
93             for an in-browser "screen", and JavaScript code for displaying various
94             "widgets" (menus, forms, etc.) in that screen and for processing user input
95             from within those widgets.
96              
97             In addition, infrastructure is included (but need not be used) for user
98             authentication, session management, and communication with a backend server via
99             AJAX calls.
100              
101             Front ends built with L will typicaly be menu-driven,
102             consisting exclusively of fixed-width Unicode text, and capable of being
103             controlled from the keyboard, without the use of a mouse. The overall
104             look-and-feel is reminiscent of the text terminal era.
105              
106             The distro comes with a prototype (demo) application to illustrate how the
107             various widgets are used.
108              
109              
110              
111             =head1 QUICK START (DEMO)
112              
113             L can be run as a standalone "front-end web application" written
114             in JavaScript with an embedded HTTP server.
115              
116             Assuming L has been installed properly, this mode of operation
117             can be started by running C, as a normal user (even 'nobody'), with
118             no arguments or options:
119              
120             $ mfile-www
121              
122             The start-up script will write information about its state to the standard
123             output. This includes the location of its log file, the port where the HTTP
124             server is listening (default is 5001), etc. For a detailed description of what
125             happens when the start-up script is run, see the POD of C itself
126             - e.g. "man mfile-www".
127              
128              
129              
130             =head1 DERIVED WWW CLIENTS
131              
132             When you write your own web frontend using this distro, from
133             L's perspective it is a "derived client" and will be referred
134             to as such in this document.
135              
136              
137             =head2 Derived client operation
138              
139             In a derived-client scenario, L serves as the foundation
140             upon which the "real" application is built.
141              
142             The derived-client handling is triggered by providing the C<--ddist>
143             command-line option, i.e.
144              
145             $ mfile-www --ddist=App-Dochazka-WWW
146              
147             where 'App-Dochazka-WWW' refers to the Perl module L,
148             which is assumed to contain the derived client source code.
149              
150             So, in the first place it is necessary to create such a Perl module. It should
151             have a sharedir configured and present. One such derived client,
152             L, is available on CPAN.
153              
154              
155              
156             =head1 PERL AND JAVASCRIPT
157              
158             The L codebase has two parts, or "sides": the "Perl side"
159             and the "JavaScript side". The Perl side implements the embedded web server
160             and the JavaScript side implements the front-end application served to
161             browsers by the Perl side.
162              
163             Control passes from the Perl side to the JavaScript side
164              
165             =over
166              
167             =item * B whenever the user (re)loads the page
168              
169             =item * B whenever the user triggers an AJAX call
170              
171             =back
172              
173             =head2 Perl side
174              
175             The HTTP request-response cycle implemented by the Perl side is designed to
176             work approximately like this:
177              
178             =over
179              
180             =item * B listens for incoming connections on port 80/443 of the server
181              
182             =item * When a connection comes in, B decrypts it and forwards it to a
183             high-numbered port where a PSGI-compatible HTTP server (such as L) is
184             listening
185              
186             =item * The embedded HTTP server takes the connection and passes it to the
187             Plack middleware. The key middleware component is
188             L, which assigns an ID to the session, stores
189             whatever data the server-side code needs to associate with the session, links
190             the session to the user's browser via a cookie, and provides the application a
191             hook (in the Plack environment stored in the HTTP request) to access the
192             session data
193              
194             =item * if the connection is asking for static content (defined as anything in
195             C, C, or C), that content is served immediately and the
196             request doesn't even make it into our Perl code
197              
198             =item * any other path is considered dynamic content and is passed to
199             L for processing -- L implements the HTTP standard
200             as a state machine
201              
202             =item * the L state machine takes the incoming request and runs
203             it through several functions that are overlayed in L
204             - an appropriate HTTP error code is returned if the request doesn't make it
205             through the state machine. Along the way, log messages are written to the log.
206              
207             =item * as part of the state machine, all incoming requests are subject to
208             "authorization" (in the HTTP sense, which actually means authentication).
209             First, the session data is examined to determine if the request belongs to an
210             existing authorized session. If it doesn't, the request is treated as a
211             login/logout attempt -- the session is cleared and control passes to the
212             JavaScript side, which, lacking a currentUser object, displays the login
213             dialog.
214              
215             =item * once an authorized session is established, there are two types of
216             requests: GET and POST
217              
218             =item * incoming GET requests happen whenever the page is reloaded -
219             in an authorized session, this causes the main menu to be displayed, but all
220             static content (CSS and JavaScript modules) are reloaded for a "clean slate",
221             as if the user had just logged in.
222              
223             =item * Note that L pays no attention to the URI - if the user
224             enters a path (e.g. http://mfile.site/some/bogus/path), this will be treated
225             like any other page (re)load and the path is simply ignored.
226              
227             =item * if the session is expired or invalid, any incoming GET request will
228             cause the login dialog to be displayed.
229              
230             =item * well-formed POST requests are directed to the C routine
231             in L. In derived-distro mode, the derived distro
232             must provide its own dispatch module.
233              
234             =item * under ordinary operation, the user will spend 99% of her time
235             interacting with the JavaScript code running in her browser, which will
236             communicate asynchronously as needed with the back-end (which must be
237             implemented separately) via AJAX calls.
238              
239             =back
240              
241              
242             =head2 JavaScript side
243              
244             The JavaScript side provides a toolkit for building web applications that
245              
246             =over
247              
248             =item do not require the use of a mouse; and
249              
250             =item look and feel very much like text terminal applications from the 1980s
251              
252             =back
253              
254             Developing a front-end application with L currently assumes
255             that you, the developer, will want to use RequireJS, jQuery, and QUnit.
256              
257             The JavaScript code is modular. Each code module has its own file and
258             modules are loaded asynchronously by L.
259             Also, jQuery and QUnit L are loaded automatically.
260              
261             In addition to the above, L provides a number of primitives,
262             also referred to as "targets", that can be used to quickly slap together a web
263             application. The next chapter explains what these widgets are and how to use
264             them.
265              
266              
267              
268             =head1 FRONT-END PRIMITIVES
269              
270             The JavaScript side implements a set of primitives, or widgets, from which the
271             front-end application is built up. These include a menu primitive, a form
272             primitive for entering data, table and "browser" primitives for viewing
273             datasets, and a "rowselect" primitive for selecting among
274              
275             =head2 daction
276              
277             The C primitive is a generalized widget that can do anything.
278              
279              
280             =head2 dbrowser
281              
282             The C primitive is like C, except that it displays a set
283             of data objects and enables the user to "browse" the dataset using arrow keys.
284             Like C, the primitive includes "miniMenu" functionality through which
285             the user can potentially trigger actions that take the current object as input.
286              
287              
288             =head2 dcallback
289              
290             The C primitive is useful for cases when none of the other primitives
291             are appropriate for displaying a given type of object, and no interactivity is
292             needed beyond that provided by miniMenu. The C primitive writes the
293             target title and miniMenu to the screen, along with a "dcallback" div in
294             between, which it populates by calling the callback function. Since the callback
295             function part of the target definition, it can be app-specific.
296              
297              
298             =head2 dform
299              
300             The C primitive is used to implement forms consisting of read-only
301             fields (for viewing data), read-write fields (for entering data), or a
302             combination of both. A "miniMenu" can be defined, allowing the user to trigger
303             actions that take the current object as input.
304              
305              
306             =head2 dmenu
307              
308             The C primitive is used to implement menus.
309              
310              
311             =head2 dnotice
312              
313             The C primitive takes an HTML string and displays it. The same
314             functionality can be accomplished with a C, of course, but using the
315             C primitive ensures that the notice will have the same "look and feel"
316             as the other widgets.
317              
318              
319             =head2 drowselect
320              
321             The C primitive takes an array of strings, displays them vertically
322             as a list, and allows the user to choose one and perform an action on it. Actions
323             are defined via a C. The currently-selected item is displayed in
324             reverse-video.
325              
326              
327             =head2 dtable
328              
329             The C primitive is similar to C in that it takes a set of
330             objects and allows the user to choose one and perform actions on it via a
331             C. Unlike C, however, it display the objects in table form.
332             The currently-selected object is displayed in reverse video.
333              
334              
335              
336             =head1 JAVASCRIPT UNIT TESTS
337              
338             The JavaScript side has unit tests and functional tests that can be run by
339             starting the application and then pointing the browser to a URL like:
340              
341             http://localhost:5001/test
342              
343             The tests are implemented using QUnit. A good source of practical advise on the
344             use of QUnit is the QUnit Cookbook, available here:
345              
346             https://qunitjs.com/cookbook/
347              
348             The QUnit API itself is documented here:
349              
350             http://api.qunitjs.com/
351              
352              
353             =head2 Adding new test cases
354              
355             There are separate sets of JavaScript unit tests for each of the following
356             three components:
357              
358             =over
359              
360             =item mfile-www core
361              
362             =item mfile-www demo app
363              
364             =item derived application (e.g. dochazka-www)
365              
366             =back
367              
368             To add a new test case, go to the appropriate C directory (in mfile-www
369             core, in the mfile-www demo app, or in your derived application, as
370             appropriate) and create a js file (use one of the other C files
371             as a model). Then add this file to the C file in the parent directory.
372              
373              
374             =head1 DEVELOPMENT NOTES
375              
376              
377             =head2 UTF-8
378              
379             In conformance with the JSON standard, all data passing to and from the
380             server are assumed to be encoded in UTF-8. Users who need to use non-ASCII
381             characters should check their browser's settings.
382              
383              
384             =head2 Deployment
385              
386             To minimize latency, L can be deployed on the same server
387             as the back-end (e.g. L), but this is not required.
388              
389              
390              
391             =head1 PACKAGE VARIABLES
392              
393             For convenience, the following variables are declared with package scope:
394              
395             =cut
396              
397             my $dist_dir = File::ShareDir::dist_dir( 'App-MFILE-WWW' );
398              
399              
400              
401             =head1 FUNCTIONS
402              
403             =head2 init
404              
405             Initialization routine - run from C, the server startup script.
406             This routine loads configuration parameters from files in the distro and site
407             configuration directories, and sets up logging.
408              
409             FIXME: This code could be moved into the startup script.
410              
411             =cut
412              
413             sub init {
414 0     0 1   my %ARGS = validate( @_, {
415             mode => { type => SCALAR, optional => 1 }, # 'STANDALONE' or 'DDIST', defaults to 'STANDALONE'
416             ddist_sharedir => { type => SCALAR, optional => 1 },
417             sitedir => { type => SCALAR, optional => 1 },
418             debug_mode => { type => SCALAR, optional => 1 },
419             } );
420              
421             # * determine mode
422 0   0       my $mode = $ARGS{'mode'} || 'STANDALONE';
423 0 0         if ( $mode =~ m/^(standalone|ddist)$/i ) {
424 0 0         if ( $mode =~ m/^ddist$/i ) {
425 0           $mode = 'DDIST';
426             } else {
427 0           $mode = 'STANDALONE';
428             }
429             }
430              
431             # * load site configuration
432 0           my $status = _load_config( %ARGS );
433 0 0         return $status if $status->not_ok;
434              
435             # * mode-specific meta configuration
436 0           $meta->set( 'META_WWW_STANDALONE_MODE', ( $mode eq 'STANDALONE' ) );
437              
438             # * set up logging
439 0 0         return $CELL->status_not_ok( "MFILE_APPNAME not set!" ) if not $site->MFILE_APPNAME;
440 0           my $debug_mode;
441 0 0         if ( exists $ARGS{'debug_mode'} ) {
442 0           $debug_mode = $ARGS{'debug_mode'};
443             } else {
444 0   0       $debug_mode = $site->MFILE_WWW_DEBUG_MODE || 0;
445             }
446 0 0         unlink $site->MFILE_WWW_LOG_FILE if $site->MFILE_WWW_LOG_FILE_RESET;
447 0           Log::Any::Adapter->set('File', $site->MFILE_WWW_LOG_FILE );
448 0           $log->init( ident => $site->MFILE_APPNAME, debug_mode => $debug_mode );
449 0           $log->info( "Initializing " . $site->MFILE_APPNAME );
450              
451 0           return $CELL->status_ok;
452             }
453              
454             sub _load_config {
455 0     0     my %ARGS = @_;
456 0           my $status;
457 0           my $sitedir_loaded = 0;
458              
459             #Log::Any::Adapter->set( 'File', "$ENV{HOME}/.mfile-www-early-debug.log" );
460             #$log->init( ident => 'MFILE-WWW', debug_mode => 1 );
461              
462             # always load the App::MFILE::WWW distro sharedir
463 0           my $target = File::Spec->catfile( $dist_dir, 'config' );
464 0           print "Loading App::MFILE::WWW configuration parameters from $target\n";
465 0           $status = $CELL->load( sitedir => $target );
466 0 0         return $status if $status->not_ok;
467              
468             # load additional sitedir if provided by caller in argument list
469 0 0         if ( $ARGS{sitedir} ) {
470 0           $target = $ARGS{sitedir};
471 0           print "Loading App::MFILE::WWW configuration parameters from $target\n";
472 0           $status = $CELL->load( sitedir => $target );
473 0 0         return $status if $status->not_ok;
474 0           $sitedir_loaded = 1;
475             }
476              
477             # if ddist_sharedir was given, attempt to load configuration from that, too
478 0 0 0       if ( $ARGS{ddist_sharedir} and ! $sitedir_loaded ) {
479 0           $target = File::Spec->catfile( $ARGS{ddist_sharedir}, 'config' );
480 0           print "Loading App::MFILE::WWW configuration parameters from $target\n";
481 0           $status = $CELL->load( sitedir => $target );
482 0 0         return $status if $status->not_ok;
483             }
484              
485 0           return $CELL->status_ok;
486             }
487              
488             1;