no warnings;
use Zanas::Presentation;
use Zanas::Content;
use Zanas::Apache;
use Zanas::SQL;
use Zanas::Request;
################################################################################
sub require_fresh {
my ($module_name) = @_;
if ($_USER and $$_USER{role} and $module_name =~ /Content|Presentation/) {
my $specific_module_name = $module_name;
$specific_module_name =~ s/(Content|Presentation)/$$_USER{role}::$1/;
my $error;
eval {$error = require_fresh_internal ($specific_module_name)};
return unless $error;
}
require_fresh_internal ($module_name, 1);
}
################################################################################
sub fix_module_for_role {
my ($file_name) = @_;
my $tmp_file_name = $file_name . '~';
open (IN, $file_name) or die "Cannot open $file_name: $!\n";
open (OUT, ">$tmp_file_name") or die "Cannot write to $tmp_file_name: $!\n";
my $suffix = ($_USER and $$_USER{role}) ? '_for_' . $_USER -> {role} : '';
while (my $s = ) {
$s =~ s/sub\s+get_menu\w*/sub get_menu$suffix/;
print OUT $s;
}
close (OUT);
close (IN);
}
################################################################################
sub require_fresh_internal {
my ($module_name, $fatal) = @_;
if ($conf -> {core_spy_modules} || $preconf -> {core_spy_modules}) {
my $file_name = $module_name;
$file_name =~ s{::}{\/}g;
my $inc_key = $file_name . '.pm';
$file_name =~ s{^(.+?)\/}{\/};
$file_name = $PACKAGE_ROOT . $file_name . '.pm';
-f $file_name or return "File not found: $file_name\n";
fix_module_for_role ($file_name) if $conf -> {core_fix_modules} and $module_name =~ /Content|Presentation/;
my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $last_modified, $ctime, $blksize, $blocks) = stat ($file_name);
my $last_load = $INC_FRESH {$module_name} + 0;
my $need_refresh = $last_load < $last_modified;
$need_refresh or return;
# eval { do $file_name };
delete $INC {$inc_key};
eval "require $module_name";
}
else {
eval "require $module_name";
}
$INC_FRESH {$module_name} = time;
if ($@ and $fatal) {
$_REQUEST {error} = $@;
print STDERR "require_fresh: error load module $module_name: $@\n";
}
return $@;
}
################################################################################
BEGIN {
# print STDERR Dumper ($preconf);
if ($ENV {GATEWAY_INTERFACE} =~ m{^CGI/} || $conf -> {use_cgi} || $preconf -> {use_cgi}) {
eval 'require CGI';
} else {
eval 'require Apache::Request';
if ($@) {
eval 'require CGI';
eval 'require Zanas::Request';
};
}
$INC {'Apache/Request.pm'} or eval 'require Zanas::Request';
our $STATIC_ROOT = __FILE__;
$STATIC_ROOT =~ s{\.pm}{/static/};
eval 'require Compress::Zlib';
if ($@) {
delete $conf -> {core_gzip};
delete $preconf -> {core_gzip};
};
our %INC_FRESH = ();
while (my ($name, $path) = each %INC) {
delete $INC {$name} if $name =~ m{Zanas[\./]};
}
our $PACKAGE_ROOT = $INC {__PACKAGE__ . '/Config.pm'};
$PACKAGE_ROOT ||= '';
$PACKAGE_ROOT =~ s{\/Config\.pm}{};
$conf = {%$conf, %$preconf};
if ($conf -> {core_load_modules}) {
opendir (DIR, "$PACKAGE_ROOT/Content") || die "can't opendir $PACKAGE_ROOT/Content: $!";
my @files = grep {/\.pm$/} map { "Content/$_" } readdir(DIR);
closedir DIR;
opendir (DIR, "$PACKAGE_ROOT/Presentation") || die "can't opendir $PACKAGE_ROOT/Presentation: $!";
push @files, grep {/\.pm$/} map { "Presentation/$_" } readdir(DIR);
closedir DIR;
foreach my $file (@files) {
$file =~ s{\.pm$}{};
$file =~ s{\/}{\:\:};
require_fresh (__PACKAGE__ . "::$file");
}
}
if ($conf -> {db_dsn}) {
sql_reconnect ();
$model_update -> assert (
tables => {
sessions => {
columns => {
id => {
TYPE_NAME => 'bigint',
_PK => 1,
},
id_user => {
TYPE_NAME => 'int',
},
ts => {
TYPE_NAME => 'timestamp',
},
}
},
},
);
$model_update -> assert (
default_columns => {
id => {TYPE_NAME => 'int', _EXTRA => 'auto_increment', _PK => 1},
fake => {TYPE_NAME => 'bigint', COLUMN_DEF => 0, NULLABLE => 0},
},
tables => {
roles => {
columns => {
name => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
label => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
},
},
users => {
columns => {
name => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
login => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
label => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
password => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
id_role => {TYPE_NAME => 'int'},
}
},
log => {
columns => {
id_user => {TYPE_NAME => 'int'},
id_object => {TYPE_NAME => 'int'},
ip => {TYPE_NAME => 'varchar', COLUMN_SIZE => 15},
ip_fw => {TYPE_NAME => 'varchar', COLUMN_SIZE => 15},
type => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
action => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
error => {TYPE_NAME => 'varchar', COLUMN_SIZE => 255},
params => {TYPE_NAME => 'text'},
dt => {TYPE_NAME => 'timestamp'},
}
},
},
)
}
}
################################################################################
package Zanas;
$VERSION = '0.9908';
=head1 NAME
Zanas.pm - a RAD platform for WEB GUIs with rich DHTML widget set.
=head1 DESCRIPTION
Zanas.pm is a set of naming conventions, utility functions, and a basic Apache request handler that help to quickly build robust, efficient and good-looking Web interfaces with standard design. The last doesn't mean that you can't alter hardcoded HTML fragments at all. But building public Web sites with original graphics and layout is not the primary goal of Zanas development. Zanas is good for developing client database editing GUIs ('thin clients') and is conditionnally comparable to Windows API and java Swing.
Zanas' basic features are:
=over
=item GUI base
usable set of DHTML widgets (forms, toolbars, etc);
=item transparent DB scheme maintenace
when some table or field lacks, the application creates it silently;
=item ALC support
standard routines to backup, mirror and synchronize multiple installations of the same application (Zanas::Install);
=item sessions
session management subsystem with transparent query rewriting;
=item js alerting
server side error handling and data validation with client javaScript notifications without page reloading (yes, it really is);
=item logging
action logging is a part of core process, additionl API calls aren't needed;
=item fake records/garbage collection
handling of temporary records that are only visible on creation forms and wizards;
=back
=head1 DESIGN PRINCIPLES
There is a whole a lot of univesal web application platforms. So, why develop another one instead of using some mature product? 'Cause we've already tried many of this and have'nt found a good one.
When developing Zanas, we use the following principles:
=over
=item no OO
HTTP is nothing else than evaluting string functions with sets of named parameters. Request handler must do nothing else than decompose the top function to some more primitive functions. So, Zanas is purely procedure-oriented framework.
=item content/presentation separation
Request handler reduce the top function f (x) to a superposition of a content and a presentation function: c (x) and p (c, x), where c (x) can't produce any HTML fragment in its result and p (c, x) can't use any info stored in the database.
f (x) = p (c (x), x).
=item URL discipline and strict callback naming
Content and presentation functions can be reduced to swithes between some elementary callback functions, where the switch is directly governed by known CGI parameters. Say, for C C and C
.
=item no ASP or like
Perl is ideal for implementing templating languages. That's why people love to implement new templating languages in Perl. But most of them ignore the fact that Perl I a templating language. Heredoc syntax is much more usable than any ASP-like. And it doesn't require any additional processing: everything is done by the Perl interpreter.
=item no XML
Nested Perl datastructures like list-of-hashes and more complex offer the same functionnality as the XML DOM model. And it doesn't require any external libraries: everything is done by the Perl interpreter.
=item no XSLT
It would be very strange to use XSLT without XML, but we must underline here that there was one more reason to not use XSLT. Its syntax is even much crappier and less flexible than ASP-like.
=back
=head1 MAGIC CGI PARAMETERS
The next CGI parameters have special meaning in Zanas URLs and can be used only as described.
=over
=item sid
Session ID. If not set, the client is automatically redirected to the logon screen.
=item type
Type of the current screen. Can have values like C<'users'> or, for example, C<'users_creation_wizard_step_2'>. Influences the callback functions selection and the main menu rendering.
=item id
Current object ID. Influences the callback functions selection. When set, the screen presents detailed info of one object, otherwise, it contains some search results.
=item action
Name of the action to execute. If set, the request handler executes some editing callback, then evalutes the new URL where C is unset and redirects the client there.
=item salt
Fake random parameter for preventing the client HTML cacheing.
=back
=head1 GLOBAL VARIABLES
The next variables are accessible in all callback subs.
=over
=item %_REQUEST
The hash of CGI parameters and its values
=item $_USER
The hashref containing the current user information:
{
id => ...
name => ...
role => ...
}
=back
=head1 CALLBACK SUBS
Under differnent circumstances, Zanas Apache request handler executes appropriate callback subs. The name of the callback to execute depends on current program context, C value and the role of the current user.
Suppose that the context imply the callback name C<$my_callback>, C<$_REQUEST{type}> is C<$type> and C<$$_USER {role}> is C<$role>. In this case, if the sub named "${my_callback}_${type}_for_${role}" is defined, it will be called. Otherwise, if the sub named "${my_callback}_${type}" is defined, it will be called. Otherwise, undef value will be used instead of missing sub result.
In the next sections, "${my_callback}_${type}_for_${role}" always means one of 3 cases described above.
=over
=item validate_{$action}_${type}_for_${role}
This sub must analyze the values of parameters in C<%_REQUEST> hash for consistency. In most cases, the object id is stored in C<$_REQUEST {id}> and the names of all other fields are underscore prefixed (C<$_REQUEST {_name}>, C<$_REQUEST {_login}>, C<$_REQUEST {_password}> etc).
If everythig's OK, the validator must return C. Otherwise, the return value is an error code. We'll call it C<$error>. So, if C<$error> is defined, an error message template C<$$error_messages {"{$action}_${type}_${error}"}> is interpolated as a qq-string and then sent to the user as the error message.
For example, if the sub C returns C<'duplicate_login'>, C<$_REQUEST {_login} eq 'scott'> and C<$$error_messages {"update_users_duplicate_login"} eq 'Duplicate login: \'$_REQUEST{_login}\''>, then the error message will be C<"Duplicate login: 'scott'">.
=item do_{$action}_${type}_for_${role}
This sub must execute the C<$action>. Note that you can choose the next screen shown to the user by manipulating the C<%_REQUEST> hash. For example, it's usual to set the C parameter after creating new object:
sub do_create_users_for_admin {
sql_do ("INSERT INTO ... ");
$_REQUEST {id} = sql_last_insert_id ();
}
The client window will be rediredted to "/?type=users&id=1&sid=...".
=item get_item_of_${type}_for_${role}
This sub must fetch the info for the screen of type C<$type> having the obgect id C<$_REQUEST {id}> and the role C<${role}>. Usually it's a reference to a hash, may be nested.
=item select_${type}_for_${role}
This sub must fetch the info for the screen of type C<$type> and the role C<${role}>. Usually it's a reference to a list of references to hashes, may be nested.
=item draw_item_of_${type}_for_${role}
This sub must render the screen of type C<$type> having the obgect id C<$_REQUEST {id}> and the role C<${role}> as HTML. The info fetched with C is passed as its 1st parameter.
=item draw_${type}_for_${role}
This sub must render the screen of type C<$type> sand the role C<${role}> as HTML. The info fetched with C is passed as its 1st parameter.
=back
=head1 HTTP REQUEST HANDLING
=head2 SESSION CHECKING
First of all, the handler checks for the C param and, if the session is alive, it sets the C<$USER> variable, otherwise, redirects the client to the logon screen.
=head2 EDITING REQUEST
If the C CGI parameter is set, then the sub named C is invoked. If if returns a non-empty error message, it's logged and presented with a js popup window. Otherwise the sub named C is invoked, then the client is redirected to the new URL composed from all C<%_REQUEST> key-value pairs except C and those which names start with an C<'_'>.
In any case, the HTTP response has status 200 (OK) and contains a tiny HTML document consisting of a singular C tag with a non-empty C event handler. When an error occurs, this handler displays the message in a js popup window. Otherwise the C handler opens the new URL in the top browser window.
Every conventional HTML page generated by Zanas Apache handler has a zero sized internal frame called C. In order to improve the GUI usability, every anchor with non-empty C parameter value in its href and every form with a non-empty value for C input must use C as the target:
[New Folder]
Standard Zanas HTML rendering API does this automatically.
=head2 OBJECT BROWSING REQUEST
If the C CGI parameter is unset and C CGI parameter is set, then the HTML resuls from the superposition of C and C callbacks.
=head2 SELECTION BROWSING REQUEST
If both C and C CGI parameters are unset, then the HTML resuls from the superposition of C and C callbacks.
=head1 MODULES STRUCTURE
Zanas modules don't have a C directive. All the stuff is loaded in one package.
Callback subs must be placed in strictly named .pm files. Suppose that you've chosen C<$applib> as your application library root and have placed it in your C<@INC> array. Then, create C<$applib/Content> and C<$applib/Presentation> directories.
Now, all content callbacks (C, C, C and C) must be defined in C<$applib/Content/${type}.pm> and presentation callbacks (C and C) in C<$applib/Presentation/${type}.pm>.
$applib
Content
roles.pm
users.pm
Presentation
roles.pm
users.pm
=head1 MORE API DOCS
Generate it:
perl -MZanas::Docs -e generate
=head1 SEE ALSO
DBIx::ModelUpdate Zanas::Install
=head1 AUTHORS
Dmitry Ovsyanko
Pavel Kudryavtzev
Yaroslav Ivanov <... hekima ...>
1;
0.9908:
- check_title sub is added, draw_table_header, draw_row_button and draw_text_cell are fixed to use it;
0.9907:
- magic %_REQUEST params documented;
- trees in 'checkboxes' are supported;
0.9906:
- new $conf options: kb_options_menu, kb_options_buttons, kb_options_pager and kb_options_focus;
- hotkey and hotkeys subs added;
- draw_toolbar_input_select sub added;
- draw_form_field_checkboxes redone dramatically;
0.9905:
- Zanas::Docs added;
0.9904:
- added 'read_only' option in draw_menu sub;
0.9903:
- new magic parameter: __help_url;
- draw_text_cell fix: no hrefs rendered when lpt=1;
- added 'position' option in add_totals sub;
- added 'read_only' option in draw_input_cell sub;
- another XLS fix: $conf -> {site_root} removed, $$ added in temporary filename;
0.9902:
- serving XLS responses refactored: $conf -> {site_root} is obsoleted & no more filesystem garbage;
- fixed to $$conf{page_title} in lpt mode;
- new magic parameter: $_REQUEST{_xls_checksum}. Added in the 1st
in an invisible extra row of the main table (LPT mode only);
0.9901:
- added magic parameter _xml (included in );
- added namespace for excel;
- added title attribute for table headers an cells;
- added 'a_class' option to draw_text_cells sub;
# - storing sid in session-only cookie;
0.99: 15.03.2004 10:30
- pulldown menu rendering fixed (no glitches now);
- js function open_popup_menu is moved to navigation.js;
- HTTP_X_FORWARDED_FOR logging;
0.98: 12.03.2004 11:00
- new feature: 2-level main menu;
sub get_menu_for_my_role {
return [
{
name => 'type1',
label => 'Screen Type 1',
items => [
{
name => 'type11',
label => 'Screen Type 11',
},
{
name => 'type12',
label => 'Screen Type 12',
},
]
},
{
name => 'type2',
label => 'Screen Type 2',
},
]
}
0.97: 10.03.2004 16:40
- sql_reconnect and Config reloading order fixed;
- target option added in draw_toolbar_button sub;
0.96: 09.03.2004 17:00
- target option added in draw_toolbar sub;
- Zanas::Install is forked out;
- order sub fixed for correct DESC handling;
- draw_table fixed for not scrolling through totals;
- added is_total option to draw_text_cell sub;
- added real_path option to upload_file output;
- added add_columns option in sql_upload_file sub (by pashka);
- Zanas::Request fixed for the case where directory listing is denied;
- Apache::Constants::OK fixed for prototype matching.
0.95: 03.03.2004 12:30
- add_totals sub added;
- $_REQUEST {__response_sent} is now used when doing actions (by pashka);
- redirection target is changed from _top to _parent (by pashka);
0.94: 02.03.2004 16:00
- (by pashka) no more strong dependency on Apache::Request or even mod_perl.
One can use Zanas.pm based apps on any raw CGI hosting. The script is
----------- cut here ------------------------
#!/usr/bin/perl -w
use lib '/path/to/webapp/library';
use MYAPP;
$MYAPP::preconf = {
db_dsn => "DBI:mysql:database=mybase",
db_user => 'myuser',
db_password => 'mypassword',
core_load_modules => 0,
core_spy_modules => 0,
core_fix_modules => 0,
core_gzip => 1,
};
MYAPP::handler;
----------- cut here ------------------------
It's 20 times more slow, but it works;
- fixed a security hole with 'type=users_for_admin' (by pashka);
- multirole fixes (by pashka);
- added $_REQUEST {redirect_params} handling (idea by pashka);
- new log fields: id_object and ip;
0.93: 26.02.04 12:50
- targets are now in use with activate_link js function;
- target option is now passed in draw_text_cells;
- don't show labels for hrgoups items that are off;
-
are now marked as id="tr_$$field{name}" in draw_form;
- added setVisible js function;
- added onChange option to draw_form_field_select sub;
- added onClose option to draw_form_field_datetime sub;
- fixed a bug with calendar format;
0.92: 19.02.04 10:40
- added href option in draw_text_cells sub;
- draw_text_cell now receives 2 args: data and options, data can be scalar (label only);
- '..' option in draw_table;
- don't load Mozilla3;
0.91: 13.02.04 10:30
- added a 'no_nobr' option in draw_text_cell;
- added a 'title' option in draw_text_cell;
0.90: 12.02.04 10:30
- added a configuration option: $conf -> {exit_url};
0.89:
- fixed a bug in draw_toolbar_input_text (useless sid hidden input);
- fixed a bug in check_href (useless sid appending);
- href option in check_href sub ( => almost *EVRYWHERE*) can now be a HASHref (fed to create_url);
- added 'additional_buttons' option in draw_form.
- added 'status_switch' utility function. Synopsis (in YOUR_APP/Config.pm):
our ($SQL_STATUS, $status) = status_switch (< my_table.expire_dt THEN 2 # Expired
WHEN my_table.is_ok THEN 1 # OK
ELSE 0 # New
END
EOS
To use it, add 'use Zanas::Util;' BEFORE 'use YOUR_APP::Config;'.
- added 'read_only' option for all form inputs;
- now working unless Apache::Request is installed: in this case, params are fetched with CGI.pm;
- 'values' option in draw_form_field_static sub can be a hashref;
0.88:
added 'headers' sub:
draw_table (
headers (qw(
№ no
Label label
_
)),
sub {
...
}
)
- added 'hrefs' sub (for column ordering):
href => create_url (order => $order, desc => 0),
href_asc => create_url (order => $order, desc => 0),
href_desc => create_url (order => $order, desc => 1),
- added 'href_asc' and 'href_desc' options to draw_text_cell.
- new magic parameter: __pack. Resizes the new opened window to fit its contents.
- added js function: nop. Does nothing.
- new magic parameter: __read_only. All form inputs are static.
- added 'picture' option to draw_form_field_string and draw_form_field_static subs
0.87: 30.01.04 10:30
- added 'value' option to draw_checkbox_cell sub
0.86: 29.01.04 16:30
- added 'confirm' option to draw_centered_toolbar_button sub
- added 'target' option to draw_toolbar_button sub
0.85: 22.01.04 11:52
- 0.gif is relocated to the root
- no more 'keepalive' iframe on logon screen
0.84: 21.01.04 13:45
- Accept-Encoding header is now considered
0.83: 21.01.04 12:15
- menu.js is killed
0.82: 21.01.04 11:00
- 0.html is relocated to the root
0.81: 20.01.04 14:00
- Zanas::Install module added;
- gzip encoding for basic js and css;
- sub sql_select_subtree added;
0.80: 26.12.03 14:30
- basic js and css is now served from Zanas.pm itself;
- fixed a memory leak related to __include_js and __include_css handling;
- distro is cleaned up;
0.79: 25.12.03 12:30
- added html sweeping option ($conf -> {core_sweep_spaces});
- _W_A_R_N_I_N_G_ !!! Basic navigation javaScript is now served as static content. Copy or symlink static/navigation.js to your app /docroot/i/ dir;
- added gzipping option ($conf -> {core_gzip}). Requires Compress::Zlib.
- added Content-Range header in download_file sub;
0.78: 24.12.03 12:30
- added Content-Length header in download_file sub;
- added support for $preconf configuration hash (setting in section);
- added support for __focused_input magic parameter;
- sub add_vocabularies added;
- sub sql_select_vocabulary added;
- $conf -> {top_banner} is now rendered;
0.77: 17.12.03 10:30
- added db schema autocheck with DBIx::ModelUpdate;
- added support for multiroled users (by pashka);
- __no_navigation is now passed through URL rewriting (by pashka);
- fixed check_href for /i/ (by pashka);
0.76: 16.12.03 10:30
- various js fixes in table scrolling/focus handling;
0.75: 15.12.03 14:00
- new field type: 'calendar' (control taken from http://dynarch.com/mishoo/calendar.epl);
- added support for:
$conf -> {include_js} ||= ['js'];
$_REQUEST {__include_js} = $conf -> {include_js};
$conf -> {include_css} ||= ['new'];
$_REQUEST {__include_css} = $conf -> {include_css};
Fckeditor js include moved there.
- added off option to draw_toolbar_input_htmleditor sub;
0.74: 04.12.03 14:00
- added confirm option to draw_toolbar_button sub;
- added target option to draw_form sub;
0.73: 02.12.03 15:00
- added off option to draw_toolbar_input_text sub;
- added top_banner configuration option;
0.72: 28.11.03 16:00
- added type-ahead facility to all