git: 38b4813662 - main - support opensearch autocomplete

From: Wolfram Schneider <wosch_at_FreeBSD.org>
Date: Wed, 04 Jan 2023 18:29:02 UTC
The branch main has been updated by wosch:

URL: https://cgit.FreeBSD.org/doc/commit/?id=38b481366298701a4accb03f7d096262eb326f97

commit 38b481366298701a4accb03f7d096262eb326f97
Author:     Wolfram Schneider <wosch@FreeBSD.org>
AuthorDate: 2023-01-04 17:50:46 +0000
Commit:     Wolfram Schneider <wosch@FreeBSD.org>
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 (<IN>) {
+        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