From nobody Wed Jan 04 18:29:02 2023 X-Original-To: dev-commits-doc-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4NnJ5p1qTBz2rBCM for ; Wed, 4 Jan 2023 18:29:02 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4NnJ5p1gmTz4FXh; Wed, 4 Jan 2023 18:29:02 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1672856942; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=i8iIhkj5oirxhEiBCPZ1c41q6g8AwcBZ/LizgfexjuM=; b=vLhmbhd7UvMBGgbH7Z/09MI1bXIym0lMdfoz0/9nyM/sW5RKXxsM9GFj7jQAQSHUvwHj4g 42S81tX7LynsMMmuoZEchdLyrQkxbc7kqij7tZvAyP9OSRS/1+PjJuB0+1e0lUm+3mhImo 3hR40/FJXntaCbDNyAw0GN6ufhoIQCRrJVu7o8bAgDUYgpIx9ia3LoJI2X7U8Ix5vlFhdZ 15llso57t0AVGeZkO7B45vIF7YhXwjTORdExhhl4GNVqxZTyq2GmyG8QVCQ9rESJIOyRsK I2mbrKQ4a+VAbcI5JHa5szq+HapIBZDrFJ7DdttvcwfnurvObvmmgZQIqtHh1Q== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1672856942; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=i8iIhkj5oirxhEiBCPZ1c41q6g8AwcBZ/LizgfexjuM=; b=ZQIsl3NxfTStti83SpeIaWgcAaIHGcXVkdCTD5nqNLjcrK6ikpVFepiuLrqTe86Hl3vErF v902Q4DZazyNb5UlS3lBtMPJhRCbwdnBeT8JJ0jsk+RFnxwQOhEAMUxG+Hj+EC06fHMjN5 GJ1d/mSU/YAwJkmUbN6xc0IHJmemD17iAX+kqcP+wJgMTT9Mhc1BOuJu9HP+R+kkKeeGxS zVI9Qaef1CuRvpbCoom3dWUxRD+G/X0cfLaLnZYyiEwbrldzAH8TYN669pnQzx9w225W/+ D6m95C4LH61BEXziMvlzWwQoOSe2WSPthI2EluzaSiap/C4+CJpgo10pTbO+jQ== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1672856942; a=rsa-sha256; cv=none; b=IjDQr114vZshx5VgOTni1XLUzIsZDrI+rLTxp1UMKod7pyylAgg5gCBNA3yKupdoaUJXBo AXFgT0XXj4J5eysZqrgK40aThUDqKFWySyX8HTyEDYwTPJDE0iLf099ZuIAmrTpvZsPxuF K6vcwa/jT4R5oIGekUCrN4w72gfPgrJuh8gimW0dYwhO25XgRXo0Yj0px1Xyf5eEJRDCl4 Cl/c0dAVqVmmTJEs8UuPcYcc7ydqybt5hWVTvMbQP3HUS63F2CiTzPWQnLMYq4xs7v3lFU qU9zB1wHpMBbpaDP+J7DApwC+wRBJZ9gEt1zw/ZCYxLL6CTJz/nNe6Yd1GTLaQ== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4NnJ5p0nVTzL2Q; Wed, 4 Jan 2023 18:29:02 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.16.1/8.16.1) with ESMTP id 304IT2Ik017111; Wed, 4 Jan 2023 18:29:02 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.16.1/8.16.1/Submit) id 304IT2wq017110; Wed, 4 Jan 2023 18:29:02 GMT (envelope-from git) Date: Wed, 4 Jan 2023 18:29:02 GMT Message-Id: <202301041829.304IT2wq017110@gitrepo.freebsd.org> To: doc-committers@FreeBSD.org, dev-commits-doc-all@FreeBSD.org From: Wolfram Schneider Subject: git: 38b4813662 - main - support opensearch autocomplete List-Id: Commit messages for all branches of the doc repository List-Archive: https://lists.freebsd.org/archives/dev-commits-doc-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: Sender: owner-dev-commits-doc-all@freebsd.org X-BeenThere: dev-commits-doc-all@freebsd.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: wosch X-Git-Repository: doc X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 38b481366298701a4accb03f7d096262eb326f97 Auto-Submitted: auto-generated X-ThisMailContainsUnwantedMimeParts: N The branch main has been updated by wosch: URL: https://cgit.FreeBSD.org/doc/commit/?id=38b481366298701a4accb03f7d096262eb326f97 commit 38b481366298701a4accb03f7d096262eb326f97 Author: Wolfram Schneider AuthorDate: 2023-01-04 17:50:46 +0000 Commit: Wolfram Schneider CommitDate: 2023-01-04 17:50:46 +0000 support opensearch autocomplete --- website/content/en/cgi/MyCgiSimple.pm | 433 ++++++++++++++++++++++++++ website/content/en/cgi/ports-autocomplete.cgi | 219 +++++++++++++ 2 files changed, 652 insertions(+) diff --git a/website/content/en/cgi/MyCgiSimple.pm b/website/content/en/cgi/MyCgiSimple.pm new file mode 100644 index 0000000000..d31623ba75 --- /dev/null +++ b/website/content/en/cgi/MyCgiSimple.pm @@ -0,0 +1,433 @@ +# This module is free software; you can redistribute it and/or modify it under the +# same terms as Perl itself. See https://metacpan.org/pod/perlartistic +# +# MyCgiSimple.pm - striped down version of CGI::Simple; + +package MyCgiSimple; + +sub charset { + return 'utf-8'; +} + +sub param { + my ( $self, $param, @p ) = @_; + unless ( defined $param ) { # return list of all params + my @params = $self->{'.parameters'} ? @{ $self->{'.parameters'} } : (); + return @params; + } + unless (@p) { # return values for $param + return () unless exists $self->{$param}; + return wantarray ? @{ $self->{$param} } : $self->{$param}->[0]; + } + if ( $param =~ m/^-name$/i and @p == 1 ) { + return () unless exists $self->{ $p[0] }; + return wantarray ? @{ $self->{ $p[0] } } : $self->{ $p[0] }->[0]; + } + + # set values using -name=>'foo',-value=>'bar' syntax. + # also allows for $q->param( 'foo', 'some', 'new', 'values' ) syntax + ( $param, undef, @p ) = @p + if $param =~ m/^-name$/i; # undef represents -value token + $self->_add_param( $param, ( ref $p[0] eq 'ARRAY' ? $p[0] : [@p] ), + 'overwrite' ); + return wantarray ? @{ $self->{$param} } : $self->{$param}->[0]; +} + +sub new { + my ( $class, $init ) = @_; + $class = ref($class) || $class; + my $self = {}; + bless $self, $class; + + $self->_initialize_globals; + $self->_store_globals; + $self->_initialize($init); + + return $self; +} + +sub path_info { + my ( $self, $info ) = @_; + if ( defined $info ) { + $info = "/$info" if $info !~ m|^/|; + $self->{'.path_info'} = $info; + } + elsif ( !defined( $self->{'.path_info'} ) ) { + $self->{'.path_info'} = + defined( $ENV{'PATH_INFO'} ) ? $ENV{'PATH_INFO'} : ''; + + # hack to fix broken path info in IIS source CGI.pm + $self->{'.path_info'} =~ s/^\Q$ENV{'SCRIPT_NAME'}\E// + if defined( $ENV{'SERVER_SOFTWARE'} ) + && $ENV{'SERVER_SOFTWARE'} =~ /IIS/; + } + return $self->{'.path_info'}; +} + +sub _initialize { + my ( $self, $init ) = @_; + + if ( !defined $init ) { + + # initialize from QUERY_STRING, STDIN or @ARGV + $self->_read_parse(); + } + elsif ( ( ref $init ) =~ m/HASH/i ) { + + # initialize from param hash + for my $param ( keys %{$init} ) { + $self->_add_param( $param, $init->{$param} ); + } + } + + # chromatic's blessed GLOB patch + # elsif ( (ref $init) =~ m/GLOB/i ) { # initialize from a file + elsif ( UNIVERSAL::isa( $init, 'GLOB' ) ) { # initialize from a file + $self->_init_from_file($init); + } + elsif ( ( ref $init ) eq 'CGI::Simple' ) { + + # initialize from a CGI::Simple object + require Data::Dumper; + + # avoid problems with strict when Data::Dumper returns $VAR1 + my $VAR1; + my $clone = eval( Data::Dumper::Dumper($init) ); + if ($@) { + $self->cgi_error("Can't clone CGI::Simple object: $@"); + } + else { + $_[0] = $clone; + } + } + else { + $self->_parse_params($init); # initialize from a query string + } +} + +sub _initialize_globals { + + # set this to 1 to use CGI.pm default global settings + $USE_CGI_PM_DEFAULTS = 0 + unless defined $USE_CGI_PM_DEFAULTS; + + # see if user wants old CGI.pm defaults + if ($USE_CGI_PM_DEFAULTS) { + _use_cgi_pm_global_settings(); + return; + } + + # no file uploads by default, set to 0 to enable uploads + $DISABLE_UPLOADS = 1 + unless defined $DISABLE_UPLOADS; + + # use a post max of 100K, set to -1 for no limits + $POST_MAX = 102_400 + unless defined $POST_MAX; + + # set to 1 to not include undefined params parsed from query string + $NO_UNDEF_PARAMS = 0 + unless defined $NO_UNDEF_PARAMS; + + # separate the name=value pairs with ; rather than & + $USE_PARAM_SEMICOLONS = 0 + unless defined $USE_PARAM_SEMICOLONS; + + # return everything as utf-8 + $PARAM_UTF8 ||= 0; + $PARAM_UTF8 and require Encode; + + # only print headers once + $HEADERS_ONCE = 0 + unless defined $HEADERS_ONCE; + + # Set this to 1 to enable NPH scripts + $NPH = 0 + unless defined $NPH; + + # 0 => no debug, 1 => from @ARGV, 2 => from STDIN + $DEBUG = 0 + unless defined $DEBUG; + + # filter out null bytes in param - value pairs + $NO_NULL = 1 + unless defined $NO_NULL; + + # set behavior when cgi_err() called -1 => silent, 0 => carp, 1 => croak + $FATAL = -1 + unless defined $FATAL; +} + +# this is called by new, we will never directly reference the globals again +sub _store_globals { + my $self = shift; + + $self->{'.globals'}->{'DISABLE_UPLOADS'} = $DISABLE_UPLOADS; + $self->{'.globals'}->{'POST_MAX'} = $POST_MAX; + $self->{'.globals'}->{'NO_UNDEF_PARAMS'} = $NO_UNDEF_PARAMS; + $self->{'.globals'}->{'USE_PARAM_SEMICOLONS'} = $USE_PARAM_SEMICOLONS; + $self->{'.globals'}->{'HEADERS_ONCE'} = $HEADERS_ONCE; + $self->{'.globals'}->{'NPH'} = $NPH; + $self->{'.globals'}->{'DEBUG'} = $DEBUG; + $self->{'.globals'}->{'NO_NULL'} = $NO_NULL; + $self->{'.globals'}->{'FATAL'} = $FATAL; + $self->{'.globals'}->{'USE_CGI_PM_DEFAULTS'} = $USE_CGI_PM_DEFAULTS; + $self->{'.globals'}->{'PARAM_UTF8'} = $PARAM_UTF8; +} + +sub _read_parse { + my $self = shift; + my $data = ''; + my $type = $ENV{'CONTENT_TYPE'} || 'No CONTENT_TYPE received'; + my $length = $ENV{'CONTENT_LENGTH'} || 0; + my $method = $ENV{'REQUEST_METHOD'} || 'No REQUEST_METHOD received'; + + # first check POST_MAX Steve Purkis pointed out the previous bug + if ( ( $method eq 'POST' or $method eq "PUT" ) + and $self->{'.globals'}->{'POST_MAX'} != -1 + and $length > $self->{'.globals'}->{'POST_MAX'} ) + { + $self->cgi_error( +"413 Request entity too large: $length bytes on STDIN exceeds \$POST_MAX!" + ); + + # silently discard data ??? better to just close the socket ??? + while ( $length > 0 ) { + last unless _internal_read( $self, my $buffer ); + $length -= length($buffer); + } + + return; + } + + if ( $length and $type =~ m|^multipart/form-data|i ) { + my $got_length = $self->_parse_multipart; + if ( $length != $got_length ) { + $self->cgi_error( +"500 Bad read on multipart/form-data! wanted $length, got $got_length" + ); + } + + return; + } + elsif ( $method eq 'POST' or $method eq 'PUT' ) { + if ($length) { + + # we may not get all the data we want with a single read on large + # POSTs as it may not be here yet! Credit Jason Luther for patch + # CGI.pm < 2.99 suffers from same bug + _internal_read( $self, $data, $length ); + while ( length($data) < $length ) { + last unless _internal_read( $self, my $buffer ); + $data .= $buffer; + } + + unless ( $length == length $data ) { + $self->cgi_error( "500 Bad read on POST! wanted $length, got " + . length($data) ); + return; + } + + if ( $type !~ m|^application/x-www-form-urlencoded| ) { + $self->_add_param( $method . "DATA", $data ); + } + else { + $self->_parse_params($data); + } + } + } + elsif ( $method eq 'GET' or $method eq 'HEAD' ) { + $data = + $self->{'.mod_perl'} + ? $self->_mod_perl_request()->args() + : $ENV{'QUERY_STRING'} + || $ENV{'REDIRECT_QUERY_STRING'} + || ''; + $self->_parse_params($data); + } + else { + unless ($self->{'.globals'}->{'DEBUG'} + and $data = $self->read_from_cmdline() ) + { + $self->cgi_error("400 Unknown method $method"); + return; + } + + unless ($data) { + + # I liked this reporting but CGI.pm does not behave like this so + # out it goes...... + # $self->cgi_error("400 No data received via method: $method, type: $type"); + return; + } + + $self->_parse_params($data); + } +} + +sub cgi_error { + my ( $self, $err ) = @_; + if ($err) { + require Carp; + $self->{'.cgi_error'} = $err; + $self->{'.globals'}->{'FATAL'} == 1 ? croak $err + : $self->{'.globals'}->{'FATAL'} == 0 ? carp $err + : return $err; + } + return $self->{'.cgi_error'}; +} + +# This internal routine creates date strings suitable for use in +# cookies and HTTP headers. (They differ, unfortunately.) +# Thanks to Mark Fisher for this. +sub expires { + my ( $time, $format ) = @_; + $format ||= 'http'; + + my (@MON) = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/; + my (@WDAY) = qw/Sun Mon Tue Wed Thu Fri Sat/; + + # pass through preformatted dates for the sake of expire_calc() + $time = expire_calc($time); + return $time unless $time =~ /^\d+$/; + + # make HTTP/cookie date string from GMT'ed time + # (cookies use '-' as date separator, HTTP uses ' ') + my ($sc) = ' '; + $sc = '-' if $format eq "cookie"; + my ( $sec, $min, $hour, $mday, $mon, $year, $wday ) = gmtime($time); + $year += 1900; + return sprintf( "%s, %02d$sc%s$sc%04d %02d:%02d:%02d GMT", + $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec ); +} + +# This internal routine creates an expires time exactly some number of +# hours from the current time. It incorporates modifications from +# Mark Fisher. +sub expire_calc { + my ($time) = @_; + my (%mult) = ( + 's' => 1, + 'm' => 60, + 'h' => 60 * 60, + 'd' => 60 * 60 * 24, + 'M' => 60 * 60 * 24 * 30, + 'y' => 60 * 60 * 24 * 365 + ); + + # format for time can be in any of the forms... + # "now" -- expire immediately + # "+180s" -- in 180 seconds + # "+2m" -- in 2 minutes + # "+12h" -- in 12 hours + # "+1d" -- in 1 day + # "+3M" -- in 3 months + # "+2y" -- in 2 years + # "-3m" -- 3 minutes ago(!) + # If you don't supply one of these forms, we assume you are + # specifying the date yourself + my ($offset); + if ( !$time || ( lc($time) eq 'now' ) ) { + $offset = 0; + } + elsif ( $time =~ /^\d+/ ) { + return $time; + } + elsif ( $time =~ /^([+-]?(?:\d+|\d*\.\d*))([mhdMy]?)/ ) { + $offset = ( $mult{$2} || 1 ) * $1; + } + else { + return $time; + } + return ( time + $offset ); +} + +sub header { + my $self = shift; + my %args = @_; + + my $type = $args{-type}; + my $charset = $args{-charset}; + my $expires = $args{-expires}; + + push( @header, "Expires: " . expires( $expires, 'http' ) ) + if $expires; + push( @header, "Date: " . expires( 0, 'http' ) ) if $expires; + + if ( $type && $charset ) { + $type .= '; charset=' . $charset; + } + push @header, "Content-Type: $type" if $type; + + return join( "\n", @header ), "\n\n"; +} + +sub _parse_params { + my ( $self, $data ) = @_; + return () unless defined $data; + unless ( $data =~ /[&=;]/ ) { + + #$self->{'keywords'} = [ $self->_parse_keywordlist( $data ) ]; + $self->{'keywords'} = []; + return; + } + my @pairs = split /[&;]/, $data; + for my $pair (@pairs) { + my ( $param, $value ) = split /=/, $pair, 2; + next unless defined $param; + $value = '' unless defined $value; + $self->_add_param( $self->url_decode($param), + $self->url_decode($value) ); + } +} + +# use correct encoding conversion to handle non ASCII char sets. +# we import and install the complex routines only if we have to. +BEGIN { + + sub url_decode { + my ( $self, $decode ) = @_; + return () unless defined $decode; + $decode =~ tr/+/ /; + $decode =~ s/%([a-fA-F0-9]{2})/ pack "C", hex $1 /eg; + return $decode; + } + + sub url_encode { + my ( $self, $encode ) = @_; + return () unless defined $encode; + $encode =~ s/([^A-Za-z0-9\-_.!~*'() ])/ uc sprintf "%%%02x",ord $1 /eg; + $encode =~ tr/ /+/; + return $encode; + } + +} + +sub _add_param { + my ( $self, $param, $value, $overwrite ) = @_; + return () unless defined $param and defined $value; + $param =~ tr/\000//d if $self->{'.globals'}->{'NO_NULL'}; + @{ $self->{$param} } = () if $overwrite; + @{ $self->{$param} } = () unless exists $self->{$param}; + my @values = ref $value ? @{$value} : ($value); + for my $value (@values) { + next + if $value eq '' + and $self->{'.globals'}->{'NO_UNDEF_PARAMS'}; + $value =~ tr/\000//d if $self->{'.globals'}->{'NO_NULL'}; + $value = Encode::decode( utf8 => $value ) + if $self->{'.globals'}->{PARAM_UTF8}; + push @{ $self->{$param} }, $value; + unless ( $self->{'.fieldnames'}->{$param} ) { + push @{ $self->{'.parameters'} }, $param; + $self->{'.fieldnames'}->{$param}++; + } + } + return scalar @values; # for compatibility with CGI.pm request.t +} + +# from CGI::Simple 1.115 +sub script_name { $ENV{'SCRIPT_NAME'} || $0 || '' } +sub server_name { $ENV{'SERVER_NAME'} || 'localhost' } + +1; diff --git a/website/content/en/cgi/ports-autocomplete.cgi b/website/content/en/cgi/ports-autocomplete.cgi new file mode 100755 index 0000000000..438b1f3fbc --- /dev/null +++ b/website/content/en/cgi/ports-autocomplete.cgi @@ -0,0 +1,219 @@ +#!/usr/local/bin/perl -T +# Copyright (c) 2009-2023 Wolfram Schneider, https://wolfram.schneider.org +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# ports-autocomplete.cgi - autocomplete/suggestion service for FreeBSD ports +# +# For the OpenSearch protocol, please read https://en.wikipedia.org/wiki/OpenSearch +# +# expected run time on a modern CPU: ca. 16ms (10ms for perl, 6ms for GNU grep) + +use lib qw(. ../../lib); +use MyCgiSimple; + +use strict; +use warnings; # 3% slower + +$ENV{PATH} = "/usr/local/bin:/bin:/usr/bin"; +$ENV{'LANG'} = 'C'; + +my $debug = 0; +binmode( \*STDIN, ":bytes" ); +binmode( \*STDOUT, ":bytes" ); +binmode( \*STDERR, ":bytes" ); + +sub suggestion { + my %args = @_; + + my $database = $args{'database'}; + my $icase = $args{'icase'}; + my $stype = $args{'stype'}; + my $query = $args{'query'}; + my $limit = $args{'limit'}; + + if ( !-e $database ) { + warn "$!: $database\n"; + return; + } + + # GNU grep, ripgrep, agrep, BSD grep etc. + my @command = ('grep'); + + # read more data for prefix match <=> sub-string match + my $limit_factor = 8; + + push @command, ( '-m', $limit * $limit_factor ); + push @command, '-i' if $icase == 1; + push @command, ( '--', $query, $database ); + + warn join( " ", @command ), "\n" if $debug >= 2; + if ( !open( IN, '-|' ) ) { + exec @command; + die "@command: $! :: $?\n"; + } + binmode( \*IN, ":bytes" ); + + my @data = (); + while () { + chomp; + s,["],,g; + +# XXX: workaround for Firefox which ignores entries with "::" or "." inside a string +# Note: we have to undo this in ports.cgi + s/::/: :/g; + s/\./ \./g; + push @data, $_; + + last if scalar(@data) >= $limit * $limit_factor; + } + + close IN; + + # a sorted list, but prefix matches first + # e.g. 'sort(1)' is before 'alphasort(3) if you searched for 'sor' + my $lc_query = $icase ? lc($query) : $query; + my @prefix = grep { index( lc($_), $lc_query ) == 0 } @data; + my @non_prefix = grep { index( lc($_), $lc_query ) != 0 } @data; + + # prefix first & real limit + @data = ( @prefix, @non_prefix ); + @data = splice( @data, 0, $limit ); + + warn "data: ", join( " ", @data ), "\n" if $debug >= 2; + return @data; +} + +sub escapeQuote { + my $string = shift; + + $string =~ s/"/\\"/g; + + return $string; +} + +# create devbridge autocomplete response JSON object +sub devbridge_autocomplete { + my $query = shift; + my $suggestion = shift; + + my @suggestion = @$suggestion; + + print qq/{ query:"/, escapeQuote($query), qq/", suggestions:[/; + print '"', join( '","', map { escapeQuote($_) } @suggestion ), '"' + if scalar(@suggestion) > 0; + print "] }\n"; + + warn "query '$query', suggestions: ", join ", ", @suggestion, "\n" + if $debug >= 1; +} + +# create opensearch autocomplete response JSON object +sub opensearch_autocomplete { + my $query = shift; + my $suggestion = shift; + + my @suggestion = @$suggestion; + + print '["', escapeQuote($query), '", ['; + + print qq{"}, join( '","', map { escapeQuote($_) } @suggestion ), qq{"} + if scalar(@suggestion) > 0; + print "]]\n"; + + warn "query '$query', suggestions: ", join ", ", @suggestion, "\n" + if $debug >= 1; +} + +sub check_query { + my $query = shift; + + # XXX: Firefox opensearch autocomplete workarounds + # remove space before a dot + $query =~ s/ \././g; + + # remove space between double colon + $query =~ s/: :/::/g; + + return $query; +} + +###################################################################### +# param alias: query, q: search query +# stype, s: name or name + description +# icase,i : case sensitive +# debug, d: debug level + +my $max_suggestions = 24; +my $database_name = '/usr/local/www/ports/etc/autocomplete/autocomplete-name.txt'; +my $database_description = '/usr/local/www/ports/etc/autocomplete/autocomplete-description.txt'; + +my $q = new MyCgiSimple; + +my $test_query = "bbbike"; + +my $query = $q->param('query') // $q->param('q') // $test_query; +my $stype = $q->param('stype') // $q->param('s') // ""; +my $d = $q->param('debug') // $q->param('d') // $debug; + +# we always use case insensive search for autocomplete +my $icase = $q->param('icase') // $q->param('i') // 1; + +$query = ( $query =~ /^(.+)$/ ? $1 : "" ); +$stype = ( $stype =~ /^([a-z\-]+)$/ ? $1 : "" ); +$icase = ( $icase =~ /^([01])$/ ? $1 : 0 ); +$debug = ( $d =~ /^([0-3])$/ ? $1 : $debug ); + +my $database = + $stype eq 'name' + ? $database_name + : $database_description; + +my $expire = $debug >= 2 ? '+1s' : '+1h'; +print $q->header( + -type => 'text/javascript', + -charset => 'utf-8', + -expires => $expire, +); + +my $query_original = $query; +$query = &check_query($query); + +my @suggestion = (); +if ( length($query) >= 2 ) { + @suggestion = &suggestion( + 'database' => $database, + 'limit' => $max_suggestions, + 'query' => $query, + 'icase' => $icase, + 'stype' => $stype, + ); +} + +# ns=devbridge, for jQuery devbridge plugin +# &devbridge_autocomplete( $query, \@suggestion ); + +# ns=opensearch (Firefox etc.) +&opensearch_autocomplete( $query_original, \@suggestion ); + +#EOF