Subversion Repositories DevTools

Rev

Rev 1295 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

###############################################################################
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
# sits@users.sourceforge.net
#
# This program is free software; you can redistribute it and modify it under
# the terms of the GPL.

# Collection of routines for processing the HTTP input.

package Codestriker::Http::Input;

use strict;
use Encode qw(decode_utf8);

use CGI::Carp 'fatalsToBrowser';

use Codestriker::Http::Cookie;

sub _set_property_from_cookie( $$$ );
sub _untaint( $$$ );
sub _untaint_name( $$ );
sub _untaint_digits( $$ );
sub _untaint_filename( $$ );
sub _untaint_revision( $$ );
sub _untaint_email( $$ );
sub _untaint_emails( $$ );
sub _untaint_bug_ids( $$ );
sub _untaint_alphanumeric( $$ );

# Default valiue to set the context if it is not set.
my $DEFAULT_CONTEXT = 2;

# Constructor for this class.
sub new($$$) {
    my ($type, $query, $http_response) = @_;
    my $self = {};
    $self->{query} = $query;
    $self->{http_response} = $http_response;
    return bless $self, $type;
}

# Process the CGI parameters, and untaint them.  If any of them look
# suspicious, immediately output an error.
sub process($) {
    my ($self) = @_;

    my $query = $self->{query};
    my %cookie = Codestriker::Http::Cookie->get($query);

    # Retrieve all of the known Codestriker CGI parameters, and check they
    # are valid.
    $self->{action} = $query->param('action');
    $self->{button} = $query->param('button');
    $self->{topic} = $query->param('topic');
    $self->{line} = $query->param('line');
    $self->{fn} = $query->param('fn');
    $self->{context} = $query->param('context');
    $self->{action} = $query->param('action');
    $self->{comments} = decode_utf8($query->param('comments'));
    $self->{email} = $query->param('email');
    $self->{author} = $query->param('author');
    $self->{topic_text} = decode_utf8($query->param('topic_text'));
    $self->{topic_title} = decode_utf8($query->param('topic_title'));
    $self->{topic_description} = decode_utf8($query->param('topic_description'));
    $self->{reviewers} = $query->param('reviewers');
    $self->{cc} = $query->param('cc');
    $self->{comment_cc} = $query->param('comment_cc');
    $self->{topic_state} = decode_utf8($query->param('topic_state'));
    $self->{comment_state} = decode_utf8($query->param('comment_state'));
    $self->{revision} = $query->param('revision');
    $self->{filename} = decode_utf8($query->param('filename'));
    $self->{linenumber} = $query->param('linenumber');
    $self->{mode} = $query->param('mode');
    $self->{brmode} = $query->param('brmode');  
    $self->{fview} = $query->param('fview');
    $self->{bug_ids} = $query->param('bug_ids');
    $self->{new} = $query->param('new');
    $self->{tabwidth} = $query->param('tabwidth');
    $self->{sauthor} = $query->param('sauthor');
    $self->{sreviewer} = $query->param('sreviewer');
    $self->{scc} = $query->param('scc');
    $self->{sbugid} = $query->param('sbugid');
    $self->{stext} = decode_utf8($query->param('stext'));
    $self->{stitle} = decode_utf8($query->param('stitle'));
    $self->{sdescription} = decode_utf8($query->param('sdescription'));
    $self->{scomments} = decode_utf8($query->param('scomments'));
    $self->{sbody} = decode_utf8($query->param('sbody'));
    $self->{sfilename} = decode_utf8($query->param('sfilename'));
    $self->{sstate} = decode_utf8($query->param('sstate'));
    $self->{sproject} = decode_utf8($query->param('sproject'));
    $self->{scontext} = $query->param('scontext');
    $self->{version} = $query->param('version');
    $self->{redirect} = $query->param('redirect');
    $self->{a} = $query->param('a');
    $self->{updated} = decode_utf8($query->param('updated'));
    $self->{repository} = decode_utf8($query->param('repository'));
    $self->{parallel} = $query->param('parallel');
    $self->{projectid} = $query->param('projectid');
    $self->{project_name} = decode_utf8($query->param('project_name'));
    $self->{project_description} = decode_utf8($query->param('project_description'));
    $self->{project_state} = decode_utf8($query->param('project_state'));
    $self->{start_tag} = decode_utf8($query->param('start_tag'));
    $self->{end_tag} = decode_utf8($query->param('end_tag'));
    $self->{module} = decode_utf8($query->param('module'));
    $self->{topic_sort_change} = $query->param('topic_sort_change');
    $self->{format} = $query->param('format');
    $self->{obsoletes} = $query->param('obsoletes');
    my @selected_topics = $query->param('selected_topics');
    $self->{selected_topics} = \@selected_topics;
    my @selected_comments = $query->param('selected_comments');
    $self->{selected_comments} = \@selected_comments;
    $self->{default_to_head} = $query->param('default_to_head');

    # Set things to the empty string rather than undefined.
    $self->{cc} = "" if ! defined $self->{cc};
    $self->{reviewers} = "" if ! defined $self->{reviewers};
    $self->{bug_ids} = "" if ! defined $self->{bug_ids};
    $self->{sstate} = "" if ! defined $self->{sstate};
    $self->{sproject} = "" if ! defined $self->{sproject};
    $self->{sauthor} = "" if ! defined $self->{sauthor};
    $self->{a} = "" if ! defined $self->{a};
    $self->{updated} = 0 if ! defined $self->{updated};
    $self->{repository} = "" if ! defined $self->{repository};
    $self->{project_name} = "" if ! defined $self->{project_name};
    $self->{project_description} = "" if ! defined $self->{project_description};
    $self->{project_state} = "" if ! defined $self->{project_state};
    $self->{topic_sort_change} = "" if ! defined $self->{topic_sort_change};
    $self->{format} = "html" if ! defined $self->{format};
    $self->{obsoletes} = "" if ! defined $self->{obsoletes};
    $self->{default_to_head} = 0 if ! defined $self->{default_to_head};

    my @topic_metrics = $query->param('topic_metric');
    $self->{topic_metric} = \@topic_metrics;

    my @author_metrics = $query->param('author_metric');
    $self->{author_metric} = \@author_metrics;

    for (my $userindex = 0; $userindex < 100; ++$userindex)
    {
        my @reviewer_metrics = $query->param("reviewer_metric,$userindex");

        last if (scalar(@reviewer_metrics) == 0);
        $self->{"reviewer_metric,$userindex"} = \@reviewer_metrics;
    }

    # Set the comment state metric data.
    foreach my $comment_state_metric (@{$Codestriker::comment_state_metrics}) {
        my $name = "comment_state_metric_" . $comment_state_metric->{name};
        $self->{$name} = $query->param($name);
    }

    # Remove those annoying \r's in textareas.
    if (defined $self->{topic_description}) {
        $self->{topic_description} =~ s/\r//g;
    } else {
        $self->{topic_description} = "";
    }

    if (defined $self->{comments}) {
        $self->{comments} =~ s/\r//g;
    } else {
        $self->{comments} = "";
    }

    # Record the file handler for a topic text upload, if any.  Also record the
    # mime type of the file if it has been set, default to text/plain
    # otherwise.
    # Note topic_file is forced to be a string to get the filename (and
    # not have any confusion with the file object).  CGI.pm weirdness.
    if (defined $query->param('topic_file')) {
        $self->{fh_filename} = "" . $query->param('topic_file');
    }
    else {
        $self->{fh_filename} = undef;
    }
    $self->{fh} = $query->upload('topic_file');
    $self->{fh_mime_type} = 'text/plain';

# This code doesn't work, it produces a warning like:
#
# Use of uninitialized value in hash element at (eval 34) line 3.
#
# Since mime-types aren't used yet, this code is skipped for now.
#
#    if ((defined $self->{fh_filename})) {
#       (defined $query->uploadInfo($query->param('topic_file'))) {
#       $self->{fh_mime_type} =
#           $query->uploadInfo($self->{fh_filename})->{'Content-Type'};
#    }

    # Set parameter values from the cookie if they are not set.
    $self->_set_property_from_cookie('context', $DEFAULT_CONTEXT);
    $self->_set_property_from_cookie('mode',
                                     $Codestriker::default_topic_create_mode);
    $self->_set_property_from_cookie('tabwidth',
                                     $Codestriker::default_tabwidth);
    $self->_set_property_from_cookie('fview',
                                     $Codestriker::default_file_to_view);
    $self->_set_property_from_cookie('email', "");
    $self->_set_property_from_cookie('repository', "");
    $self->_set_property_from_cookie('projectid', 0);
    $self->_set_property_from_cookie('module', "");
    $self->_set_property_from_cookie('topicsort', "");

    $self->_untaint('topic_sort_change', '(title)|(author)|(created)|(state)');

    # Untaint the required input.
    $self->_untaint_name('action');
    $self->_untaint_digits('topic');
    $self->_untaint_digits('projectid');
    $self->_untaint_email('email');
    $self->_untaint_email('author');
    $self->_untaint_emails('reviewers');
    $self->_untaint_emails('cc');
    $self->_untaint_filename('filename');
    $self->_untaint_revision('revision');
    $self->_untaint_bug_ids('bug_ids');
    $self->_untaint_digits('new');
    $self->_untaint_digits('tabwidth');
    $self->_untaint_filename('start_tag');
    $self->_untaint_filename('end_tag');
    
    # VSS module names can be things like $/TestProject/Project-name, so 
    # this needs to be handled in a special way.
    $self->_untaint('module', '\$?[-_\/\w\.\s]+');

    $self->_untaint_digits('scontext');
    $self->_untaint_comma_digits('sstate');
    $self->_untaint_comma_digits('sproject');
    $self->_untaint_comma_digits('obsoletes');
    
    # Canonicalise the bug_ids and email list parameters if required.
    $self->{reviewers} = $self->make_canonical_email_list($self->{reviewers});
    $self->{cc} = $self->make_canonical_email_list($self->{cc});
    $self->{bug_ids} = $self->make_canonical_bug_list($self->{bug_ids});
    $self->{comment_cc} = $self->make_canonical_email_list($self->{comment_cc});

    # Initialise the feedback field to empty.
    $self->{feedback} = "";
}

# Return the query object associated with this object.
sub get_query($) {
    my ($self) = @_;

    return $self->{query};
}

# Return the specified parameter.
sub get($$) {
    my ($self, $param) = @_;

    return $self->{$param};
}

# Given a list of email addresses separated by commas and spaces, return
# a canonical form, where they are separated by a comma and a space.
sub make_canonical_email_list($$) {
    my ($type, $emails) = @_;

    if (defined $emails && $emails ne "") {
        # Chew off white space that is around the emails addresses.
        $emails =~ s/^[\s]*//;
        $emails =~ s/[\s]*$//;

        return join ', ', split /[\s,;]+/, $emails;
    } else {
        return $emails;
    }
}

# Given a list of bug ids separated by commas and spaces, return
# a canonical form, where they are separated by a comma and a space.
sub make_canonical_bug_list($$) {
    my ($type, $bugs) = @_;

    if (defined $bugs && $bugs ne "") {
        return join ', ', split /[\s,;]+/, $bugs;
    } else {
        return "";
    }
}

# Set the specified property from the cookie if it is not set.  If the cookie
# is not set, use the supplied default value.
sub _set_property_from_cookie($$$) {
    my ($self, $name, $default) = @_;

    my %cookie = Codestriker::Http::Cookie->get($self->{query});
    if (! defined $self->{$name} || $self->{$name} eq "") {
        $self->{$name} = exists $cookie{$name} ? $cookie{$name} : $default;
    }
}

# Untaint the specified property, against the expected regular expression.
# Remove leading and trailing whitespace.
sub _untaint($$$) {
    my ($self, $name, $regexp) = @_;

    my $value = $self->{$name};
    if (defined $value && $value ne "") {
        if ($value =~ /^\s*(${regexp})\s*$/) {
            # Untaint the value.
            $self->{$name} = $1;
        } else {
            my $error_message = "Input parameter $name has invalid value: " .
                "\"$value\"";
            $self->{http_response}->error($error_message);
        }
    } else {
        $self->{$name} = "";
    }
}

# Untaint a parameter which should be a bunch of alphabetical characters and
# underscores.
sub _untaint_name($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[A-Za-z_]+');
}

# Untaint a parameter which should be a bunch of digits.
sub _untaint_digits($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '\d+');
}

# Untaint a parameter which should be a valid filename.
sub _untaint_filename($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[-_\/\@\w\.\s]+');
}

# Untaint a parameter that should be a revision number.
sub _untaint_revision($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[\d\.]+');
}

# Untaint a parameter that should be a comma separated list of digits.
sub _untaint_comma_digits($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[\d\,]+');
}
            
# Untaint a single email address, which should be a regular email address.
sub _untaint_email($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[\s]*[-_\w\.]{1,200}(\@[-_\w\.]{1,200})?[\s]*');
}

# Untaint a list of email addresses.
sub _untaint_emails($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '[\s]*([-_\w\.]{1,200}(\@[-_\w\.]{1,200})?[\s,;]*){1,100}[\s]*');
}

# Untaint a list of bug ids.
sub _untaint_bug_ids($$) {
    my ($self, $name) = @_;

    $self->_untaint($name, '([0-9]+[\s,;]*){1,100}');
}

1;