From: Ivan Kohler Date: Tue, 18 Jul 2023 23:28:58 +0000 (-0700) Subject: default to a session cookie instead of setting an explicit timeout, weird timezone... X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=HEAD;hp=eb11834cb545a83c1fddf8febcf309153a707f06 default to a session cookie instead of setting an explicit timeout, weird timezone/clock skew effects on server can cause firefox and other browsers to reject the session cookie, leading to silent login failures --- diff --git a/FS/FS/Auth/internal.pm b/FS/FS/Auth/internal.pm index dfc5f301d..92dff0318 100644 --- a/FS/FS/Auth/internal.pm +++ b/FS/FS/Auth/internal.pm @@ -7,7 +7,7 @@ use FS::Record qw( qsearchs ); use FS::access_user; sub authenticate { - my($self, $username, $check_password ) = @_; + my($self, $username, $check_password, $totp_code ) = @_; my $access_user = ref($username) ? $username @@ -17,6 +17,7 @@ sub authenticate { ) or return 0; + my $pw_check; if ( $access_user->_password_encoding eq 'bcrypt' ) { my( $cost, $salt, $hash ) = split(',', $access_user->_password); @@ -29,17 +30,21 @@ sub authenticate { ) ); - $hash eq $check_hash; + $pw_check = $hash eq $check_hash; - } else { + } else { return 0 if $access_user->_password eq 'notyet' || $access_user->_password eq ''; - $access_user->_password eq $check_password; + $pw_check = $access_user->_password eq $check_password; } + return $pw_check if ! $pw_check || ! length($access_user->totp_secret32); + + #2fa + $access_user->google_auth->verify( $totp_code, 1 ); } sub autocreate { 0; } diff --git a/FS/FS/AuthCookieHandler.pm b/FS/FS/AuthCookieHandler.pm index 93d8ea6a5..b7d0dbf5b 100644 --- a/FS/FS/AuthCookieHandler.pm +++ b/FS/FS/AuthCookieHandler.pm @@ -13,13 +13,13 @@ sub useragent_ip { } sub authen_cred { - my( $self, $r, $username, $password ) = @_; + my( $self, $r, $username, $password, $totp_code ) = @_; preuser_setup(); my $info = {}; - unless ( FS::Auth->authenticate($username, $password, $info) ) { + unless ( FS::Auth->authenticate($username, $password, $totp_code, $info) ) { warn "failed auth $username from ". $self->useragent_ip($r). "\n"; return undef; } diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index 383fb0e88..57a886770 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -2452,8 +2452,8 @@ and customer address. Include units.', { 'key' => 'selfservice-timeout', - 'section' => 'self-service', - 'description' => 'Timeout for the self-service login cookie, in seconds. Defaults to 1 hour.', + 'section' => 'deprecated', + 'description' => 'Deprecated. Was the timeout for the self-service login cookie, in seconds. Defaulted to 1 hour.', 'type' => 'text', }, diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index 8dd72ac79..ebd40adda 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -85,6 +85,7 @@ if ( -e $addl_handler_use_file ) { die $@ if $@; } use Text::CSV_XS; + use Archive::Zip; use Spreadsheet::WriteExcel; use Spreadsheet::WriteExcel::Utility; use OLE::Storage_Lite; @@ -120,7 +121,10 @@ if ( -e $addl_handler_use_file ) { use Locale::Currency::Format; use Number::Phone::Country qw( noexport ); use Business::US::USPS::WebTools::AddressStandardization; - use Geo::GoogleEarth::Pluggable; + use Geo::GoogleEarth::Pluggable 0.16; + use Geo::Shapelib; + use Geo::JSON; + use Geo::JSON::FeatureCollection; use LWP::UserAgent; use Storable qw( nfreeze thaw ); use FS; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index d88403644..61b793bb4 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5931,6 +5931,7 @@ sub tables_hashref { 'username', 'varchar', '', $char_d, '', '', '_password', 'varchar', 'NULL', $char_d, '', '', '_password_encoding', 'varchar', 'NULL', $char_d, '', '', + 'totp_secret32', 'char', 'NULL', 32, '', '', 'last', 'varchar', 'NULL', $char_d, '', '', 'first', 'varchar', 'NULL', $char_d, '', '', 'user_custnum', 'int', 'NULL', '', '', '', diff --git a/FS/FS/access_user.pm b/FS/FS/access_user.pm index f23aa77f9..270f8bb27 100644 --- a/FS/FS/access_user.pm +++ b/FS/FS/access_user.pm @@ -13,6 +13,7 @@ use FS::agent; use FS::cust_main; use FS::sales; use Carp qw( croak ); +use Auth::GoogleAuth; $DEBUG = 0; $me = '[FS::access_user]'; @@ -239,6 +240,7 @@ sub check { $self->ut_numbern('usernum') || $self->ut_alpha_lower('username') || $self->ut_textn('_password') + || $self->ut_alphan('totp_secret32') || $self->ut_textn('last') || $self->ut_textn('first') || $self->ut_foreign_keyn('user_custnum', 'cust_main', 'custnum') @@ -733,6 +735,44 @@ sub change_password_fields { FS::Auth->auth_class->change_password_fields( @_ ); } +=item google_auth + +=cut + +sub google_auth { + my( $self ) = @_; + my $issuer = FS::Conf->new->config('company_name'). ' Freeside'; + my $label = $issuer. ':'. $self->username; + + Auth::GoogleAuth->new({ + secret => $self->totp_secret32, + issuer => $issuer, + key_id => $label, + }); + +} + +=item set_totp_secret32 + +=cut + +sub set_totp_secret32 { + my( $self ) = @_; + + $self->totp_secret32( $self->google_auth->generate_secret32 ); + $self->replace; +} + +=item totp_qr_code_url + +=cut + +sub totp_qr_code_url { + my( $self ) = @_; + + $self->google_auth->qr_code; +} + =item locale =cut diff --git a/FS/FS/deploy_zone.pm b/FS/FS/deploy_zone.pm index 723b491c8..c618fb987 100644 --- a/FS/FS/deploy_zone.pm +++ b/FS/FS/deploy_zone.pm @@ -10,8 +10,12 @@ use Cpanel::JSON::XS; use LWP::UserAgent; use HTTP::Request::Common; -# update this in 2020, along with the URL for the TIGERweb service -our $CENSUS_YEAR = 2010; +use Geo::JSON::Polygon; +use Geo::JSON::Feature; + +our $CENSUS_YEAR = 2020; + +our $tech_label = FS::part_pkg_fcc_option->technology_labels; =head1 NAME @@ -275,6 +279,27 @@ sub deploy_zone_vertex { }); } +=item shapefile_add SHAPEFILE + +Adds this deployment zone to the supplied Geo::Shapelib shapefile. + +=cut + +sub shapefile_add { + my( $self, $shapefile ) = @_; + + my @coordinates = map { [ $_->longitude, $_->latitude, 0, 0 ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + push @{$shapefile->{Shapes}}, { 'Vertices' => \@coordinates }; + push @{$shapefile->{ShapeRecords}}, [ $tech_label->{$self->technology}, + $self->adv_speed_down, + $self->adv_speed_up, + ]; + ''; +} + =item vertices_json Returns the vertex list for this zone, as a JSON string of @@ -289,6 +314,51 @@ sub vertices_json { encode_json(\@vertices); } +=item geo_json_feature + +Returns this zone as a Geo::JSON::Feature object + +=cut + +sub geo_json_feature { + my $self = shift; + + my @coordinates = map { [ $_->longitude, $_->latitude ] } + $self->deploy_zone_vertex; + push @coordinates, $coordinates[0]; + + Geo::JSON::Feature->new({ + geometry => Geo::JSON::Polygon->new({ coordinates => [ \@coordinates ] }), + properties => { 'Technology' => $tech_label->{$self->technology}, + 'Down' => $self->adv_speed_down, + 'Up' => $self->adv_speed_up, + }, + }) +} + +=item kml_add + +Adds this deployment zone to the supplied Geo::GoogleEarth::Pluggable object. + +=cut + +sub kml_polygon { + my( $self, $kml ) = @_; + + my $name = $self->description. ' ('. $self->adv_speed_down. '/'. + $self->adv_speed_up. ')'; + + $kml->Polygon( 'name' => $name, + 'coordinates' => [ [ #outerBoundary + map { [ $_->longitude, $_->latitude, 0 ] } + $self->deploy_zone_vertex + ], + #[ #innerBoundary + #] + ] + ); +} + =head2 SUBROUTINES =over 4 @@ -386,7 +456,7 @@ sub process_block_lookup { inSR => 4326, outSR => 4326, spatialRel => 'esriSpatialRelIntersects', # the test to perform - outFields => 'OID,GEOID', + outFields => 'GEOID', returnGeometry => 'false', orderByFields => 'OID', ); @@ -410,16 +480,12 @@ sub process_block_lookup { #warn "Census block lookup: $count\n"; - # we have to do our own pagination on this, because the census bureau - # doesn't support resultOffset (maybe they don't have ArcGIS 10.3 yet). - # that's why we're ordering by OID, it's globally unique - my $last_oid = 0; my $done = 0; while (!$done) { $response = $ua->request( POST $url, Content => [ %query, - where => "OID>$last_oid", + resultOffset => $inserted, ] ); die $response->status_line unless $response->is_success; @@ -444,7 +510,6 @@ sub process_block_lookup { } #warn "Inserted $inserted records\n"; - $last_oid = $data->{features}[-1]{attributes}{OID}; $done = 1 unless $data->{exceededTransferLimit}; } diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 0a16724a8..33e150ae3 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -11,6 +11,7 @@ use FS::cust_msg; use FS::template_content; use Date::Format qw(time2str); +use PDF::WebKit; FS::UID->install_callback( sub { $conf = new FS::Conf; } ); @@ -411,8 +412,6 @@ Options are as for 'prepare', but 'from' and 'to' are meaningless. sub render { my $self = shift; - eval "use PDF::WebKit"; - die $@ if $@; my %opt = @_; my %hash = $self->prepare(%opt); my $html = $hash{'html_body'}; diff --git a/FS/FS/password_history.pm b/FS/FS/password_history.pm index 13d16010d..1915a2185 100644 --- a/FS/FS/password_history.pm +++ b/FS/FS/password_history.pm @@ -173,6 +173,7 @@ sub _upgrade_schema { push @where, " ( $fk IS NOT NULL AND NOT EXISTS(SELECT 1 FROM $table WHERE $table.$key = $fk) )"; } + return '' unless @where; my @recs = qsearch({ 'table' => 'password_history', 'extra_sql' => ' WHERE ' . join(' AND ', @where), diff --git a/FS/bin/freeside-cdr-sftp_and_import b/FS/bin/freeside-cdr-sftp_and_import index bae805151..f2bf2944f 100755 --- a/FS/bin/freeside-cdr-sftp_and_import +++ b/FS/bin/freeside-cdr-sftp_and_import @@ -21,8 +21,8 @@ $opt_e =~ s/^\.//; $opt_p ||= ''; -die "invalid cdrtypenum" if $opt_c && $opt_c !~ /^\d+$/; -die "invalid carrierid" if $opt_i && $opt_i !~ /^\d+$/; +die "invalid cdrtypenum" if defined $opt_c && $opt_c !~ /^\d+$/; +die "invalid carrierid" if defined $opt_i && $opt_i !~ /^\d+$/; my %options = (); @@ -114,8 +114,8 @@ foreach my $filename ( @$ls ) { 'batch_namevalue' => $file_timestamp, 'empty_ok' => 1, }; - $import_options->{'cdrtypenum'} = $opt_c if $opt_c; - $import_options->{'carrierid'} = $opt_i if $opt_i; + $import_options->{'cdrtypenum'} = $opt_c if defined $opt_c; + $import_options->{'carrierid'} = $opt_i if defined $opt_i; my $error = FS::cdr::batch_import($import_options); @@ -164,7 +164,7 @@ foreach my $filename ( @$ls ) { sub usage { "Usage: - cdr.sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] + freeside-cdr-sftp_and_import [ -m method ] [ -p prefix ] [ -e extension ] [ -r remotefolder ] [ -d donefolder ] [ -v level ] [ -P port ] [ -a ] [ -g ] [ -s ] [ -c cdrtypenum ] user format [sftpuser@]servername "; diff --git a/debian/control b/debian/control index 10a9df0e1..043294fc8 100644 --- a/debian/control +++ b/debian/control @@ -78,7 +78,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libipc-run-safehandles-perl,libpoe-perl,libsoap-lite-perl,libxmlrpc-lite-perl, libhtml-tableextract-perl,libhtml-element-extended-perl,libcam-pdf-perl, libnet-openssh-perl,libgd-barcode-perl,sam2p,libsys-sigaction-perl, - libgeo-googleearth-pluggable-perl,libgeo-coder-googlev3-perl,libnet-snmp-perl, + libgeo-googleearth-pluggable-perl (>=0.16),libgeo-coder-googlev3-perl, + libnet-snmp-perl, libcrypt-openssl-rsa-perl,libregexp-common-perl,libnet-cidr-perl, libregexp-ipv6-perl,libhtml-quoted-perl,libtext-password-pronounceable-perl, libconvert-color-perl,liburi-perl,libhtml-rewriteattributes-perl, @@ -107,7 +108,8 @@ Depends: aspell-en,gnupg,ghostscript,gsfonts,gzip, libspreadsheet-parsexlsx-perl, libunicode-truncate-perl (>= 0.303-1), libspreadsheet-xlsx-perl, libpod-simple-perl, libwebservice-northern911-perl, liblocale-codes-perl, liblocale-po-perl, libgeo-uscensus-geocoding-perl, - libnet-sftp-foreign-perl + libnet-sftp-foreign-perl, libpdf-webkit-perl, libgeo-shapelib-perl, + libgeo-json-perl, libauth-googleauth-perl Conflicts: libparams-classify-perl (>= 0.013-6) Replaces: freeside (<<4) Breaks: freeside (<<4) diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 6eab11dae..b1fea7da2 100755 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -1250,10 +1250,8 @@ sub do_template { $fill_in->{$_} = $access_info->{$_} foreach keys %$access_info; # update the user's authentication - my $timeout = $access_info->{'timeout'} || '3600'; my $cookie = CGI::Cookie->new('-name' => 'session', '-value' => $session_id, - '-expires' => '+'.$timeout.'s', #'-secure' => 1, # would be a good idea... ); if ( $name eq 'logout' ) { diff --git a/httemplate/browse/access_user.html b/httemplate/browse/access_user.html index 446bfe0be..658762763 100644 --- a/httemplate/browse/access_user.html +++ b/httemplate/browse/access_user.html @@ -49,6 +49,11 @@ my $groups_sub = sub { }; +my $goog_auth_sub = sub { + my $access_user = shift; + $access_user->totp_secret32 ? 'Enabled' : ''; +}; + my $installer_sub = sub { my $access_user = shift; my @sched_item = $access_user->sched_item or return ''; @@ -66,11 +71,23 @@ my $count_query = 'SELECT COUNT(*) FROM access_user'; my $link = [ $p.'edit/access_user.html?', 'usernum' ]; my @header = ( - 'Username', 'Full name', 'Groups', 'Installer', 'Customer' ); + 'Username', + 'Full name', + 'Groups', + 'Google Auth', + 'Installer', + 'Customer', +); my @fields = ( - 'username', 'name', $groups_sub, $installer_sub, $cust_sub, ); -my $align = 'lllcl'; -my @links = ( $link, $link, $link, '', '', $cust_link ); + 'username', + 'name', + $groups_sub, + $goog_auth_sub, + $installer_sub, + $cust_sub, +); +my $align = 'lllccl'; +my @links = ( $link, $link, $link, '', '', '', $cust_link ); #if ( FS::Conf->new->config('ticket_system') ) { # push @header, 'Ticketing'; diff --git a/httemplate/browse/deploy_zone.html b/httemplate/browse/deploy_zone.html index 5514d7db8..0533d6e0f 100644 --- a/httemplate/browse/deploy_zone.html +++ b/httemplate/browse/deploy_zone.html @@ -19,6 +19,21 @@ 'Contractual Mbps', 'Vertices', 'Census blocks', + 'Shapefile', + 'KMZ', + 'GeoJSON', + ], + footer => [ '', + 'All fixed zones', + '', + '', + '', + '', + '', + '', + 'download', + 'download', + 'download', ], fields => [ 'zonenum', 'description', @@ -48,6 +63,18 @@ sub { my $self = shift; FS::deploy_zone_block->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -56,7 +83,7 @@ '(adv_speed_down, adv_speed_up)', '(cir_speed_down, cir_speed_up)', ], - links => [ $link_fixed, $link_fixed, ], + links => [ $link_fixed, $link_fixed, '', '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'cllllrrr', nohtmlheader => 1, disable_maxselect => 1, @@ -79,6 +106,9 @@ 'Service Type', 'Advertised Mbps', 'Vertices', # number of vertices? not so useful + 'Shapefile', + 'KMZ', + 'GeoJSON', ], fields => [ 'zonenum', 'description', @@ -101,6 +131,18 @@ sub { my $self = shift; FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, + sub { my $self = shift; + FS::deploy_zone_vertex->count('zonenum = '.$self->zonenum) + ? 'download' : '' + }, ], sort_fields => [ 'zonenum', 'description', @@ -109,7 +151,7 @@ '(is_voice is not null, is_broadband is not null)', '(adv_speed_down, adv_speed_up)', ], - links => [ '', $link_mobile, ], + links => [ $link_mobile, $link_mobile, '', '', '', '', '', $link_shp, $link_kmz, $link_json, ], align => 'clllllr', nohtmlheader => 1, disable_maxselect => 1, @@ -120,15 +162,24 @@ <& /elements/footer.html &> <%init> + my $curuser = $FS::CurrentUser::CurrentUser; my $acl_edit = $curuser->access_right('Edit FCC report configuration'); my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); die "access denied" unless $acl_edit or $acl_edit_global; -my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; -my $link_mobile= [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_fixed = [ $p.'edit/deploy_zone-fixed.html?', 'zonenum' ]; +my $link_mobile = [ $p.'edit/deploy_zone-mobile.html?', 'zonenum' ]; +my $link_shp = [ $p.'view/deploy_zone-shp.cgi?', 'zonenum' ]; +my $link_kmz = [ $p.'view/deploy_zone-kmz.cgi?', 'zonenum' ]; +my $link_json = [ $p.'view/deploy_zone-geojson.cgi?', 'zonenum' ]; + +my $fixed_shp = $p.'view/deploy_zone-shp.cgi?zonetype=B'; +my $fixed_kmz = $p.'view/deploy_zone-kmz.cgi?zonetype=B'; +my $fixed_json = $p.'view/deploy_zone-geojson.cgi?zonetype=B'; + +my $tech_label = FS::part_pkg_fcc_option->technology_labels; +my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; -my $tech_label = FS::part_pkg_fcc_option->technology_labels; -my $spec_label = FS::part_pkg_fcc_option->spectrum_labels; diff --git a/httemplate/edit/process/access_user.html b/httemplate/edit/process/access_user.html index c27262017..8e264c1a9 100644 --- a/httemplate/edit/process/access_user.html +++ b/httemplate/edit/process/access_user.html @@ -5,7 +5,7 @@ <% include( 'elements/process.html', 'table' => 'access_user', 'viewall_dir' => 'browse', - 'copy_on_empty' => [ '_password', '_password_encoding' ], + 'copy_on_empty' => [ '_password', '_password_encoding', 'totp_secret32' ], 'clear_on_error' => [ '_password', '_password2' ], 'process_m2m' => { 'link_table' => 'access_usergroup', 'target_table' => 'access_group', diff --git a/httemplate/loginout/login.html b/httemplate/loginout/login.html index 72e9525c4..1785ea796 100644 --- a/httemplate/loginout/login.html +++ b/httemplate/loginout/login.html @@ -27,6 +27,10 @@ Password: + + One-time code: + +
@@ -42,7 +46,7 @@ my %error = ( 'no_cookie' => '', #First login, don't display an error 'bad_cookie' => 'Bad Cookie', #timed out? - 'bad_credentials' => 'Incorrect username / password', + 'bad_credentials' => 'Incorrect username / password / one-time code', #'logout' => 'You have been logged out.', ); diff --git a/httemplate/pref/pref.html b/httemplate/pref/pref.html index 56fde6d44..5f68d3e46 100644 --- a/httemplate/pref/pref.html +++ b/httemplate/pref/pref.html @@ -29,6 +29,18 @@
+ <% emt('Google Authenticator') %> + + +% if ( $curuser->totp_secret32 ) { + +% } else { + +% } + +
Enable
+
+ % } <% emt("Interface") %> diff --git a/httemplate/pref/set_totp_secret32.html b/httemplate/pref/set_totp_secret32.html new file mode 100644 index 000000000..f5676bc38 --- /dev/null +++ b/httemplate/pref/set_totp_secret32.html @@ -0,0 +1,19 @@ +<& /elements/header.html, mt('Google Authenticator for [_1]', $FS::CurrentUser::CurrentUser->username) &> + +Scan this code with the Google Authenticator application on your phone. +

+ + +

+ +Future logins will require a 6-digit code generated by the application. + +<& /elements/footer.html &> +<%init> + +my $access_user = $FS::CurrentUser::CurrentUser; + +my $error = $access_user->set_totp_secret32 unless length($access_user->totp_secret32); +die $error if $error; #better error handling for this "shouldn't happen" case? + + diff --git a/httemplate/view/deploy_zone-geojson.cgi b/httemplate/view/deploy_zone-geojson.cgi new file mode 100644 index 000000000..0c9d19353 --- /dev/null +++ b/httemplate/view/deploy_zone-geojson.cgi @@ -0,0 +1,42 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my($name, $content); + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $content = $deploy_zone->geo_json_feature->to_json; + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + my $fc = Geo::JSON::FeatureCollection->new({ + features => [ map $_->geo_json_feature, @deploy_zone ], + }); + + $content = $fc->to_json; + +} else { + die "no zonenum or zonetype\n"; +} + +http_header('Content-Type' => 'application/geo+json' ); +http_header('Content-Disposition' => "filename=$name.geojson" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + + diff --git a/httemplate/view/deploy_zone-kmz.cgi b/httemplate/view/deploy_zone-kmz.cgi new file mode 100644 index 000000000..d2af171e5 --- /dev/null +++ b/httemplate/view/deploy_zone-kmz.cgi @@ -0,0 +1,42 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my $kml = Geo::GoogleEarth::Pluggable->new; + +my $name; + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $deploy_zone->kml_polygon($kml); + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + $_->kml_polygon($kml) foreach @deploy_zone; + +} else { + die "no zonenum or zonetype\n"; +} + +my $content = $kml->archive; + +http_header('Content-Type' => 'application/vnd.google-earth.kmz' ); #kmz +http_header('Content-Disposition' => "filename=$name.kmz" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + + diff --git a/httemplate/view/deploy_zone-shp.cgi b/httemplate/view/deploy_zone-shp.cgi new file mode 100644 index 000000000..4b5dfbbc6 --- /dev/null +++ b/httemplate/view/deploy_zone-shp.cgi @@ -0,0 +1,76 @@ +<% $content %>\ +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +my $acl_edit = $curuser->access_right('Edit FCC report configuration'); +my $acl_edit_global = $curuser->access_right('Edit FCC report configuration for all agents'); +die "access denied" + unless $acl_edit or $acl_edit_global; + +my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; + +my %shapelib_opts = ( + Shapetype => Geo::Shapelib::POLYGON, + FieldNames => [ 'Tech', 'Down', 'Up' ], + FieldTypes => [ 'String:32', 'Double', 'Double' ], +); + +my( $name, $shapefile ); + +my($query) = $cgi->keywords; +if ( $query =~ /^(\d+)$/ || $cgi->param('zonenum') =~ /^(\d+$)/ ) { + my $zonenum = $1; + $name = $zonenum; + my $deploy_zone = qsearchs('deploy_zone', { 'zonenum' => $zonenum }) + or die 'unknown zonenum'; + + $shapefile = new Geo::Shapelib { + Name => "$dir/$zonenum-$$", + %shapelib_opts + }; + + $deploy_zone->shapefile_add($shapefile); + +} elsif ( $cgi->param('zonetype') =~ /^(\w)$/ ) { + my $zonetype = $1; + $name = $zonetype; + my @deploy_zone = qsearch('deploy_zone', { 'zonetype' => $zonetype, + 'disabled' => '', }); + + $shapefile = new Geo::Shapelib { + Name => "$dir/$zonetype-$$", + %shapelib_opts + }; + + $_->shapefile_add($shapefile) foreach @deploy_zone; + +} else { + die "no zonenum or zonetype\n"; +} + +$shapefile->set_bounds; + +$shapefile->save; + +#slurp up .shp .shx and .dbf files and put them in a zip.. return that +#and delete the files + +my $content = ''; +open(my $fh, '>', \$content); + +my $zip = new Archive::Zip; +$zip->addFile("$dir/$name-$$.$_", "$name.$_") foreach qw( shp shx dbf ); +unless ( $zip->writeToFileHandle($fh) == Archive::Zip::AZ_OK() ) { + die "failed to create .shz file\n"; +} +close $fh; + +unlink("$dir/$name-$$.$_") foreach qw( shp shx dbf ); + +#http_header('Content-Type' => 'x-gis/x-shapefile' ); +http_header('Content-Type' => 'archive/zip' ); +http_header('Content-Disposition' => "filename=$name.shz" ); +http_header('Content-Length' => length($content) ); +http_header('Cache-control' => 'max-age=60' ); + +