#!/usr/bin/perl -w -I../perllib # index.cgi: # Main code for FixMyStreet # # Copyright (c) 2006 UK Citizens Online Democracy. All rights reserved. # Email: matthew@mysociety.org. WWW: http://www.mysociety.org # # $Id: index.cgi,v 1.261 2009-08-10 12:24:06 matthew Exp $ use strict; use Standard; use Error qw(:try); use File::Slurp; use LWP::Simple; use RABX; use CGI::Carp; use URI::Escape; use CrossSell; use mySociety::AuthToken; use mySociety::Config; use mySociety::DBHandle qw(select_all); use mySociety::EmailUtil; use mySociety::GeoUtil; use mySociety::Locale; use mySociety::MaPit; use mySociety::PostcodeUtil; use mySociety::Random; use mySociety::VotingArea; use mySociety::Web qw(ent NewURL); BEGIN { if (!dbh()->selectrow_array('select secret from secret for update of secret')) { local dbh()->{HandleError}; dbh()->do('insert into secret (secret) values (?)', {}, unpack('h*', mySociety::Random::random_bytes(32))); } dbh()->commit(); } # Main code for index.cgi sub main { my $q = shift; if (my $partial = $q->param('partial_token')) { # We have a partial token, so fetch data from database and see where we're at. my $id = mySociety::AuthToken::retrieve('partial', $partial); if ($id) { my @row = dbh()->selectrow_array( "select easting, northing, name, email, title, (photo is not null) as has_photo, phone from problem where id=? and state='partial'", {}, $id); if (@row) { $q->param('anonymous', 1); $q->param('submit_map', 1); $q->param('easting', $row[0]); $q->param('northing', $row[1]); $q->param('name', $row[2]); $q->param('email', $row[3]); $q->param('title', $row[4]); $q->param('has_photo', $row[5]); $q->param('phone', $row[6]); $q->param('partial', $partial); } else { my $base = mySociety::Config::get('BASE_URL'); print $q->redirect(-location => $base . '/report/' . $id); } } } my $out = ''; my %params; if ($q->param('submit_problem') || ($q->param('submit_map') && $q->param('submit_map')==2)) { $params{title} = _('Submitting your report'); ($out) = submit_problem($q); } elsif ($q->param('submit_update')) { $params{title} = _('Submitting your update'); ($out) = submit_update($q); } elsif ($q->param('submit_map')) { ($out, %params) = display_form($q); $params{title} = _('Reporting a problem'); } elsif ($q->param('id')) { ($out, %params) = display_problem($q); $params{title} .= ' - ' . _('Viewing a problem'); } elsif ($q->param('pc') || ($q->param('x') && $q->param('y'))) { ($out, %params) = display_location($q); $params{title} = _('Viewing a location'); } else { $out = front_page($q); } print Page::header($q, %params); print $out; my %footerparams; $footerparams{js} = $params{js} if $params{js}; print Page::footer($q, %footerparams); } Page::do_fastcgi(\&main); # Display front page sub front_page { my ($q, $error) = @_; my $pc_h = ent($q->param('pc') || ''); my $out = '

' . _('Report, view, or discuss local problems') . ''; my $subhead = _('(like graffiti, fly tipping, broken paving slabs, or street lighting)'); $subhead = '(like graffiti, fly tipping, or neighbourhood noise)' if $q->{site} eq 'scambs'; $out .= '
' . $subhead . '' if $subhead ne ' '; $out .= '

'; #if (my $url = mySociety::Config::get('IPHONE_URL')) { # my $getiphone = _("Get FixMyStreet on your iPhone"); # my $new = _("New!"); # if ($q->{site} eq 'fixmystreet') { # $out .= < #$new #$getiphone #

#EOF # } #} $out .= '

' . $error . '

' if ($error); my $fixed = Problems::recent_fixed(); my $updates = Problems::number_comments(); my $new = Problems::recent_new('1 week'); (my $new_pretty = $new) =~ s/(?<=\d)(?=(?:\d\d\d)+$)/,/g; my $new_text = sprintf(mySociety::Locale::nget('%s report in past week', '%s reports in past week', $new), $new_pretty); if ($q->{site} ne 'emptyhomes' && $new > $fixed) { $new = Problems::recent_new('3 days'); ($new_pretty = $new) =~ s/(?<=\d)(?=(?:\d\d\d)+$)/,/g; $new_text = sprintf(mySociety::Locale::nget('%s report recently', '%s reports recently', $new), $new_pretty); } # Add pretty commas for display $out .= '
'; if (my $token = $q->param('partial')) { my $id = mySociety::AuthToken::retrieve('partial', $token); if ($id) { my $thanks = _("Thanks for uploading your photo. We now need to locate your problem, so please enter a nearby street name or postcode in the box below :"); $out .= <$thanks

EOF } } my $question = _("Enter a nearby GB postcode, or street name and area:"); my $activate = _("Go"); $out .= <$question    
EOF $out .= $q->h2(_('How to report a problem')); my $step4 = $q->li(_('We send it to the council on your behalf')); $step4 = $q->li('The council receives your report and acts upon it') if $q->{site} eq 'scambs'; $out .= $q->ol( $q->li(_('Enter a nearby GB postcode, or street name and area')), $q->li(_('Locate the problem on a map of the area')), $q->li(_('Enter details of the problem')), $step4 ); (my $fixed_pretty = $fixed) =~ s/(?<=\d)(?=(?:\d\d\d)+$)/,/g; (my $updates_pretty = $updates) =~ s/(?<=\d)(?=(?:\d\d\d)+$)/,/g; $out .= $q->h2(_('FixMyStreet updates')); $out .= $q->div({-id => 'front_stats'}, $q->div($new_text), ($q->{site} ne 'emptyhomes' ? $q->div(sprintf(mySociety::Locale::nget("%s fixed in past month", "%s fixed in past month", $fixed), $fixed_pretty)) : ''), # $q->div(sprintf(_('%s back in use in past month'), $fixed)), $q->div(sprintf(mySociety::Locale::nget("%s update on reports", "%s updates on reports", $updates), $updates_pretty)) ); $out .= <
EOF my $recent_photos = Problems::recent_photos(3); $out .= $q->h2(_('Photos of recent reports')) . $recent_photos if $recent_photos; my $probs = Problems::recent(); $out .= $q->h2(_('Recently reported problems')) . ' ' if @$probs; $out .= '
'; return $out; } sub submit_update { my $q = shift; my @vars = qw(id name rznvy update fixed upload_fileid add_alert); my %input = map { $_ => $q->param($_) || '' } @vars; my @errors; my $fh = $q->upload('photo'); if ($fh) { my $err = Page::check_photo($q, $fh); push @errors, $err if $err; } push(@errors, _('Please enter a message')) unless $input{update} =~ /\S/; $input{name} = undef unless $input{name} =~ /\S/; if ($input{rznvy} !~ /\S/) { push(@errors, _('Please enter your email')); } elsif (!mySociety::EmailUtil::is_valid_email($input{rznvy})) { push(@errors, _('Please enter a valid email')); } my $image; if ($fh) { try { $image = Page::process_photo($fh); } catch Error::Simple with { my $e = shift; push(@errors, sprintf(_("That image doesn't appear to have uploaded correctly (%s), please try again."), $e)); }; } if ($input{upload_fileid}) { open FP, mySociety::Config::get('UPLOAD_CACHE') . $input{upload_fileid}; $image = join('', ); close FP; } return display_problem($q, @errors) if (@errors); my $id = dbh()->selectrow_array("select nextval('comment_id_seq');"); Utils::workaround_pg_bytea("insert into comment (id, problem_id, name, email, website, text, state, mark_fixed, photo, lang) values (?, ?, ?, ?, '', ?, 'unconfirmed', ?, ?, ?)", 7, $id, $input{id}, $input{name}, $input{rznvy}, $input{update}, $input{fixed} ? 't' : 'f', $image, $mySociety::Locale::lang); my %h = (); $h{update} = $input{update}; $h{name} = $input{name} ? $input{name} : _("Anonymous"); my $base = mySociety::Config::get('BASE_URL'); $base =~ s/matthew/scambs.matthew/ if $q->{site} eq 'scambs'; # XXX Temp $h{url} = $base . '/C/' . mySociety::AuthToken::store('update', { id => $id, add_alert => $input{add_alert} } ); dbh()->commit(); my $out = Page::send_email($q, $input{rznvy}, $input{name}, 'update', %h); return $out; } sub submit_problem { my $q = shift; my @vars = qw(council title detail name email phone pc easting northing skipped anonymous category partial upload_fileid); my %input = map { $_ => scalar $q->param($_) } @vars; for (qw(title detail)) { $input{$_} = lc $input{$_} if $input{$_} !~ /[a-z]/; $input{$_} = ucfirst $input{$_}; $input{$_} =~ s/\b(dog\s*)shit\b/$1poo/ig; } my @errors; my $fh = $q->upload('photo'); if ($fh) { my $err = Page::check_photo($q, $fh); push @errors, $err if $err; } $input{council} = 2260 if $q->{site} eq 'scambs'; # All reports go to S. Cambs push(@errors, _('No council selected')) unless ($input{council} && $input{council} =~ /^(?:-1|[\d,]+(?:\|[\d,]+)?)$/); push(@errors, _('Please enter a subject')) unless $input{title} =~ /\S/; push(@errors, _('Please enter some details')) unless $input{detail} =~ /\S/; if ($input{name} !~ /\S/) { push @errors, _('Please enter your name'); } elsif (length($input{name}) < 5 || $input{name} !~ /\s/ || $input{name} =~ /\ba\s*n+on+((y|o)mo?u?s)?(ly)?\b/i) { push @errors, _('Please enter your full name, councils need this information - if you do not wish your name to be shown on the site, untick the box'); } if ($input{email} !~ /\S/) { push(@errors, _('Please enter your email')); } elsif (!mySociety::EmailUtil::is_valid_email($input{email})) { push(@errors, _('Please enter a valid email')); } if ($input{category} && $input{category} eq '-- Pick a category --') { push (@errors, _('Please choose a category')); $input{category} = ''; } elsif ($input{category} && $input{category} eq '-- Pick a property type --') { push (@errors, _('Please choose a property type')); $input{category} = ''; } return display_form($q, @errors) if (@errors); # Short circuit my $areas; if ($input{easting} && $input{northing}) { $areas = mySociety::MaPit::get_voting_areas_by_location( { easting=>$input{easting}, northing=>$input{northing} }, 'polygon', [qw(WMC CTY CED DIS DIW MTD MTW COI COP LGD LGE UTA UTE UTW LBO LBW LAC SPC WAC NIE)] ); if ($input{council} =~ /^[\d,]+(\|[\d,]+)?$/) { my $no_details = $1 || ''; my %va = map { $_ => 1 } @$mySociety::VotingArea::council_parent_types; my %councils; foreach (keys %$areas) { $councils{$_} = 1 if $va{$areas->{$_}}; } my @input_councils = split /,|\|/, $input{council}; foreach (@input_councils) { if (!$councils{$_}) { push(@errors, _('That location is not part of that council')); last; } } if ($no_details) { $input{council} =~ s/\Q$no_details\E//; @input_councils = split /,/, $input{council}; } # Check category here, won't be present if council is -1 my @valid_councils = @input_councils; if ($input{category} && $q->{site} ne 'emptyhomes') { my $categories = select_all("select area_id from contacts where deleted='f' and area_id in (" . $input{council} . ') and category = ?', $input{category}); push (@errors, 'Please choose a category') unless @$categories; @valid_councils = map { $_->{area_id} } @$categories; foreach my $c (@valid_councils) { if ($no_details =~ /$c/) { push(@errors, _('We have details for that council')); $no_details =~ s/,?$c//; } } } $input{council} = join(',', @valid_councils) . $no_details; } $areas = ',' . join(',', sort keys %$areas) . ','; } elsif ($input{easting} || $input{northing}) { push(@errors, _('Somehow, you only have one co-ordinate. Please try again.')); } else { push(@errors, _('You haven\'t specified any sort of co-ordinates. Please try again.')); } my $image; if ($fh) { try { $image = Page::process_photo($fh); } catch Error::Simple with { my $e = shift; push(@errors, sprintf(_("That image doesn't appear to have uploaded correctly (%s), please try again."), $e)); }; } if ($input{upload_fileid}) { open FP, mySociety::Config::get('UPLOAD_CACHE') . $input{upload_fileid}; $image = join('', ); close FP; } return display_form($q, @errors) if (@errors); delete $input{council} if $input{council} eq '-1'; my $used_map = $input{skipped} ? 'f' : 't'; $input{category} = _('Other') unless $input{category}; my ($id, $out); if (my $token = $input{partial}) { my $id = mySociety::AuthToken::retrieve('partial', $token); if ($id) { dbh()->do("update problem set postcode=?, easting=?, northing=?, title=?, detail=?, name=?, email=?, phone=?, state='confirmed', council=?, used_map='t', anonymous=?, category=?, areas=?, confirmed=ms_current_timestamp(), lastupdate=ms_current_timestamp() where id=?", {}, $input{pc}, $input{easting}, $input{northing}, $input{title}, $input{detail}, $input{name}, $input{email}, $input{phone}, $input{council}, $input{anonymous} ? 'f' : 't', $input{category}, $areas, $id); Utils::workaround_pg_bytea('update problem set photo=? where id=?', 1, $image, $id) if $image; dbh()->commit(); $out = $q->p(sprintf(_('You have successfully confirmed your report and you can now view it on the site.'), "/report/$id")); $out .= CrossSell::display_advert($q, $input{email}, $input{name}); } else { $out = $q->p('There appears to have been a problem updating the details of your report. Please let us know what went on and we\'ll look into it.'); } } else { $id = dbh()->selectrow_array("select nextval('problem_id_seq');"); Utils::workaround_pg_bytea("insert into problem (id, postcode, easting, northing, title, detail, name, email, phone, photo, state, council, used_map, anonymous, category, areas, lang) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unconfirmed', ?, ?, ?, ?, ?, ?)", 10, $id, $input{pc}, $input{easting}, $input{northing}, $input{title}, $input{detail}, $input{name}, $input{email}, $input{phone}, $image, $input{council}, $used_map, $input{anonymous} ? 'f': 't', $input{category}, $areas, $mySociety::Locale::lang); my %h = (); $h{title} = $input{title}; $h{detail} = $input{detail}; $h{name} = $input{name}; my $base = mySociety::Config::get('BASE_URL'); $base =~ s/matthew/scambs.matthew/ if $q->{site} eq 'scambs'; # XXX Temp $h{url} = $base . '/P/' . mySociety::AuthToken::store('problem', $id); dbh()->commit(); $out = Page::send_email($q, $input{email}, $input{name}, 'problem', %h); } return $out; } sub display_form { my ($q, @errors) = @_; my ($pin_x, $pin_y, $pin_tile_x, $pin_tile_y) = (0,0,0,0); my @vars = qw(title detail name email phone pc easting northing x y skipped council anonymous partial upload_fileid); my %input = map { $_ => $q->param($_) || '' } @vars; my %input_h = map { $_ => $q->param($_) ? ent($q->param($_)) : '' } @vars; ($input{x}) = $input{x} =~ /^(\d+)/; $input{x} ||= 0; ($input{y}) = $input{y} =~ /^(\d+)/; $input{y} ||= 0; my @ps = $q->param; foreach (@ps) { ($pin_tile_x, $pin_tile_y, $pin_x) = ($1, $2, $q->param($_)) if /^tile_(\d+)\.(\d+)\.x$/; $pin_y = $q->param($_) if /\.y$/; } return display_location($q) unless ($pin_x && $pin_y) || ($input{easting} && $input{northing}) || ($input{skipped} && $input{x} && $input{y}) || ($input{skipped} && $input{pc}) || ($input{partial} && $input{pc}); my $out = ''; my ($px, $py, $easting, $northing); if ($input{skipped}) { # Map is being skipped if ($input{x} && $input{y}) { $easting = Page::tile_to_os($input{x}); $northing = Page::tile_to_os($input{y}); } else { my ($x, $y, $e, $n, $error) = Page::geocode($input{pc}); $easting = $e; $northing = $n; } } elsif ($pin_x && $pin_y) { # Map was clicked on $pin_x = Page::click_to_tile($pin_tile_x, $pin_x); $pin_y = Page::click_to_tile($pin_tile_y, $pin_y, 1); $input{x} ||= int($pin_x) - 1; $input{y} ||= int($pin_y) - 1; $px = Page::tile_to_px($pin_x, $input{x}); $py = Page::tile_to_px($pin_y, $input{y}, 1); $easting = Page::tile_to_os($pin_x); $northing = Page::tile_to_os($pin_y); } elsif ($input{partial} && $input{pc} && !$input{easting} && !$input{northing}) { my ($x, $y, $error); try { ($x, $y, $easting, $northing, $error) = Page::geocode($input{pc}); } catch Error::Simple with { $error = shift; }; return Page::geocode_choice($error, '/') if ref($error) eq 'ARRAY'; return front_page($q, $error) if $error; $input{x} = int(Page::os_to_tile($easting)); $input{y} = int(Page::os_to_tile($northing)); $px = Page::os_to_px($easting, $input{x}); $py = Page::os_to_px($northing, $input{y}, 1); } else { # Normal form submission my ($x, $y, $tile_x, $tile_y); ($x, $y, $tile_x, $tile_y, $px, $py) = Page::os_to_px_with_adjust($q, $input{easting}, $input{northing}, undef, undef); $input{x} = $tile_x; $input{y} = $tile_y; $easting = $input_h{easting}; $northing = $input_h{northing}; } my $parent_types = $mySociety::VotingArea::council_parent_types; $parent_types = [qw(DIS LBO MTD UTA LGD COI)] # No CTY if $q->{site} eq 'emptyhomes'; # XXX: I think we want in_gb_locale around the next line, needs testing my $all_councils = mySociety::MaPit::get_voting_areas_by_location( { easting => $easting, northing => $northing }, 'polygon', $parent_types); # Ipswich & St Edmundsbury are responsible for everything in their areas, no Suffolk delete $all_councils->{2241} if $all_councils->{2446} || $all_councils->{2443}; # Norwich is responsible for everything in its areas, no Norfolk delete $all_councils->{2233} if $all_councils->{2391}; if ($q->{site} eq 'scambs') { delete $all_councils->{2218}; return display_location($q, 'That location is not within the boundary of South Cambridgeshire District Council - you can report problems elsewhere in Great Britain using FixMyStreet.') unless $all_councils->{2260}; } $all_councils = [ keys %$all_councils ]; return display_location($q, _('That spot does not appear to be covered by a council. If you have tried to report an issue past the shoreline, for example, please specify the closest point on land.')) unless @$all_councils; my $areas_info = mySociety::MaPit::get_voting_areas_info($all_councils); # Look up categories for this council or councils my $category = ''; my (%council_ok, @categories); my $categories = select_all("select area_id, category from contacts where deleted='f' and area_id in (" . join(',', @$all_councils) . ')'); if ($q->{site} ne 'emptyhomes') { @$categories = sort { $a->{category} cmp $b->{category} } @$categories; foreach (@$categories) { $council_ok{$_->{area_id}} = 1; next if $_->{category} eq _('Other'); push @categories, $_->{category}; } if ($q->{site} eq 'scambs') { @categories = Page::scambs_categories(); } if (@categories) { @categories = ('-- Pick a category --', @categories, _('Other')); $category = _('Category:'); } } else { foreach (@$categories) { $council_ok{$_->{area_id}} = 1; } @categories = (_('-- Pick a property type --'), _('Empty house or bungalow'), _('Empty flat or maisonette'), _('Whole block of empty flats'), _('Empty office or other commercial'), _('Empty pub or bar'), _('Empty public building - school, hospital, etc.')); $category = _('Property type:'); } $category = $q->div($q->label({'for'=>'form_category'}, $category), $q->popup_menu(-name=>'category', -values=>\@categories, -attributes=>{id=>'form_category'}) ) if $category; my @councils = keys %council_ok; my $details; if (@councils == @$all_councils) { $details = 'all'; } elsif (@councils == 0) { $details = 'none'; } else { $details = 'some'; } if ($input{skipped}) { $out .= < EOF $out .= $q->h1(_('Reporting a problem')) . '
    '; } else { my $pins = Page::display_pin($q, $px, $py, 'purple'); $out .= Page::display_map($q, x => $input{x}, y => $input{y}, type => 2, pins => $pins, px => $px, py => $py ); my $partial_id; if (my $token = $input{partial}) { $partial_id = mySociety::AuthToken::retrieve('partial', $token); if ($partial_id) { $out .= $q->p({id=>'unknown'}, 'Please note your report has not yet been sent. Choose a category and add further information below, then submit.'); } } $out .= $q->h1(_('Reporting a problem')) . ' '; $out .= $q->p(_('You have located the problem at the point marked with a purple pin on the map. If this is not the correct location, simply click on the map again. ')); } if ($details eq 'all') { $out .= '

    ' . sprintf(_('All the information you provide here will be sent to %s. On the site, we will show the subject and details of the problem, plus your name if you give us permission.'), join(' or ', map { $areas_info->{$_}->{name} } @$all_councils)); $out .= ''; } elsif ($details eq 'some') { my $e = mySociety::Config::get('CONTACT_EMAIL'); my %councils = map { $_ => 1 } @councils; my @missing; foreach (@$all_councils) { push @missing, $_ unless $councils{$_}; } my $n = @missing; my $list = join(' or ', map { $areas_info->{$_}->{name} } @missing); $out .= '

    All the information you provide here will be sent to ' . join(' or ', map { $areas_info->{$_}->{name} } @councils) . '. On the site, we will show the subject and details of the problem, plus your name if you give us permission.'; $out .= ' We do not yet have details for the other council'; $out .= ($n>1) ? 's that cover' : ' that covers'; $out .= " this location. You can help us by finding a contact email address for local problems for $list and emailing it to us at $e."; $out .= ''; } else { my $e = mySociety::Config::get('CONTACT_EMAIL'); my $list = join(' or ', map { $areas_info->{$_}->{name} } @$all_councils); my $n = @$all_councils; if ($q->{site} ne 'emptyhomes') { $out .= '

    We do not yet have details for the council'; $out .= ($n>1) ? 's that cover' : ' that covers'; $out .= " this location. If you submit a problem here it will be left on the site, but not reported to the council. You can help us by finding a contact email address for local problems for $list and emailing it to us at $e."; } else { $out .= _("

    We do not yet have details for the council that covers this location. If you submit a report here it will be left on the site, but not reported to the council – please still leave your report, so that we can show to the council the activity in their area."); } $out .= ''; } if ($input{skipped}) { $out .= $q->p(_('Please fill in the form below with details of the problem, and describe the location as precisely as possible in the details box.')); } elsif ($q->{site} eq 'scambs') { $out .= '

    Please fill in details of the problem below. We won\'t be able to help unless you leave as much detail as you can, so please describe the exact location of the problem (e.g. on a wall), what it is, how long it has been there, a description (and a photo of the problem if you have one), etc.'; } elsif ($q->{site} eq 'emptyhomes') { $out .= $q->p(_(<p(_('Please fill in details of the problem below.')); } $out .= ' '; if (@errors) { $out .= '

    • ' . join('
    • ', @errors) . '
    '; } my $anon = ($input{anonymous}) ? ' checked' : ($input{title} ? '' : ' checked'); $out .= '
    '; $out .= $q->h2(_('Empty property details form')) if $q->{site} eq 'emptyhomes'; $out .= < $category EOF my $subject_label = _('Subject:'); my $detail_label = _('Details:'); my $photo_label = _('Photo:'); my $name_label = _('Name:'); my $email_label = _('Email:'); my $phone_label = _('Phone:'); my $optional = _('(optional)'); my $anonymous = _('Can we show your name on the site?'); my $anonymous2 = _('(we never show your email address or phone number)'); $out .= <
    EOF $out .= <
EOF my $partial_id; if (my $token = $input{partial}) { $partial_id = mySociety::AuthToken::retrieve('partial', $token); if ($partial_id) { $out .= ''; } } if ($partial_id && $q->param('has_photo')) { $out .= "

The photo you uploaded was:

"; } else { $out .= <