home *** CD-ROM | disk | FTP | other *** search
/ PC Open 100 / PC Open 100 CD 1.bin / CD1 / INTERNET / EMAIL / pop file / setup.exe / UI / HTML.pm next >
Encoding:
Perl POD Document  |  2004-03-16  |  180.6 KB  |  4,197 lines

  1. # POPFILE LOADABLE MODULE
  2. package UI::HTML;
  3.  
  4. #----------------------------------------------------------------------------
  5. #
  6. # This package contains an HTML UI for POPFile
  7. #
  8. # Copyright (c) 2001-2003 John Graham-Cumming
  9. #
  10. #   This file is part of POPFile
  11. #
  12. #   POPFile is free software; you can redistribute it and/or modify
  13. #   it under the terms of the GNU General Public License as published by
  14. #   the Free Software Foundation; either version 2 of the License, or
  15. #   (at your option) any later version.
  16. #
  17. #   POPFile is distributed in the hope that it will be useful,
  18. #   but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  20. #   GNU General Public License for more details.
  21. #
  22. #   You should have received a copy of the GNU General Public License
  23. #   along with POPFile; if not, write to the Free Software
  24. #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  25. #
  26. #----------------------------------------------------------------------------
  27.  
  28. use UI::HTTP;
  29. @ISA = ("UI::HTTP");
  30.  
  31. use strict;
  32. use warnings;
  33. use locale;
  34.  
  35. use IO::Socket;
  36. use IO::Select;
  37. use Digest::MD5 qw( md5_hex );
  38.  
  39. # A handy variable containing the value of an EOL for the network
  40.  
  41. my $eol = "\015\012";
  42.  
  43. # Constant used by the history deletion code
  44.  
  45. my $seconds_per_day = 60 * 60 * 24;
  46.  
  47. # These are used for Japanese support
  48.  
  49. # ASCII characters
  50. my $ascii = '[\x00-\x7F]';
  51.  
  52. # EUC-JP 2 byte characters
  53. my $two_bytes_euc_jp = '(?:[\x8E\xA1-\xFE][\xA1-\xFE])';
  54.  
  55. # EUC-JP 3 byte characters
  56. my $three_bytes_euc_jp = '(?:\x8F[\xA1-\xFE][\xA1-\xFE])';
  57.  
  58. # EUC-JP characters
  59. my $euc_jp = "(?:$ascii|$two_bytes_euc_jp|$three_bytes_euc_jp)";
  60.  
  61. #----------------------------------------------------------------------------
  62. # new
  63. #
  64. #   Class new() function
  65. #----------------------------------------------------------------------------
  66. sub new
  67. {
  68.     my $type = shift;
  69.     my $self = UI::HTTP->new();
  70.  
  71.     # The classifier (Classifier::Bayes)
  72.  
  73.     $self->{classifier__}      = 0;
  74.  
  75.     # Session key to make the UI safer
  76.  
  77.     $self->{session_key__}     = '';
  78.  
  79.     # The available skins
  80.  
  81.     $self->{skins__}           = ();
  82.  
  83.     # Used to keep the history information around so that we don't have to reglob every time we hit the
  84.     # history page
  85.     #
  86.     # The history hash contains information about ALL the files stored in the history
  87.     # folder (by default messages/) and is updated by the load_history_cache__ method
  88.     #
  89.     # Access to the history cache is formatted $self->{history__}{file}{subkey} where
  90.     # the file is the name of the file that is related to this history entry.
  91.     #
  92.     # The subkeys are
  93.     #
  94.     #   cull            Used internally by load_history_cache__ (see there for details)
  95.     #   from            The address the email was from
  96.     #   short_from      Version of from with max 40 characters
  97.     #   subject         The subject of the email
  98.     #   short_subject   Version of subject with max 40 characters
  99.     #   magnet          If a magnet was used to classify the mail contains the magnet string
  100.     #   bucket          The classification of the mail
  101.     #   reclassified    1 if the mail has already been reclassified
  102.     #
  103.     # The history_keys array stores the list of keys in the history hash and are a
  104.     # (perhaps strict) subset of the keys of $self->{history__} set by calls to
  105.     # sory_filter_history.  history_keys references the elements on history that are
  106.     # in the current filter, sort or search set.
  107.     #
  108.     # If new items have been added to the history the set need_resort__ to 1 to ensure
  109.     # that the next time a history page is being displayed the appropriate sort, search
  110.     # and filter is applied
  111.  
  112.     $self->{history__}         = {};
  113.     $self->{history_keys__}    = ();
  114.     $self->{need_resort__}     = 0;
  115.  
  116.     # Hash containing pre-cached messages loaded upon receipt of NEWFL message. Moved to
  117.     # $self->{history_keys__} on each invocation of the history page.
  118.     # Structure is identical to $self->{history_keys__}
  119.  
  120.     $self->{history_pre_cache__} = {};
  121.  
  122.     # A hash containing a mapping between alphanumeric identifiers and appropriate strings used
  123.     # for localization.  The string may contain sprintf patterns for use in creating grammatically
  124.     # correct strings, or simply be a string
  125.  
  126.     $self->{language__}        = {};
  127.  
  128.     # This is the list of available languages
  129.  
  130.     $self->{languages__}       = ();
  131.  
  132.     # The last user to login via a proxy
  133.  
  134.     $self->{last_login__}      = '';
  135.  
  136.     # Used to determine whehter the cache needs to be saved
  137.  
  138.     $self->{save_cache__}      = 0;
  139.  
  140.     # Stores a Classifier::Bayes session and is set up on the first UI connection
  141.  
  142.     $self->{api_session__}     = '';
  143.  
  144.     # Must call bless before attempting to call any methods
  145.  
  146.     bless $self, $type;
  147.  
  148.     # This is the HTML module which we know as the HTML module
  149.  
  150.     $self->name( 'html' );
  151.  
  152.     return $self;
  153. }
  154.  
  155. # ---------------------------------------------------------------------------------------------
  156. #
  157. # initialize
  158. #
  159. # Called to initialize the interface
  160. #
  161. # ---------------------------------------------------------------------------------------------
  162. sub initialize
  163. {
  164.     my ( $self ) = @_;
  165.  
  166.     $self->config_( 'port', 8080 );
  167.  
  168.     # Checking for updates if off by default
  169.  
  170.     $self->config_( 'update_check', 0 );
  171.  
  172.     # Sending of statistics is off
  173.  
  174.     $self->config_( 'send_stats', 0 );
  175.  
  176.     # The size of a history page
  177.  
  178.     $self->config_( 'page_size', 20 );
  179.  
  180.     # Only accept connections from the local machine for the UI
  181.  
  182.     $self->config_( 'local', 1 );
  183.  
  184.     # Use the default skin
  185.  
  186.     $self->config_( 'skin', 'SimplyBlue' );
  187.  
  188.     # Keep the history for two days
  189.  
  190.     $self->config_( 'history_days', 2 );
  191.  
  192.     # The last time we checked for an update using the local epoch
  193.  
  194.     $self->config_( 'last_update_check', 0 );
  195.  
  196.     # The user interface password
  197.  
  198.     $self->config_( 'password', md5_hex( '__popfile__' ) );
  199.  
  200.     # The last time (textual) that the statistics were reset
  201.  
  202.     my $lt = localtime;
  203.     $self->config_( 'last_reset', $lt );
  204.  
  205.     # We start by assuming that the user speaks English like the
  206.     # perfidious Anglo-Saxons that we are... :-)
  207.  
  208.     $self->config_( 'language', 'English' );
  209.  
  210.     # If this is 1 then when the language is loaded we will use the language string identifier as the
  211.     # string shown in the UI.  This is used to test whether which identifiers are used where.
  212.  
  213.     $self->config_( 'test_language', 0 );
  214.  
  215.     # If 1, Messages are saved to an archive when they are removed or expired from the history cache
  216.  
  217.     $self->config_( 'archive', 0, 1 );
  218.  
  219.     # The directory where messages will be archived to, in sub-directories for each bucket
  220.  
  221.     $self->config_( 'archive_dir', 'archive' );
  222.  
  223.     # This is an advanced setting which will save archived files to a randomly numbered
  224.     # sub-directory, if set to greater than zero, otherwise messages will be saved in the
  225.     # bucket directory
  226.     # 0 <= directory name < archive_classes
  227.  
  228.     $self->config_( 'archive_classes', 0 );
  229.  
  230.     # This setting defines what is displayed in the word matrix: 'freq' for frequencies,
  231.     # 'prob' for probabilities, 'score' for logarithmic scores, if blank then the word
  232.     # table is not shown
  233.  
  234.     $self->config_( 'wordtable_format', '' );
  235.  
  236.     # Load skins
  237.  
  238.     load_skins($self);
  239.  
  240.     # Load the list of available user interface languages
  241.  
  242.     load_languages($self);
  243.  
  244.     # Calculate a session key
  245.  
  246.     change_session_key($self);
  247.  
  248.     # The parent needs a reference to the url handler function
  249.  
  250.     $self->{url_handler_} = \&url_handler__;
  251.  
  252.     # Finally register for the messages that we need to receive
  253.  
  254.     $self->mq_register_( 'NEWFL', $self );
  255.     $self->mq_register_( 'UIREG', $self );
  256.     $self->mq_register_( 'TICKD', $self );
  257.     $self->mq_register_( 'LOGIN', $self );
  258.  
  259.     $self->calculate_today();
  260.  
  261.     return 1;
  262. }
  263.  
  264. # ---------------------------------------------------------------------------------------------
  265. #
  266. # start
  267. #
  268. # Called to start the HTML interface running
  269. #
  270. # ---------------------------------------------------------------------------------------------
  271. sub start
  272. {
  273.     my ( $self ) = @_;
  274.  
  275.     # In pre v0.21.0 POPFile the UI password was stored in plaintext in the configuration
  276.     # data.  Check to see if the password is not a hash and upgrade it automatically here.
  277.  
  278.     if ( length( $self->config_( 'password' ) ) != 32 ) {
  279.         $self->config_( 'password', md5_hex( '__popfile__' . $self->config_( 'password' ) ) );
  280.     }
  281.  
  282.     # Ensure that the messages subdirectory exists
  283.  
  284.     if ( !$self->make_directory__( $self->get_user_path_( $self->global_config_( 'msgdir' ) ) ) ) {
  285.         print STDERR "Failed to create the messages subdirectory\n";
  286.         return 0;
  287.     }
  288.  
  289.     # Load the current configuration from disk and then load up the
  290.     # appropriate language, note that we always load English first
  291.     # so that any extensions to the user interface that have not yet
  292.     # been translated will still appear
  293.  
  294.     load_language( $self, 'English' );
  295.     load_language( $self, $self->config_( 'language' ) ) if ( $self->config_( 'language' ) ne 'English' );
  296.  
  297.     # We need to force a history cache reload, note that this needs
  298.     # to come after loading the language since we might need History_NoFrom
  299.     # or History_NoSubject in while loading the cache
  300.  
  301.     $self->load_disk_cache__();
  302.     $self->load_history_cache__();
  303.  
  304.     # Set the classifier option wmformat__ according to our wordtable_format
  305.     # option.
  306.  
  307.     $self->{classifier__}->wmformat( $self->config_( 'wordtable_format' ) );
  308.  
  309.     return $self->SUPER::start();
  310. }
  311.  
  312. # ---------------------------------------------------------------------------------------------
  313. #
  314. # start
  315. #
  316. # Called to stop the HTML interface running
  317. #
  318. # ---------------------------------------------------------------------------------------------
  319. sub stop
  320. {
  321.     my ( $self ) = @_;
  322.  
  323.     $self->copy_pre_cache__();
  324.     $self->save_disk_cache__();
  325.  
  326.     if ( $self->{api_session__} ne '' ) {
  327.         $self->{classifier__}->release_session_key( $self->{api_session__} );
  328.     }
  329.  
  330.     $self->SUPER::stop();
  331. }
  332.  
  333. # ---------------------------------------------------------------------------------------------
  334. #
  335. # deliver
  336. #
  337. # Called by the message queue to deliver a message
  338. #
  339. # There is no return value from this method
  340. #
  341. # ---------------------------------------------------------------------------------------------
  342. sub deliver
  343. {
  344.     my ( $self, $type, $message, $parameter ) = @_;
  345.  
  346.     # Handle registration of UI components
  347.  
  348.     if ( $type eq 'UIREG' ) {
  349.         $message =~ /(.*):(.*)/;
  350.  
  351.         $self->register_configuration_item__( $1, $2, $parameter );
  352.     }
  353.  
  354.     # Get the new file in the history
  355.  
  356.     if ( $type eq 'NEWFL' ) {
  357.         $self->log_( "Got NEWFL for $message" );
  358.         $self->new_history_file__( $message );
  359.     }
  360.  
  361.     # If a day has passed then clean up the history
  362.  
  363.     if ( $type eq 'TICKD' ) {
  364.         $self->remove_mail_files();
  365.     }
  366.  
  367.     # We keep track of the last username to login to show on the UI
  368.  
  369.     if ( $type eq 'LOGIN' ) {
  370.         $self->{last_login__} = $message;
  371.     }
  372. }
  373.  
  374. # ---------------------------------------------------------------------------------------------
  375. #
  376. # url_handler__ - Handle a URL request
  377. #
  378. # $client     The web browser to send the results to
  379. # $url        URL to process
  380. # $command    The HTTP command used (GET or POST)
  381. # $content    Any non-header data in the HTTP command
  382. #
  383. # Checks the session
  384. # key and refuses access unless it matches.  Serves up a small set of specific urls that are
  385. # the main UI pages and then any GIF file in the POPFile directory and CSS files in the skins
  386. # subdirectory
  387. #
  388. # ---------------------------------------------------------------------------------------------
  389. sub url_handler__
  390. {
  391.     my ( $self, $client, $url, $command, $content ) = @_;
  392.  
  393.     # Check to see if we obtained the session key yet
  394.     if ( $self->{api_session__} eq '' ) {
  395.         $self->{api_session__} = $self->{classifier__}->get_session_key( 'admin', '' );
  396.     }
  397.  
  398.     # See if there are any form parameters and if there are parse them into the %form hash
  399.  
  400.     delete $self->{form_};
  401.  
  402.     # Remove a # element
  403.  
  404.     $url =~ s/#.*//;
  405.  
  406.     # If the URL was passed in through a GET then it may contain form arguments
  407.     # separated by & signs, which we parse out into the $self->{form_} where the
  408.     # key is the argument name and the value the argument value, for example if
  409.     # you have foo=bar in the URL then $self->{form_}{foo} is bar.
  410.  
  411.     if ( $command =~ /GET/i ) {
  412.         if ( $url =~ s/\?(.*)// )  {
  413.             $self->parse_form_( $1 );
  414.         }
  415.     }
  416.  
  417.     # If the URL was passed in through a POST then look for the POST data
  418.     # and parse it filling the $self->{form_} in the same way as for GET
  419.     # arguments
  420.  
  421.     if ( $command =~ /POST/i ) {
  422.         $content =~ s/[\r\n]//g;
  423.         $self->parse_form_( $content );
  424.     }
  425.  
  426.     if ( $url =~ /\/(.+\.gif)/ ) {
  427.         $self->http_file_( $client, $self->get_root_path_( $1 ), 'image/gif' );
  428.         return 1;
  429.     }
  430.  
  431.     if ( $url =~ /\/(.+\.png)/ ) {
  432.         $self->http_file_( $client, $self->get_root_path_( $1 ), 'image/png' );
  433.         return 1;
  434.     }
  435.  
  436.     if ( $url =~ /\/(.+\.ico)/ ) {
  437.         $self->http_file_( $client, $self->get_root_path_( $1 ), 'image/x-icon' );
  438.         return 1;
  439.     }
  440.  
  441.     if ( $url =~ /(skins\/.+\.css)/ ) {
  442.         $self->http_file_( $client, $self->get_root_path_( $1 ), 'text/css' );
  443.         return 1;
  444.     }
  445.  
  446.     if ( $url =~ /(manual\/.+\.html)/ ) {
  447.         $self->http_file_( $client, $self->get_root_path_( $1 ), 'text/html' );
  448.         return 1;
  449.     }
  450.  
  451.     # Check the password
  452.  
  453.     if ( $url eq '/password' )  {
  454.         if ( md5_hex( '__popfile__' . $self->{form_}{password} ) eq $self->config_( 'password' ) )  {
  455.             change_session_key( $self );
  456.             delete $self->{form_}{password};
  457.             $self->{form_}{session} = $self->{session_key__};
  458.             if ( defined( $self->{form_}{redirect} ) ) {
  459.                 $url = $self->{form_}{redirect};
  460.                 if ( $url =~ s/\?(.*)// )  {
  461.                     $self->parse_form_( $1 );
  462.                 }
  463.             }
  464.         } else {
  465.             $self->password_page( $client, 1, '/' );
  466.             return 1;
  467.         }
  468.     }
  469.  
  470.     # If there's a password defined then check to see if the user already knows the
  471.     # session key, if they don't then drop to the password screen
  472.  
  473.     if ( ( (!defined($self->{form_}{session})) || ($self->{form_}{session} eq '' ) || ( $self->{form_}{session} ne $self->{session_key__} ) ) && ( $self->config_( 'password' ) ne md5_hex( '__popfile__' ) ) ) {
  474.  
  475.         # Since the URL that has caused us to hit the password page might have information stored in the
  476.         # form hash we need to extract it out (except for the session key) and package it up so that
  477.         # the password page can redirect to the right place if the correct password is entered. This
  478.         # is especially important for the XPL functionality.
  479.  
  480.         my $redirect_url = $url . '?';
  481.  
  482.         foreach my $k (keys %{$self->{form_}}) {
  483.  
  484.             # Skip the session key since we are in the process of
  485.             # assigning a new one through the password page
  486.  
  487.             if ( $k ne 'session' ) {
  488.  
  489.                 # If we are dealing with an array of values (see parse_form_
  490.                 # for details) then we need to unpack it into separate entries),
  491.  
  492.                 if ( $k =~ /^(.+)_array$/ ) {
  493.                     my $field = $1;
  494.  
  495.                     foreach my $v (@{$self->{form_}{$k}}) {
  496.                         $redirect_url .= "$field=$v&"
  497.                     }
  498.                 } else {
  499.                     $redirect_url .= "$k=$self->{form_}{$k}&"
  500.                 }
  501.             }
  502.         }
  503.  
  504.         $redirect_url =~ s/&$//;
  505.  
  506.         $self->password_page( $client, 0, $redirect_url );
  507.  
  508.         return 1;
  509.     }
  510.  
  511.     if ( $url eq '/jump_to_message' )  {
  512.         my $found = 0;
  513.         my $file = $self->{form_}{view};
  514.  
  515.         $self->copy_pre_cache__();
  516.  
  517.         foreach my $akey ( keys %{ $self->{history__} } ) {
  518.             if ($akey eq $file) {
  519.                 $found = 1;
  520.                 last;
  521.             }
  522.         }
  523.  
  524.         # Reset any filters
  525.  
  526.         $self->{form_}{filter}    = '';
  527.         $self->{form_}{search}    = '';
  528.         $self->{form_}{setsearch} = 1;
  529.  
  530.         if ( $found ) {
  531.             $self->http_redirect_( $client, "/view?session=$self->{session_key__}&view=$self->{form_}{view}" );
  532.         } else {
  533.             $self->http_redirect_( $client, "/history" );
  534.         }
  535.  
  536.         return 1;
  537.     }
  538.  
  539.     if ( $url =~ /(popfile.*\.log)/ ) {
  540.         $self->http_file_( $client, $self->logger()->debug_filename(), 'text/plain' );
  541.         return 1;
  542.     }
  543.  
  544.     if ( ( defined($self->{form_}{session}) ) && ( $self->{form_}{session} ne $self->{session_key__} ) ) {
  545.         $self->session_page( $client, 0, $url );
  546.         return 1;
  547.     }
  548.  
  549.     if ( ( $url eq '/' ) || (!defined($self->{form_}{session})) ) {
  550.         delete $self->{form_};
  551.     }
  552.  
  553.     if ( $url eq '/shutdown' )  {
  554.         $self->http_ok( $client, "POPFile shutdown", -1 );
  555.         return 0;
  556.     }
  557.  
  558.     my %url_table = ( '/security'      => \&security_page,       # PROFILE BLOCK START
  559.                       '/configuration' => \&configuration_page,
  560.                       '/buckets'       => \&corpus_page,
  561.                       '/magnets'       => \&magnet_page,
  562.                       '/advanced'      => \&advanced_page,
  563.                       '/history'       => \&history_page,
  564.                       '/view'          => \&view_page,
  565.                       '/'              => \&history_page );      # PROFILE BLOCK STOP
  566.  
  567.     # Any of the standard pages can be found in the url_table, the other pages are probably
  568.     # files on disk
  569.  
  570.     if ( defined($url_table{$url}) )  {
  571.         if ( !defined( $self->{api_session__} ) ) {
  572.             $self->http_error_( $client, 500 );
  573.             return;
  574.         }
  575.  
  576.         &{$url_table{$url}}($self, $client);
  577.         return 1;
  578.     }
  579.  
  580.     $self->http_error_( $client, 404 );
  581.     return 1;
  582. }
  583.  
  584. # ---------------------------------------------------------------------------------------------
  585. #
  586. # http_ok - Output a standard HTTP 200 message with a body of data
  587. #
  588. # $client    The web browser to send result to
  589. # $text      The body of the page
  590. # $selected  Which tab is to be selected
  591. #
  592. # Returns an HTTP 200 message with a body of data passed in $text wrapping it with the standard
  593. # header and footer.  The header is updated with the appropriate tab selected, and the
  594. # various elements in the footer are updated.  This function also checks whether POPFile is
  595. # up to date and if it is not it inserts the appropriate image to tell the user to update.
  596. #
  597. # ---------------------------------------------------------------------------------------------
  598. sub http_ok
  599. {
  600.     my ( $self, $client, $text, $selected ) = @_;
  601.  
  602.     my @tab = ( 'menuStandard', 'menuStandard', 'menuStandard', 'menuStandard', 'menuStandard', 'menuStandard' );
  603.     $tab[$selected] = 'menuSelected' if ( ( $selected <= $#tab ) && ( $selected >= 0 ) );
  604.     my $update_check = '';
  605.  
  606.     # Check to see if we've checked for updates today.  If we have not then insert a reference to an image
  607.     # that is generated through a CGI on UseTheSource.  Also send stats to the same site if that is allowed
  608.  
  609.     if ( $self->{today} ne $self->config_( 'last_update_check' ) ) {
  610.         calculate_today( $self );
  611.  
  612.         if ( $self->config_( 'update_check' ) ) {
  613.             $update_check = "<a href=\"http://sourceforge.net/project/showfiles.php?group_id=63137\">\n";
  614.             my ( $major_version, $minor_version, $build_version ) = $self->version() =~ /^v([^.]*)\.([^.]*)\.(.*)$/;
  615.             $update_check .= "<img border=\"0\" alt=\"\" src=\"http://www.usethesource.com/cgi-bin/popfile_update.pl?ma=" . $major_version . "&mi=" . $minor_version . "&bu=" . $build_version . "\" />\n</a>\n";
  616.         }
  617.  
  618.         if ( $self->config_( 'send_stats' ) ) {
  619.             my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  620.             my $bc      = $#buckets + 1;
  621.             $update_check .= "<img border=\"0\" alt=\"\" src=\"http://www.usethesource.com/cgi-bin/popfile_stats.pl?bc=$bc&mc=" . $self->mcount__() . "&ec=" . $self->ecount__() . "\" />\n";
  622.         }
  623.  
  624.         $self->config_( 'last_update_check', $self->{today}, 1 );
  625.     }
  626.  
  627.     # Build the full page of HTML by preprending the standard header and append the standard
  628.     # footer
  629.     $text =  html_common_top($self, $selected) . html_common_middle($self, $text, $update_check, @tab)  # PROFILE BLOCK START
  630.         . html_common_bottom($self);                                                                    # PROFILE BLOCK STOP
  631.  
  632.     # Build an HTTP header for standard HTML
  633.     my $http_header = "HTTP/1.1 200 OK\r\n";
  634.     $http_header .= "Connection: close\r\n";
  635.     $http_header .= "Pragma: no-cache\r\n";
  636.     $http_header .= "Expires: 0\r\n";
  637.     $http_header .= "Cache-Control: no-cache\r\n";
  638.     $http_header .= "Content-Type: text/html";
  639.     $http_header .= "; charset=$self->{language__}{LanguageCharset}\r\n";
  640.     $http_header .= "Content-Length: ";
  641.     $http_header .= length($text);
  642.     $http_header .= "$eol$eol";
  643.  
  644.     print $client $http_header . $text;
  645. }
  646.  
  647. # ---------------------------------------------------------------------------------------------
  648. #
  649. # html_common_top - Creates a string containing the standard header for each POPFile page
  650. #                   as HTML.   This is the title portion of the page and the META tags that
  651. #                   inform the browser of various information including the style sheet that
  652. #                   is used for the current skin.
  653. #
  654. # $selected  Which tab is to be selected
  655. #
  656. # Returns a string of HTML
  657. #
  658. # ---------------------------------------------------------------------------------------------
  659.  
  660. sub html_common_top
  661. {
  662.     my ($self, $selected) = @_;
  663.  
  664.     # The returned string contains the HEAD portion of an HTML page with the title, a link
  665.     # to the skin CSS file and information about caching (we do not want to be cached as
  666.     # every page is dynamically generated) and a Content-Type header that this is HTML
  667.  
  668.     my $result = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" ";
  669.     $result .= "\"http://www.w3.org/TR/html4/loose.dtd\">\n";
  670.     $result .= "<html lang=\"$self->{language__}{LanguageCode}\">\n<head>\n<title>$self->{language__}{Header_Title}</title>\n";
  671.  
  672.     $result .= "<link rel=\"icon\" href=\"favicon.ico\">\n";
  673.  
  674.     # If we are handling the shutdown page, then send the CSS along with the
  675.     # page to avoid a request back from the browser _after_ we've shutdown,
  676.     # otherwise, send the link to the CSS file so it is cached by the browser.
  677.  
  678.     if ( $selected == -1 ) {
  679.         $result .= "<style type=\"text/css\">\n";
  680.         if ( open FILE, '<' . $self->get_root_path_( 'skins/' . $self->config_( 'skin' ) . '.css' ) ) {
  681.             while (<FILE>) {
  682.                 $result .= $_;
  683.             }
  684.             close FILE;
  685.         }
  686.         $result .= "</style>\n";
  687.     } else {
  688.         $result .= "<link rel=\"stylesheet\" type=\"text/css\" ";
  689.         $result .= "href=\"skins/" . $self->config_( 'skin' ) . ".css\" title=\"" . $self->config_( 'skin' ) . "\">\n";
  690.     }
  691.  
  692.     $result .= "</head>\n";
  693.  
  694.     return $result;
  695. }
  696.  
  697. # ---------------------------------------------------------------------------------------------
  698. #
  699. # html_common_middle - Called from http_ok to build the common middle part of an html page
  700. #                      that consists of the title at the top of the page and the tabs for
  701. #                       selecting parts of the program
  702. #
  703. # $text      The body of the page
  704. # $update_check      Contains html for updating, as required
  705. # @tab      Array of interface tabs -- one of which is selected
  706. #
  707. # Returns a string of html
  708. #
  709. # ---------------------------------------------------------------------------------------------
  710.  
  711. sub html_common_middle
  712. {
  713.     my ($self, $text, $update_check, @tab) = @_;
  714.  
  715.     # The returned string consists of the BODY portion of the page with the header
  716.     # tabs and the passed in $text.  Note that the BODY is not closed as the standard
  717.     # footer created by html_common_bottom takes care of that.
  718.  
  719.     my $result = "<body dir=\"$self->{language__}{LanguageDirection}\">\n<table class=\"shellTop\" align=\"center\" width=\"100%\" summary=\"\">\n";
  720.  
  721.     # upper whitespace
  722.     $result .= "<tr class=\"shellTopRow\">\n<td class=\"shellTopLeft\"></td>\n<td class=\"shellTopCenter\"></td>\n";
  723.     $result .= "<td class=\"shellTopRight\"></td>\n</tr>\n";
  724.  
  725.     # logo
  726.     $result .= "<tr>\n<td class=\"shellLeft\"></td>\n";
  727.     $result .= "<td class=\"naked\">\n";
  728.     $result .= "<table class=\"head\" cellspacing=\"0\" summary=\"\">\n<tr>\n";
  729.     $result .= "<td class=\"head\">$self->{language__}{Header_Title}</td>\n";
  730.  
  731.     # shutdown
  732.     $result .= "<td align=\"right\" valign=\"bottom\">\n";
  733.     $result .= "<a class=\"shutdownLink\" href=\"/shutdown\">$self->{language__}{Header_Shutdown}</a> \n";
  734.  
  735.     $result .= "</td>\n</tr>\n<tr>\n";
  736.     $result .= "<td height=\"1%\" colspan=\"3\"></td>\n</tr>\n";
  737.     $result .= "</table>\n</td>\n"; # colspan 2 ?? srk
  738.     $result .= "<td class=\"shellRight\"></td>\n</tr>\n<tr class=\"shellBottomRow\">\n";
  739.  
  740.     $result .= "<td class=\"shellBottomLeft\"></td>\n<td class=\"shellBottomCenter\"></td>\n";
  741.     $result .= "<td class=\"shellBottomRight\"></td>\n</tr>\n</table>\n";
  742.  
  743.     # update check
  744.     if ( $update_check ne '' ) {
  745.         $result .= "<table align=\"center\" summary=\"\">\n<tr>\n<td class=\"logo2menuSpace\">$update_check</td></tr></table>\n";
  746.     } else {
  747.         $result .= "<p>";
  748.     }
  749.  
  750.     # menu start
  751.     $result .= "<table class=\"menu\" cellspacing=\"0\" summary=\"$self->{language__}{Header_MenuSummary}\">\n";
  752.     $result .= "<tr>\n";
  753.  
  754.     # blank menu item for indentation
  755.     $result .= "<td class=\"menuIndent\"> </td>";
  756.  
  757.     # History menu item
  758.     $result .= "<td class=\"$tab[2]\" align=\"center\">\n";
  759.     $result .= "<a class=\"menuLink\" href=\"/history?session=$self->{session_key__}\">";
  760.     $result .= "\n$self->{language__}{Header_History}</a>\n";
  761.     $result .= "</td>\n<td class=\"menuSpacer\"></td>\n";
  762.  
  763.     # Buckets menu item
  764.     $result .= "<td class=\"$tab[1]\" align=\"center\">\n";
  765.     $result .= "<a class=\"menuLink\" href=\"/buckets?session=$self->{session_key__}\">";
  766.     $result .= "\n$self->{language__}{Header_Buckets}</a>\n";
  767.     $result .= "</td>\n<td class=\"menuSpacer\"></td>\n";
  768.  
  769.     # Magnets menu item
  770.     $result .= "<td class=\"$tab[4]\" align=\"center\">\n";
  771.     $result .= "<a class=\"menuLink\" href=\"/magnets?session=$self->{session_key__}&start_magnet=0\">";
  772.     $result .= "\n$self->{language__}{Header_Magnets}</a>\n";
  773.     $result .= "</td>\n<td class=\"menuSpacer\"></td>\n";
  774.  
  775.     # Configuration menu item
  776.     $result .= "<td class=\"$tab[0]\" align=\"center\">\n";
  777.     $result .= "<a class=\"menuLink\" href=\"/configuration?session=$self->{session_key__}\">";
  778.     $result .= "\n$self->{language__}{Header_Configuration}</a>\n";
  779.     $result .= "</td>\n<td class=\"menuSpacer\"></td>\n";
  780.  
  781.     # Security menu item
  782.     $result .= "<td class=\"$tab[3]\" align=\"center\">\n";
  783.     $result .= "<a class=\"menuLink\" href=\"/security?session=$self->{session_key__}\">";
  784.     $result .= "\n$self->{language__}{Header_Security}</a>\n";
  785.     $result .= "</td>\n<td class=\"menuSpacer\"></td>\n";
  786.  
  787.     # Advanced menu item
  788.     $result .= "<td class=\"$tab[5]\" align=\"center\">\n";
  789.     $result .= "<a class=\"menuLink\" href=\"/advanced?session=$self->{session_key__}\">";
  790.     $result .= "\n$self->{language__}{Header_Advanced}</a>\n";
  791.     $result .= "</td>\n";
  792.  
  793.     # blank menu item for indentation
  794.     $result .= "<td class=\"menuIndent\"> </td>";
  795.  
  796.     # finish up the menu
  797.     $result .= "</tr>\n</table>\n";
  798.  
  799.     # main content area
  800.     $result .= "<table class=\"shell\" align=\"center\" width=\"100%\" summary=\"\">\n<tr class=\"shellTopRow\">\n";
  801.     $result .= "<td class=\"shellTopLeft\"></td>\n<td class=\"shellTopCenter\"></td>\n";
  802.     $result .= "<td class=\"shellTopRight\"></td>\n</tr>\n<tr>\n";
  803.     $result .= "<td class=\"shellLeft\"></td>\n";
  804.     $result .= "<td class=\"naked\">\n" . $text . "\n</td>\n";
  805.  
  806.     $result .= "<td class=\"shellRight\"></td>\n</tr>\n";
  807.     $result .= "<tr class=\"shellBottomRow\">\n<td class=\"shellBottomLeft\"></td>\n";
  808.     $result .= "<td class=\"shellBottomCenter\"></td>\n<td class=\"shellBottomRight\"></td>\n";
  809.     $result .= "</tr>\n</table>\n";
  810.  
  811.     return $result;
  812. }
  813.  
  814. # ---------------------------------------------------------------------------------------------
  815. #
  816. # html_common_bottom - Called from http_ok to build the common bottom part of an html page
  817. #
  818. # Returns a string of html
  819. #
  820. # ---------------------------------------------------------------------------------------------
  821.  
  822. sub html_common_bottom
  823. {
  824.     my ($self) = @_;
  825.  
  826.     my $time = localtime;
  827.  
  828.     # The returned string has the standard footer that appears on every HTML page in
  829.     # POPFile with links to the POPFile home page and other information and closes
  830.     # both the BODY and the complete page
  831.  
  832.     my $result = "<table class=\"footer\" summary=\"\">\n<tr>\n";
  833.     $result .= "<td class=\"footerBody\">";
  834.     $result .= "<a class=\"bottomLink\" href=\"http://popfile.sourceforge.net/\">$self->{language__}{Footer_HomePage}</a><br>\n";
  835.     $result .= "<a class=\"bottomLink\" href=\"";
  836.  
  837.     # To save space on the download of POPFile only the English language manual
  838.     # is shipped and available locally, all other languages are referenced through
  839.     # the POPFile home page on SourceForge
  840.  
  841.     if ( $self->{language__}{ManualLanguage} eq 'en' ) {
  842.         $result .= 'manual/en';
  843.     } else {
  844.         $result .= "http://popfile.sourceforge.net/manual/$self->{language__}{ManualLanguage}";
  845.     }
  846.  
  847.     $result .= "/manual.html\">\n";
  848.     $result .= "$self->{language__}{Footer_Manual}</a><br>\n";
  849.  
  850.     my $faq_prefix = ( $self->config_( 'language' ) eq 'Nihongo' )?'JP_':'';
  851.  
  852.     $result .= "<a class=\"bottomLink\" href=\"http://popfile.sourceforge.net/cgi-bin/wiki.pl?$faq_prefix" . "FrequentlyAskedQuestions\">$self->{language__}{FAQ}</a><br>\n";
  853.  
  854.     $result .= "</td><td class=\"footerBody\">\n<a class=\"bottomLink\" href=\"http://popfile.sourceforge.net/\"><img src=\"otto.gif\" border=\"0\" alt=\"\"></a><br>$self->{version_}<br>($time - $self->{last_login__})</td>\n";
  855.  
  856.     $result .= "<td class=\"footerBody\"><a class=\"bottomLink\" href=\"http://sourceforge.net/tracker/index.php?group_id=63137&atid=502959\">$self->{language__}{Footer_RequestFeature}</a><br>\n";
  857.     $result .= "<a class=\"bottomLink\" href=\"http://lists.sourceforge.net/lists/listinfo/popfile-announce\">$self->{language__}{Footer_MailingList}</a><br>\n";
  858.     $result .= "<a class=\"bottomLink\" href=\"http://sourceforge.net/forum/forum.php?forum_id=213876\">$self->{language__}{Footer_FeedMe}</a>\n";
  859.  
  860.     $result .= "</td>\n</tr>\n</table>\n</body>\n</html>\n";
  861.  
  862.     return $result;
  863. }
  864.  
  865. # ---------------------------------------------------------------------------------------------
  866. #
  867. # configuration_page - get the configuration options
  868. #
  869. # $client     The web browser to send the results to
  870. #
  871. # ---------------------------------------------------------------------------------------------
  872. sub configuration_page
  873. {
  874.     my ( $self, $client ) = @_;
  875.  
  876.     my $body;
  877.     my $port_error = '';
  878.     my $ui_port_error = '';
  879.     my $page_size_error = '';
  880.     my $history_days_error = '';
  881.     my $timeout_error = '';
  882.     my $separator_error = '';
  883.  
  884.     $self->config_( 'skin', $self->{form_}{skin} )      if ( defined($self->{form_}{skin}) );
  885.     $self->global_config_( 'debug', $self->{form_}{debug}-1 )   if ( ( defined($self->{form_}{debug}) ) && ( ( $self->{form_}{debug} >= 1 ) && ( $self->{form_}{debug} <= 4 ) ) );
  886.  
  887.     for my $name (keys %{$self->{dynamic_ui__}{configuration}}) {
  888.         $body .= $self->{dynamic_ui__}{configuration}{$name}->validate_item( $name,
  889.                                                                              \%{$self->{language__}},
  890.                                                                              \%{$self->{form_}} );
  891.     }
  892.  
  893.     if ( defined($self->{form_}{language}) ) {
  894.         if ( $self->config_( 'language' ) ne $self->{form_}{language} ) {
  895.             $self->config_( 'language', $self->{form_}{language} );
  896.             load_language( $self,  $self->config_( 'language' ) );
  897.         }
  898.     }
  899.  
  900.     if ( defined($self->{form_}{ui_port}) ) {
  901.         if ( ( $self->{form_}{ui_port} >= 1 ) && ( $self->{form_}{ui_port} < 65536 ) ) {
  902.             $self->config_( 'port', $self->{form_}{ui_port} );
  903.         } else {
  904.             $ui_port_error = "<blockquote>\n<div class=\"error01\">\n";
  905.             $ui_port_error .= "$self->{language__}{Configuration_Error2}</div>\n</blockquote>\n";
  906.             delete $self->{form_}{ui_port};
  907.         }
  908.     }
  909.  
  910.     if ( defined($self->{form_}{page_size}) ) {
  911.         if ( ( $self->{form_}{page_size} >= 1 ) && ( $self->{form_}{page_size} <= 1000 ) ) {
  912.             $self->config_( 'page_size', $self->{form_}{page_size} );
  913.         } else {
  914.             $page_size_error = "<blockquote><div class=\"error01\">$self->{language__}{Configuration_Error4}</div></blockquote>";
  915.             delete $self->{form_}{page_size};
  916.         }
  917.     }
  918.  
  919.     if ( defined($self->{form_}{history_days}) ) {
  920.         if ( ( $self->{form_}{history_days} >= 1 ) && ( $self->{form_}{history_days} <= 366 ) ) {
  921.             $self->config_( 'history_days', $self->{form_}{history_days} );
  922.         } else {
  923.             $history_days_error = "<blockquote><div class=\"error01\">$self->{language__}{Configuration_Error5}</div></blockquote>";
  924.             delete $self->{form_}{history_days};
  925.         }
  926.     }
  927.  
  928.     if ( defined($self->{form_}{timeout}) ) {
  929.         if ( ( $self->{form_}{timeout} >= 10 ) && ( $self->{form_}{timeout} <= 300 ) ) {
  930.             $self->global_config_( 'timeout', $self->{form_}{timeout} );
  931.         } else {
  932.             $timeout_error = "<blockquote><div class=\"error01\">$self->{language__}{Configuration_Error6}</div></blockquote>";
  933.             $self->{form_}{update_timeout} = '';
  934.         }
  935.     }
  936.  
  937.     # User Interface panel
  938.     $body .= "<table class=\"settingsTable\" width=\"100%\" cellpadding=\"10%\" cellspacing=\"0\" summary=\"$self->{language__}{Configuration_MainTableSummary}\">\n";
  939.     $body .= "<tr>\n<td class=\"settingsPanel\" width=\"33%\" valign=\"top\">\n";
  940.     $body .= "<h2 class=\"configuration\">$self->{language__}{Configuration_UserInterface}</h2>\n";
  941.     $body .= "<form action=\"/configuration\">\n";
  942.     $body .= "<label class=\"configurationLabel\" for=\"configSkin\">$self->{language__}{Configuration_SkinsChoose}:</label><br />\n";
  943.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  944.     $body .= "<select name=\"skin\" id=\"configSkin\">\n";
  945.  
  946.     # Create three groupings for skins
  947.  
  948.     # Normal skins
  949.     $body .= "<optgroup label=\"$self->{language__}{Configuration_GeneralSkins}\">\n";
  950.     for my $i (0..$#{$self->{skins__}}) {
  951.         if ( !( $self->{skins__}[$i] =~ /^(small|tiny)/i  ) ) {
  952.             $body .= "<option value=\"$self->{skins__}[$i]\"";
  953.             $body .= " selected=\"selected\"" if ( $self->{skins__}[$i] eq $self->config_( 'skin' ) );
  954.             $body .= ">$self->{skins__}[$i]</option>\n";
  955.         }
  956.     }
  957.     $body .= "</optgroup>\n";
  958.  
  959.     # Small skins
  960.     $body .= "<optgroup label=\"$self->{language__}{Configuration_SmallSkins}\">\n";
  961.     for my $i (0..$#{$self->{skins__}}) {
  962.         if ( $self->{skins__}[$i] =~ /^small/i  ) {
  963.             $body .= "<option value=\"$self->{skins__}[$i]\"";
  964.             $body .= " selected=\"selected\"" if ( $self->{skins__}[$i] eq $self->config_( 'skin' ) );
  965.             $body .= ">$self->{skins__}[$i]</option>\n";
  966.         }
  967.     }
  968.     $body .= "</optgroup>\n";
  969.  
  970.     # Tiny skins
  971.     $body .= "<optgroup label=\"$self->{language__}{Configuration_TinySkins}\">\n";
  972.     for my $i (0..$#{$self->{skins__}}) {
  973.         if ( $self->{skins__}[$i] =~ /^tiny/i  ) {
  974.             $body .= "<option value=\"$self->{skins__}[$i]\"";
  975.             $body .= " selected=\"selected\"" if ( $self->{skins__}[$i] eq $self->config_( 'skin' ) );
  976.             $body .= ">$self->{skins__}[$i]</option>\n";
  977.         }
  978.     }
  979.     $body .= "</optgroup>\n";
  980.  
  981.     $body .= "</select>\n<input type=\"submit\" class=\"submit\" name=\"change_skin\" value=\"$self->{language__}{Apply}\" />\n";
  982.     $body .= "</form>\n";
  983.  
  984.     # Choose Language widget
  985.     $body .= "<form action=\"/configuration\">\n";
  986.     $body .= "<label class=\"configurationLabel\" for=\"configLanguage\">$self->{language__}{Configuration_LanguageChoose}:</label><br />\n";
  987.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  988.     $body .= "<select name=\"language\" id=\"configLanguage\">\n";
  989.     for my $i (0..$#{$self->{languages__}}) {
  990.         $body .= "<option value=\"$self->{languages__}[$i]\"";
  991.         $body .= " selected=\"selected\"" if ( $self->{languages__}[$i] eq $self->config_( 'language' ) );
  992.         $body .= ">$self->{languages__}[$i]</option>\n";
  993.     }
  994.     $body .= "</select>\n";
  995.     $body .= "<input type=\"submit\" class=\"submit\" name=\"change_language\" value=\"$self->{language__}{Apply}\" />\n";
  996.     $body .= "</form>\n</td>\n";
  997.  
  998.     # History View panel
  999.  
  1000.     $body .= "<td class=\"settingsPanel\" width=\"33%\" valign=\"top\">\n";
  1001.     $body .= "<h2 class=\"configuration\">$self->{language__}{Configuration_HistoryView}</h2>\n";
  1002.  
  1003.     # Emails per Page widget
  1004.  
  1005.     $body .= "<form action=\"/configuration\">\n";
  1006.     $body .= "<label class=\"configurationLabel\" for=\"configPageSize\">$self->{language__}{Configuration_History}:</label><br />\n";
  1007.     $body .= "<input name=\"page_size\" id=\"configPageSize\" type=\"text\" value=\"" . $self->config_( 'page_size' ) . "\" />\n";
  1008.     $body .= "<input type=\"submit\" class=\"submit\" name=\"update_page_size\" value=\"$self->{language__}{Apply}\" />\n";
  1009.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n$page_size_error\n";
  1010.     $body .= sprintf( $self->{language__}{Configuration_HistoryUpdate}, $self->config_( 'page_size' ) ) if ( defined($self->{form_}{page_size}) );
  1011.  
  1012.     # Days of History to Keep widget
  1013.  
  1014.     $body .= "\n<form action=\"/configuration\">\n";
  1015.     $body .= "<label class=\"configurationLabel\" for=\"configHistoryDays\">$self->{language__}{Configuration_Days}:</label> <br />\n";
  1016.     $body .= "<input name=\"history_days\" id=\"configHistoryDays\" type=\"text\" value=\"" . $self->config_( 'history_days' ) . "\" />\n";
  1017.     $body .= "<input type=\"submit\" class=\"submit\" name=\"update_history_days\" value=\"$self->{language__}{Apply}\" />\n";
  1018.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  1019.     $body .= "</form>\n$history_days_error\n";
  1020.     $body .= sprintf( $self->{language__}{Configuration_DaysUpdate}, $self->config_( 'history_days' ) ) if ( defined($self->{form_}{history_days}) );
  1021.  
  1022.     # Listen Ports panel
  1023.  
  1024.     $body .= "<td class=\"settingsPanel\" width=\"33%\" valign=\"top\" rowspan=\"2\">\n";
  1025.     $body .= "<h2 class=\"configuration\">$self->{language__}{Configuration_ListenPorts}</h2>\n";
  1026.  
  1027.     # User Interface Port widget
  1028.  
  1029.     $body .= "\n<form action=\"/configuration\">\n";
  1030.     $body .= "<label class=\"configurationLabel\" for=\"configUIPort\">$self->{language__}{Configuration_UI}:</label><br />\n";
  1031.     $body .= "<input name=\"ui_port\" id=\"configUIPort\" type=\"text\" value=\"" . $self->config_( 'port' ) . "\" />\n";
  1032.     $body .= "<input type=\"submit\" class=\"submit\" name=\"update_ui_port\" value=\"$self->{language__}{Apply}\" />\n";
  1033.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n$ui_port_error";
  1034.     $body .= sprintf( $self->{language__}{Configuration_UIUpdate}, $self->config_( 'port' ) ) if ( defined($self->{form_}{ui_port}) );
  1035.     $body .= "\n";
  1036.  
  1037.     # Insert all the items that are dynamically created from the modules that are loaded
  1038.  
  1039.     my $last_module = '';
  1040.     for my $name (sort keys %{$self->{dynamic_ui__}{configuration}}) {
  1041.         $name =~ /^([^_]+)_/;
  1042.         my $module = $1;
  1043.         if ( $last_module ne $module ) {
  1044.             $last_module = $module;
  1045.             $body .= "<hr>\n<h2 class=\"configuration\">";
  1046.             $body .= uc($module);
  1047.             $body .= "</h2>\n";
  1048.     }
  1049.         $body .= $self->{dynamic_ui__}{configuration}{$name}->configure_item( $name,                    # PROFILE BLOCK START
  1050.                                                                               \%{$self->{language__}},
  1051.                                                                               $self->{session_key__} ); # PROFILE BLOCK STOP
  1052.     }
  1053.  
  1054.     # TCP Connection Timeout panel
  1055.  
  1056.     $body .= "<tr>\n<td class=\"settingsPanel\" width=\"33%\" valign=\"top\">\n";
  1057.     $body .= "<h3 class=\"configuration\">$self->{language__}{Configuration_TCPTimeout}</h3>\n";
  1058.  
  1059.     # TCP Conn TO widget
  1060.  
  1061.     $body .= "<form action=\"/configuration\">\n";
  1062.     $body .= "<label class=\"configurationLabel\" for=\"configTCPTimeout\">$self->{language__}{Configuration_TCPTimeoutSecs}:</label><br />\n";
  1063.     $body .= "<input name=\"timeout\" type=\"text\" id=\"configTCPTimeout\" value=\"" . $self->global_config_( 'timeout' ) . "\" />\n";
  1064.     $body .= "<input type=\"submit\" class=\"submit\" name=\"update_timeout\" value=\"$self->{language__}{Apply}\" />\n";
  1065.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n$timeout_error";
  1066.     $body .= sprintf( $self->{language__}{Configuration_TCPTimeoutUpdate}, $self->global_config_( 'timeout' ) ) if ( defined($self->{form_}{timeout}) );
  1067.     $body .= "</td>\n";
  1068.  
  1069.     # Logging panel
  1070.  
  1071.     $body .= "<td class=\"settingsPanel\" width=\"33%\" valign=\"top\">\n";
  1072.     $body .= "<h2 class=\"configuration\">$self->{language__}{Configuration_Logging}</h2>\n";
  1073.     $body .= "<form action=\"/configuration\">\n";
  1074.     $body .= "<label class=\"configurationLabel\" for=\"configLogging\">$self->{language__}{Configuration_LoggerOutput}:</label>\n";
  1075.     $body .= "<input type=\"hidden\" value=\"$self->{session_key__}\" name=\"session\" />\n";
  1076.     $body .= "<select name=\"debug\" id=\"configLogging\">\n";
  1077.     $body .= "<option value=\"1\"";
  1078.     $body .= " selected=\"selected\"" if ( $self->global_config_( 'debug' ) == 0 );
  1079.     $body .= ">$self->{language__}{Configuration_None}</option>\n";
  1080.     $body .= "<option value=\"2\"";
  1081.     $body .= " selected=\"selected\"" if ( $self->global_config_( 'debug' ) == 1 );
  1082.     $body .= ">$self->{language__}{Configuration_ToFile}</option>\n";
  1083.     $body .= "<option value=\"3\"";
  1084.     $body .= " selected=\"selected\"" if ( $self->global_config_( 'debug' ) == 2 );
  1085.     $body .= ">$self->{language__}{Configuration_ToScreen}</option>\n";
  1086.     $body .= "<option value=\"4\"";
  1087.     $body .= " selected=\"selected\"" if ( $self->global_config_( 'debug' ) == 3 );
  1088.     $body .= ">$self->{language__}{Configuration_ToScreenFile}</option>\n";
  1089.     $body .= "</select>\n<input type=\"submit\" class=\"submit\" name=\"submit_debug\" value=\"$self->{language__}{Apply}\" />\n";
  1090.     $body .= "</form>\n";
  1091.  
  1092.     if ( $self->global_config_( 'debug' ) & 1 ) {
  1093.         $body .= "<p><a href=\"popfile_current_log.log?session=$self->{session_key__}\">$self->{language__}{Configuration_CurrentLogFile}</a>";
  1094.     }
  1095.  
  1096.     if ( $self->global_config_( 'debug' ) != 0 ) {
  1097.         my @log_entries = $self->last_ten_log_entries();
  1098.  
  1099.         if ( $#log_entries >= -1 ) {
  1100.             $body .= '<p><tt>';
  1101.             foreach my $line (@log_entries) {
  1102.                  $line =~ s/[\"\r\n]//g;
  1103.                  my $full_line = $line;
  1104.                  $line =~ /^(.{0,80})/;
  1105.                  $line = "$1...";
  1106.  
  1107.                  $body .= "<a title=\"$full_line\">$line</a><br>";
  1108.             }
  1109.  
  1110.             $body .= '</tt>';
  1111.         }
  1112.     }
  1113.  
  1114.     $body .= "</td>\n</tr>\n</table>\n";
  1115.  
  1116.     http_ok($self, $client,$body,0);
  1117. }
  1118.  
  1119. # ---------------------------------------------------------------------------------------------
  1120. #
  1121. # security_page - get the security configuration page
  1122. #
  1123. # $client     The web browser to send the results to
  1124. #
  1125. # ---------------------------------------------------------------------------------------------
  1126. sub security_page
  1127. {
  1128.     my ( $self, $client ) = @_;
  1129.  
  1130.     my $body;
  1131.     my $server_error = '';
  1132.     my $port_error   = '';
  1133.  
  1134.     if ( ( defined($self->{form_}{password}) ) &&
  1135.          ( $self->{form_}{password} ne $self->config_( 'password' ) ) ) {
  1136.         $self->config_( 'password', md5_hex( '__popfile__' . $self->{form_}{password} ) )
  1137.     }
  1138.     $self->config_( 'local', $self->{form_}{localui}-1 )      if ( defined($self->{form_}{localui}) );
  1139.     $self->config_( 'update_check', $self->{form_}{update_check}-1 ) if ( defined($self->{form_}{update_check}) );
  1140.     $self->config_( 'send_stats', $self->{form_}{send_stats}-1 )   if ( defined($self->{form_}{send_stats}) );
  1141.  
  1142.     for my $name (keys %{$self->{dynamic_ui__}{security}}) {
  1143.         $body .= $self->{dynamic_ui__}{security}{$name}->validate_item( $name,
  1144.                                                                              \%{$self->{language__}},
  1145.                                                                              \%{$self->{form_}} );
  1146.     }
  1147.  
  1148.     for my $name (keys %{$self->{dynamic_ui__}{chain}}) {
  1149.         $body .= $self->{dynamic_ui__}{chain}{$name}->validate_item( $name,
  1150.                                                                              \%{$self->{language__}},
  1151.                                                                              \%{$self->{form_}} );
  1152.     }
  1153.  
  1154.     $body .= "<table class=\"settingsTable\" width=\"100%\" cellpadding=\"10%\" cellspacing=\"0\" summary=\"$self->{language__}{Security_MainTableSummary}\">\n<tr>\n";
  1155.  
  1156.     # Stealth Mode / Server Operation panel
  1157.     $body .= "<td class=\"settingsPanel\" width=\"50%\" valign=\"top\">\n";
  1158.     $body .= "<h2 class=\"security\">$self->{language__}{Security_Stealth}</h2>\n";
  1159.  
  1160.     for my $name (sort keys %{$self->{dynamic_ui__}{security}}) {
  1161.         $body .= $self->{dynamic_ui__}{security}{$name}->configure_item( $name,                         # PROFILE BLOCK START
  1162.                                                                               \%{$self->{language__}},
  1163.                                                                               $self->{session_key__} ); # PROFILE BLOCK STOP
  1164.     }
  1165.  
  1166.     # Accept HTTP from Remote Machines widget
  1167.     $body .= "<span class=\"securityLabel\">$self->{language__}{Security_UI}:</span><br />\n";
  1168.  
  1169.     $body .= "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" summary=\"\"><tr><td>\n";
  1170.     if ( $self->config_( 'local' ) == 1 ) {
  1171.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1172.         $body .= "<span class=\"securityWidgetStateOff\">$self->{language__}{Security_NoStealthMode}</span>\n";
  1173.         $body .= "<input type=\"submit\" class=\"toggleOn\" id=\"securityAcceptHTTPOn\" name=\"toggle\" value=\"$self->{language__}{ChangeToYes}\" />\n";
  1174.         $body .= "<input type=\"hidden\" name=\"localui\" value=\"1\" />\n";
  1175.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1176.     } else {
  1177.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1178.         $body .= "<span class=\"securityWidgetStateOn\">$self->{language__}{Yes}</span>\n";
  1179.         $body .= "<input type=\"submit\" class=\"toggleOff\" id=\"securityAcceptHTTPOff\" name=\"toggle\" value=\"$self->{language__}{ChangeToNo} $self->{language__}{Security_StealthMode}\" />\n";
  1180.         $body .= "<input type=\"hidden\" name=\"localui\" value=\"2\" />\n";
  1181.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1182.     }
  1183.     $body .= "</td></tr></table>\n";
  1184.  
  1185.     # Secure Password Authentication/AUTH panel
  1186.     $body .= "<hr><h2 class=\"security\">$self->{language__}{Security_AUTHTitle}</h2>\n";
  1187.  
  1188.     # optional widgets placement
  1189.     $body .= "<div class=\"securityAuthWidgets\">\n";
  1190.  
  1191.     for my $name (sort keys %{$self->{dynamic_ui__}{chain}}) {
  1192.         $body .= $self->{dynamic_ui__}{chain}{$name}->configure_item( $name,                            # PROFILE BLOCK START
  1193.                                                                               \%{$self->{language__}},
  1194.                                                                               $self->{session_key__} ); # PROFILE BLOCK STOP
  1195.     }
  1196.  
  1197.     # end optional widgets placement
  1198.     $body .= "</div>\n</td>\n";
  1199.  
  1200.     # User Interface Password panel
  1201.     $body .= "<td class=\"settingsPanel\" width=\"50%\" valign=\"top\" >\n";
  1202.     $body .= "<h2 class=\"security\">$self->{language__}{Security_PasswordTitle}</h2>\n";
  1203.  
  1204.     # optional widget placement
  1205.     $body .= "<div class=\"securityPassWidget\">\n";
  1206.  
  1207.     # Password widget
  1208.     $body .= "<form action=\"/security\" method=\"post\">\n";
  1209.     $body .= "<label class=\"securityLabel\" for=\"securityPassword\">$self->{language__}{Security_Password}:</label> <br />\n";
  1210.     if ( $self->config_( 'password' ) eq md5_hex( '__popfile__' ) ) {
  1211.         $body .= "<input type=\"password\" id=\"securityPassword\" name=\"password\" value=\"\" />\n";
  1212.     } else {
  1213.         $body .= "<input type=\"password\" id=\"securityPassword\" name=\"password\" value=\"" . $self->config_( 'password' ) . "\" />\n";
  1214.     }
  1215.     $body .= "<input type=\"submit\" class=\"submit\" name=\"update_server\" value=\"$self->{language__}{Apply}\" />\n";
  1216.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1217.     $body .= $self->{language__}{Security_PasswordUpdate} if ( defined($self->{form_}{password}) );
  1218.  
  1219.    # end optional widget placement
  1220.    $body .= "</div>\n";
  1221.  
  1222.     # Automatic Update Checking panel
  1223.     $body .= "<hr><h2 class=\"security\">$self->{language__}{Security_UpdateTitle}</h2>\n";
  1224.  
  1225.     # Check Daily for Updates widget
  1226.     $body .= "<span class=\"securityLabel\">$self->{language__}{Security_Update}:</span><br />\n";
  1227.  
  1228.     $body .= "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" summary=\"\"><tr><td>\n";
  1229.     if ( $self->config_( 'update_check' ) == 1 ) {
  1230.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1231.         $body .= "<span class=\"securityWidgetStateOn\">$self->{language__}{Yes}</span>\n";
  1232.         $body .= "<input type=\"submit\" class=\"toggleOff\" id=\"securityUpdateCheckOff\" name=\"toggle\" value=\"$self->{language__}{ChangeToNo}\" />\n";
  1233.         $body .= "<input type=\"hidden\" name=\"update_check\" value=\"1\" />\n";
  1234.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1235.     } else {
  1236.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1237.         $body .= "<span class=\"securityWidgetStateOff\">$self->{language__}{No}</span>\n";
  1238.         $body .= "<input type=\"submit\" class=\"toggleOn\" id=\"securityUpdateCheckOn\" name=\"toggle\" value=\"$self->{language__}{ChangeToYes}\" />\n";
  1239.         $body .= "<input type=\"hidden\" name=\"update_check\" value=\"2\" />\n";
  1240.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1241.     }
  1242.     $body .= "</td></tr></table>\n";
  1243.  
  1244.     # explanation of same
  1245.     $body .= "<div class=\"securityExplanation\">$self->{language__}{Security_ExplainUpdate}</div>\n";
  1246.  
  1247.     # Reporting Statistics panel
  1248.     $body .= "<hr><h2 class=\"security\">$self->{language__}{Security_StatsTitle}</h2>\n";
  1249.  
  1250.     # Send Statistics Daily widget
  1251.     $body .= "<span class=\"securityLabel\">$self->{language__}{Security_Stats}:</span>\n<br />\n";
  1252.  
  1253.     $body .= "<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" summary=\"\"><tr><td>\n";
  1254.     if ( $self->config_( 'send_stats' ) == 1 ) {
  1255.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1256.         $body .= "<span class=\"securityWidgetStateOn\">$self->{language__}{Yes}</span>\n";
  1257.         $body .= "<input type=\"submit\" class=\"toggleOff\" id=\"securitySendStatsOff\" name=\"toggle\" value=\"$self->{language__}{ChangeToNo}\" />\n";
  1258.         $body .= "<input type=\"hidden\" name=\"send_stats\" value=\"1\" />\n";
  1259.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1260.     } else {
  1261.         $body .= "<form class=\"securitySwitch\" action=\"/security\">\n";
  1262.         $body .= "<span class=\"securityWidgetStateOff\">$self->{language__}{No}</span>\n";
  1263.         $body .= "<input type=\"submit\" class=\"toggleOn\" id=\"securitySendStatsOn\" name=\"toggle\" value=\"$self->{language__}{ChangeToYes}\" />\n";
  1264.         $body .= "<input type=\"hidden\" name=\"send_stats\" value=\"2\" />\n";
  1265.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n";
  1266.     }
  1267.     $body .= "</td></tr></table>\n";
  1268.     # explanation of same
  1269.     $body .= "<div class=\"securityExplanation\">$self->{language__}{Security_ExplainStats}</div>\n</td>\n</tr>\n";
  1270.  
  1271.     $body .= "</table>\n";
  1272.  
  1273.     http_ok($self, $client,$body,3);
  1274. }
  1275.  
  1276. # ---------------------------------------------------------------------------------------------
  1277. #
  1278. # pretty_number - format a number with ,s every 1000
  1279. #
  1280. # $number       The number to format
  1281. #
  1282. # TODO: replace this with something that uses locale information to format numbers in a way
  1283. # that is specific to the locale since not everyone likes ,s every 1000.
  1284. #
  1285. # ---------------------------------------------------------------------------------------------
  1286. sub pretty_number
  1287. {
  1288.     my ( $self, $number ) = @_;
  1289.  
  1290.     $number = reverse $number;
  1291.     $number =~ s/(\d{3})/$1,/g;
  1292.     $number = reverse $number;
  1293.     $number =~ s/^,(.*)/$1/;
  1294.  
  1295.     return $number;
  1296. }
  1297.  
  1298. # ---------------------------------------------------------------------------------------------
  1299. #
  1300. # advanced_page - very advanced configuration options
  1301. #
  1302. # $client     The web browser to send the results to
  1303. #
  1304. # ---------------------------------------------------------------------------------------------
  1305. sub advanced_page
  1306. {
  1307.     my ( $self, $client ) = @_;
  1308.  
  1309.     # Handle updating the parameter table
  1310.  
  1311.     if ( defined( $self->{form_}{update_params} ) ) {
  1312.         foreach my $param (sort keys %{$self->{form_}}) {
  1313.             if ( $param =~ /parameter_(.*)/ ) {
  1314.                 $self->{configuration__}->parameter( $1, $self->{form_}{$param} );
  1315.             }
  1316.         }
  1317.  
  1318.         $self->{configuration__}->save_configuration();
  1319.     }
  1320.  
  1321.     my $add_message = '';
  1322.     my $deletemessage = '';
  1323.     if ( defined($self->{form_}{newword}) ) {
  1324.         my $result = $self->{classifier__}->add_stopword( $self->{api_session__}, $self->{form_}{newword} );
  1325.         if ( $result == 0 ) {
  1326.             $add_message = "<blockquote><div class=\"error02\"><b>$self->{language__}{Advanced_Error2}</b></div></blockquote>";
  1327.         }
  1328.     }
  1329.  
  1330.     if ( defined($self->{form_}{word}) ) {
  1331.         my $result = $self->{classifier__}->remove_stopword( $self->{api_session__}, $self->{form_}{word} );
  1332.         if ( $result == 0 ) {
  1333.             $deletemessage = "<blockquote><div class=\"error02\"><b>$self->{language__}{Advanced_Error2}</b></div></blockquote>";
  1334.         }
  1335.     }
  1336.  
  1337.     # title and heading
  1338.     my $body = "<table cellpadding=\"10%\" cellspacing=\"0\" class=\"settingsTable\"><tr><td class=\"settingsPanel\" valign=\"top\"><h2 class=\"advanced\">$self->{language__}{Advanced_StopWords}</h2>\n";
  1339.     $body .= "$self->{language__}{Advanced_Message1}\n<br /><br />\n<table summary=\"$self->{language__}{Advanced_MainTableSummary}\">\n";
  1340.  
  1341.     # the word census
  1342.     my $last = '';
  1343.     my $need_comma = 0;
  1344.     my $groupCounter = 0;
  1345.     my $groupSize = 5;
  1346.     my $firstRow = 1;
  1347.     my @words = $self->{classifier__}->get_stopword_list( $self->{api_session__} );
  1348.  
  1349.     for my $word (sort @words) {
  1350.         my $c;
  1351.         if ( $self->config_( 'language' ) =~ /^Korean$/ ) {
  1352.             no locale;
  1353.             $word =~ /^(.)/;
  1354.             $c = $1;
  1355.     } else {
  1356.             if ( $self->config_( 'language' ) =~ /^Nihongo$/ ) {
  1357.                no locale;
  1358.                $word =~ /^($euc_jp)/;
  1359.                $c = $1;
  1360.         } else {
  1361.                $word =~ /^(.)/;
  1362.                $c = $1;
  1363.             }
  1364.         }
  1365.  
  1366.         if ( $c ne $last ) {
  1367.             if ( !$firstRow ) {
  1368.                 $body .= "</td></tr>\n";
  1369.             } else {
  1370.                 $firstRow = 0;
  1371.             }
  1372.             $body .= "<tr><th scope=\"row\" class=\"advancedAlphabet";
  1373.             if ( $groupCounter == $groupSize ) {
  1374.                 $body .= "GroupSpacing";
  1375.             }
  1376.             $body .= "\"><b>$c</b></th>\n";
  1377.             $body .= "<td class=\"advancedWords";
  1378.             if ( $groupCounter == $groupSize ) {
  1379.                 $body .= "GroupSpacing";
  1380.                 $groupCounter = 0;
  1381.             }
  1382.             $body .= "\">";
  1383.             $last = $c;
  1384.             $need_comma = 0;
  1385.             $groupCounter += 1;
  1386.         }
  1387.         if ( $need_comma == 1 ) {
  1388.             $body .= ", $word";
  1389.         } else {
  1390.             $body .= $word;
  1391.             $need_comma = 1;
  1392.         }
  1393.     }
  1394.  
  1395.     $body .= "</td></tr>\n</table>\n";
  1396.  
  1397.     # optional widget placement
  1398.     $body .= "<div class=\"advancedWidgets\">\n";
  1399.  
  1400.     # Add Word widget
  1401.     $body .= "<form action=\"/advanced\">\n";
  1402.     $body .= "<label class=\"advancedLabel\" for=\"advancedAddWordText\">$self->{language__}{Advanced_AddWord}:</label><br />\n";
  1403.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  1404.     $body .= "<input type=\"text\" id=\"advancedAddWordText\" name=\"newword\" />\n";
  1405.     $body .= "<input type=\"submit\" class=\"submit\" name=\"add\" value=\"$self->{language__}{Add}\" />\n";
  1406.     $body .= "</form>\n$add_message\n";
  1407.  
  1408.     # Remove Word widget
  1409.     $body .= "<form action=\"/advanced\">\n";
  1410.     $body .= "<label class=\"advancedLabel\" for=\"advancedRemoveWordText\">$self->{language__}{Advanced_RemoveWord}:</label><br />\n";
  1411.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  1412.     $body .= "<input type=\"text\" id=\"advancedRemoveWordText\" name=\"word\" />\n";
  1413.     $body .= "<input type=\"submit\" class=\"submit\" name=\"remove\" value=\"$self->{language__}{Remove}\" />\n";
  1414.     $body .= "</form>\n$deletemessage\n";
  1415.  
  1416.     # end optional widget placement
  1417.     $body .= "</div>\n";
  1418.  
  1419.     $body .= "</td><td class=\"settingsPanel\" width=\"50%\" valign=\"top\"><h2 class=\"advanced\">$self->{language__}{Advanced_AllParameters}</h2>\n<p>$self->{language__}{Advanced_Warning}<p>$self->{language__}{Advanced_ConfigFile} " . $self->get_user_path_( 'popfile.cfg' );
  1420.  
  1421.     $body .= "<form action=\"/advanced\" method=\"POST\">\n";
  1422.     $body .= "<table width=\"100%\"><tr><th width=\"50%\">$self->{language__}{Advanced_Parameter}</th><th width=\"50%\">$self->{language__}{Advanced_Value}</th></tr>\n";
  1423.  
  1424.     my $last_module = '';
  1425.  
  1426.     foreach my $param ($self->{configuration__}->configuration_parameters()) {
  1427.         my $value = $self->{configuration__}->parameter( $param );
  1428.         $param =~ /^([^_]+)_/;
  1429.         if ( ( $last_module ne '' ) && ( $last_module ne $1 ) ) {
  1430.             $body .= "<tr><td colspan=\"2\"><hr></td></tr>";
  1431.         }
  1432.         $last_module = $1;
  1433.         $body .= "<tr><td>$param</td><td><input type=\"text\" name=\"parameter_$param\" value=\"$value\">";
  1434.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</td></tr>\n";
  1435.     }
  1436.  
  1437.     $body .= "</table><p><input type=\"submit\" value=\"$self->{language__}{Update}\" name=\"update_params\"></form></td></tr></table>";
  1438.  
  1439.     $self->http_ok( $client, $body, 5 );
  1440. }
  1441.  
  1442. sub max
  1443. {
  1444.     my ( $a, $b ) = @_;
  1445.  
  1446.     return ( $a > $b )?$a:$b;
  1447. }
  1448.  
  1449. # ---------------------------------------------------------------------------------------------
  1450. #
  1451. # magnet_page - the list of bucket magnets
  1452. #
  1453. # $client     The web browser to send the results to
  1454. #
  1455. # ---------------------------------------------------------------------------------------------
  1456. sub magnet_page
  1457. {
  1458.     my ( $self, $client ) = @_;
  1459.  
  1460.     my $magnet_message = '';
  1461.  
  1462.     if ( defined( $self->{form_}{delete} ) ) {
  1463.         for my $i ( 1 .. $self->{form_}{count} ) {
  1464.             if ( defined( $self->{form_}{"remove$i"} ) && ( $self->{form_}{"remove$i"} ) ) {
  1465.                 my $mtype   = $self->{form_}{"type$i"};
  1466.                 my $mtext   = $self->{form_}{"text$i"};
  1467.                 my $mbucket = $self->{form_}{"bucket$i"};
  1468.  
  1469.                 $self->{classifier__}->delete_magnet( $self->{api_session__}, $mbucket, $mtype, $mtext );
  1470.             }
  1471.         }
  1472.     }
  1473.  
  1474.     if ( defined( $self->{form_}{count} ) && ( defined( $self->{form_}{update} ) || defined( $self->{form_}{create} ) ) ) {
  1475.         for my $i ( 0 .. $self->{form_}{count} ) {
  1476.             my $mtype   = $self->{form_}{"type$i"};
  1477.             my $mtext   = $self->{form_}{"text$i"};
  1478.             my $mbucket = $self->{form_}{"bucket$i"};
  1479.  
  1480.             if ( defined( $self->{form_}{update} ) ) {
  1481.                 my $otype   = $self->{form_}{"otype$i"};
  1482.                 my $otext   = $self->{form_}{"otext$i"};
  1483.                 my $obucket = $self->{form_}{"obucket$i"};
  1484.  
  1485.                 if ( defined( $otype ) ) {
  1486.                     $self->{classifier__}->delete_magnet( $self->{api_session__}, $obucket, $otype, $otext );
  1487.         }
  1488.             }
  1489.  
  1490.             if ( ( defined($mbucket) ) && ( $mbucket ne '' ) && ( $mtext ne '' ) ) {
  1491.                 my $found = 0;
  1492.  
  1493.                 for my $bucket ($self->{classifier__}->get_buckets_with_magnets( $self->{api_session__} )) {
  1494.                     my %magnets;
  1495.                     @magnets{ $self->{classifier__}->get_magnets( $self->{api_session__}, $bucket, $mtype )} = ();
  1496.  
  1497.                     if ( exists( $magnets{$mtext} ) ) {
  1498.                         $found  = 1;
  1499.                         $magnet_message .= "<blockquote>\n<div class=\"error02\">\n<b>";
  1500.                         $magnet_message .= sprintf( $self->{language__}{Magnet_Error1}, "$mtype: $mtext", $bucket );
  1501.                         $magnet_message .= "</b>\n</div>\n</blockquote>\n";
  1502.                         last;
  1503.                     }
  1504.                 }
  1505.  
  1506.                 if ( $found == 0 )  {
  1507.                     for my $bucket ($self->{classifier__}->get_buckets_with_magnets( $self->{api_session__} )) {
  1508.                         my %magnets;
  1509.                         @magnets{ $self->{classifier__}->get_magnets( $self->{api_session__}, $bucket, $mtype )} = ();
  1510.  
  1511.                         for my $from (keys %magnets)  {
  1512.                             if ( ( $mtext =~ /\Q$from\E/ ) || ( $from =~ /\Q$mtext\E/ ) )  {
  1513.                                 $found = 1;
  1514.                                 $magnet_message .= "<blockquote><div class=\"error02\"><b>" . sprintf( $self->{language__}{Magnet_Error2}, "$mtype: $mtext", "$mtype: $from", $bucket ) . "</b></div></blockquote>";
  1515.                                 last;
  1516.                             }
  1517.                         }
  1518.                     }
  1519.                 }
  1520.  
  1521.                 if ( $found == 0 ) {
  1522.  
  1523.                     # It is possible to type leading or trailing white space in a magnet definition
  1524.                     # which can later cause mysterious failures because the whitespace is eaten by
  1525.                     # the browser when the magnet is displayed but is matched in the regular expression
  1526.                     # that does the magnet matching and will cause failures... so strip off the whitespace
  1527.  
  1528.                     $mtext =~ s/^[ \t]+//;
  1529.                     $mtext =~ s/[ \t]+$//;
  1530.  
  1531.                     $self->{classifier__}->create_magnet( $self->{api_session__}, $mbucket, $mtype, $mtext );
  1532.                     if ( !defined( $self->{form_}{update} ) ) {
  1533.                         $magnet_message .= "<blockquote>" . sprintf( $self->{language__}{Magnet_Error3}, "$mtype: $mtext", $mbucket ) . "</blockquote>";
  1534.                     }
  1535.                 }
  1536.             }
  1537.         }
  1538.     }
  1539.  
  1540.     # Current Magnets panel
  1541.  
  1542.     my $body = "<h2 class=\"magnets\">$self->{language__}{Magnet_CurrentMagnets}</h2>\n";
  1543.  
  1544.     my $start_magnet = $self->{form_}{start_magnet};
  1545.     my $stop_magnet  = $self->{form_}{stop_magnet};
  1546.     my $magnet_count = $self->{classifier__}->magnet_count( $self->{api_session__} );
  1547.     my $navigator = '';
  1548.  
  1549.     if ( !defined( $start_magnet ) ) {
  1550.         $start_magnet = 0;
  1551.     }
  1552.  
  1553.     if ( !defined( $stop_magnet ) ) {
  1554.         $stop_magnet = $start_magnet + $self->config_( 'page_size' ) - 1;
  1555.     }
  1556.  
  1557.     if ( $self->config_( 'page_size' ) < $magnet_count ) {
  1558.         $navigator = $self->get_magnet_navigator( $start_magnet, $stop_magnet, $magnet_count );
  1559.     }
  1560.  
  1561.     $body .= $navigator;
  1562.  
  1563.     # magnet listing headings
  1564.  
  1565.     $body .= "<form action=\"/magnets\" method=\"POST\">\n";
  1566.     $body .= "<table width=\"75%\" class=\"magnetsTable\" summary=\"$self->{language__}{Magnet_MainTableSummary}\">\n";
  1567.     $body .= "<caption>$self->{language__}{Magnet_Message1}</caption>\n";
  1568.     $body .= "<tr>\n<th class=\"magnetsLabel\" scope=\"col\">$self->{language__}{Magnet}</th>\n";
  1569.     $body .= "<th class=\"magnetsLabel\" scope=\"col\">$self->{language__}{Bucket}</th>\n";
  1570.     $body .= "<th class=\"magnetsLabel\" scope=\"col\">$self->{language__}{Remove}</th>\n</tr>\n";
  1571.  
  1572.     my %magnet_types = $self->{classifier__}->get_magnet_types( $self->{api_session__} );
  1573.     my $i = 0;
  1574.     my $count = -1;
  1575.  
  1576.     # magnet listing
  1577.  
  1578.     my $stripe = 0;
  1579.  
  1580.     for my $bucket ($self->{classifier__}->get_buckets_with_magnets( $self->{api_session__} )) {
  1581.         for my $type ($self->{classifier__}->get_magnet_types_in_bucket( $self->{api_session__}, $bucket )) {
  1582.             for my $magnet ($self->{classifier__}->get_magnets( $self->{api_session__}, $bucket, $type ))  {
  1583.                 $count += 1;
  1584.                 if ( ( $count < $start_magnet ) || ( $count > $stop_magnet ) ) {
  1585.                     next;
  1586.                 }
  1587.  
  1588.                 $i += 1;
  1589.                 $body .= "<tr ";
  1590.                 if ( $stripe )  {
  1591.                     $body .= "class=\"rowEven\"";
  1592.                 } else {
  1593.                     $body .= "class=\"rowOdd\"";
  1594.                 }
  1595.                 # to validate, must replace & with &
  1596.                 # stan todo note: come up with a smarter regex, this one's a bludgeon
  1597.                 # another todo: Move this stuff into a function to make text
  1598.                 # safe for inclusion in a form field
  1599.  
  1600.                 my $validatingMagnet = $magnet;
  1601.                 $validatingMagnet =~ s/&/&/g;
  1602.                 $validatingMagnet =~ s/</</g;
  1603.                 $validatingMagnet =~ s/>/>/g;
  1604.  
  1605.                 # escape quotation characters to avoid orphan data within tags
  1606.                 # todo: function to make arbitrary data safe for inclusion within
  1607.                 # a html tag attribute (inside double-quotes)
  1608.  
  1609.                 $validatingMagnet =~ s/\"/\"\;/g;
  1610.  
  1611.                 $body .= ">\n<td><select name=\"type$i\" id=\"magnetsAddType\">\n";
  1612.  
  1613.                 for my $mtype (keys %magnet_types) {
  1614.                     my $selected = ( $mtype eq $type )?"selected":"";
  1615.                     $body .= "<option value=\"$mtype\" $selected>\n$self->{language__}{$magnet_types{$mtype}}</option>\n";
  1616.                 }
  1617.                 $body .= "</select>: <input type=\"text\" name=\"text$i\" value=\"$validatingMagnet\" size=\"" . max(length($magnet),50) . "\" /></td>\n";
  1618.                 $body .= "<td><select name=\"bucket$i\" id=\"magnetsAddBucket\">\n";
  1619.  
  1620.                 my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  1621.                 foreach my $mbucket (@buckets) {
  1622.                     my $selected = ( $bucket eq $mbucket )?"selected":"";
  1623.                     my $bcolor   = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $mbucket );
  1624.                     $body .= "<option value=\"$mbucket\" $selected style=\"color: $bcolor\">$mbucket</option>\n";
  1625.                 }
  1626.                 $body .= "</select></td>\n";
  1627.  
  1628.                 $body .= "<td>\n";
  1629.                 $body .= "<input type=\"checkbox\" class=\"deleteButton\" name=\"remove$i\" />$self->{language__}{Remove}\n";
  1630.  
  1631.                 $body .= "<input name=\"otype$i\" type=\"hidden\" value=\"$type\" />";
  1632.                 $body .= "<input name=\"otext$i\" type=\"hidden\" value=\"$validatingMagnet\" />";
  1633.                 $body .= "<input name=\"obucket$i\" type=\"hidden\" value=\"$bucket\" />";
  1634.  
  1635.                 $body .= "</td>\n";
  1636.                 $body .= "</tr>";
  1637.                 $stripe = 1 - $stripe;
  1638.             }
  1639.         }
  1640.     }
  1641.  
  1642.     $body .= "<tr><td></td><td><input type=\"submit\" class=\"deleteButton\" name=\"update\" value=\"$self->{language__}{Update}\" /></td><td><input type=\"submit\" class=\"deleteButton\" name=\"delete\" value=\"$self->{language__}{Remove}\" /></td></tr></table>";
  1643.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  1644.     $body .= "<input type=\"hidden\" name=\"start_magnet\" value=\"$start_magnet\" />\n";
  1645.     $body .= "<input type=\"hidden\" name=\"count\" value=\"$i\" />\n</form>\n<br /><br />\n";
  1646.  
  1647.     $body .= $navigator;
  1648.  
  1649.     # Create New Magnet panel
  1650.  
  1651.     $body .= "<hr />\n<h2 class=\"magnets\">$self->{language__}{Magnet_CreateNew}</h2>\n";
  1652.     $body .= "<table cellspacing=\"0\" summary=\"\">\n<tr>\n<td>\n";
  1653.     $body .= "<b>$self->{language__}{Magnet_Explanation}\n";
  1654.     $body .= "</td>\n</tr>\n</table>\n";
  1655.  
  1656.     # optional widget placement
  1657.  
  1658.     $body .= "<div class=\"magnetsNewWidget\">\n";
  1659.  
  1660.     # New Magnets form
  1661.  
  1662.     $body .= "<form action=\"/magnets\">\n";
  1663.  
  1664.     # Magnet Type widget
  1665.  
  1666.     $body .= "<label class=\"magnetsLabel\" for=\"magnetsAddType\">$self->{language__}{Magnet_MagnetType}:</label><br />\n";
  1667.     $body .= "<select name=\"type0\" id=\"magnetsAddType\">\n";
  1668.  
  1669.     for my $mtype (keys %magnet_types) {
  1670.         $body .= "<option value=\"$mtype\">\n$self->{language__}{$magnet_types{$mtype}}</option>\n";
  1671.     }
  1672.     $body .= "</select>\n<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n<br /><br />\n";
  1673.     $body .= "<input type=\"hidden\" name=\"count\" value=\"1\" />\n";
  1674.  
  1675.     # Value widget
  1676.     $body .= "<label class=\"magnetsLabel\" for=\"magnetsAddText\">$self->{language__}{Magnet_Value}:</label><br />\n";
  1677.     $body .= "<input type=\"text\" name=\"text0\" id=\"magnetsAddText\" />\n<br /><br />\n";
  1678.  
  1679.     # Always Goes to Bucket widget
  1680.     $body .= "<label class=\"magnetsLabel\" for=\"magnetsAddBucket\">$self->{language__}{Magnet_Always}:</label><br />\n";
  1681.     $body .= "<select name=\"bucket0\" id=\"magnetsAddBucket\">\n<option value=\"\"></option>\n";
  1682.  
  1683.     my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  1684.     foreach my $bucket (@buckets) {
  1685.         my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  1686.         $body .= "<option value=\"$bucket\" style=\"color: $bcolor\">$bucket</option>\n";
  1687.     }
  1688.     $body .= "</select>\n<input type=\"submit\" class=\"submit\" name=\"create\" value=\"$self->{language__}{Create}\" />\n";
  1689.     $body .= "<input type=\"hidden\" name=\"start_magnet\" value=\"$start_magnet\" />\n";
  1690.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n$magnet_message\n";
  1691.     $body .="<br />\n";
  1692.  
  1693.     # end optional widget placement
  1694.     $body .= "</div>\n";
  1695.  
  1696.     http_ok($self, $client,$body,4);
  1697. }
  1698.  
  1699. # ---------------------------------------------------------------------------------------------
  1700. #
  1701. # bucket_page - information about a specific bucket
  1702. #
  1703. # $client     The web browser to send the results to
  1704. # ---------------------------------------------------------------------------------------------
  1705. sub bucket_page
  1706. {
  1707.     my ( $self, $client ) = @_;
  1708.  
  1709.     my $bucket_count = $self->{classifier__}->get_bucket_word_count( $self->{api_session__}, $self->{form_}{showbucket} );
  1710.  
  1711.     my $body = "<h2 class=\"buckets\">";
  1712.     $body .= sprintf( $self->{language__}{SingleBucket_Title}, "<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $self->{form_}{showbucket}) . "\">$self->{form_}{showbucket}</font>");
  1713.     $body .= "</h2>\n<table summary=\"\">\n<tr>\n<th scope=\"row\" class=\"bucketsLabel\">$self->{language__}{SingleBucket_WordCount}</th>\n";
  1714.     $body .= "<td> </td>\n<td align=\"right\">\n";
  1715.     $body .= pretty_number( $self, $bucket_count);
  1716.     $body .= "</td>\n<td>\n(" . sprintf( $self->{language__}{SingleBucket_Unique}, pretty_number( $self,  $self->{classifier__}->get_bucket_unique_count( $self->{api_session__}, $self->{form_}{showbucket})) ). ")";
  1717.     $body .= "</td>\n</tr>\n<tr>\n<th scope=\"row\" class=\"bucketsLabel\">$self->{language__}{SingleBucket_TotalWordCount}</th>\n";
  1718.     $body .= "<td> </td>\n<td align=\"right\">\n" . pretty_number( $self, $self->{classifier__}->get_word_count( $self->{api_session__} ));
  1719.  
  1720.     my $percent = "0%";
  1721.     if ( $self->{classifier__}->get_word_count( $self->{api_session__} ) > 0 )  {
  1722.         $percent = sprintf( '%6.2f%%', int( 10000 * $bucket_count / $self->{classifier__}->get_word_count( $self->{api_session__} ) ) / 100 );
  1723.     }
  1724.     $body .= "</td>\n<td></td>\n</tr>\n<tr><td colspan=\"3\"><hr /></td></tr>\n";
  1725.     $body .= "<tr>\n<th scope=\"row\" class=\"bucketsLabel\">$self->{language__}{SingleBucket_Percentage}</th>\n";
  1726.     $body .= "<td></td>\n<td align=\"right\">$percent</td>\n<td></td>\n</tr>\n</table>\n";
  1727.  
  1728.     $body .= "<form action=\"/buckets\"><input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />";
  1729.     $body .= "<input type=\"hidden\" name=\"showbucket\" value=\"$self->{form_}{showbucket}\" />";
  1730.     $body .= "<input type=\"submit\" name=\"clearbucket\" value=\"$self->{language__}{SingleBucket_ClearBucket}\" />";
  1731.     $body .= "</form>";
  1732.  
  1733.     $body .= "<h2 class=\"buckets\">";
  1734.     $body .= sprintf( $self->{language__}{SingleBucket_WordTable},  "<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $self->{form_}{showbucket} ) . "\">$self->{form_}{showbucket}" ) ;
  1735.     $body .= "</font>\n</h2>\n$self->{language__}{SingleBucket_Message1}\n<br /><br />\n<table summary=\"$self->{language__}{Bucket_WordListTableSummary}\">\n";
  1736.     $body .= "<tr><td colspan=2>";
  1737.  
  1738.     if ( $self->{classifier__}->get_bucket_word_count( $self->{api_session__}, $self->{form_}{showbucket} ) > 0 ) {
  1739.         for my $i ($self->{classifier__}->get_bucket_word_prefixes( $self->{api_session__}, $self->{form_}{showbucket} )) {
  1740.             if ( defined( $self->{form_}{showletter} ) && ( $i eq $self->{form_}{showletter} ) ) {
  1741.                 my %temp;
  1742.  
  1743.                 for my $j ( $self->{classifier__}->get_bucket_word_list( $self->{api_session__}, $self->{form_}{showbucket}, $i ) ) {
  1744.                     $temp{$j} = $self->{classifier__}->get_count_for_word( $self->{api_session__}, $self->{form_}{showbucket}, $j );
  1745.                 }
  1746.  
  1747.                 $body .= "</td></tr><tr><td colspan=2> </td></tr><tr>\n<td valign=\"top\">\n<b>$i</b>\n</td>\n<td valign=\"top\">\n<table><tr valign=\"top\">";
  1748.  
  1749.                 my $count = 0;
  1750.  
  1751.                 for my $word (sort { $temp{$b} <=> $temp{$a} } keys %temp) {
  1752.                     $body .= "</tr><tr valign=\"top\">" if ( ( $count % 6 ) ==  0 );
  1753.                     $body .= "<td><a class=\"wordListLink\" href=\"\/buckets\?session=$self->{session_key__}\&lookup=Lookup\&word=". $self->url_encode_( $word ) . "#Lookup\"><b>$word</b><\/a></td><td>$temp{$word}</td><td> </td>";
  1754.                     $count += 1;
  1755.                 }
  1756.  
  1757.                 $body .= "</tr></table></td>\n</tr>\n<tr><td colspan=2> </td></tr><tr><td colspan=2>";
  1758.           } else {
  1759.             $body .= "<a href=/buckets?session=$self->{session_key__}\&showbucket=$self->{form_}{showbucket}\&showletter=" . $self->url_encode_($i) . "><b>$i</b></a>\n";
  1760.           }
  1761.        }
  1762.     }
  1763.  
  1764.     $body .= "</td></tr>";
  1765.     $body .= "</table>\n";
  1766.  
  1767.     http_ok($self, $client,$body,1);
  1768. }
  1769.  
  1770. # ---------------------------------------------------------------------------------------------
  1771. #
  1772. # bar_chart_100 - Output an HTML bar chart
  1773. #
  1774. # %values       A hash of bucket names with values in series 0, 1, 2, ...
  1775. #
  1776. # ---------------------------------------------------------------------------------------------
  1777. sub bar_chart_100
  1778. {
  1779.     my ( $self, %values ) = @_;
  1780.     my $body = '';
  1781.     my $total_count = 0;
  1782.     my @xaxis = sort { 
  1783.         if ( $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $a ) == $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $b ) ) {
  1784.             $a cmp $b;   
  1785.         } else {
  1786.             $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $a ) <=> $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $b );
  1787.       }
  1788.      } keys %values;
  1789.  
  1790.     return '' if ( $#xaxis < 0 );
  1791.  
  1792.     my @series = sort keys %{$values{$xaxis[0]}};
  1793.  
  1794.     for my $bucket (@xaxis)  {
  1795.         $total_count += $values{$bucket}{0};
  1796.     }
  1797.  
  1798.     for my $bucket (@xaxis)  {
  1799.         $body .= "<tr>\n<td align=\"left\"><font color=\"". $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\">$bucket</font></td>\n<td> </td>";
  1800.  
  1801.         for my $s (@series) {
  1802.             my $value = $values{$bucket}{$s} || 0;
  1803.             my $count   = $self->pretty_number( $value );
  1804.             my $percent = '';
  1805.  
  1806.             if ( $s == 0 ) {
  1807.                 if ( $total_count == 0 ) {
  1808.                     $percent = " (  0.00%)";
  1809.                 } else {
  1810.                    $percent = sprintf( ' (%.2f%%)', int( $value * 10000 / $total_count ) / 100 );
  1811.                 }
  1812.             }
  1813.  
  1814.             if ( ( $s == 2 ) &&
  1815.                  ( $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) ) {
  1816.             $count = '';
  1817.         $percent = '';
  1818.         }
  1819.  
  1820.             $body .= "\n<td align=\"right\">$count$percent</td>";
  1821.         }
  1822.         $body .= "\n</tr>\n";
  1823.     }
  1824.  
  1825.     my $colspan = 3 + $#series;
  1826.  
  1827.     $body .= "<tr>\n<td colspan=\"$colspan\"> </td>\n</tr>\n<tr>\n<td colspan=\"$colspan\">\n";
  1828.  
  1829.     if ( $total_count != 0 ) {
  1830.         $body .= "<table class=\"barChart\" width=\"100%\" summary=\"$self->{language__}{Bucket_BarChartSummary}\">\n<tr>\n";
  1831.         foreach my $bucket (@xaxis) {
  1832.             my $percent = int( $values{$bucket}{0} * 10000 / $total_count ) / 100;
  1833.             if ( $percent != 0 )  {
  1834.                 $body .= "<td bgcolor=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\" title=\"$bucket ($percent%)\" width=\"";
  1835.                 $body .= (int($percent)<1)?1:int($percent);
  1836.                 $body .= "%\"><img src=\"pix.gif\" alt=\"\" height=\"20\" width=\"1\" /></td>\n";
  1837.             }
  1838.         }
  1839.         $body .= "</tr>\n</table>";
  1840.     }
  1841.  
  1842.     $body .= "</td>\n</tr>\n";
  1843.  
  1844.     if ( $total_count != 0 )  {
  1845.         $body .= "<tr>\n<td colspan=\"$colspan\" align=\"right\"><span class=\"graphFont\">100%</span></td>\n</tr>\n";
  1846.     }
  1847.  
  1848.     return $body;
  1849. }
  1850.  
  1851. # ---------------------------------------------------------------------------------------------
  1852. #
  1853. # corpus_page - the corpus management page
  1854. #
  1855. # $client     The web browser to send the results to
  1856. #
  1857. # ---------------------------------------------------------------------------------------------
  1858. sub corpus_page
  1859. {
  1860.     my ( $self, $client ) = @_;
  1861.  
  1862.     if ( defined( $self->{form_}{clearbucket} ) ) {
  1863.         $self->{classifier__}->clear_bucket( $self->{api_session__}, $self->{form_}{showbucket} );
  1864.     }
  1865.  
  1866.     if ( defined($self->{form_}{reset_stats}) ) {
  1867.         foreach my $bucket ($self->{classifier__}->get_all_buckets( $self->{api_session__} )) {
  1868.             $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'count', 0 );
  1869.             $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'fpcount', 0 );
  1870.             $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'fncount', 0 );
  1871.         }
  1872.         my $lasttime = localtime;
  1873.         $self->config_( 'last_reset', $lasttime );
  1874.         $self->{configuration__}->save_configuration();
  1875.     }
  1876.  
  1877.     if ( defined($self->{form_}{showbucket}) )  {
  1878.         $self->bucket_page( $client );
  1879.         return;
  1880.     }
  1881.  
  1882.     my $result;
  1883.     my $create_message = '';
  1884.     my $deletemessage = '';
  1885.     my $rename_message = '';
  1886.  
  1887.     if ( ( defined($self->{form_}{color}) ) && ( defined($self->{form_}{bucket}) ) ) {
  1888.         $self->{classifier__}->set_bucket_color( $self->{api_session__}, $self->{form_}{bucket}, $self->{form_}{color});
  1889.     }
  1890.  
  1891.     if ( ( defined($self->{form_}{bucket}) ) && ( defined($self->{form_}{subject}) ) && ( $self->{form_}{subject} > 0 ) ) {
  1892.         $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $self->{form_}{bucket}, 'subject', $self->{form_}{subject} - 1 );
  1893.     }
  1894.  
  1895.     if ( ( defined($self->{form_}{bucket}) ) && ( defined($self->{form_}{xtc}) ) && ( $self->{form_}{xtc} > 0 ) ) {
  1896.         $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $self->{form_}{bucket}, 'xtc', $self->{form_}{xtc} - 1 );
  1897.     }
  1898.  
  1899.     if ( ( defined($self->{form_}{bucket}) ) && ( defined($self->{form_}{xpl}) ) && ( $self->{form_}{xpl} > 0 ) ) {
  1900.         $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $self->{form_}{bucket}, 'xpl', $self->{form_}{xpl} - 1 );
  1901.     }
  1902.  
  1903.     if ( ( defined($self->{form_}{bucket}) ) &&  ( defined($self->{form_}{quarantine}) ) && ( $self->{form_}{quarantine} > 0 ) ) {
  1904.         $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $self->{form_}{bucket}, 'quarantine', $self->{form_}{quarantine} - 1 );
  1905.     }
  1906.  
  1907.     # This regular expression defines the characters that are NOT valid
  1908.     # within a bucket name
  1909.  
  1910.     my $invalid_bucket_chars = '[^[:lower:]\-_0-9]';
  1911.  
  1912.     if ( ( defined($self->{form_}{cname}) ) && ( $self->{form_}{cname} ne '' ) ) {
  1913.         if ( $self->{form_}{cname} =~ /$invalid_bucket_chars/ )  {
  1914.             $create_message = "<blockquote><div class=\"error01\">$self->{language__}{Bucket_Error1}</div></blockquote>";
  1915.         } else {
  1916.             if ( $self->{classifier__}->is_bucket( $self->{api_session__}, $self->{form_}{cname} ) ||
  1917.                  $self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $self->{form_}{cname} ) ) {
  1918.                 $create_message = "<blockquote><b>" . sprintf( $self->{language__}{Bucket_Error2}, $self->{form_}{cname} ) . "</b></blockquote>";
  1919.             } else {
  1920.                 $self->{classifier__}->create_bucket( $self->{api_session__}, $self->{form_}{cname} );
  1921.                 $create_message = "<blockquote><b>" . sprintf( $self->{language__}{Bucket_Error3}, $self->{form_}{cname} ) . "</b></blockquote>";
  1922.             }
  1923.        }
  1924.     }
  1925.  
  1926.     if ( ( defined($self->{form_}{delete}) ) && ( $self->{form_}{name} ne '' ) ) {
  1927.         $self->{form_}{name} = lc($self->{form_}{name});
  1928.         $self->{classifier__}->delete_bucket( $self->{api_session__}, $self->{form_}{name} );
  1929.         $deletemessage = "<blockquote><b>" . sprintf( $self->{language__}{Bucket_Error6}, $self->{form_}{name} ) . "</b></blockquote>";
  1930.     }
  1931.  
  1932.     if ( ( defined($self->{form_}{newname}) ) && ( $self->{form_}{oname} ne '' ) ) {
  1933.         if ( $self->{form_}{newname} =~ /$invalid_bucket_chars/ )  {
  1934.             $rename_message = "<blockquote><div class=\"error01\">$self->{language__}{Bucket_Error1}</div></blockquote>";
  1935.         } else {
  1936.             $self->{form_}{oname} = lc($self->{form_}{oname});
  1937.             $self->{form_}{newname} = lc($self->{form_}{newname});
  1938.             if ( $self->{classifier__}->rename_bucket( $self->{api_session__}, $self->{form_}{oname}, $self->{form_}{newname} ) == 1 ) {
  1939.                 $rename_message = "<blockquote><b>" . sprintf( $self->{language__}{Bucket_Error5}, $self->{form_}{oname}, $self->{form_}{newname} ) . "</b></blockquote>";
  1940.         } else {
  1941.                 $rename_message = "<blockquote><b>RENAME FAILED: INTERNAL ERROR</b></blockquote>";
  1942.         }
  1943.         }
  1944.     }
  1945.  
  1946.     # Summary panel
  1947.     my $body = "<h2 class=\"buckets\">$self->{language__}{Bucket_Title}</h2>\n";
  1948.  
  1949.     # column headings
  1950.     $body .= "<table class=\"bucketsTable\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" summary=\"$self->{language__}{Bucket_MaintenanceTableSummary}\">\n<tr>\n";
  1951.     $body .= "<th class=\"bucketsLabel\" scope=\"col\">$self->{language__}{Bucket_BucketName}</th>\n";
  1952.     $body .= "<th width=\"1%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"right\">$self->{language__}{Bucket_UniqueWords}</th>\n";
  1953.     $body .= "<th width=\"1%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"center\">$self->{language__}{Bucket_SubjectModification}</th>\n";
  1954.     $body .= "<th width=\"1%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"center\">$self->{language__}{Configuration_XTCInsertion}</th>\n";
  1955.     $body .= "<th width=\"1%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"center\">$self->{language__}{Configuration_XPLInsertion}</th>\n";
  1956.     $body .= "<th width=\"1%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"center\">$self->{language__}{Bucket_Quarantine}</th>\n";
  1957.     $body .= "<th width=\"2%\"> </th>\n<th class=\"bucketsLabel\" scope=\"col\" align=\"left\">$self->{language__}{Bucket_ChangeColor}</th>\n</tr>\n";
  1958.  
  1959.     my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  1960.     my $stripe = 0;
  1961.  
  1962.     my $total_count = 0;
  1963.     foreach my $bucket (@buckets) {
  1964.         $total_count += $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'count' );
  1965.     }
  1966.  
  1967.     my @pseudos = $self->{classifier__}->get_pseudo_buckets( $self->{api_session__} );
  1968.     push @buckets, @pseudos;
  1969.  
  1970.     foreach my $bucket (@buckets) {
  1971.         my $unique  = pretty_number( $self,  $self->{classifier__}->get_bucket_unique_count( $self->{api_session__}, $bucket ) );
  1972.  
  1973.         $body .= "<tr";
  1974.         if ( $stripe == 1 )  {
  1975.             $body .= " class=\"rowEven\"";
  1976.         } else {
  1977.             $body .= " class=\"rowOdd\"";
  1978.         }
  1979.         $stripe = 1 - $stripe;
  1980.         $body .= '><td>';
  1981.         if ( !$self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) {
  1982.             $body .= "<a href=\"/buckets?session=$self->{session_key__}&showbucket=$bucket\">\n";
  1983.     }
  1984.         $body .= "<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\">$bucket</font>";
  1985.         if ( !$self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) {
  1986.             $body .= '</a>';
  1987.     }
  1988.         $body .= "</td>\n<td width=\"1%\"> </td>\n";
  1989.         if ( !$self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) {
  1990.             $body .= "<td align=\"right\">$unique</td><td width=\"1%\"> </td>";
  1991.     } else {
  1992.             $body .= "<td align=\"right\"> </td><td width=\"1%\"> </td>";
  1993.     }
  1994.  
  1995.         # Subject Modification on/off widget
  1996.  
  1997.         $body .= "<td align=\"center\">\n";
  1998.         if ( $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'subject' ) == 0 ) {
  1999.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2000.             $body .= "<span class=\"bucketsWidgetStateOff\">$self->{language__}{Off} </span>\n";
  2001.             $body .= "<input type=\"submit\" class=\"toggleOn\" name=\"toggle\" value=\"$self->{language__}{TurnOn}\" />\n";
  2002.             $body .= "<input type=\"hidden\" name=\"subject\" value=\"2\" />\n";
  2003.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2004.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2005.         } else {
  2006.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2007.             $body .= "<span class=\"bucketsWidgetStateOn\">$self->{language__}{On} </span>\n";
  2008.             $body .= "<input type=\"submit\" class=\"toggleOff\" name=\"toggle\" value=\"$self->{language__}{TurnOff}\" />\n";
  2009.             $body .= "<input type=\"hidden\" name=\"subject\" value=\"1\" />\n";
  2010.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2011.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2012.         }
  2013.  
  2014.         # XTC on/off widget
  2015.  
  2016.         $body .= "<td width=\"1%\"> </td><td align=\"center\">\n";
  2017.         if ( $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'xtc' ) == 0 ) {
  2018.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2019.             $body .= "<span class=\"bucketsWidgetStateOff\">$self->{language__}{Off} </span>\n";
  2020.             $body .= "<input type=\"submit\" class=\"toggleOn\" name=\"toggle\" value=\"$self->{language__}{TurnOn}\" />\n";
  2021.             $body .= "<input type=\"hidden\" name=\"xtc\" value=\"2\" />\n";
  2022.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2023.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2024.         } else {
  2025.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2026.             $body .= "<span class=\"bucketsWidgetStateOn\">$self->{language__}{On} </span>\n";
  2027.             $body .= "<input type=\"submit\" class=\"toggleOff\" name=\"toggle\" value=\"$self->{language__}{TurnOff}\" />\n";
  2028.             $body .= "<input type=\"hidden\" name=\"xtc\" value=\"1\" />\n";
  2029.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2030.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2031.         }
  2032.  
  2033.         # XPL on/off widget
  2034.  
  2035.         $body .= "<td width=\"1%\"> </td><td align=\"center\">\n";
  2036.         if ( $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'xpl' ) == 0 ) {
  2037.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2038.             $body .= "<span class=\"bucketsWidgetStateOff\">$self->{language__}{Off} </span>\n";
  2039.             $body .= "<input type=\"submit\" class=\"toggleOn\" name=\"toggle\" value=\"$self->{language__}{TurnOn}\" />\n";
  2040.             $body .= "<input type=\"hidden\" name=\"xpl\" value=\"2\" />\n";
  2041.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2042.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2043.         } else {
  2044.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2045.             $body .= "<span class=\"bucketsWidgetStateOn\">$self->{language__}{On} </span>\n";
  2046.             $body .= "<input type=\"submit\" class=\"toggleOff\" name=\"toggle\" value=\"$self->{language__}{TurnOff}\" />\n";
  2047.             $body .= "<input type=\"hidden\" name=\"xpl\" value=\"1\" />\n";
  2048.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2049.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2050.         }
  2051.  
  2052.         # Quarantine on/off widget
  2053.  
  2054.         $body .= "<td width=\"1%\"> </td><td align=\"center\">\n";
  2055.         if ( $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'quarantine' ) == 0 ) {
  2056.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2057.             $body .= "<span class=\"bucketsWidgetStateOff\">$self->{language__}{Off} </span>\n";
  2058.             $body .= "<input type=\"submit\" class=\"toggleOn\" name=\"toggle\" value=\"$self->{language__}{TurnOn}\" />\n";
  2059.             $body .= "<input type=\"hidden\" name=\"quarantine\" value=\"2\" />\n";
  2060.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2061.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2062.         } else {
  2063.             $body .= "<form class=\"bucketsSwitch\" style=\"margin: 0\" action=\"/buckets\">\n";
  2064.             $body .= "<span class=\"bucketsWidgetStateOn\">$self->{language__}{On} </span>\n";
  2065.             $body .= "<input type=\"submit\" class=\"toggleOff\" name=\"toggle\" value=\"$self->{language__}{TurnOff}\" />\n";
  2066.             $body .= "<input type=\"hidden\" name=\"quarantine\" value=\"1\" />\n";
  2067.             $body .= "<input type=\"hidden\" name=\"bucket\" value=\"$bucket\" />\n";
  2068.             $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" /></form></td>\n";
  2069.         }
  2070.  
  2071.         # Change Color widget
  2072.  
  2073.         if ( !$self->{classifier__}->is_pseudo_bucket( $self->{api_session__}, $bucket ) ) {
  2074.             $body .= "<td> </td>\n<td align=\"left\">\n<table class=\"colorChooserTable\" cellpadding=\"0\" cellspacing=\"1\" summary=\"\">\n<tr>\n";
  2075.             my $color = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  2076.             $body .= "<td bgcolor=\"$color\" title='" . sprintf( $self->{language__}{Bucket_CurrentColor}, $bucket, $color ) . "'>\n<img class=\"colorChooserImg\" border=\"0\" alt='" . sprintf( $self->{language__}{Bucket_CurrentColor}, $bucket, $color ) . "' src=\"pix.gif\" width=\"10\" height=\"20\" /></td>\n<td> </td>\n";
  2077.             for my $i ( 0 .. $#{$self->{classifier__}->{possible_colors__}} ) {
  2078.                 my $color = $self->{classifier__}->{possible_colors__}[$i];
  2079.                 if ( $color ne $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) )  {
  2080.                     $body .= "<td bgcolor=\"$color\" title=\"". sprintf( $self->{language__}{Bucket_SetColorTo}, $bucket, $color ) . "\">\n";
  2081.                     $body .= "<a class=\"colorChooserLink\" href=\"/buckets?color=$color&bucket=$bucket&session=$self->{session_key__}\">\n";
  2082.                     $body .= "<img class=\"colorChooserImg\" border=\"0\" alt=\"". sprintf( $self->{language__}{Bucket_SetColorTo}, $bucket, $color ) . "\" src=\"pix.gif\" width=\"10\" height=\"20\" /></a>\n";
  2083.                     $body .= "</td>\n";
  2084.                 }
  2085.             }
  2086.             $body .= "</tr></table></td>\n";
  2087.     } else {
  2088.             $body .= "<td> </td>\n<td> </td>";
  2089.     }
  2090.  
  2091.         # Close odd/even row
  2092.         $body .= "</tr>\n";
  2093.     }
  2094.  
  2095.     # figure some performance numbers
  2096.  
  2097.     my $number = pretty_number( $self,  $self->{classifier__}->get_unique_word_count( $self->{api_session__} ) );
  2098.     my $pmcount = pretty_number( $self,  $self->mcount__() );
  2099.     my $pecount = pretty_number( $self,  $self->ecount__() );
  2100.     my $accuracy = $self->{language__}{Bucket_NotEnoughData};
  2101.     my $percent = 0;
  2102.     if ( $self->mcount__() > $self->ecount__() ) {
  2103.         $percent = int( 10000 * ( $self->mcount__() - $self->ecount__() ) / $self->mcount__() ) / 100;
  2104.         $accuracy = "$percent%";
  2105.       }
  2106.  
  2107.      # finish off Summary panel
  2108.  
  2109.     $body .= "<tr>\n<td colspan=\"3\"><hr /></td>\n</tr>\n";
  2110.     $body .= "<tr>\n<th class=\"bucketsLabel\" scope=\"row\">$self->{language__}{Total}</th>\n<td width=\"1%\"></td>\n";
  2111.     $body .= "<td align=\"right\">$number</td>\n<td></td>\n<td></td>\n</tr>\n</table>\n<br />\n";
  2112.  
  2113.     # middle panel group
  2114.     $body .= "<table class=\"settingsTable\" width=\"100%\" cellpadding=\"10%\" cellspacing=\"0\" summary=\"$self->{language__}{Bucket_StatisticsTableSummary}\">\n";
  2115.  
  2116.     # Classification Accuracy panel
  2117.     $body .= "<tr>\n<td class=\"settingsPanel\" valign=\"top\" width=\"33%\" align=\"center\">\n";
  2118.     $body .= "<h2 class=\"buckets\">$self->{language__}{Bucket_ClassificationAccuracy}</h2>\n";
  2119.  
  2120.     $body .= "<table summary=\"\">\n";
  2121.     # emails classified line
  2122.     $body .= "<tr>\n<th scope=\"row\" align=\"left\">$self->{language__}{Bucket_EmailsClassified}:</th>\n";
  2123.     $body .= "<td align=\"right\">$pmcount</td>\n</tr>\n";
  2124.     # classification errors line
  2125.     $body .= "<tr>\n<th scope=\"row\" align=\"left\">$self->{language__}{Bucket_ClassificationErrors}:</th>\n";
  2126.     $body .= "<td align=\"right\">$pecount</td>\n</tr>\n";
  2127.     # rules
  2128.     $body .= "<tr>\n<td colspan=\"2\"><hr /></td>\n</tr>\n";
  2129.  
  2130.     # $body .= "<tr>\n<td colspan=\"2\"><hr /></td></tr>\n";
  2131.     $body .= "<tr>\n<th scope=\"row\" align=\"left\">";
  2132.     $body .= "$self->{language__}{Bucket_Accuracy}:</th>\n<td align=\"right\">$accuracy</td>\n</tr>\n";
  2133.  
  2134.     if ( $percent > 0 )  {
  2135.         $body .= "<tr>\n<td colspan=\"2\"> </td>\n</tr>\n<tr>\n<td colspan=\"2\">\n";
  2136.         $body .= "<table class=\"barChart\" id=\"accuracyChart\" width=\"100%\" cellspacing=\"0\"";
  2137.         $body .= " cellpadding=\"0\" border=\"0\" summary=\"$self->{language__}{Bucket_AccuracyChartSummary}\">\n";
  2138.         $body .= "<tr>\n";
  2139.  
  2140.         for my $i ( 0..49 ) {
  2141.             $body .= "<td valign=\"middle\" class=";
  2142.             $body .= "\"accuracy0to49\"" if ( $i < 25 );
  2143.             $body .= "\"accuracy50to93\"" if ( ( $i > 24 ) && ( $i < 47 ) );
  2144.             $body .= "\"accuracy94to100\"" if ( $i > 46 );
  2145.             $body .= ">";
  2146.             if ( ( $i * 2 ) < $percent ) {
  2147.                 $body .= "<img class=\"lineImg\" src=\"black.gif\" height=\"4\" width=\"6\" alt=\"\" />";
  2148.             } else {
  2149.                 $body .= "<img src=\"pix.gif\" height=\"4\" width=\"6\" alt=\"\" />";
  2150.             }
  2151.             $body .= "</td>\n";
  2152.         }
  2153.  
  2154.         # Extra td to hold the vertical spacer gif
  2155.  
  2156.         $body .= "<td><img src=\"pix.gif\" height=\"10\" width=\"1\" alt=\"\" /></td>";
  2157.         $body .= "</tr>\n<tr>\n";
  2158.         $body .= "<td colspan=\"25\" align=\"left\"><span class=\"graphFont\">0%</span></td>\n";
  2159.         $body .= "<td colspan=\"26\" align=\"right\"><span class=\"graphFont\">100%</span></td>\n</tr></table>\n";
  2160.     }
  2161.  
  2162.  
  2163.     $body .= "</td></tr>\n</table>\n";
  2164.     $body .= "<form action=\"/buckets\">\n";
  2165.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  2166.     $body .= "<input type=\"submit\" class=\"submit\" name=\"reset_stats\" value=\"$self->{language__}{Bucket_ResetStatistics}\" />\n";
  2167.  
  2168.     if ( $self->config_( 'last_reset' ) ne '' ) {
  2169.         $body .= "<br />\n($self->{language__}{Bucket_LastReset}: " . $self->config_( 'last_reset' ) . ")\n";
  2170.     }
  2171.  
  2172.     # Emails Classified panel
  2173.     $body .= "</form>\n</td>\n<td class=\"settingsPanel\" valign=\"top\" width=\"33%\" align=\"center\">\n";
  2174.     $body .= "<h2 class=\"buckets\">$self->{language__}{Bucket_EmailsClassifiedUpper}</h2>\n";
  2175.  
  2176.     $body .= "<table summary=\"\">\n<tr>\n";
  2177.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"left\">$self->{language__}{Bucket}</th>\n<th> </th>\n";
  2178.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"right\">$self->{language__}{Bucket_ClassificationCount}</th>\n";
  2179.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"right\">$self->{language__}{Bucket_ClassificationFP}</th>\n";
  2180.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"right\">$self->{language__}{Bucket_ClassificationFN}</th>\n</tr>\n";
  2181.  
  2182.     my %bar_values;
  2183.     for my $bucket (@buckets)  {
  2184.         $bar_values{$bucket}{0} = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'count' );
  2185.         $bar_values{$bucket}{1} = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'fpcount' );
  2186.         $bar_values{$bucket}{2} = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'fncount' );
  2187.     }
  2188.  
  2189.     $body .= bar_chart_100( $self, %bar_values );
  2190.  
  2191.     # Word Counts panel
  2192.     $body .= "</table>\n</td>\n<td class=\"settingsPanel\" width=\"34%\" valign=\"top\" align=\"center\">\n";
  2193.     $body .= "<h2 class=\"buckets\">$self->{language__}{Bucket_WordCounts}</h2>\n<table summary=\"\">\n<tr>\n";
  2194.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"left\">$self->{language__}{Bucket}</th>\n<th> </th>\n";
  2195.     $body .= "<th class=\"bucketsLabel\" scope=\"col\" align=\"right\">$self->{language__}{Bucket_WordCount}</th>\n</tr>\n";
  2196.  
  2197.     @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  2198.  
  2199.     delete $bar_values{unclassified};
  2200.  
  2201.     for my $bucket (@buckets)  {
  2202.         $bar_values{$bucket}{0} = $self->{classifier__}->get_bucket_word_count( $self->{api_session__}, $bucket );
  2203.         delete $bar_values{$bucket}{1};
  2204.         delete $bar_values{$bucket}{2};
  2205.     }
  2206.  
  2207.     $body .= bar_chart_100( $self, %bar_values );
  2208.  
  2209.     $body .= "</table>\n</td>\n</tr>\n</table>\n<br />\n";
  2210.  
  2211.     # bottom panel group
  2212.     $body .= "<table class=\"settingsTable\" width=\"100%\" cellpadding=\"10%\" cellspacing=\"0\" summary=\"$self->{language__}{Bucket_MaintenanceTableSummary}\">\n";
  2213.  
  2214.     # Maintenance panel
  2215.     $body .= "<tr>\n<td class=\"settingsPanel\" valign=\"top\" width=\"50%\">\n";
  2216.     $body .= "<h2 class=\"buckets\">$self->{language__}{Bucket_Maintenance}</h2>\n";
  2217.  
  2218.     # optional widget placement
  2219.     $body .= "<div class=\"bucketsMaintenanceWidget\">\n";
  2220.  
  2221.     $body .= "<form action=\"/buckets\">\n";
  2222.     $body .= "<label class=\"bucketsLabel\" for=\"bucketsCreateBucket\">$self->{language__}{Bucket_CreateBucket}:</label><br />\n";
  2223.     $body .= "<input name=\"cname\" id=\"bucketsCreateBucket\" type=\"text\" />\n";
  2224.     $body .= "<input type=\"submit\" class=\"submit\" name=\"create\" value=\"$self->{language__}{Create}\" />\n";
  2225.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  2226.     $body .= "</form>\n$create_message\n";
  2227.     $body .= "<form action=\"/buckets\">\n";
  2228.     $body .= "<label class=\"bucketsLabel\" for=\"bucketsDeleteBucket\">$self->{language__}{Bucket_DeleteBucket}:</label><br />\n";
  2229.     $body .= "<select name=\"name\" id=\"bucketsDeleteBucket\">\n<option value=\"\"></option>\n";
  2230.  
  2231.     foreach my $bucket (@buckets) {
  2232.         my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  2233.         $body .= "<option value=\"$bucket\" style=\"color: $bcolor\">$bucket</option>\n";
  2234.     }
  2235.     $body .= "</select>\n<input type=\"submit\" class=\"submit\" name=\"delete\" value=\"$self->{language__}{Delete}\" />\n";
  2236.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n$deletemessage\n";
  2237.  
  2238.     $body .= "<form action=\"/buckets\">\n";
  2239.     $body .= "<label class=\"bucketsLabel\" for=\"bucketsRenameBucketFrom\">$self->{language__}{Bucket_RenameBucket}:</label><br />\n";
  2240.     $body .= "<select name=\"oname\" id=\"bucketsRenameBucketFrom\">\n<option value=\"\"></option>\n";
  2241.  
  2242.     foreach my $bucket (@buckets) {
  2243.         my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  2244.         $body .= "<option value=\"$bucket\" style=\"color: $bcolor\">$bucket</option>\n";
  2245.     }
  2246.     $body .= "</select>\n<label class=\"bucketsLabel\" for=\"bucketsRenameBucketTo\">$self->{language__}{Bucket_To}</label>\n";
  2247.     $body .= "<input type=\"text\" id=\"bucketsRenameBucketTo\" name=\"newname\" /> \n";
  2248.     $body .= "<input type=\"submit\" class=\"submit\" name=\"rename\" value=\"$self->{language__}{Rename}\" />\n";
  2249.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  2250.     $body .= "</form>\n$rename_message\n<br />\n";
  2251.  
  2252.     # end optional widget placement
  2253.     $body .= "</div>\n</td>\n";
  2254.  
  2255.     # Lookup panel
  2256.     $body .= "<td class=\"settingsPanel\" valign=\"top\" width=\"50%\">\n<a name=\"Lookup\"></a>\n";
  2257.     $body .= "<h2 class=\"buckets\">$self->{language__}{Bucket_Lookup}</h2>\n";
  2258.  
  2259.     # optional widget placement
  2260.     $body .= "<div class=\"bucketsLookupWidget\">\n";
  2261.  
  2262.     $body .= "<form action=\"/buckets#Lookup\">\n";
  2263.     $body .= "<label class=\"bucketsLabel\" for=\"bucketsLookup\">$self->{language__}{Bucket_LookupMessage}:</label><br />\n";
  2264.     $body .= "<input name=\"word\" id=\"bucketsLookup\" type=\"text\" /> \n";
  2265.     $body .= "<input type=\"submit\" class=\"submit\" name=\"lookup\" value=\"$self->{language__}{Lookup}\" />\n";
  2266.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n</form>\n<br />\n";
  2267.  
  2268.     # end optional widget placement
  2269.     $body .= "</div>\n";
  2270.  
  2271.     if ( ( defined($self->{form_}{lookup}) ) || ( defined($self->{form_}{word}) ) ) {
  2272.         my $word = $self->{form_}{word};
  2273.  
  2274.         if ( !( $word =~ /^[A-Za-z0-9\-_]+:/ ) ) {
  2275.            $word = $self->{classifier__}->{parser__}->{mangle__}->mangle($word, 1);
  2276.         }
  2277.  
  2278.         $body .= "<blockquote>\n";
  2279.  
  2280.         # Don't print the headings if there are no entries.
  2281.  
  2282.         my $heading = "<table class=\"lookupResultsTable\" cellpadding=\"10%\" cellspacing=\"0\" summary=\"$self->{language__}{Bucket_LookupResultsSummary}\">\n";
  2283.         $heading .= "<tr>\n<td>\n";
  2284.         $heading .= "<table summary=\"\">\n";
  2285.         $heading .= "<caption><strong>$self->{language__}{Bucket_LookupMessage2} $word</strong><br /><br /></caption>";
  2286.         $heading .= "<tr>\n<th scope=\"col\">$self->{language__}{Bucket}</th>\n<th> </th>\n";
  2287.         $heading .= "<th scope=\"col\">$self->{language__}{Frequency}</th>\n<th> </th>\n";
  2288.         $heading .= "<th scope=\"col\">$self->{language__}{Probability}</th>\n<th> </th>\n";
  2289.         $heading .= "<th scope=\"col\">$self->{language__}{Score}</th>\n</tr>\n";
  2290.  
  2291.         if ( $self->{form_}{word} ne '' ) {
  2292.             my $max = 0;
  2293.             my $max_bucket = '';
  2294.             my $total = 0;
  2295.             foreach my $bucket (@buckets) {
  2296.                 my $val = $self->{classifier__}->get_value_( $self->{api_session__}, $bucket, $word );
  2297.                 if ( $val != 0 ) {
  2298.                     my $prob = exp( $val );
  2299.                     $total += $prob;
  2300.                     if ( $max_bucket eq '' ) {
  2301.                         $body .= $heading;
  2302.                     }
  2303.                     if ( $prob > $max ) {
  2304.                         $max = $prob;
  2305.                         $max_bucket = $bucket;
  2306.                     }
  2307.                 } else {
  2308.  
  2309.                     # Take into account the probability the Bayes calculation applies
  2310.                     # for the buckets in which the word is not found.
  2311.  
  2312.                     $total += exp( $self->{classifier__}->get_not_likely_( $self->{api_session__} ) );
  2313.                 }
  2314.             }
  2315.  
  2316.             foreach my $bucket (@buckets) {
  2317.                 my $val = $self->{classifier__}->get_value_( $self->{api_session__}, $bucket, $word );
  2318.                 if ( $val != 0 ) {
  2319.                     my $prob    = exp( $val );
  2320.                     my $n       = ($total > 0)?$prob / $total:0;
  2321.                     my $score   = ($#buckets >= 0)?($val - $self->{classifier__}->get_not_likely_( $self->{api_session__} ) )/log(10.0):0;
  2322.                     my $normal  = sprintf("%.10f", $n);
  2323.                     $score      = sprintf("%.10f", $score);
  2324.                     my $probf   = sprintf("%.10f", $prob);
  2325.                     my $bold    = '';
  2326.                     my $endbold = '';
  2327.                     if ( $score =~ /^[^\-]/ ) {
  2328.                         $score = " $score";
  2329.                     }
  2330.                     $bold    = "<b>"  if ( $max == $prob );
  2331.                     $endbold = "</b>" if ( $max == $prob );
  2332.                     $body .= "<tr>\n<td>$bold<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\">$bucket</font>$endbold</td>\n";
  2333.                     $body .= "<td></td>\n<td>$bold<tt>$probf</tt>$endbold</td>\n<td></td>\n";
  2334.                     $body .= "<td>$bold<tt>$normal</tt>$endbold</td>\n<td></td>\n<td>$bold<tt>$score</tt>$endbold</td>\n</tr>\n";
  2335.                 }
  2336.             }
  2337.  
  2338.             if ( $max_bucket ne '' ) {
  2339.                 $body .= "</table>\n<br /><br />";
  2340.                 $body .= sprintf( $self->{language__}{Bucket_LookupMostLikely}, $word, $self->{classifier__}->get_bucket_color( $self->{api_session__}, $max_bucket ), $max_bucket);
  2341.                 $body .= "</td>\n</tr>\n</table>";
  2342.             } else {
  2343.                 $body .= sprintf( $self->{language__}{Bucket_DoesNotAppear}, $word );
  2344.             }
  2345.         }
  2346.  
  2347.         $body .= "\n</blockquote>\n";
  2348.     }
  2349.  
  2350.     $body .= "</td>\n</tr>\n</table>";
  2351.  
  2352.     $self->http_ok( $client, $body, 1 );
  2353. }
  2354.  
  2355. # ---------------------------------------------------------------------------------------------
  2356. #
  2357. # compare_mf - Compares two mailfiles, used for sorting mail into order
  2358. #
  2359. # ---------------------------------------------------------------------------------------------
  2360. sub compare_mf
  2361. {
  2362.     my $ad;
  2363.     my $bd;
  2364.     my $am;
  2365.     my $bm;
  2366.  
  2367.     $a =~ /popfile(\d+)=(\d+)\.msg/;
  2368.     $ad = $1;
  2369.     $am = $2;
  2370.  
  2371.     $b =~ /popfile(\d+)=(\d+)\.msg/;
  2372.     $bd = $1;
  2373.     $bm = $2;
  2374.  
  2375.     if ( $ad == $bd ) {
  2376.         return ( $bm <=> $am );
  2377.     } else {
  2378.         return ( $bd <=> $ad );
  2379.     }
  2380. }
  2381.  
  2382. # ---------------------------------------------------------------------------------------------
  2383. #
  2384. # sort_filter_history
  2385. #
  2386. # Called to set up the history_keys array with the appropriate order set of keys from the
  2387. # history based on the passed in filter, search and sort settings
  2388. #
  2389. # $filter       Name of bucket to filter on
  2390. # $search       From/Subject line to search for
  2391. # $sort         The field to sort on (from, subject, bucket)
  2392. #
  2393. # ---------------------------------------------------------------------------------------------
  2394. sub sort_filter_history
  2395. {
  2396.     my ( $self, $filter, $search, $sort ) = @_;
  2397.  
  2398.     # If the need_resort__ is set then we reindex the history indexes
  2399.  
  2400.     if ( $self->{need_resort__} == 1 ) {
  2401.         my $i = 0;
  2402.  
  2403.         foreach my $key (sort compare_mf keys %{$self->{history__}}) {
  2404.             $self->{history__}{$key}{index} = $i;
  2405.             $i += 1;
  2406.         }
  2407.     }
  2408.  
  2409.     # Place entries in the history_keys array based on three critera:
  2410.     #
  2411.     # 1. Whether the bucket they are classified in matches the $filter
  2412.     # 2. Whether their from/subject matches the $search
  2413.     # 3. In the order of $sort which can be from, subject or bucket
  2414.  
  2415.     delete $self->{history_keys__};
  2416.  
  2417.     if ( ( $filter ne '' ) || ( $search ne '' ) ) {
  2418.         foreach my $file (sort compare_mf keys %{$self->{history__}}) {
  2419.             if ( ( $filter eq '' ) ||                                                                            # PROFILE BLOCK START
  2420.                  ( $self->{history__}{$file}{bucket} eq $filter ) ||
  2421.                  ( ( $filter eq '__filter__magnet' ) && ( $self->{history__}{$file}{magnet} ne '' ) ) ||
  2422.                  ( ( $filter eq '__filter__no__magnet' ) && ( $self->{history__}{$file}{magnet} eq '' ) ) ) {    # PROFILE BLOCK STOP
  2423.                 if ( ( $search eq '' ) ||                                                                        # PROFILE BLOCK START
  2424.                    ( $self->{history__}{$file}{from}    =~ /\Q$search\E/i ) ||
  2425.                    ( $self->{history__}{$file}{subject} =~ /\Q$search\E/i ) ) {                                  # PROFILE BLOCK STOP
  2426.                            if ( defined( $self->{history_keys__} ) ) {
  2427.                             @{$self->{history_keys__}} = (@{$self->{history_keys__}}, $file);
  2428.                         } else {
  2429.                             @{$self->{history_keys__}} = ($file);
  2430.                         }
  2431.                    }
  2432.             }
  2433.         }
  2434.     } else {
  2435.         @{$self->{history_keys__}} = keys %{$self->{history__}};
  2436.     }
  2437.  
  2438.     # If a sort is specified then use it to sort the history items by an a subkey
  2439.     # (from, subject or bucket) otherwise use compare_mf to give the history back
  2440.     # in the order the messages were received.  Note that when sorting on a alphanumeric
  2441.     # field we ignore all punctuation characters so that "John and 'John and John
  2442.     # all sort next to each other
  2443.  
  2444.     # Ascending or Descending? Ascending is noted by /-field/
  2445.  
  2446.     my $descending = 0;
  2447.     if ($sort =~ s/^\-//) {
  2448.         $descending = 1;
  2449.     }
  2450.  
  2451.     if ( ( $sort ne '' ) &&                                           # PROFILE BLOCK START
  2452.  
  2453.          # If the filter had no messages, this will be undefined
  2454.          # and there are no ways to sort nothing
  2455.  
  2456.          defined @{$self->{history_keys__}} ) {                       # PROFILE BLOCK STOP
  2457.  
  2458.         @{$self->{history_keys__}} = sort {
  2459.                                             my ($a1,$b1) = ($self->{history__}{$a}{$sort},  # PROFILE BLOCK START
  2460.                                               $self->{history__}{$b}{$sort});               # PROFILE BLOCK STOP
  2461.                                               $a1 =~ s/&(l|g)t;//ig;
  2462.                                               $b1 =~ s/&(l|g)t;//ig;
  2463.                                               $a1 =~ s/[^A-Z0-9]//ig;
  2464.                                               $b1 =~ s/[^A-Z0-9]//ig;
  2465.                                               return ( $a1 cmp $b1 );
  2466.                                           } @{$self->{history_keys__}};
  2467.     } else {
  2468.  
  2469.         # Here's a quick shortcut so that we don't have to iterate
  2470.         # if there's no work for us to do
  2471.  
  2472.         if ( $self->history_size() > 0 ) {
  2473.             @{$self->{history_keys__}} = sort compare_mf @{$self->{history_keys__}};
  2474.         }
  2475.     }
  2476.  
  2477.     @{$self->{history_keys__}} = reverse @{$self->{history_keys__}} if ($descending);
  2478.  
  2479.     $self->{need_resort__} = 0;
  2480. }
  2481.  
  2482. # ---------------------------------------------------------------------------------------------
  2483. #
  2484. # load_disk_cache__
  2485. #
  2486. # Preloads the history__ hash with information from the disk which will have been saved
  2487. # the last time we shutdown
  2488. #
  2489. # ---------------------------------------------------------------------------------------------
  2490. sub load_disk_cache__
  2491. {
  2492.     my ( $self ) = @_;
  2493.  
  2494.     my $cache_file = $self->get_user_path_( $self->global_config_( 'msgdir' ) . 'history_cache' );
  2495.     if ( !(-e $cache_file) ) {
  2496.         return;
  2497.     }
  2498.  
  2499.     open CACHE, "<$cache_file";
  2500.  
  2501.     my $first = <CACHE>;
  2502.  
  2503.     if ( $first =~ /___HISTORY__ __ VERSION__ 1/ ) {
  2504.         while ( my $line = <CACHE> ) {
  2505.             last if ( !( $line =~ /__HISTORY__ __BOUNDARY__/ ) );
  2506.  
  2507.             $line = <CACHE>;
  2508.             $line =~ s/[\r\n]//g;
  2509.             my $key = $line;
  2510.             $line = <CACHE>;
  2511.             $line =~ s/[\r\n]//g;
  2512.             $self->{history__}{$key}{bucket} = $line;
  2513.             $line = <CACHE>;
  2514.             $line =~ s/[\r\n]//g;
  2515.             $self->{history__}{$key}{reclassified} = $line;
  2516.             $line = <CACHE>;
  2517.             $line =~ s/[\r\n]//g;
  2518.             $self->{history__}{$key}{magnet} = $line;
  2519.             $line = <CACHE>;
  2520.             $line =~ s/[\r\n]//g;
  2521.             $self->{history__}{$key}{subject} = $line;
  2522.             $line = <CACHE>;
  2523.             $line =~ s/[\r\n]//g;
  2524.             $self->{history__}{$key}{from} = $line;
  2525.             $line = <CACHE>;
  2526.             $line =~ s/[\r\n]//g;
  2527.             $self->{history__}{$key}{short_subject} = $line;
  2528.             $line = <CACHE>;
  2529.             $line =~ s/[\r\n]//g;
  2530.             $self->{history__}{$key}{short_from} = $line;
  2531.             $self->{history__}{$key}{cull}       = 0;
  2532.         }
  2533.     }
  2534.     close CACHE;
  2535. }
  2536.  
  2537. # ---------------------------------------------------------------------------------------------
  2538. #
  2539. # save_disk_cache__
  2540. #
  2541. # Save the current of the history cache so that it can be reloaded next time on startup
  2542. #
  2543. # ---------------------------------------------------------------------------------------------
  2544. sub save_disk_cache__
  2545. {
  2546.     my ( $self ) = @_;
  2547.  
  2548.     if ( $self->{save_cache__} == 0 ) {
  2549.         return;
  2550.     }
  2551.  
  2552.     open CACHE, '>' . $self->get_user_path_( $self->global_config_( 'msgdir' ) . 'history_cache' );
  2553.     print CACHE "___HISTORY__ __ VERSION__ 1\n";
  2554.     foreach my $key (keys %{$self->{history__}}) {
  2555.         print CACHE "__HISTORY__ __BOUNDARY__\n";
  2556.         print CACHE "$key\n";
  2557.         print CACHE "$self->{history__}{$key}{bucket}\n";
  2558.         print CACHE "$self->{history__}{$key}{reclassified}\n";
  2559.         print CACHE "$self->{history__}{$key}{magnet}\n";
  2560.         print CACHE "$self->{history__}{$key}{subject}\n";
  2561.         print CACHE "$self->{history__}{$key}{from}\n";
  2562.         print CACHE "$self->{history__}{$key}{short_subject}\n";
  2563.         print CACHE "$self->{history__}{$key}{short_from}\n";
  2564.     }
  2565.     close CACHE;
  2566. }
  2567.  
  2568. # ---------------------------------------------------------------------------------------------
  2569. #
  2570. # load_history_cache__
  2571. #
  2572. # Forces a reload of the history cache from disk.  This works by globbing the history
  2573. # directory and then checking for new files that need to be loaded into the history cache
  2574. # and culling any files that have been removed without telling us
  2575. #
  2576. # ---------------------------------------------------------------------------------------------
  2577. sub load_history_cache__
  2578. {
  2579.     my ( $self ) = @_;
  2580.  
  2581.     # We calculate the largest value for the first number in the MSG file
  2582.     # names to verify at the end that the global download_count parameter
  2583.     # has not been corrupted.
  2584.  
  2585.     my $max = 0;
  2586.  
  2587.     # First we mark every entry in the history cache with cull set to one, after we have
  2588.     # looked through the messages directory for message we will delete any of the entries
  2589.     # in the hash that have cull still set to 1.  cull gets set to 0 everytime we see an
  2590.     # existing history cache entry that is still on the disk, or when we create a new
  2591.     # entry.  Strictly speaking this should not be necessary because when files are deleted
  2592.     # their corresponding history entry is meant to be deleted, but since disk is not 100%
  2593.     # reliable we do this check so that the history cache is in sync with the disk at all
  2594.     # times
  2595.  
  2596.     foreach my $key (keys %{$self->{history__}}) {
  2597.         $self->{history__}{$key}{cull} = 1;
  2598.     }
  2599.  
  2600.     # Now get all the names of files from the appropriate history subdirectory and run
  2601.     # through them looking for existing entries in the history which must be marked
  2602.     # for non-culling and new entries that need to be added to the end
  2603.  
  2604.     opendir MESSAGES, $self->get_user_path_( $self->global_config_( 'msgdir' ) );
  2605.  
  2606.     my @history_files;
  2607.  
  2608.     while ( my $entry = readdir MESSAGES ) {
  2609.         if ( $entry =~ /(popfile(\d+)=\d+\.msg)$/ ) {
  2610.             $entry = $1;
  2611.  
  2612.             if ( $2 > $max ) {
  2613.                 $max = $2;
  2614.             }
  2615.  
  2616.             if ( defined( $self->{history__}{$entry} ) ) {
  2617.                 $self->{history__}{$entry}{cull} = 0;
  2618.         } else {
  2619.                 push @history_files, ($entry);
  2620.             }
  2621.         }
  2622.     }
  2623.  
  2624.     closedir MESSAGES;
  2625.  
  2626.     foreach my $i ( 0 .. $#history_files ) {
  2627.         $self->new_history_file__( $history_files[$i] );
  2628.     }
  2629.  
  2630.     # Remove any entries from the history that have been removed from disk, see the big
  2631.     # comment at the start of this function for more detail
  2632.  
  2633.     my $index = 0;
  2634.  
  2635.     foreach my $key (sort compare_mf keys %{$self->{history__}}) {
  2636.         if ( $self->{history__}{$key}{cull} == 1 ) {
  2637.             delete $self->{history__}{$key};
  2638.         } else {
  2639.             $self->{history__}{$key}{index} = $index;
  2640.             $index += 1;
  2641.         }
  2642.     }
  2643.  
  2644.     $self->{need_resort__}     = 0;
  2645.     $self->sort_filter_history( '', '', '' );
  2646.  
  2647.     if ( $max > $self->global_config_( 'download_count' ) ) {
  2648.         $self->global_config_( 'download_count', $max+1 );
  2649.     }
  2650. }
  2651.  
  2652. # ---------------------------------------------------------------------------------------------
  2653. #
  2654. # new_history_file__
  2655. #
  2656. # Adds a new file to the history cache
  2657. #
  2658. # $file                The name of the file added
  2659. # $index               (optional) The history keys index
  2660. #
  2661. # ---------------------------------------------------------------------------------------------
  2662. sub new_history_file__
  2663. {
  2664.     my ( $self, $file, $index ) = @_;
  2665.  
  2666.     # Find the class information for this file using the history_read_class helper
  2667.     # function, and then parse the MSG file for the From and Subject information
  2668.  
  2669.     my ( $reclassified, $bucket, $usedtobe, $magnet ) = $self->{classifier__}->history_read_class( $file );
  2670.     my $from    = '';
  2671.     my $subject = '';
  2672.     my $long_header = '';
  2673.  
  2674.     $magnet       = '' if ( !defined( $magnet ) );
  2675.     $reclassified = '' if ( !defined( $reclassified ) );
  2676.  
  2677.     if ( open MAIL, '<'. $self->get_user_path_( $self->global_config_( 'msgdir' ) . $file ) ) {
  2678.         while ( <MAIL> )  {
  2679.             last if ( /^(\r\n|\r|\n)/ );
  2680.  
  2681.             # Support long header that has more than 2 lines
  2682.  
  2683.             if ( /^[\t ]+(=\?[\w-]+\?[BQ]\?.*\?=.*)/ ) {
  2684.                 if ( $long_header eq 'from' ) {
  2685.                     $from .= $1;
  2686.                     next;
  2687.                 }
  2688.  
  2689.                 if ( $long_header eq 'subject' ) {
  2690.                     $subject .= $1;
  2691.                     next;
  2692.                 }
  2693.             } else {
  2694.                 if ( /^From: *(.*)/i ) {
  2695.                     $long_header = 'from';
  2696.                     $from = $1;
  2697.                     next;
  2698.                 } else {
  2699.                     if ( /^Subject: *(.*)/i ) {
  2700.                         $long_header = 'subject';
  2701.                         $subject = $1;
  2702.                         next;
  2703.             }
  2704.                 }
  2705.                 $long_header = '';
  2706.             }
  2707.  
  2708.             last if ( ( $from ne '' ) && ( $subject ne '' ) );
  2709.         }
  2710.         close MAIL;
  2711.     }
  2712.  
  2713.     $from    = "<$self->{language__}{History_NoFrom}>"    if ( $from eq '' );
  2714.     $subject = "<$self->{language__}{History_NoSubject}>" if ( !( $subject =~ /[^ \t\r\n]/ ) );
  2715.  
  2716.     $from    =~ s/\"(.*)\"/$1/g;
  2717.     $subject =~ s/\"(.*)\"/$1/g;
  2718.  
  2719.     # TODO Interface violation here, need to clean up
  2720.     # Pass language parameter to decode_string()
  2721.  
  2722.     $from    = $self->{classifier__}->{parser__}->decode_string( $from, $self->config_( 'language' ) );
  2723.     $subject = $self->{classifier__}->{parser__}->decode_string( $subject, $self->config_( 'language' ) );
  2724.  
  2725.     my ( $short_from, $short_subject ) = ( $from, $subject );
  2726.  
  2727.     if ( length($short_from)>40 )  {
  2728.         $short_from =~ /(.{40})/;
  2729.         $short_from = "$1...";
  2730.     }
  2731.  
  2732.     if ( length($short_subject)>40 )  {
  2733.         $short_subject =~ s/=20/ /g;
  2734.         $short_subject =~ /(.{40})/;
  2735.         $short_subject = $1;
  2736.  
  2737.         # Do not truncate at 39 if the last char is the first byte of DBCS char(pair of two bytes).
  2738.         # Truncate it 1 byte shorter.
  2739.         if ( $self->config_( 'language' ) =~ /^Korean|Nihongo$/ ) {
  2740.             $short_subject =~ s/(([\x80-\xff].)*)[\x80-\xff]?$/$1/;
  2741.             $short_subject .= "...";
  2742.         } else {
  2743.             $short_subject .= "...";
  2744.         }
  2745.     }
  2746.  
  2747.     $from =~ s/&/&/g;
  2748.     $from =~ s/</</g;
  2749.     $from =~ s/>/>/g;
  2750.     $from =~ s/"/"/g;
  2751.  
  2752.     $short_from =~ s/&/&/g;
  2753.     $short_from =~ s/</</g;
  2754.     $short_from =~ s/>/>/g;
  2755.     $short_from =~ s/"/"/g;
  2756.  
  2757.     $subject =~ s/&/&/g;
  2758.     $subject =~ s/</</g;
  2759.     $subject =~ s/>/>/g;
  2760.     $subject =~ s/"/"/g;
  2761.  
  2762.     $short_subject =~ s/&/&/g;
  2763.     $short_subject =~ s/</</g;
  2764.     $short_subject =~ s/>/>/g;
  2765.     $short_subject =~ s/"/"/g;
  2766.  
  2767.     # If the index is known, stick it straight into the history else go into
  2768.     # the precache for merging into history when the history is viewed next
  2769.  
  2770.     my $cache = 'history__';
  2771.     if ( !defined( $index ) ) {
  2772.         $cache = 'history_pre_cache__';
  2773.     }
  2774.  
  2775.     $self->{$cache}{$file}{bucket}        = $bucket;
  2776.     $self->{$cache}{$file}{reclassified}  = $reclassified;
  2777.     $self->{$cache}{$file}{magnet}        = $magnet;
  2778.     $self->{$cache}{$file}{subject}       = $subject;
  2779.     $self->{$cache}{$file}{from}          = $from;
  2780.     $self->{$cache}{$file}{short_subject} = $short_subject;
  2781.     $self->{$cache}{$file}{short_from}    = $short_from;
  2782.     $self->{$cache}{$file}{cull}          = 0;
  2783.  
  2784.     if ( !defined( $index ) ) {
  2785.         $index = 0;
  2786.         $self->{need_resort__} = 1;
  2787.     }
  2788.  
  2789.     $self->{$cache}{$file}{index}         = $index;
  2790. }
  2791.  
  2792. # ---------------------------------------------------------------------------------------------
  2793. #
  2794. # history_cache_empty
  2795. #
  2796. # Returns whether the cache is empty or not
  2797. #
  2798. # ---------------------------------------------------------------------------------------------
  2799. sub history_cache_empty
  2800. {
  2801.     my ( $self ) = @_;
  2802.  
  2803.     return ( $self->history_size() == 0 );
  2804. }
  2805.  
  2806. # ---------------------------------------------------------------------------------------------
  2807. #
  2808. # history_size
  2809. #
  2810. # Returns the size of the history cache, note that this is actually the size of the
  2811. # history_keys array since that is used to access selected entries in the history cache
  2812. # itself
  2813. #
  2814. # ---------------------------------------------------------------------------------------------
  2815. sub history_size
  2816. {
  2817.     my ( $self ) = @_;
  2818.  
  2819.     if ( defined( $self->{history_keys__} ) ) {
  2820.         my @keys = @{$self->{history_keys__}};
  2821.  
  2822.         return ($#keys + 1);
  2823.     } else {
  2824.         return 0;
  2825.     }
  2826. }
  2827.  
  2828. # ---------------------------------------------------------------------------------------------
  2829. #
  2830. # get_history_navigator
  2831. #
  2832. # Return the HTML for the Next, Previous and page numbers for the history navigation
  2833. #
  2834. # $start_message        - The number of the first message displayed
  2835. # $stop_message         - The number of the last message displayed
  2836. #
  2837. # ---------------------------------------------------------------------------------------------
  2838. sub get_history_navigator
  2839. {
  2840.     my ( $self, $start_message, $stop_message ) = @_;
  2841.  
  2842.     my $body = "$self->{language__}{History_Jump}: ";
  2843.     if ( $start_message != 0 )  {
  2844.         $body .= "[<a href=\"/history?start_message=";
  2845.         $body .= $start_message - $self->config_( 'page_size' );
  2846.         $body .= $self->print_form_fields_(0,1,('session','filter','search','sort')) . "\">< $self->{language__}{Previous}</a>] ";
  2847.     }
  2848.  
  2849.     # Only show two pages either side of the current page, the first page and the last page
  2850.     #
  2851.     # e.g. [1] ... [4] [5] [6] [7] [8] ... [24]
  2852.  
  2853.     my $i = 0;
  2854.     my $p = 1;
  2855.     my $dots = 0;
  2856.     while ( $i < $self->history_size() ) {
  2857.         if ( ( $i == 0 ) ||
  2858.              ( ( $i + $self->config_( 'page_size' ) ) >= $self->history_size() ) ||
  2859.              ( ( ( $i - 2 * $self->config_( 'page_size' ) ) <= $start_message ) &&
  2860.                ( ( $i + 2 * $self->config_( 'page_size' ) ) >= $start_message ) ) ) {
  2861.             if ( $i == $start_message ) {
  2862.                 $body .= "<b>";
  2863.                 $body .= $p . "</b>";
  2864.             } else {
  2865.                 $body .= "[<a href=\"/history?start_message=$i" . $self->print_form_fields_(0,1,('session','filter','search','sort')). "\">";
  2866.                 $body .= $p . "</a>]";
  2867.             }
  2868.  
  2869.             $body .= " ";
  2870.             $dots = 1;
  2871.     } else {
  2872.             $body .= " ... " if $dots;
  2873.             $dots = 0;
  2874.     }
  2875.  
  2876.         $i += $self->config_( 'page_size' );
  2877.         $p++;
  2878.     }
  2879.     if ( $start_message < ( $self->history_size() - $self->config_( 'page_size' ) ) )  {
  2880.         $body .= "[<a href=\"/history?start_message=";
  2881.         $body .= $start_message + $self->config_( 'page_size' );
  2882.         $body .= $self->print_form_fields_(0,1,('session','filter','search','sort')) . "\">$self->{language__}{Next} ></a>]";
  2883.     }
  2884.  
  2885.    $body .= " (<a class=\"history\" href=\"/history?session=$self->{session_key__}&setfilter=\">$self->{language__}{Refresh}</a>)\n";
  2886.  
  2887.     return $body;
  2888. }
  2889.  
  2890. # ---------------------------------------------------------------------------------------------
  2891. #
  2892. # get_magnet_navigator
  2893. #
  2894. # Return the HTML for the Next, Previous and page numbers for magnet navigation
  2895. #
  2896. # $start_magnet  - The number of the first magnet
  2897. # $stop_magnet   - The number of the last magnet
  2898. # $magnet_count  - Total number of magnets
  2899. #
  2900. # ---------------------------------------------------------------------------------------------
  2901. sub get_magnet_navigator
  2902. {
  2903.     my ( $self, $start_magnet, $stop_magnet, $magnet_count ) = @_;
  2904.  
  2905.     my $body = "$self->{language__}{Magnet_Jump}: ";
  2906.  
  2907.     if ( $start_magnet != 0 )  {
  2908.         $body .= "[<a href=\"/magnets?start_magnet=";
  2909.         $body .= $start_magnet - $self->config_( 'page_size' );
  2910.         $body .= $self->print_form_fields_(0,1,('session')) . "\">< $self->{language__}{Previous}</a>] ";
  2911.     }
  2912.     my $i = 0;
  2913.     my $count = 0;
  2914.     while ( $i < $magnet_count ) {
  2915.         $count += 1;
  2916.         if ( $i == $start_magnet )  {
  2917.             $body .= "<b>";
  2918.             $body .= $count . "</b>";
  2919.         } else {
  2920.             $body .= "[<a href=\"/magnets?start_magnet=$i" . $self->print_form_fields_(0,1,('session')). "\">";
  2921.             $body .= $count . "</a>]";
  2922.         }
  2923.  
  2924.         $body .= " ";
  2925.         $i += $self->config_( 'page_size' );
  2926.     }
  2927.     if ( $start_magnet < ( $magnet_count - $self->config_( 'page_size' ) ) )  {
  2928.         $body .= "[<a href=\"/magnets?start_magnet=";
  2929.         $body .= $start_magnet + $self->config_( 'page_size' );
  2930.         $body .= $self->print_form_fields_(0,1,('session')) . "\">$self->{language__}{Next} ></a>]";
  2931.     }
  2932.  
  2933.     return $body;
  2934. }
  2935.  
  2936.  
  2937. # ---------------------------------------------------------------------------------------------
  2938. #
  2939. # history_reclassify - handle the reclassification of messages on the history page
  2940. #
  2941. # ---------------------------------------------------------------------------------------------
  2942. sub history_reclassify
  2943. {
  2944.     my ( $self ) = @_;
  2945.  
  2946.     if ( defined( $self->{form_}{change} ) ) {
  2947.  
  2948.         $self->{save_cache__} = 1;
  2949.  
  2950.         # This hash will map filenames of MSG files in the history to the
  2951.         # new classification that they should be, it is built by iterating
  2952.         # through the $self->{form_} looking for entries with the message number
  2953.         # of each message that is displayed and then creating an entry in
  2954.         # %messages if there is a corresponding entry in $self->{form_} for
  2955.         # that message number
  2956.  
  2957.         my %messages;
  2958.  
  2959.         foreach my $i ( $self->{form_}{start_message}  .. $self->{form_}{start_message} + $self->config_( 'page_size' ) - 1) {
  2960.             my $mail_file = $self->{history_keys__}[$i];
  2961.  
  2962.             # The first check makes sure we didn't run off the end of the history table
  2963.             # the second that there is something defined for this message number and the
  2964.             # third that this message number has a value (i.e. a bucket name)
  2965.  
  2966.             if ( defined( $mail_file ) && defined( $self->{form_}{$i} ) && ( $self->{form_}{$i} ne '' ) ) {
  2967.                 $messages{$mail_file} = $self->{form_}{$i};
  2968.             }
  2969.         }
  2970.  
  2971.         # At this point %messages maps that files that need reclassifying to their
  2972.         # new bucket classification
  2973.  
  2974.         # This hash maps buckets to list of files to place in those buckets
  2975.  
  2976.         my %work;
  2977.  
  2978.         while ( my ($mail_file, $newbucket) = each %messages ) {
  2979.  
  2980.             # Get the current classification for this message
  2981.  
  2982.             my ( $reclassified, $bucket, $usedtobe, $magnet) = $self->{classifier__}->history_read_class( $mail_file );
  2983.  
  2984.             # Only reclassify messages that haven't been reclassified before
  2985.  
  2986.             if ( !$reclassified ) {
  2987.                 push @{$work{$newbucket}}, $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file );
  2988.  
  2989.                 $self->log_( "Reclassifying $mail_file from $bucket to $newbucket" );
  2990.  
  2991.                 if ( $bucket ne $newbucket ) {
  2992.                     my $count = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $newbucket, 'count' );
  2993.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $newbucket, 'count', $count+1 );
  2994.  
  2995.                     $count = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'count' );
  2996.                     $count -= 1;
  2997.                     $count = 0 if ( $count < 0 ) ;
  2998.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'count', $count );
  2999.  
  3000.                     my $fncount = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $newbucket, 'fncount' );
  3001.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $newbucket, 'fncount', $fncount+1 );
  3002.  
  3003.                     my $fpcount = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'fpcount' );
  3004.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'fpcount', $fpcount+1 );
  3005.                 }
  3006.  
  3007.                 # Update the class file
  3008.  
  3009.                 $self->{classifier__}->history_write_class( $mail_file, 1, $newbucket, ( $bucket || "unclassified" ) , '');
  3010.  
  3011.                 # Since we have just changed the classification of this file and it has
  3012.                 # now been reclassified and has a new bucket name then we need to update the
  3013.                 # history cache to reflect that
  3014.  
  3015.                 $self->{history__}{$mail_file}{reclassified} = 1;
  3016.                 $self->{history__}{$mail_file}{bucket}       = $newbucket;
  3017.  
  3018.                 # Add message feedback
  3019.  
  3020.                 $self->{feedback}{$mail_file} = sprintf( $self->{language__}{History_ChangedTo}, $self->{classifier__}->get_bucket_color( $self->{api_session__}, $newbucket ), $newbucket );
  3021.  
  3022.                 $self->{configuration__}->save_configuration();
  3023.             }
  3024.         }
  3025.  
  3026.         # At this point the work hash maps the buckets to lists of files to reclassify, so run through
  3027.         # them doing bulk updates
  3028.  
  3029.         foreach my $newbucket (keys %work) {
  3030.             $self->{classifier__}->add_messages_to_bucket( $self->{api_session__}, $newbucket, @{$work{$newbucket}} );
  3031.         }
  3032.     }
  3033. }
  3034.  
  3035. # ---------------------------------------------------------------------------------------------
  3036. #
  3037. # history_undo - handle undoing of reclassifications of messages on the history page
  3038. #
  3039. # ---------------------------------------------------------------------------------------------
  3040. sub history_undo
  3041. {
  3042.     my( $self ) = @_;
  3043.  
  3044.     foreach my $key (keys %{$self->{form_}}) {
  3045.         if ( $key =~ /^undo_([0-9]+)$/ ) {
  3046.             my $mail_file = $self->{history_keys__}[$1];
  3047.             my %temp_corpus;
  3048.  
  3049.             # Load the class file
  3050.  
  3051.             my ( $reclassified, $bucket, $usedtobe, $magnet ) = $self->{classifier__}->history_read_class( $mail_file );
  3052.  
  3053.             # Only undo if the message has been classified...
  3054.  
  3055.             if ( defined( $usedtobe ) ) {
  3056.                 $self->{classifier__}->remove_message_from_bucket( $self->{api_session__}, $bucket, $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ) );
  3057.  
  3058.                 $self->{save_cache__} = 1;
  3059.  
  3060.                 $self->log_( "Undoing $mail_file from $bucket to $usedtobe" );
  3061.  
  3062.                 if ( $bucket ne $usedtobe ) {
  3063.                     my $count = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'count' ) - 1;
  3064.                     $count = 0 if ( $count < 0 );
  3065.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'count', $count );
  3066.  
  3067.                     $count = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $usedtobe, 'count' ) + 1;
  3068.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $usedtobe, 'count', $count );
  3069.  
  3070.                     my $fncount = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'fncount' ) - 1;
  3071.                     $fncount = 0 if ( $fncount < 0 );
  3072.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $bucket, 'fncount', $fncount );
  3073.  
  3074.                     my $fpcount = $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $usedtobe, 'fpcount' ) - 1;
  3075.                     $fpcount = 0 if ( $fpcount < 0 );
  3076.                     $self->{classifier__}->set_bucket_parameter( $self->{api_session__}, $usedtobe, 'fpcount', $fpcount );
  3077.                 }
  3078.  
  3079.                 # Since we have just changed the classification of this file and it has
  3080.                 # not been reclassified and has a new bucket name then we need to update the
  3081.                 # history cache to reflect that
  3082.  
  3083.                 $self->{history__}{$mail_file}{reclassified} = 0;
  3084.                 $self->{history__}{$mail_file}{bucket}       = $usedtobe;
  3085.  
  3086.                 # Update the class file
  3087.  
  3088.                 $self->{classifier__}->history_write_class( $mail_file, 0, ( $usedtobe || "unclassified" ), '', '');
  3089.  
  3090.                 # Add message feedback
  3091.  
  3092.                 $self->{feedback}{$mail_file} = sprintf( $self->{language__}{History_ChangedTo}, ($self->{classifier__}->get_bucket_color( $self->{api_session__}, $usedtobe ) || ''), $usedtobe );
  3093.  
  3094.                 $self->{configuration__}->save_configuration();
  3095.             }
  3096.         }
  3097.     }
  3098. }
  3099.  
  3100. # ---------------------------------------------------------------------------------------------
  3101. #
  3102. # get_search_filter_widget
  3103. #
  3104. # Returns the form that contains the fields for searching and filtering the history
  3105. # page
  3106. #
  3107. # ---------------------------------------------------------------------------------------------
  3108. sub get_search_filter_widget
  3109. {
  3110.     my ( $self ) = @_;
  3111.  
  3112.     my $body = "<form action=\"/history\">\n";
  3113.     $body .= "<label class=\"historyLabel\" for=\"historySearch\">$self->{language__}{History_SearchMessage}:</label>\n";
  3114.     $body .= "<input type=\"text\" id=\"historySearch\" name=\"search\" ";
  3115.     $body .= "value=\"$self->{form_}{search}\"" if (defined $self->{form_}{search});
  3116.     $body .= " />\n";
  3117.     $body .= "<input type=\"submit\" class=\"submit\" name=\"setsearch\" value=\"$self->{language__}{Find}\" />\n";
  3118.     $body .= "  <label class=\"historyLabel\" for=\"historyFilter\">$self->{language__}{History_FilterBy}:</label>\n";
  3119.     $body .= "<input type=\"hidden\" name=\"sort\" value=\"$self->{form_}{sort}\" />\n";
  3120.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  3121.     $body .= "<select name=\"filter\" id=\"historyFilter\">\n<option value=\"\"></option>";
  3122.     my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  3123.     foreach my $abucket (@buckets) {
  3124.         my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $abucket );
  3125.         $body .= "<option value=\"$abucket\"";
  3126.         $body .= " selected" if ( ( defined($self->{form_}{filter}) ) && ( $self->{form_}{filter} eq $abucket ) );
  3127.         $body .= " style=\"color: $bcolor\">$abucket</option>\n";
  3128.     }
  3129.     $body .= "<option value=\"__filter__magnet\"" . ($self->{form_}{filter} eq '__filter__magnet'?' selected':'') . "><$self->{language__}{History_ShowMagnet}></option>\n";
  3130.     $body .= "<option value=\"__filter__no__magnet\"" . ($self->{form_}{filter} eq '__filter__no__magnet'?' selected':'') . "><$self->{language__}{History_ShowNoMagnet}></option>\n";
  3131.     $body .= "<option value=\"unclassified\"" . ($self->{form_}{filter} eq 'unclassified'?' selected':'') . "><unclassified></option>\n";
  3132.     $body .= "</select>\n<input type=\"submit\" class=\"submit\" name=\"setfilter\" value=\"$self->{language__}{Filter}\" />\n";
  3133.     $body .= "<input type=\"submit\" class=\"submit\" name=\"reset_filter_search\" value=\"$self->{language__}{History_ResetSearch}\" />\n";
  3134.     $body .= "</form>\n";
  3135.  
  3136.     return $body;
  3137. }
  3138.  
  3139. # ---------------------------------------------------------------------------------------------
  3140. #
  3141. # history_page - get the message classification history page
  3142. #
  3143. # $client     The web browser to send the results to
  3144. #
  3145. # ---------------------------------------------------------------------------------------------
  3146. sub history_page
  3147. {
  3148.     my ( $self, $client ) = @_;
  3149.  
  3150.     # Set up default values for various form elements that have been passed
  3151.     # in or not so that we don't have to worry about undefined values later
  3152.     # on in the function
  3153.  
  3154.     $self->{form_}{sort}   = $self->{old_sort__} || '' if ( !defined( $self->{form_}{sort}   ) );
  3155.     $self->{form_}{search} = (!defined($self->{form_}{setsearch})?$self->{old_search__}:'') || '' if ( !defined( $self->{form_}{search} ) );
  3156.     $self->{form_}{filter} = (!defined($self->{form_}{setfilter})?$self->{old_filter__}:'') || '' if ( !defined( $self->{form_}{filter} ) );
  3157.  
  3158.     # If the user is asking for a new sort option then it needs to get
  3159.     # stored in the sort form variable so that it can be used for subsequent
  3160.     # page views of the History to keep the sort in place
  3161.  
  3162.     $self->{form_}{sort} = $self->{form_}{setsort} if ( defined( $self->{form_}{setsort} ) );
  3163.  
  3164.     # Cache some values to keep interface widgets updated if history is re-accessed without parameters
  3165.  
  3166.     $self->{old_sort__} = $self->{form_}{sort};
  3167.  
  3168.     # If the user hits the Reset button on a search then we need to clear
  3169.     # the search value but make it look as though they hit the search button
  3170.     # so that sort_filter_history will get called below to get the right values
  3171.     # in history_keys
  3172.  
  3173.     if ( defined( $self->{form_}{reset_filter_search} ) ) {
  3174.         $self->{form_}{filter}    = '';
  3175.         $self->{form_}{search}    = '';
  3176.         $self->{form_}{setsearch} = 1;
  3177.     }
  3178.  
  3179.     # Information from submit buttons isn't always preserved if the buttons aren't
  3180.     # pressed. This compares values in some fields and sets the button-values as
  3181.     # though they had been pressed
  3182.  
  3183.     # Set setsearch if search changed and setsearch is undefined
  3184.     $self->{form_}{setsearch} = 'on' if ( ( ( !defined($self->{old_search__}) && ($self->{form_}{search} ne '') ) || ( defined($self->{old_search__}) && ( $self->{old_search__} ne $self->{form_}{search} ) ) ) && !defined($self->{form_}{setsearch} ) );
  3185.     $self->{old_search__} = $self->{form_}{search};
  3186.  
  3187.     # Set setfilter if filter changed and setfilter is undefined
  3188.     $self->{form_}{setfilter} = 'Filter' if ( ( ( !defined($self->{old_filter__}) && ($self->{form_}{filter} ne '') ) || ( defined($self->{old_filter__}) && ( $self->{old_filter__} ne $self->{form_}{filter} ) ) ) && !defined($self->{form_}{setfilter} ) );
  3189.     $self->{old_filter__} = $self->{form_}{filter};
  3190.  
  3191.     # Set up the text that will appear at the top of the history page
  3192.     # indicating the current filter and search settings
  3193.  
  3194.     my $filter = $self->{form_}{filter};
  3195.     my $filtered = '';
  3196.     if ( !( $filter eq '' ) ) {
  3197.         if ( $filter eq '__filter__magnet' ) {
  3198.             $filtered .= $self->{language__}{History_Magnet};
  3199.         } else {
  3200.             if ( $filter eq '__filter__no__magnet' ) {
  3201.                 $filtered .= $self->{language__}{History_NoMagnet};
  3202.             } else {
  3203.                 $filtered = sprintf( $self->{language__}{History_Filter}, $self->{classifier__}->get_bucket_color( $self->{api_session__}, $self->{form_}{filter} ), $self->{form_}{filter} ) if ( $self->{form_}{filter} ne '' );
  3204.             }
  3205.         }
  3206.     }
  3207.  
  3208.     $filtered .= sprintf( $self->{language__}{History_Search}, $self->{form_}{search} ) if ( $self->{form_}{search} ne '' );
  3209.  
  3210.     # Handle the reinsertion of a message file or the user hitting the
  3211.     # undo button
  3212.  
  3213.     $self->history_reclassify();
  3214.     $self->history_undo();
  3215.  
  3216.     # Handle removal of one or more items from the history page, the remove_array form, if defined,
  3217.     # will contain all the indexes into history_keys that need to be deleted. If undefined, the remove
  3218.     # form element will contain the single index to be deleted. We pass each file that needs
  3219.     # deleting into the history_delete_file helper
  3220.  
  3221.     if ( defined( $self->{form_}{deletemessage} ) ) {
  3222.  
  3223.         # Remove the list of marked messages using the array of "remove" checkboxes, the fact
  3224.         # that deletemessage is defined will later on cause a call to sort_filter_history
  3225.         # that will reload the history_keys with the appropriate messages that now exist
  3226.         # in the cache.  Note that there is no need to invalidate the history cache since
  3227.         # we are in control of deleting messages
  3228.  
  3229.         for my $i ( keys %{$self->{form_}} ) {
  3230.         if ( $i =~ /^remove_(\d+)$/ ) {
  3231.                 $self->history_delete_file( $self->{history_keys__}[$1 - 1], 0);
  3232.         }
  3233.         }
  3234.     }
  3235.  
  3236.     # Handle clearing the history files, there are two options here, clear the current page
  3237.     # or clear all the files in the cache
  3238.  
  3239.     if ( defined( $self->{form_}{clearall} ) ) {
  3240.         foreach my $i (0 .. $self->history_size()-1 ) {
  3241.             $self->history_delete_file( $self->{history_keys__}[$i],   # PROFILE BLOCK START
  3242.                                         $self->config_( 'archive' ) ); # PROFILE BLOCK STOP
  3243.         }
  3244.     }
  3245.  
  3246.     if ( defined($self->{form_}{clearpage}) ) {
  3247.         foreach my $i ( $self->{form_}{start_message} .. $self->{form_}{start_message} + $self->config_( 'page_size' ) - 1 ) {
  3248.             if ( defined( $self->{history_keys__}[$i] ) ) {
  3249.                 $self->history_delete_file( $self->{history_keys__}[$i],   # PROFILE BLOCK START
  3250.                                             $self->config_( 'archive' ) ); # PROFILE BLOCK STOP
  3251.             }
  3252.         }
  3253.  
  3254.         # Check that the start_message now exists, if not then go back a page
  3255.  
  3256.         while ( ( $self->{form_}{start_message} + $self->config_( 'page_size' ) ) >= $self->history_size() ) {
  3257.             $self->{form_}{start_message} -= $self->config_( 'page_size' );
  3258.     }
  3259.     }
  3260.  
  3261.     $self->copy_pre_cache__();
  3262.  
  3263.     # If the history cache is invalid then we need to reload it and then if
  3264.     # any of the sort, search or filter options have changed they must be
  3265.     # applied.  The watch word here is to avoid doing work
  3266.  
  3267.     $self->sort_filter_history( $self->{form_}{filter}, # PROFILE BLOCK START
  3268.                                 $self->{form_}{search},
  3269.                                 $self->{form_}{sort} ) if ( ( defined( $self->{form_}{setfilter}     ) ) ||
  3270.                                                             ( defined( $self->{form_}{setsort}       ) ) ||
  3271.                                                             ( defined( $self->{form_}{setsearch}     ) ) ||
  3272.                                                             ( defined( $self->{form_}{deletemessage} ) ) ||
  3273.                                                             ( defined( $self->{form_}{clearall}      ) ) ||
  3274.                                                             ( defined( $self->{form_}{clearpage}     ) ) ||
  3275.                                                             ( $self->{need_resort__} == 1 )            );      # PROFILE BLOCK STOP
  3276.  
  3277.     # Redirect somewhere safe if non-idempotent action has been taken
  3278.  
  3279.     if ( defined( $self->{form_}{deletemessage}  ) ||  # PROFILE BLOCK START
  3280.          defined( $self->{form_}{clearpage}      ) ||
  3281.          defined( $self->{form_}{undo}           ) ||
  3282.          defined( $self->{form_}{reclassify}     ) ) { # PROFILE BLOCK STOP
  3283.         return $self->http_redirect_( $client, "/history?" . $self->print_form_fields_(1,0,('start_message','filter','search','sort','session') ) );
  3284.     }
  3285.  
  3286.     my $body    = '';
  3287.  
  3288.     if ( !$self->history_cache_empty() )  {
  3289.         my $start_message = 0;
  3290.  
  3291.         $start_message = $self->{form_}{start_message} if ( ( defined($self->{form_}{start_message}) ) && ($self->{form_}{start_message} > 0 ) );
  3292.         $self->{form_}{start_message} = $start_message;
  3293.         my $stop_message  = $start_message + $self->config_( 'page_size' ) - 1;
  3294.         $stop_message = $self->history_size() - 1 if ( $stop_message >= $self->history_size() );
  3295.  
  3296.         if ( $self->config_( 'page_size' ) <= $self->history_size() ) {
  3297.             $body .= "<table width=\"100%\" summary=\"\">\n<tr>\n<td align=\"left\">\n";
  3298.             # title
  3299.             $body .= "<h2 class=\"history\">$self->{language__}{History_Title}$filtered</h2>\n</td>\n";
  3300.             # navigator
  3301.             $body .= "<td class=\"historyNavigatorTop\">\n";
  3302.             $body .= get_history_navigator( $self, $start_message, $stop_message );
  3303.             $body .= "</td>\n</tr>\n</table>\n";
  3304.         } else {
  3305.             $body .="<h2 class=\"history\">$self->{language__}{History_Title}$filtered</h2>\n";
  3306.         }
  3307.  
  3308.         # History widgets top
  3309.         $body .= "<table class=\"historyWidgetsTop\" summary=\"\">\n<tr>\n";
  3310.  
  3311.         # Search From/Subject widget
  3312.         my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  3313.         $body .= "<td colspan=\"5\" valign=middle>\n";
  3314.         $body .= $self->get_search_filter_widget();
  3315.         $body .= "</td>\n</tr>\n</table>\n";
  3316.  
  3317.         # History page main form
  3318.  
  3319.         $body .= "<form id=\"HistoryMainForm\" action=\"/history\" method=\"POST\">\n";
  3320.         $body .= "<input type=\"hidden\" name=\"search\" value=\"$self->{form_}{search}\" />\n";
  3321.         $body .= "<input type=\"hidden\" name=\"sort\" value=\"$self->{form_}{sort}\" />\n";
  3322.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  3323.         $body .= "<input type=\"hidden\" name=\"start_message\" value=\"$start_message\" />\n";
  3324.         $body .= "<input type=\"hidden\" name=\"filter\" value=\"$self->{form_}{filter}\" />\n";
  3325.  
  3326.         # History messages
  3327.         $body .= "<table class=\"historyTable\" width=\"100%\" summary=\"$self->{language__}{History_MainTableSummary}\">\n";
  3328.  
  3329.         # Column headers
  3330.  
  3331.         my %headers_table = ( '',        'ID',              # PROFILE BLOCK START
  3332.                               'from',    'From',
  3333.                               'subject', 'Subject',
  3334.                               'bucket',  'Classification'); # PROFILE BLOCK STOP
  3335.  
  3336.         $body .= "<tr valign=\"bottom\">\n";
  3337.  
  3338.         # It would be tempting to do keys %headers_table here but there is not guarantee that
  3339.         # they will come back in the right order
  3340.  
  3341.         foreach my $header ('', 'from', 'subject', 'bucket') {
  3342.             $body .= "<th class=\"historyLabel\" scope=\"col\">\n";
  3343.             $body .= "<a href=\"/history?" . $self->print_form_fields_(1,1,('filter','session','search')) . "&setsort=" . ($self->{form_}{sort} eq "$header"?"-":"");
  3344.             $body .= "$header\">";
  3345.  
  3346.             my $label = '';
  3347.             if ( defined $self->{language__}{ $headers_table{$header} }) {
  3348.                 $label = $self->{language__}{ $headers_table{$header} };
  3349.             } else {
  3350.                 $label = $headers_table{$header};
  3351.             }
  3352.  
  3353.             if ( $self->{form_}{sort} =~ /^\-?\Q$header\E$/ ) {
  3354.                 $body .= "<em class=\"historyLabelSort\">" . ($self->{form_}{sort} =~ /^-/ ? "<" : ">") . "$label</em>";
  3355.             } else {
  3356.                 $body .= "$label";
  3357.             }
  3358.             $body .= "</a>\n</th>\n";
  3359.         }
  3360.  
  3361.         $body .= "<th class=\"historyLabel\" scope=\"col\"><input type=\"submit\" class=\"reclassifyButton\" name=\"change\" value=\"$self->{language__}{Reclassify}\" /></th>\n";
  3362.         $body .= "<th class=\"historyLabel\" scope=\"col\"><input type=\"submit\" class=\"deleteButton\" name=\"deletemessage\" value=\"$self->{language__}{Remove}\" /></th>\n</tr>\n";
  3363.  
  3364.         my $stripe = 0;
  3365.  
  3366.         foreach my $i ($start_message ..  $stop_message) {
  3367.             my $mail_file     = $self->{history_keys__}[$i];
  3368.             my $from          = $self->{history__}{$mail_file}{from};
  3369.             my $subject       = $self->{history__}{$mail_file}{subject};
  3370.             my $short_from    = $self->{history__}{$mail_file}{short_from};
  3371.             my $short_subject = $self->{history__}{$mail_file}{short_subject};
  3372.             my $bucket        = $self->{history__}{$mail_file}{bucket};
  3373.             my $reclassified  = $self->{history__}{$mail_file}{reclassified};
  3374.             my $index         = $self->{history__}{$mail_file}{index} + 1;
  3375.  
  3376.             $body .= "<tr";
  3377.             $body .= " class=\"";
  3378.             $body .= $stripe?"rowEven\"":"rowOdd\"";
  3379.  
  3380.             $stripe = 1 - $stripe;
  3381.  
  3382.             $body .= ">\n<td>";
  3383.             $body .= "<a name=\"$mail_file\"></a>";
  3384.             $body .= $index . "</td>\n<td>";
  3385.             $mail_file =~ /popfile\d+=(\d+)\.msg$/;
  3386.             $body .= "<a title=\"$from\">$short_from</a></td>\n";
  3387.             $body .= "<td><a class=\"messageLink\" title=\"$subject\" href=\"/view?view=$mail_file" . $self->print_form_fields_(0,1,('start_message','session','filter','search','sort')) . "\">";
  3388.             $body .= "$short_subject</a></td>\n<td>";
  3389.             my $sbs = ($bucket ne 'unclassified')?"<a href=\"buckets?session=$self->{session_key__}&showbucket=$bucket\">":'';
  3390.             my $sbe = ($bucket ne 'unclassified')?'</a>':'';
  3391.             if ( $reclassified )  {
  3392.                 $body .= "$sbs<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\">$bucket</font>$sbe</td>\n<td>";
  3393.                 $body .= sprintf( $self->{language__}{History_Already}, ($self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) || ''), ($bucket || '') );
  3394.                 $body .= " <input type=\"submit\" class=\"undoButton\" name=\"undo_$i\" value=\"$self->{language__}{Undo}\">\n";
  3395.             } else {
  3396.                 $body .= "$sbs<font color=\"" . $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket ) . "\">$bucket</font>$sbe</td>\n<td>";
  3397.  
  3398.                 if ( $self->{history__}{$mail_file}{magnet} eq '' )  {
  3399.                     $body .= "\n<select name=\"$i\">\n";
  3400.  
  3401.                     # Show a blank bucket field
  3402.                     $body .= "<option selected=\"selected\"></option>\n";
  3403.  
  3404.                     foreach my $abucket (@buckets) {
  3405.                         my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $abucket );
  3406.                         $body .= "<option value=\"$abucket\" style=\"color: $bcolor\">$abucket</option>\n";
  3407.                     }
  3408.                     $body .= "</select>\n";
  3409.                 } else {
  3410.                     $body .= " ($self->{language__}{History_MagnetUsed}: " . $self->{history__}{$mail_file}{magnet} . ")";
  3411.                 }
  3412.             }
  3413.  
  3414.             $body .= "</td>\n<td>\n";
  3415.             $body .= "<label class=\"removeLabel\" for=\"remove_" . ( $i+1 ) . "\">$self->{language__}{Remove}</label>\n";
  3416.             $body .= "<input type=\"checkbox\" id=\"remove_" . ( $i+1 ) . "\" class=\"checkbox\" name=\"remove_" . ($i+1) . "\"/>\n";
  3417.             $body .= "</td>\n</tr>\n";
  3418.  
  3419.  
  3420.             if ( defined $self->{feedback}{$mail_file} ) {
  3421.                 $body .= "<tr class=\"rowHighlighted\"><td> </td><td>$self->{feedback}{$mail_file}</td>\n";
  3422.                 delete $self->{feedback}{$mail_file};
  3423.             }
  3424.         }
  3425.  
  3426.         $body .= "<tr><td> </td><td> </td><td> </td><td> </td><td><input type=\"submit\" class=\"reclassifyButton\" name=\"change\" value=\"$self->{language__}{Reclassify}\" />\n</td><td><input type=\"submit\" class=\"deleteButton\" name=\"deletemessage\" value=\"$self->{language__}{Remove}\" />\n</td></tr>\n";
  3427.  
  3428.         $body .= "</table>\n";
  3429.  
  3430.         #END main history form
  3431.  
  3432.         $body .= "</form>\n";
  3433.  
  3434.         # History buttons bottom
  3435.         $body .= "<table class=\"historyWidgetsBottom\" summary=\"\">\n<tr>\n<td>\n";
  3436.         $body .= "<form action=\"/history\">\n<input type=\"hidden\" name=\"filter\" value=\"$self->{form_}{filter}\" />\n";
  3437.         $body .= "<input type=\"hidden\" name=\"sort\" value=\"$self->{form_}{sort}\" />\n";
  3438.         $body .= "<span class=\"historyLabel\">$self->{language__}{History_Remove}: </span>\n";
  3439.         $body .= "<input type=\"submit\" class=\"submit\" name=\"clearall\" value=\"$self->{language__}{History_RemoveAll}\" />\n";
  3440.         $body .= "<input type=\"submit\" class=\"submit\" name=\"clearpage\" value=\"$self->{language__}{History_RemovePage}\" />\n";
  3441.         $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  3442.         $body .= "<input type=\"hidden\" name=\"start_message\" value=\"$start_message\" />\n</form>\n";
  3443.         $body .= "</td>\n</tr>\n</table>\n";
  3444.  
  3445.         # navigator
  3446.         $body .= "<table width=\"100%\" summary=\"\">\n<tr>\n<td class=\"historyNavigatorBottom\">\n";
  3447.         $body .= get_history_navigator( $self, $start_message, $stop_message ) if ( $self->config_( 'page_size' ) <= $self->history_size() );
  3448.         $body .= "\n</td>\n</tr>\n</table>\n";
  3449.     } else {
  3450.         $body .= "<h2 class=\"history\">$self->{language__}{History_Title}$filtered</h2><br /><br /><span class=\"bucketsLabel\">$self->{language__}{History_NoMessages}.</span><br /><br />";
  3451.         $body .= $self->get_search_filter_widget();
  3452.     }
  3453.  
  3454.     http_ok($self, $client,$body,2);
  3455. }
  3456.  
  3457. # ---------------------------------------------------------------------------------------------
  3458. #
  3459. # view_page - Shows a single email
  3460. #
  3461. # $client     The web browser to send the results to
  3462. #
  3463. # ---------------------------------------------------------------------------------------------
  3464. sub view_page
  3465. {
  3466.     my ( $self, $client ) = @_;
  3467.  
  3468.     my $mail_file     = $self->{form_}{view};
  3469.     my $start_message = $self->{form_}{start_message} || 0;
  3470.     my $reclassified  = $self->{history__}{$mail_file}{reclassified};
  3471.     my $bucket        = $self->{history__}{$mail_file}{bucket};
  3472.     my $color         = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  3473.     my $page_size     = $self->config_( 'page_size' );
  3474.  
  3475.     $self->{form_}{sort}   = '' if ( !defined( $self->{form_}{sort}   ) );
  3476.     $self->{form_}{search} = '' if ( !defined( $self->{form_}{search} ) );
  3477.     $self->{form_}{filter} = '' if ( !defined( $self->{form_}{filter} ) );
  3478.     $self->{form_}{format} = $self->config_( 'wordtable_format' ) if ( !defined( $self->{form_}{format} ) );
  3479.  
  3480.     # If a format change was requested for the word matrix, record it in the
  3481.     # configuration and in the classifier options.
  3482.  
  3483.     $self->{classifier__}->wmformat( $self->{form_}{format} );
  3484.  
  3485.     my $index = -1;
  3486.  
  3487.     foreach my $i ( 0 .. $self->history_size()-1 ) {
  3488.         if ( $self->{history_keys__}[$i] eq $mail_file ) {
  3489.             use integer;
  3490.             $index         = $i;
  3491.             $start_message = ($i / $page_size ) * $page_size;
  3492.             $self->{form_}{start_message} = $start_message;
  3493.             last;
  3494.         }
  3495.     }
  3496.  
  3497.     my $body = "<table width=\"100%\" summary=\"\">\n<tr>\n<td align=\"left\">\n";
  3498.  
  3499.     # title
  3500.     $body .= "<h2 class=\"buckets\">$self->{language__}{View_Title}</h2>\n</td>\n";
  3501.  
  3502.     # navigator
  3503.     $body .= "<td class=\"historyNavigatorTop\">\n";
  3504.  
  3505.     if ( $index > 0 ) {
  3506.         $body .= "<a href=\"/view?view=" . $self->{history_keys__}[ $index - 1 ];
  3507.         $body .= "&start_message=". ((( $index - 1 ) >= $start_message )?$start_message:($start_message - $page_size));
  3508.         $body .= $self->print_form_fields_(0,1,('filter','session','search','sort')) . "\">< ";
  3509.         $body .= $self->{language__}{Previous};
  3510.         $body .= "</a> ";
  3511.     }
  3512.  
  3513.     if ( $index < ( $self->history_size() - 1 ) ) {
  3514.         $body .= "<a href=\"/view?view=" . $self->{history_keys__}[ $index + 1 ];
  3515.         $body .= "&start_message=". ((( $index + 1 ) < ( $start_message + $page_size ) )?$start_message:($start_message + $page_size));
  3516.         $body .= $self->print_form_fields_(0,1,('filter','session','search','sort')) . "\"> ";
  3517.         $body .= $self->{language__}{Next};
  3518.         $body .= " ></a>";
  3519.     }
  3520.  
  3521.     $body .= "</td>\n";
  3522.  
  3523.     $body .= "<td class=\"openMessageCloser\">";
  3524.     $body .= "<a class=\"messageLink\" href=\"/history?" . $self->print_form_fields_(1,1,('start_message','filter','session','search','sort')) . "\">\n";
  3525.     $body .= "<span class=\"historyLabel\">$self->{language__}{Close}</span>\n</a>\n";
  3526.     $body .= "</td>\n</tr>\n</table>\n";
  3527.  
  3528.     # message
  3529.  
  3530.     $body .= "<table class=\"openMessageTable\" cellpadding=\"10%\" cellspacing=\"0\" width=\"100%\" summary=\"$self->{language__}{History_OpenMessageSummary}\">\n";
  3531.  
  3532.     $body .= "<tr><td>";
  3533.     $body .= "<form id=\"HistoryMainForm\" action=\"/history\" method=\"POST\">\n";
  3534.     $body .= "<input type=\"hidden\" name=\"search\" value=\"$self->{form_}{search}\" />\n";
  3535.     $body .= "<input type=\"hidden\" name=\"sort\" value=\"$self->{form_}{sort}\" />\n";
  3536.     $body .= "<input type=\"hidden\" name=\"session\" value=\"$self->{session_key__}\" />\n";
  3537.     $body .= "<input type=\"hidden\" name=\"start_message\" value=\"$start_message\" />\n";
  3538.     $body .= "<input type=\"hidden\" name=\"filter\" value=\"$self->{form_}{filter}\" />\n";
  3539.     $body .= "<table align=left>";
  3540.     $body .= "<tr><td><font size=+1><b>$self->{language__}{From}</b>: </font></td><td><font size=+1>$self->{history__}{$mail_file}{from}</font></td></tr>";
  3541.     $body .= "<tr><td><font size=+1><b>$self->{language__}{Subject}</b>: </font></td><td><font size=+1>$self->{history__}{$mail_file}{subject}</font></td></tr>";
  3542.     $body .= "<tr><td><font size=+1><b>$self->{language__}{Classification}</b>: </font></td><td><font size=+1><font color=\"$color\">$self->{history__}{$mail_file}{bucket}</font></font></td></tr>";
  3543.  
  3544.     $body .= "<tr><td colspan=2><font size=+1>";
  3545.  
  3546.     if ( $reclassified ) {
  3547.         $body .= sprintf( $self->{language__}{History_Already}, ($color || ''), ($bucket || '') );
  3548.         $body .= " <input type=\"submit\" class=\"undoButton\" name=\"undo_$index\" value=\"$self->{language__}{Undo}\">\n";
  3549.     } else {
  3550.         if ( $self->{history__}{$mail_file}{magnet} eq '' ) {
  3551.                 $body .= "\n$self->{language__}{History_ShouldBe}: <select name=\"$index\">\n";
  3552.  
  3553.                 # Show a blank bucket field
  3554.                 $body .= "<option selected=\"selected\"></option>\n";
  3555.  
  3556.                 foreach my $abucket ($self->{classifier__}->get_buckets( $self->{api_session__} )) {
  3557.                     my $bcolor = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $abucket );
  3558.                     $body .= "<option value=\"$abucket\" style=\"color: $bcolor\">$abucket</option>\n";
  3559.                 }
  3560.                 $body .= "</select>\n<input type=\"submit\" class=\"reclassifyButton\" name=\"change\" value=\"$self->{language__}{Reclassify}\" />";
  3561.         } else {
  3562.                 $body .= " ($self->{language__}{History_MagnetUsed}: " . $self->{history__}{$mail_file}{magnet} . ")";
  3563.         }
  3564.     }
  3565.  
  3566.     $body .= "</font></td></tr>";
  3567.     $body .= "</table></form>";
  3568.     $body .= "</td></tr>";
  3569.  
  3570.     # Message body
  3571.     $body .= "<tr>\n<td class=\"openMessageBody\"><hr><p>";
  3572.  
  3573.     my $fmtlinks;
  3574.  
  3575.     if ( $self->{history__}{$mail_file}{magnet} eq '' ) {
  3576.  
  3577.         my %matrix;
  3578.         my %idmap;
  3579.  
  3580.         # Enable saving of word-scores
  3581.  
  3582.         $self->{classifier__}->wordscores( 1 );
  3583.  
  3584.         # Build the scores by classifying the message, since get_html_colored_message has parsed the message
  3585.         # for us we do not need to parse it again and hence we pass in undef for the filename
  3586.  
  3587.         $self->{classifier__}->classify( $self->{api_session__}, $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ), $self, \%matrix, \%idmap );
  3588.  
  3589.         # Disable, print, and clear saved word-scores
  3590.  
  3591.         $self->{classifier__}->wordscores( 0 );
  3592.  
  3593.         $body .= $self->{classifier__}->fast_get_html_colored_message(
  3594.             $self->{api_session__}, $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ), \%matrix, \%idmap );
  3595.  
  3596.         # We want to insert a link to change the output format at the start of the word
  3597.         # matrix.  The classifier puts a comment in the right place, which we can replace
  3598.         # by the link.  (There's probably a better way.)
  3599.  
  3600.         my $view = $self->{language__}{View_WordProbabilities};
  3601.         if ( $self->{form_}{format} eq 'freq' ) {
  3602.             $view = $self->{language__}{View_WordFrequencies};
  3603.     }
  3604.         if ( $self->{form_}{format} eq 'score' ) {
  3605.             $view = $self->{language__}{View_WordScores};
  3606.     }
  3607.  
  3608.         if ( $self->{form_}{format} ne '' ) {
  3609.             $fmtlinks = "<table width=\"100%\">\n<td class=\"top20\" align=\"left\"><b>$self->{language__}{View_WordMatrix} ($view)</b></td>\n<td class=\"historyNavigatorTop\">\n";
  3610.     }
  3611.         if ($self->{form_}{format} ne 'freq' ) {
  3612.             $fmtlinks .= "<a href=\"/view?view=" . $self->{history_keys__}[ $index ];
  3613.             $fmtlinks .= "&start_message=". ((( $index ) >= $start_message )?$start_message:($start_message - $self->config_( 'page_size' )));
  3614.             $fmtlinks .= $self->print_form_fields_(0,1,('filter','session','search','sort')) . "&format=freq#scores\"> ";
  3615.             $fmtlinks .= $self->{language__}{View_ShowFrequencies};
  3616.             $fmtlinks .= "</a>  \n";
  3617.         }
  3618.         if ($self->{form_}{format} ne 'prob' ) {
  3619.             $fmtlinks .= "<a href=\"/view?view=" . $self->{history_keys__}[ $index ];
  3620.             $fmtlinks .= "&start_message=". ((( $index ) >= $start_message )?$start_message:($start_message - $self->config_( 'page_size' )));
  3621.             $fmtlinks .= $self->print_form_fields_(0,1,('filter','session','search','sort')) . "&format=prob#scores\"> ";
  3622.             $fmtlinks .= $self->{language__}{View_ShowProbabilities};
  3623.             $fmtlinks .= "</a>  \n";
  3624.         }
  3625.         if ($self->{form_}{format} ne 'score' ) {
  3626.             $fmtlinks .= "<a href=\"/view?view=" . $self->{history_keys__}[ $index ];
  3627.             $fmtlinks .= "&start_message=". ((( $index ) >= $start_message )?$start_message:($start_message - $self->config_( 'page_size' )));
  3628.             $fmtlinks .= $self->print_form_fields_(0,1,('filter','session','search','sort')) . "&format=score#scores\"> ";
  3629.             $fmtlinks .= $self->{language__}{View_ShowScores};
  3630.             $fmtlinks .= "</a> \n";
  3631.         }
  3632.         if ( $self->{form_}{format} ne '' ) {
  3633.             $fmtlinks .= "</a></td></table>";
  3634.     }
  3635.     } else {
  3636.         $self->{history__}{$mail_file}{magnet} =~ /(.+): ([^\r\n]+)/;
  3637.         my $header = $1;
  3638.         my $text   = $2;
  3639.         $body .= "<tt>";
  3640.  
  3641.         open MESSAGE, '<' . $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file );
  3642.         my $line;
  3643.         # process each line of the message
  3644.         while ($line = <MESSAGE>) {
  3645.             $line =~ s/</</g;
  3646.             $line =~ s/>/>/g;
  3647.  
  3648.             $line =~ s/([^\r\n]{100,150} )/$1<br \/>/g;
  3649.             $line =~ s/([^ \r\n]{150})/$1<br \/>/g;
  3650.             $line =~ s/[\r\n]+/<br \/>/g;
  3651.  
  3652.             if ( $line =~ /^([A-Za-z-]+): ?([^\n\r]*)/ ) {
  3653.                 my $head = $1;
  3654.                 my $arg  = $2;
  3655.  
  3656.                 if ( $head =~ /\Q$header\E/i ) {
  3657.  
  3658.                     $text =~ s/</</g;
  3659.                     $text =~ s/>/>/g;
  3660.  
  3661.                     if ( $arg =~ /\Q$text\E/i ) {
  3662.                           my $new_color = $self->{classifier__}->get_bucket_color( $self->{api_session__}, $bucket );
  3663.                           $line =~ s/(\Q$text\E)/<b><font color=\"$new_color\">$1<\/font><\/b>/;
  3664.                     }
  3665.                 }
  3666.             }
  3667.  
  3668.             $body .= $line;
  3669.         }
  3670.         close MESSAGE;
  3671.         $body .= "</tt>\n";
  3672.     }
  3673.  
  3674.     $body .= "</td>\n</tr>\n";
  3675.  
  3676.     $body .= "<tr><td class=\"top20\" valign=\"top\">\n";
  3677.  
  3678.     if ($self->{history__}{$mail_file}{magnet} eq '') {
  3679.          my $score_text = $self->{classifier__}->scores();
  3680.          $score_text =~ s/\<\!--format--\>/$fmtlinks/;
  3681.          $body .= $score_text;
  3682.          $self->{classifier__}->scores('');
  3683.     } else {
  3684.         $body .= sprintf( $self->{language__}{History_MagnetBecause},                                # PROFILE BLOCK START
  3685.                           $color, $bucket,
  3686.                           Classifier::MailParse::splitline($self->{history__}{$mail_file}{magnet},0)
  3687.                           );                                                                         # PROFILE BLOCK STOP
  3688.     }
  3689.  
  3690.     # Close button
  3691.  
  3692.     $body .= "<tr>\n<td class=\"openMessageCloser\">";
  3693.     $body .= "<a class=\"messageLink\" href=\"/history?" . $self->print_form_fields_(1,1,('start_message','filter','session','search','sort')). "\">\n";
  3694.     $body .= "<span class=\"historyLabel\">$self->{language__}{Close}</span>\n</a>\n";
  3695.     $body .= "</td>\n</tr>\n";
  3696.  
  3697.     $body .= "</table>";
  3698.  
  3699.     $self->http_ok( $client, $body, 2 );
  3700. }
  3701.  
  3702. # ---------------------------------------------------------------------------------------------
  3703. #
  3704. # password_page - Simple page asking for the POPFile password
  3705. #
  3706. # $client     The web browser to send the results to
  3707. #
  3708. # ---------------------------------------------------------------------------------------------
  3709. sub password_page
  3710. {
  3711.     my ( $self, $client, $error, $redirect ) = @_;
  3712.     my $session_temp = $self->{session_key__};
  3713.  
  3714.     # Show a page asking for the password with no session key information on it
  3715.     $self->{session_key__} = '';
  3716.     my $body = "<h2 class=\"password\">$self->{language__}{Password_Title}</h2>\n<form action=\"/password\" method=\"post\">\n";
  3717.     $body .= "<label class=\"passwordLabel\" for=\"thePassword\">$self->{language__}{Password_Enter}: </label>\n";
  3718.     $body .= "<input type=\"hidden\" name=\"redirect\" value=\"$redirect\" />\n";
  3719.     $body .= "<input type=\"password\" id=\"thePassword\" name=\"password\" />\n";
  3720.     $body .= "<input type=\"submit\" class=\"submit\" name=\"submit\" value=\"$self->{language__}{Password_Go}\" />\n</form>\n";
  3721.     $body .= "<blockquote>\n<div class=\"error02\">$self->{language__}{Password_Error1}</div>\n</blockquote>" if ( $error == 1 );
  3722.     http_ok($self, $client, $body, 99);
  3723.     $self->{session_key__} = $session_temp;
  3724. }
  3725.  
  3726. # ---------------------------------------------------------------------------------------------
  3727. #
  3728. # session_page - Simple page information the user of a bad session key
  3729. #
  3730. # $client     The web browser to send the results to
  3731. #
  3732. # ---------------------------------------------------------------------------------------------
  3733. sub session_page
  3734. {
  3735.     my ( $self, $client ) = @_;
  3736.     http_ok($self, $client, "<h2 class=\"session\">$self->{language__}{Session_Title}</h2><br /><br />$self->{language__}{Session_Error}", 99);
  3737. }
  3738.  
  3739. # ---------------------------------------------------------------------------------------------
  3740. #
  3741. # load_skins
  3742. #
  3743. # Gets the names of all the CSS files in the skins subdirectory and loads them into the skins
  3744. # array.  The directory and .css portion of the file name is removed to give a simple name
  3745. #
  3746. # ---------------------------------------------------------------------------------------------
  3747. sub load_skins
  3748. {
  3749.     my ( $self ) = @_;
  3750.  
  3751.     @{$self->{skins__}} = glob $self->get_root_path_( 'skins/*.css' );
  3752.  
  3753.     for my $i (0..$#{$self->{skins__}}) {
  3754.         $self->{skins__}[$i] =~ s/.*\/(.+)\.css/$1/;
  3755.     }
  3756. }
  3757.  
  3758. # ---------------------------------------------------------------------------------------------
  3759. #
  3760. # load_languages
  3761. #
  3762. # Get the names of the available languages for the user interface
  3763. #
  3764. # ---------------------------------------------------------------------------------------------
  3765. sub load_languages
  3766. {
  3767.     my ( $self ) = @_;
  3768.  
  3769.     @{$self->{languages__}} = glob $self->get_root_path_( 'languages/*.msg' );
  3770.  
  3771.     for my $i (0..$#{$self->{languages__}}) {
  3772.         $self->{languages__}[$i] =~ s/.*\/(.+)\.msg$/$1/;
  3773.     }
  3774. }
  3775.  
  3776. # ---------------------------------------------------------------------------------------------
  3777. #
  3778. # change_session_key
  3779. #
  3780. # Changes the session key, the session key is a randomly chosen 6 to 10 character key that
  3781. # protects and identifies sessions with the POPFile user interface.  At the current time
  3782. # it is primarily used for two purposes: to prevent a malicious user telling the browser to
  3783. # hit a specific URL causing POPFile to do something undesirable (like shutdown) and to
  3784. # handle the password mechanism: if the session key is wrong the password challenge is
  3785. # made.
  3786. #
  3787. # The characters valid in the session key are A-Z, a-z and 0-9
  3788. #
  3789. # ---------------------------------------------------------------------------------------------
  3790. sub change_session_key
  3791. {
  3792.     my ( $self ) = @_;
  3793.  
  3794.     my @chars = ( 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',   # PROFILE BLOCK START
  3795.                   'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'U', 'V', 'W', 'X', 'Y',
  3796.                   'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A' ); # PROFILE BLOCK STOP
  3797.  
  3798.     $self->{session_key__} = '';
  3799.  
  3800.     my $length = int( 6 + rand(4) );
  3801.  
  3802.     for my $i (0 .. $length) {
  3803.         my $random = $chars[int( rand(36) )];
  3804.  
  3805.         # Just to add spice to things we sometimes lowercase the value
  3806.  
  3807.         if ( rand(1) < rand(1) ) {
  3808.             $random = lc($random);
  3809.         }
  3810.  
  3811.         $self->{session_key__} .= $random;
  3812.     }
  3813. }
  3814.  
  3815. # ---------------------------------------------------------------------------------------------
  3816. #
  3817. # load_language
  3818. #
  3819. # Fill the language hash with the language strings that are from the named language file
  3820. #
  3821. # $lang    - The language to load (no .msg extension)
  3822. #
  3823. # ---------------------------------------------------------------------------------------------
  3824. sub load_language
  3825. {
  3826.     my ( $self, $lang ) = @_;
  3827.  
  3828.     if ( open LANG, '<' . $self->get_root_path_( "languages/$lang.msg" ) ) {
  3829.         while ( <LANG> ) {
  3830.             next if ( /[ \t]*#/ );
  3831.  
  3832.             if ( /([^\t ]+)[ \t]+(.+)/ ) {
  3833.                 my $id  = $1;
  3834.                 my $msg = ($self->config_( 'test_language' ))?$1:$2;
  3835.                 $msg =~ s/[\r\n]//g;
  3836.  
  3837.                 $self->{language__}{$id} = $msg;
  3838.             }
  3839.         }
  3840.         close LANG;
  3841.     }
  3842. }
  3843.  
  3844. # ---------------------------------------------------------------------------------------------
  3845. #
  3846. # copy_pre_cache__
  3847. #
  3848. # Copies the history_pre_cache into the history
  3849. #
  3850. # ---------------------------------------------------------------------------------------------
  3851. sub copy_pre_cache__
  3852. {
  3853.     my ($self) = @_;
  3854.  
  3855.     # Copy the history pre-cache over AFTER any possibly index-based remove operations are complete
  3856.  
  3857.     my $index = $self->history_size() + 1;
  3858.     my $added = 0;
  3859.     foreach my $file (sort compare_mf keys %{$self->{history_pre_cache__}} ) {
  3860.         $self->{history__}{$file} = $self->{history_pre_cache__}{$file};
  3861.         $self->{history__}{$file}{index} = $index;
  3862.         $index += 1;
  3863.         $added = 1;
  3864.         delete $self->{history_pre_cache__}{$file};
  3865.         $self->{save_cache__} = 1;
  3866.     }
  3867.  
  3868.     $self->{history_pre_cache__} = {};
  3869.     $self->sort_filter_history( '', '', '' ) if ( $added );
  3870. }
  3871.  
  3872. # ---------------------------------------------------------------------------------------------
  3873. #
  3874. # remove_mail_files - Remove old popfile saved mail files
  3875. #
  3876. # Removes the popfile*.msg files that are older than a number of days configured as
  3877. # history_days.
  3878. #
  3879. # ---------------------------------------------------------------------------------------------
  3880. sub remove_mail_files
  3881. {
  3882.     my ( $self ) = @_;
  3883.  
  3884.     opendir MESSAGES, $self->get_user_path_( $self->global_config_( 'msgdir' ) );
  3885.  
  3886.     while ( my $mail_file = readdir MESSAGES ) {
  3887.         if ( $mail_file =~ /popfile(\d+)=\d+\.msg$/ ) {
  3888.             my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat( $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ) );
  3889.  
  3890.             if ( $ctime < (time - $self->config_( 'history_days' ) * $seconds_per_day) )  {
  3891.                 $self->history_delete_file( $mail_file, $self->config_( 'archive' ) );
  3892.                 $self->{need_resort__} = 1;
  3893.             }
  3894.         }
  3895.     }
  3896.  
  3897.     closedir MESSAGES;
  3898.  
  3899.     # Clean up old style msg/cls files
  3900.  
  3901.     my @mail_files = glob( $self->get_user_path_( $self->global_config_( 'msgdir' ) . "popfile*_*.???" ) );
  3902.  
  3903.     foreach my $mail_file (@mail_files) {
  3904.         unlink($mail_file);
  3905.     }
  3906. }
  3907.  
  3908. # ---------------------------------------------------------------------------------------------
  3909. #
  3910. # calculate_today - set the global $self->{today} variable to the current day in seconds
  3911. #
  3912. # ---------------------------------------------------------------------------------------------
  3913. sub calculate_today
  3914. {
  3915.     my ( $self ) = @_;
  3916.  
  3917.     $self->{today} = int( time / $seconds_per_day ) * $seconds_per_day;
  3918. }
  3919.  
  3920. # ---------------------------------------------------------------------------------------------
  3921. #
  3922. # history_delete_file   - Handle the deletion of archived message files. Deletes .cls
  3923. #                           files related to any .msg file.
  3924. #
  3925. # $mail_file    - The filename to delete with or without the directory prefix
  3926. # $archive      - Boolean, whether or not to save the file as part of an archive
  3927. #
  3928. # ---------------------------------------------------------------------------------------------
  3929. sub history_delete_file
  3930. {
  3931.     my ( $self, $mail_file, $archive ) = @_;
  3932.  
  3933.     $mail_file =~ /(popfile(\d+)\=(\d+)\.msg)$/;
  3934.     $mail_file = $1;
  3935.     $self->log_( "delete: $mail_file" );
  3936.  
  3937.     if ( $archive ) {
  3938.         my $path = $self->get_user_path_( $self->config_( 'archive_dir' ) );
  3939.  
  3940.         $self->make_directory__( $path );
  3941.  
  3942.         my ($reclassified, $bucket, $usedtobe, $magnet) = $self->{classifier__}->history_read_class( $mail_file );
  3943.  
  3944.         if ( ( $bucket ne 'unclassified' ) && ( $bucket ne 'unknown class' ) && ( $bucket ne 'unsure' ) ) {
  3945.             $path .= "\/" . $bucket;
  3946.             $self->make_directory__( $path );
  3947.  
  3948.             if ( $self->config_( 'archive_classes' ) > 0) {
  3949.                 # archive to a random sub-directory of the bucket archive
  3950.                 my $subdirectory = int( rand( $self->config_( 'archive_classes' ) ) );
  3951.                 $path .= "\/" . $subdirectory;
  3952.                 $self->make_directory__( $path );
  3953.             }
  3954.  
  3955.             # Previous comment about this potentially being unsafe (may have placed messages in
  3956.             # unusual places, or overwritten files) no longer applies
  3957.             # Files are now placed in the user directory, in the archive_dir subdirectory
  3958.  
  3959.             $self->history_copy_file( $self->get_user_path_( $self->global_config_( 'msgdir' ) . "$mail_file" ), $path, $mail_file );
  3960.         }
  3961.     }
  3962.  
  3963.     # Before deleting the file make sure that the appropriate entry in the
  3964.     # history cache is also remove
  3965.  
  3966.     delete $self->{history__}{$mail_file};
  3967.  
  3968.     # Now remove the files from the disk, remove both the msg file containing
  3969.     # the mail message and its associated CLS file
  3970.  
  3971.     unlink( $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ) );
  3972.     $mail_file =~ s/msg$/cls/;
  3973.     unlink( $self->get_user_path_( $self->global_config_( 'msgdir' ) . $mail_file ) );
  3974. }
  3975.  
  3976. # ---------------------------------------------------------------------------------------------
  3977. #
  3978. # history_copy_file     - Copies a file to a specified location and filename
  3979. #
  3980. #   $from       - The source file. May be relative or absolute.
  3981. #   $to_dir     - The destination directory. May be relative or absolute.
  3982. #                   Will not be created if non-existent.
  3983. #   $to_name    - The destination filename.
  3984. #
  3985. # ---------------------------------------------------------------------------------------------
  3986. sub history_copy_file
  3987. {
  3988.     my ( $self, $from, $to_dir, $to_name ) = @_;
  3989.  
  3990.     if ( open( FROM, "<$from") ) {
  3991.         if ( open( TO, ">$to_dir\/$to_name") ) {
  3992.             binmode FROM;
  3993.             binmode TO;
  3994.  
  3995.             while (<FROM>) {
  3996.                 print TO $_;
  3997.             }
  3998.  
  3999.             close TO;
  4000.         }
  4001.  
  4002.         close FROM;
  4003.     }
  4004. }
  4005.  
  4006. # ---------------------------------------------------------------------------------------------
  4007. #
  4008. # print_form_fields_ - Returns a form string containing any presently defined form fields
  4009. #
  4010. # $first        - 1 if the form field is at the beginning of a query, 0 otherwise
  4011. # $in_href      - 1 if the form field is printing in a href, 0 otherwise (eg, for a 302 redirect)
  4012. # $include      - a list of fields to return
  4013. #
  4014. # ---------------------------------------------------------------------------------------------
  4015. sub print_form_fields_
  4016. {
  4017.     my ($self, $first, $in_href, @include) = @_;
  4018.  
  4019.     my $amp;
  4020.     if ($in_href) {
  4021.         $amp = '&';
  4022.     } else {
  4023.         $amp = '&';
  4024.     }
  4025.  
  4026.     my $count = 0;
  4027.     my $formstring = '';
  4028.  
  4029.     $formstring = "$amp" if (!$first);
  4030.  
  4031.     foreach my $field ( @include ) {
  4032.         if ($field eq 'session') {
  4033.             $formstring .= "$amp" if ($count > 0);
  4034.             $formstring .= "session=$self->{session_key__}";
  4035.             $count++;
  4036.             next;
  4037.             }
  4038.         unless ( !defined($self->{form_}{$field}) || ( $self->{form_}{$field} eq '' ) ) {
  4039.             $formstring .= "$amp" if ($count > 0);
  4040.             $formstring .= "$field=". $self->url_encode_($self->{form_}{$field});  
  4041.             $count++;
  4042.         }
  4043.     }
  4044.  
  4045.     return ($count>0)?$formstring:'';
  4046. }
  4047.  
  4048. # ---------------------------------------------------------------------------------------------
  4049. # register_configuration_item__
  4050. #
  4051. #     $type            The type of item (configuration, security or chain)
  4052. #     $name            The name of the item
  4053. #     $object          Reference to the object calling this method
  4054. #
  4055. # This seemingly innocent method disguises a lot.  It is called by modules that wish to
  4056. # register that they have specific elements of UI that need to be dynamically added to the
  4057. # Configuration and Security screens of POPFile.  This is done so that the HTML module does
  4058. # not need to know about the modules that are loaded, their individual configuration elements
  4059. # or how to do validation
  4060. #
  4061. # A module calls this method for each separate UI element (normally an HTML form that handles
  4062. # a single configuration option) and passes in three pieces of information:
  4063. #
  4064. # The type is the position in the UI where the element is to be displayed. configuration means
  4065. # on the Configuration screen under "Module Options"; security means on the Security page
  4066. # and is used exclusively for stealth mode operation right now; chain is also on the security
  4067. # page and is used for identifying chain servers (in the case of SMTP the chained server and
  4068. # for POP3 the SPA server)
  4069. #
  4070. # The name (this is usually the name of the configuration option this registration is for, but
  4071. # any unique ID is acceptable).
  4072. #
  4073. # A reference to itself.
  4074. #
  4075. # When this module needs to display an element of UI it will call the object's configure_item
  4076. # public method passing in the name of the element required, a reference to the hash containing
  4077. # the current language strings and the session ID.  configure_item must return the complete
  4078. # HTML for the item
  4079. #
  4080. # When the module needs to validate it will call the object's validate_item interface passing
  4081. # in the name of the element, a reference to the language hash and a reference to the form
  4082. # hash which has been parsed.  validate_item returns HTML if it desires containing a
  4083. # confirmation or error message, or may return nothing if there was nothing in the form of
  4084. # interest to that specific module
  4085. #
  4086. # Example the module foo has a configuration item called bar which it needs a UI for, and
  4087. # so it calls
  4088. #
  4089. #    register_configuration_item( 'configuration', 'foo_bar', $self )
  4090. #
  4091. # later it will receive a call to its
  4092. #
  4093. #    configure_item( 'foo_bar', language hash, session key )
  4094. #
  4095. # and needs to return the HTML for the foo_bar item.  Then it will may receive a call to its
  4096. #
  4097. #    validate_item( 'foo_bar', language hash, form hash )
  4098. #
  4099. # and needs to check the form for information from any form it created and returned from the
  4100. # call to configure_item and update its own state.  It can optionally return HTML that
  4101. # will be displayed at the top of the page
  4102. #
  4103. # ---------------------------------------------------------------------------------------------
  4104. sub register_configuration_item__
  4105. {
  4106.    my ( $self, $type, $name, $object ) = @_;
  4107.  
  4108.    $self->{dynamic_ui__}{$type}{$name} = $object;
  4109. }
  4110.  
  4111. # ---------------------------------------------------------------------------------------------
  4112. #
  4113. # mcount__, ecount__ get the total message count, or the total error count
  4114. #
  4115. # ---------------------------------------------------------------------------------------------
  4116.  
  4117. sub mcount__
  4118. {
  4119.     my ( $self ) = @_;
  4120.  
  4121.     my $count = 0;
  4122.  
  4123.     my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  4124.  
  4125.     foreach my $bucket (@buckets) {
  4126.         $count += $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'count' );
  4127.     }
  4128.  
  4129.     return $count;
  4130. }
  4131.  
  4132. sub ecount__
  4133. {
  4134.     my ( $self ) = @_;
  4135.  
  4136.     my $count = 0;
  4137.  
  4138.     my @buckets = $self->{classifier__}->get_buckets( $self->{api_session__} );
  4139.  
  4140.     foreach my $bucket (@buckets) {
  4141.         $count += $self->{classifier__}->get_bucket_parameter( $self->{api_session__}, $bucket, 'fpcount' );
  4142.     }
  4143.  
  4144.     return $count;
  4145. }
  4146.  
  4147. # ---------------------------------------------------------------------------------------------
  4148. #
  4149. # make_directory__
  4150. #
  4151. # Wrapper for mkdir that ensures that the path we are making doesn't end in / or \
  4152. # (Done because your can't do mkdir 'foo/' on NextStep.
  4153. #
  4154. # $path        The directory to make
  4155. #
  4156. # Returns whatever mkdir returns
  4157. #
  4158. # ---------------------------------------------------------------------------------------------
  4159. sub make_directory__
  4160. {
  4161.     my ( $self, $path ) = @_;
  4162.  
  4163.     $path =~ s/[\\\/]$//;
  4164.  
  4165.     return 1 if ( -d $path );
  4166.     return mkdir( $path );
  4167. }
  4168.  
  4169. # GETTERS/SETTERS
  4170.  
  4171. sub classifier
  4172. {
  4173.     my ( $self, $value ) = @_;
  4174.  
  4175.     if ( defined( $value ) ) {
  4176.         $self->{classifier__} = $value;
  4177.     }
  4178.  
  4179.     return $self->{classifier__};
  4180. }
  4181.  
  4182. sub language
  4183. {
  4184.     my ( $self ) = @_;
  4185.  
  4186.     return %{$self->{language__}};
  4187. }
  4188.  
  4189. sub session_key
  4190. {
  4191.     my ( $self ) = @_;
  4192.  
  4193.     return $self->{session_key__};
  4194. }
  4195.  
  4196. 1;
  4197.