int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id);
+static char* _sanitize_tz_name( const char* tz );
static char* _sanitize_savepoint_name( const char* sp );
/**
return -1;
}
+ const char* tz = _sanitize_tz_name(ctx->session->session_tz);
+
if( enforce_pcrud ) {
timeout_needs_resetting = 1;
const jsonObject* user = verifyUserPCRUD( ctx );
jsonObject* ret = jsonNewObject( getXactId( ctx ) );
osrfAppRespondComplete( ctx, ret );
jsonObjectFree( ret );
- return 0;
+
+ }
+
+ if (tz) {
+ setenv("TZ",tz,1);
+ dbi_result tz_res = dbi_conn_queryf( writehandle, "SET LOCAL timezone TO '%s'; -- cstore", tz );
+ if( !tz_res ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error setting timezone %s", modulename, tz);
+ if( !oilsIsDBConnected( writehandle )) {
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ }
+ } else {
+ dbi_result_free( tz_res );
+ }
+ } else {
+ unsetenv("TZ");
+ dbi_result res = dbi_conn_queryf( writehandle, "SET timezone TO DEFAULT; -- no tz" );
+ if( !res ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error resetting timezone", modulename);
+ if( !oilsIsDBConnected( writehandle )) {
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ }
+ } else {
+ dbi_result_free( res );
+ }
}
+
+ return 0;
}
/**
static jsonObject* doFieldmapperSearch( osrfMethodContext* ctx, osrfHash* class_meta,
jsonObject* where_hash, jsonObject* query_hash, int* err ) {
+ const char* tz = _sanitize_tz_name(ctx->session->session_tz);
+
// XXX for now...
dbhandle = writehandle;
osrfLogDebug( OSRF_LOG_MARK, "%s SQL = %s", modulename, sql );
+ // Setting the timezone if requested and not in a transaction
+ if (!getXactId(ctx)) {
+ if (tz) {
+ setenv("TZ",tz,1);
+ dbi_result tz_res = dbi_conn_queryf( writehandle, "SET timezone TO '%s'; -- cstore", tz );
+ if( !tz_res ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error setting timezone %s", modulename, tz);
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error setting timezone" );
+ if( !oilsIsDBConnected( writehandle )) {
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ }
+ } else {
+ dbi_result_free( tz_res );
+ }
+ } else {
+ unsetenv("TZ");
+ dbi_result res = dbi_conn_queryf( writehandle, "SET timezone TO DEFAULT; -- cstore" );
+ if( !res ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error resetting timezone", modulename);
+ if( !oilsIsDBConnected( writehandle )) {
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ }
+ } else {
+ dbi_result_free( res );
+ }
+ }
+ }
+
+
dbi_result result = dbi_conn_query( dbhandle, sql );
+
if( NULL == result ) {
const char* msg;
int errnum = dbi_conn_error( dbhandle, &msg );
} else {
osrfLogDebug( OSRF_LOG_MARK, "Query returned with no errors" );
+
}
jsonObject* res_list = jsonNewObjectType( JSON_ARRAY );
return safeSpName;
}
+/**
+ @brief Remove all but safe character from TZ name
+ @param tz User-supplied TZ name
+ @return sanitized TZ name, or NULL
+
+ The caller is expected to free the returned string. Note that
+ this function exists only because we can't use PQescapeLiteral
+ without either forking libdbi or abandoning it.
+*/
+static char* _sanitize_tz_name( const char* tz ) {
+
+ if (NULL == tz) return NULL;
+
+ const char* safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345789_/-+";
+
+ // PostgreSQL uses NAMEDATALEN-1 as a max length for identifiers,
+ // and the default value of NAMEDATALEN is 64; that should be long enough
+ // for our purposes, and it's unlikely that anyone is going to recompile
+ // PostgreSQL to have a smaller value, so cap the identifier name
+ // accordingly to avoid the remote chance that someone manages to pass in a
+ // 12GB savepoint name
+ const int MAX_LITERAL_NAMELEN = 63;
+ int len = 0;
+ len = strlen( tz );
+ if (len > MAX_LITERAL_NAMELEN) {
+ len = MAX_LITERAL_NAMELEN;
+ }
+
+ char* safeSpName = safe_malloc( len + 1 );
+ int i = 0;
+ int j;
+ char* found;
+ for (j = 0; j < len; j++) {
+ found = strchr(safe_chars, tz[j]);
+ if (found) {
+ safeSpName[ i++ ] = found[0];
+ }
+ }
+ safeSpName[ i ] = '\0';
+ return safeSpName;
+}
+
/*@}*/
$log->debug("We seem to be OK...",DEBUG);
}
+sub register_method {
+ my $class = shift;
+ my %args = @_;
+
+ $args{package} ||= ref($class) || $class;
+
+ unless ($args{no_tz_force}) {
+ my %dup_args = %args;
+ $dup_args{api_name} = 'no_tz.' . $args{api_name};
+
+ $args{method} = 'force_db_tz';
+ delete $args{package};
+
+ __PACKAGE__->SUPER::register_method( %dup_args );
+
+ }
+
+ __PACKAGE__->SUPER::register_method( %args );
+
+}
+
+sub force_db_tz {
+ my $self = shift;
+ my $client = shift;
+ my @args = @_;
+
+ my ($current_xact) = $self->method_lookup('no_tz.open-ils.storage.transaction.current')->run;
+
+ if (!$current_xact && $ENV{TZ}) {
+ try {
+ OpenILS::Application::Storage::CDBI->db_Main->do(
+ 'SET timezone TO ?;',
+ {},
+ $ENV{TZ}
+ );
+ } catch Error with {
+ $log->error( "Could not set timezone: $ENV{TZ}");
+ };
+ }
+
+ my $method = $self->method_lookup('no_tz.' . $self->{api_name});
+ die unless $method;
+
+ $client->respond( $_ ) for ( $method->run(@args) );
+
+ if (!$current_xact && $ENV{TZ}) {
+ try {
+ OpenILS::Application::Storage::CDBI->db_Main->do(
+ 'SET timezone TO DEFAULT;',
+ );
+ } catch Error with {
+ $log->error( "Could not reset default timezone");
+ };
+ }
+
+ return undef;
+}
+
sub child_init {
$log->debug('Running child_init for ' . __PACKAGE__ . '...', DEBUG);
throw $e;
};
+ if ($ENV{TZ}) {
+ try {
+ $dbh->do('SET LOCAL timezone TO ?;',{},$ENV{TZ});
+
+ } catch Error with {
+ my $e = shift;
+ $log->debug("Failed to set timezone: $ENV{TZ}", WARN);
+ };
+ }
- my $death_cb = $client->session->register_callback(
- death => sub {
- __PACKAGE__->pg_rollback_xaction;
- }
- );
-
- $log->debug("Registered 'death' callback [$death_cb] for new transaction with Open-ILS XACT-ID [$xact_id]", DEBUG);
-
- $client->session->session_data( death_cb => $death_cb );
- if ($self->api_name =~ /autocommit$/o) {
- $pg->current_xact_is_auto(1);
- my $dc_cb = $client->session->register_callback(
- disconnect => sub {
- my $ses = shift;
- $ses->unregister_callback(death => $death_cb);
- __PACKAGE__->pg_commit_xaction;
+ if ($client->session) { # not a subrequest
+ my $death_cb = $client->session->register_callback(
+ death => sub {
+ __PACKAGE__->pg_rollback_xaction;
}
);
- $log->debug("Registered 'disconnect' callback [$dc_cb] for new transaction with Open-ILS XACT-ID [$xact_id]", DEBUG);
- if ($client and $client->session) {
- $client->session->session_data( disconnect_cb => $dc_cb );
+
+ $log->debug("Registered 'death' callback [$death_cb] for new transaction with Open-ILS XACT-ID [$xact_id]", DEBUG);
+
+ $client->session->session_data( death_cb => $death_cb );
+
+ if ($self->api_name =~ /autocommit$/o) {
+ $pg->current_xact_is_auto(1);
+ my $dc_cb = $client->session->register_callback(
+ disconnect => sub {
+ my $ses = shift;
+ $ses->unregister_callback(death => $death_cb);
+ __PACKAGE__->pg_commit_xaction;
+ }
+ );
+ $log->debug("Registered 'disconnect' callback [$dc_cb] for new transaction with Open-ILS XACT-ID [$xact_id]", DEBUG);
+ if ($client and $client->session) {
+ $client->session->session_data( disconnect_cb => $dc_cb );
+ }
}
}
$success = 0;
};
- $pg->current_xact_session->unregister_callback( death =>
- $pg->current_xact_session->session_data( 'death_cb' )
- ) if ($pg->current_xact_session);
-
- if ($pg->current_xact_is_auto) {
- $pg->current_xact_session->unregister_callback( disconnect =>
- $pg->current_xact_session->session_data( 'disconnect_cb' )
- );
+ if ($pg->current_xact_session) { # not a subrequest
+ $pg->current_xact_session->unregister_callback( death =>
+ $pg->current_xact_session->session_data( 'death_cb' )
+ ) if ($pg->current_xact_session);
+
+ if ($pg->current_xact_is_auto) {
+ $pg->current_xact_session->unregister_callback( disconnect =>
+ $pg->current_xact_session->session_data( 'disconnect_cb' )
+ );
+ }
}
$pg->unset_xact_session;
$success = 0;
};
- $pg->current_xact_session->unregister_callback( death =>
- $pg->current_xact_session->session_data( 'death_cb' )
- ) if ($pg->current_xact_session);
-
- if ($pg->current_xact_is_auto) {
- $pg->current_xact_session->unregister_callback( disconnect =>
- $pg->current_xact_session->session_data( 'disconnect_cb' )
- );
+ if ($pg->current_xact_session) { # not a subrequest
+ $pg->current_xact_session->unregister_callback( death =>
+ $pg->current_xact_session->session_data( 'death_cb' )
+ ) if ($pg->current_xact_session);
+
+ if ($pg->current_xact_is_auto) {
+ $pg->current_xact_session->unregister_callback( disconnect =>
+ $pg->current_xact_session->session_data( 'disconnect_cb' )
+ );
+ }
}
$pg->unset_xact_session;
$pg->set_audit_session( $client->session );
- my $death_cb = $client->session->register_callback(
- death => sub {
- __PACKAGE__->pg_clear_audit_info;
- }
- );
-
- $log->debug("Registered 'death' callback [$death_cb] for clearing audit information", DEBUG);
-
- $client->session->session_data( death_cb_ai => $death_cb );
+ if ($client->session) { # not a subrequest
+ my $death_cb = $client->session->register_callback(
+ death => sub {
+ __PACKAGE__->pg_clear_audit_info;
+ }
+ );
+
+ $log->debug("Registered 'death' callback [$death_cb] for clearing audit information", DEBUG);
+
+ $client->session->session_data( death_cb_ai => $death_cb );
+ }
return 1;
$log->debug("Failed to clear audit information: ".$e, INFO);
};
- $pg->current_audit_session->unregister_callback( death =>
- $pg->current_audit_session->session_data( 'death_cb_ai' )
- ) if ($pg->current_audit_session);
+ if ($pg->current_audit_session) { # not a subrequest
+ $pg->current_audit_session->unregister_callback( death =>
+ $pg->current_audit_session->session_data( 'death_cb_ai' )
+ ) if ($pg->current_audit_session);
+ }
$pg->unset_audit_session;
}
}
__PACKAGE__->register_method(
api_name => 'open-ils.storage.ordered.metabib.metarecord.records',
+ no_tz_force => 1,
method => 'ordered_records_from_metarecord',
api_level => 1,
cachable => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff',
+ no_tz_force => 1,
method => 'ordered_records_from_metarecord',
api_level => 1,
cachable => 1,
__PACKAGE__->register_method(
api_name => 'open-ils.storage.ordered.metabib.metarecord.records.atomic',
+ no_tz_force => 1,
method => 'ordered_records_from_metarecord',
api_level => 1,
cachable => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.ordered.metabib.metarecord.records.staff.atomic',
+ no_tz_force => 1,
method => 'ordered_records_from_metarecord',
api_level => 1,
cachable => 1,
}
__PACKAGE__->register_method(
api_name => 'open-ils.storage.id_list.biblio.record_entry.search.isbn',
+ no_tz_force => 1,
method => 'isxn_search',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.id_list.biblio.record_entry.search.issn',
+ no_tz_force => 1,
method => 'isxn_search',
api_level => 1,
stream => 1,
}
__PACKAGE__->register_method(
api_name => 'open-ils.storage.metabib.metarecord.copy_count',
+ no_tz_force => 1,
method => 'metarecord_copy_count',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.metabib.metarecord.copy_count.staff',
+ no_tz_force => 1,
method => 'metarecord_copy_count',
api_level => 1,
stream => 1,
}
__PACKAGE__->register_method(
api_name => 'open-ils.storage.biblio.full_rec.multi_search',
+ no_tz_force => 1,
method => 'biblio_multi_search_full_rec',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.biblio.full_rec.multi_search.staff',
+ no_tz_force => 1,
method => 'biblio_multi_search_full_rec',
api_level => 1,
stream => 1,
}
__PACKAGE__->register_method(
api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.value',
+ no_tz_force => 1,
method => 'search_full_rec',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => 'open-ils.storage.direct.metabib.full_rec.search_fts.index_vector',
+ no_tz_force => 1,
method => 'search_full_rec',
api_level => 1,
stream => 1,
for my $class ( qw/title author subject keyword series identifier/ ) {
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord",
+ no_tz_force => 1,
method => 'search_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.unordered",
+ no_tz_force => 1,
method => 'search_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff",
+ no_tz_force => 1,
method => 'search_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord.staff.unordered",
+ no_tz_force => 1,
method => 'search_class_fts',
api_level => 1,
stream => 1,
for my $class ( qw/title author subject keyword series identifier/ ) {
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count",
+ no_tz_force => 1,
method => 'search_class_fts_count',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.search_fts.metarecord_count.staff",
+ no_tz_force => 1,
method => 'search_class_fts_count',
api_level => 1,
stream => 1,
for my $class ( qw/title author subject keyword series identifier/ ) {
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord",
+ no_tz_force => 1,
method => 'postfilter_search_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.$class.post_filter.search_fts.metarecord.staff",
+ no_tz_force => 1,
method => 'postfilter_search_class_fts',
api_level => 1,
stream => 1,
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord",
+ no_tz_force => 1,
method => 'postfilter_search_multi_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.post_filter.multiclass.search_fts.metarecord.staff",
+ no_tz_force => 1,
method => 'postfilter_search_multi_class_fts',
api_level => 1,
stream => 1,
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.search_fts",
+ no_tz_force => 1,
method => 'postfilter_search_multi_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.search_fts.staff",
+ no_tz_force => 1,
method => 'postfilter_search_multi_class_fts',
api_level => 1,
stream => 1,
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.search_fts.record",
+ no_tz_force => 1,
method => 'biblio_search_multi_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.search_fts.record.staff",
+ no_tz_force => 1,
method => 'biblio_search_multi_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.search_fts",
+ no_tz_force => 1,
method => 'biblio_search_multi_class_fts',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.search_fts.staff",
+ no_tz_force => 1,
method => 'biblio_search_multi_class_fts',
api_level => 1,
stream => 1,
}
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
+ no_tz_force => 1,
method => 'staged_fts',
api_level => 0,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
+ no_tz_force => 1,
method => 'staged_fts',
api_level => 0,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
+ no_tz_force => 1,
method => 'staged_fts',
api_level => 0,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
+ no_tz_force => 1,
method => 'staged_fts',
api_level => 0,
stream => 1,
}
__PACKAGE__->register_method(
api_name => "open-ils.storage.fts_paging_estimate",
+ no_tz_force => 1,
method => 'FTS_paging_estimate',
argc => 5,
strict => 1,
}
__PACKAGE__->register_method(
api_name => "open-ils.storage.search.xref",
+ no_tz_force => 1,
method => 'xref_count',
api_level => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.query_parser.abstract_query.canonicalize",
+ no_tz_force => 1,
method => "abstract_query2str",
api_level => 1,
signature => {
__PACKAGE__->register_method(
api_name => "open-ils.storage.query_parser.abstract_query.from_string",
+ no_tz_force => 1,
method => "str2abstract_query",
api_level => 1,
signature => {
}
__PACKAGE__->register_method(
api_name => "open-ils.storage.query_parser_search",
+ no_tz_force => 1,
method => 'query_parser_fts',
api_level => 1,
stream => 1,
}
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.staged.search_fts",
+ no_tz_force => 1,
method => 'query_parser_fts_wrapper',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.biblio.multiclass.staged.search_fts.staff",
+ no_tz_force => 1,
method => 'query_parser_fts_wrapper',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.staged.search_fts",
+ no_tz_force => 1,
method => 'query_parser_fts_wrapper',
api_level => 1,
stream => 1,
);
__PACKAGE__->register_method(
api_name => "open-ils.storage.metabib.multiclass.staged.search_fts.staff",
+ no_tz_force => 1,
method => 'query_parser_fts_wrapper',
api_level => 1,
stream => 1,
use constant COOKIE_SES => 'ses';
use constant COOKIE_LOGGEDIN => 'eg_loggedin';
+use constant COOKIE_TZ => 'client_tz';
use constant COOKIE_PHYSICAL_LOC => 'eg_physical_loc';
use constant COOKIE_SSS_EXPAND => 'eg_sss_expand';
$ctx->{default_sort} =
($default_sort && $U->is_true($default_sort->enabled)) ? $default_sort->value : '';
+ $ctx->{client_tz} = $self->cgi->cookie(COOKIE_TZ) || $ENV{TZ};
$ctx->{referer} = $self->cgi->referer;
$ctx->{path_info} = $self->cgi->path_info;
$ctx->{full_path} = $ctx->{base_path} . $self->cgi->path_info;
$ctx->{unparsed_uri} = $self->apache->unparsed_uri;
$ctx->{opac_root} = $ctx->{base_path} . "/opac"; # absolute base url
+ local $ENV{TZ} = $ctx->{client_tz};
+
my $xul_wrapper =
($self->apache->headers_in->get('OILS-Wrapper') || '') =~ /true/;
my $password = $cgi->param('password');
my $org_unit = $ctx->{physical_loc} || $ctx->{aou_tree}->()->id;
my $persist = $cgi->param('persist');
+ my $client_tz = $cgi->param('client_tz');
# initial log form only
return Apache2::Const::OK unless $username and $password;
# both login-related cookies should expire at the same time
my $login_cookie_expires = ($persist) ? CORE::time + $response->{payload}->{authtime} : undef;
+ my $cookie_list = [
+ # contains the actual auth token and should be sent only over https
+ $cgi->cookie(
+ -name => COOKIE_SES,
+ -path => '/',
+ -secure => 1,
+ -value => $response->{payload}->{authtoken},
+ -expires => $login_cookie_expires
+ ),
+ # contains only a hint that we are logged in, and is used to
+ # trigger a redirect to https
+ $cgi->cookie(
+ -name => COOKIE_LOGGEDIN,
+ -path => '/',
+ -secure => 0,
+ -value => '1',
+ -expires => $login_cookie_expires
+ )
+ ];
+
+ if ($client_tz) {
+ # contains the client's tz, as passed by the client
+ # trigger a redirect to https
+ push @$cookie_list, $cgi->cookie(
+ -name => COOKIE_TZ,
+ -path => '/',
+ -secure => 0,
+ -value => $client_tz,
+ -expires => $login_cookie_expires
+ );
+ }
+
return $self->generic_redirect(
$cgi->param('redirect_to') || $acct,
- [
- # contains the actual auth token and should be sent only over https
- $cgi->cookie(
- -name => COOKIE_SES,
- -path => '/',
- -secure => 1,
- -value => $response->{payload}->{authtoken},
- -expires => $login_cookie_expires
- ),
- # contains only a hint that we are logged in, and is used to
- # trigger a redirect to https
- $cgi->cookie(
- -name => COOKIE_LOGGEDIN,
- -path => '/',
- -secure => 0,
- -value => '1',
- -expires => $login_cookie_expires
- )
- ]
+ $cookie_list
);
}
</script>
[% END %]
+<script type="text/javascript">$('client_tz_id').value = OpenSRF.tz</script>
[%- END; # want_dojo -%]
<input type="checkbox" name="persist" id="login_persist" /><label for="login_persist"> [% l('Stay logged in?') %]</label>
<input type="submit" value="[% l('Log in') %]" alt="[% l('Log in') %]" class="opac-button" />
</div>
+ <input id="client_tz_id" name="client_tz" type="hidden" />
</form>
</div>
[% INCLUDE "opac/parts/login/help.tt2" %]