#!/usr/bin/perl ################################################################ # # SearchScript.pl - script to search for specified products # ################################################################ #?use CGI::Carp qw(fatalsToBrowser); # #?my $DEBUG = 0; # very verbose logging if 1 # # Make sure "." is included in the @INC directory list so we can find our packages # my $bFound = 0; my $sDir; foreach $sDir (@INC) { if ($sDir eq ".") { $bFound = 1; last; } } if (!$bFound) { push (@INC, "."); } # # NT systems rarely execute the CGI scripts in the cgi-bin, so attempt to locate # the packages in that case. This may still fail if the cgi-bin folder is named # something else, but at least we will catch 80% of the cases. The INCLUDEPATHADJUSMENT # covers the remaining cases. # push (@INC, "cgi-bin"); require al000001; require ao000001; use strict; $::prog_name = "SearchScript"; # Program Name $::prog_name = $::prog_name; $::prog_ver = '$Revision: 71 $ '; # program version $::prog_ver = substr($::prog_ver, 11); # strip the revision information $::prog_ver =~ s/ \$//; # and the trailers my $nFILEVERSION = 1; # validate blob # # Enable/disable fragment length check # $::MATCH_WHOLE_WORDS_ONLY = $::FALSE; $::ANY_PRICE_BAND = -1; $::INTERSECT = 0; $::UNION = 1; $::MAX_RETRY_COUNT = 10; $::RETRY_SLEEP_DURATION = 1; # # Do the main script initialization # my $sPath = Init(); my ($status, $sError); my %MatchWords; my %UsedValues; # # A hash of array references for checkbox decoding. # Only built when at least one Property Search is detected. # my %InputArrays; # my @ResultsStack; my $bValidSearch = 0; my $bPriceSearch = $::FALSE; # # result constants for stack # my $TRUE_RESULT = 1; my $FALSE_RESULT = 0; my $SKIP_RESULT = -1; # # User interface types from property search specification # my $UI_TEXTBOX = 0; my $UI_RADIOBUTTON = 1; my $UI_CHECKBOX = 2; my $UI_DROPDOWNLIST = 3; my $UI_LIST = 4; # # default search file number # my $sSearchNum = ''; my $sSearchFile = $sPath . "customsearch"; # # Check for direct link query # if (exists $::g_InputHash{PRODREF}) # jump to product requested { my $sProdRef = ACTINIC::DecodeText($::g_InputHash{PRODREF}, $ACTINIC::FORM_URL_ENCODED); my $sHTML = DirectLinkToProduct($sPath, $sProdRef); ACTINIC::PrintPage($sHTML, ""); exit; } elsif (exists $::g_InputHash{SECTIONID}) # jump to section (file) requested { my $sSection = ACTINIC::DecodeText($::g_InputHash{SECTIONID}, $ACTINIC::FORM_URL_ENCODED); my $sHTML = DisplayDirectLinkPage($sSection); # # Make fool the XML parser here (see definition of UnregTagHandler) # $::g_bLoginPage = $::TRUE; ACTINIC::PrintPage($sHTML, ""); exit; } # # compute search file name from SN parameter # if (exists $::g_InputHash{SN}) { # $sSearchNnum is a string of digits used to build a filename. # It just looks like a number. # $sSearchNum = $::g_InputHash{SN}; unless ($sSearchNum =~ /^\d*$/) { my $filelog = ACTINIC::GetPhrase(-1, 325, $sSearchNum); SearchError($filelog, $sPath); #? ACTINIC::RecordErrors($filelog, $sPath); exit; } } $sSearchFile .= "$sSearchNum.fil"; unless (open SFILE, "<$sSearchFile") { my $filelog = ACTINIC::GetPhrase(-1, 21, $sSearchFile, $!); SearchError($filelog, $sPath); #? ACTINIC::RecordErrors($filelog, $sPath); exit; } # # read the entire file into an array # my @SearchCmd = ; close SFILE; # # check for supported versions # my $nFileVersion = shift (@SearchCmd); unless ($nFileVersion == $nFILEVERSION) { my $filelog = ACTINIC::GetPhrase(-1, 326, $nFILEVERSION, $nFileVersion); SearchError($filelog, $sPath); #? ACTINIC::RecordErrors($filelog, $sPath); exit; } # # log for debugging # #?my $filelog = "\nContents of $sSearchFile:\nversion = $nFileVersion"; my $sLine; foreach $sLine (@SearchCmd) { #? $filelog .= $sLine; chomp $sLine; my ($sCmd, $sParam1, $sParam2) = split ('!', $sLine); my $sValue1 = ''; if ($sParam1) { if (exists $::g_InputHash{$sParam1}) { $sValue1 = $::g_InputHash{$sParam1}; } } # # trim excess leading and trailing white space # $sValue1 =~ s/^\s*//o; $sValue1 =~ s/\s*$//o; # my $sValue2 = ''; if ($sParam2) { if (exists $::g_InputHash{$sParam2}) { $sValue2 = $::g_InputHash{$sParam2}; } } #? $filelog .= "sCmd = $sCmd, sParam1 = $sParam1, sValue1 = $sValue1, sValue2 = $sValue2\n"; if ($sCmd eq 'Text') { my $bText = $::UNION; if ($sValue2 eq 'A') { $bText = $::INTERSECT; } elsif ($sValue2 ne 'O') { my $sError = ACTINIC::GetPhrase(-1, 244); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } if ($sValue1 eq '') { # early detection of nothing to do # next; } # # need at least one valid Cmd # $bValidSearch = 1; my $rTextHits = {}; # # Now do the text search. # ($status, $sError) = SearchText($sPath, \$sValue1, $bText, $rTextHits); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } if (scalar (keys %$rTextHits)) { push @ResultsStack, [$TRUE_RESULT, $rTextHits]; # # save words that yielded successful search # my @matches = split (' ', $sValue1); my $word; foreach $word (@matches) { $MatchWords{$word} = 1; } #? if ($DEBUG) #? { #? my @TextResults = keys %$rTextHits; #? my $nTextResults = scalar (@TextResults); #? my $sTextResults = join (';', @TextResults); #? $filelog .= "Text search yielded $nTextResults hits\n"; #? $filelog .= " $sTextResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "TextResults were FALSE\n"; } } elsif ($sCmd eq 'Price') { $bPriceSearch = $::TRUE; # note that we had price search my $rPriceHits = {}; # retrieve the price range and make numeric # my $nPriceBand = $sValue1; if (defined $nPriceBand && ($nPriceBand != $::ANY_PRICE_BAND)) { # need at least one valid Cmd # $bValidSearch = 1; # # Do the price band search # ($status, $sError) = SearchPrice($sPath, $nPriceBand, $rPriceHits); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } if (scalar (keys %$rPriceHits)) { push @ResultsStack, [$TRUE_RESULT, $rPriceHits]; #? if ($DEBUG) #? { #? my @PriceResults = keys %$rPriceHits; #? my $nPriceResults = scalar (@PriceResults); #? my $sPriceResults = join (';', @PriceResults); #? $filelog .= "Price search yielded $nPriceResults hits\n"; #? $filelog .= " $sPriceResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "PriceResults were FALSE\n"; } } else { push @ResultsStack, [$SKIP_RESULT]; #? $filelog .= "SearchPrice was skipped\n"; } } elsif ($sCmd eq 'And') { if ($#ResultsStack < 1) { # treat empty stack or single result as no operation #? $filelog .= "And Command with nothing to do\n"; } else { my $rArray1 = pop @ResultsStack; my $rArray2 = pop @ResultsStack; if (${$rArray1}[0] == $SKIP_RESULT) { push @ResultsStack, $rArray2; #? $filelog .= "And Command skipping array1\n"; } elsif (${$rArray2}[0] == $SKIP_RESULT) { push @ResultsStack, $rArray1; #? $filelog .= "And Command skipping array2\n"; } elsif ((${$rArray1}[0] == $FALSE_RESULT) or (${$rArray2}[0] == $FALSE_RESULT)) { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "And Command computes false\n"; } else { my $rJoins = {}; JoinHashes(${$rArray1}[1], ${$rArray2}[1], $::INTERSECT, $rJoins); if (scalar (keys %$rJoins)) { push @ResultsStack, [$TRUE_RESULT, $rJoins]; #? if ($DEBUG) #? { #? my @JoinResults = keys %$rJoins; #? my $nJoinResults = scalar (@JoinResults); #? my $sJoinResults = join (';', @JoinResults); #? $filelog .= "And operation yielded $nJoinResults hits\n"; #? $filelog .= " $sJoinResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "And JoinResults were FALSE\n"; } } } } elsif ($sCmd eq 'Or') { if ($#ResultsStack < 1) { # treat empty stack or single result as no operation #? $filelog .= "Or Command with nothing to do\n"; } else { my $rArray1 = pop @ResultsStack; my $rArray2 = pop @ResultsStack; if (${$rArray1}[0] == $FALSE_RESULT) { push @ResultsStack, $rArray2; #? $filelog .= "Or Command replaces FALSE array1\n"; } elsif (${$rArray2}[0] == $FALSE_RESULT) { push @ResultsStack, $rArray1; #? $filelog .= "Or Command replaces FALSE array2\n"; } elsif (${$rArray1}[0] == $SKIP_RESULT) { if (${$rArray2}[0] == $SKIP_RESULT) { push @ResultsStack, [$SKIP_RESULT]; #? $filelog .= "Or Command treats two SKIPs as SKIP\n"; } else { push @ResultsStack, $rArray2; #? $filelog .= "Or Command replaces SKIP array1\n"; } } elsif (${$rArray2}[0] == $SKIP_RESULT) { push @ResultsStack, $rArray1; #? $filelog .= "Or Command replaces SKIP array2\n"; } else { my $rJoins = {}; JoinHashes(${$rArray1}[1], ${$rArray2}[1], $::UNION, $rJoins); if (scalar (keys %$rJoins)) { push @ResultsStack, [$TRUE_RESULT, $rJoins]; #? if ($DEBUG) #? { #? my @JoinResults = keys %$rJoins; #? my $nJoinResults = scalar (@JoinResults); #? my $sJoinResults = join (';', @JoinResults); #? $filelog .= "Or operation yielded $nJoinResults hits\n"; #? $filelog .= " $sJoinResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "Or JoinResults were FALSE, should not get here\n"; } } } } elsif (($sCmd eq 'Text Property') or ($sCmd eq 'Integer') or ($sCmd eq 'Date')) { unless (exists $$::g_pSearchSetup{$sParam1}) { my $sError = ACTINIC::GetPhrase(-1, 327, $sParam1); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } my $rBlobParam = $$::g_pSearchSetup{$sParam1}; # # MultiSelect Properties can repeat a CGI_Parameter, # but $::g_InputHash can't detect that case # #? $filelog .= "UIType = $$rBlobParam{UIType}\n"; #? $filelog .= "Optional = $$rBlobParam{Optional}\n"; #? $filelog .= "MultiSelect = $$rBlobParam{MultiSelect}\n"; #? $filelog .= "Label = $$rBlobParam{Label}\n"; # unless (scalar (keys %InputArrays)) { #? $filelog .= "Create checkbox arrays on first Property Search\n"; ($status, $sError) = ParseSearchInput(\%InputArrays); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } #? if ($DEBUG) #? { #? my ($p, $v); #? foreach $p (keys %InputArrays) #? { #? $filelog .= sprintf ("InputArrays for %s: ", $p); #? foreach $v (@{$InputArrays{$p}}) #? { #? $filelog .= "$v; "; #? } #? $filelog .= "\n"; #? } #? } } # # Multiple hits from MultiSelect Properties will OR # my $rPropertyHits = {}; # # Initialize for the nothing selected case # my $sValuem = ''; my @sMulti = (); # if ($InputArrays{$sParam1}) { @sMulti = @{$InputArrays{$sParam1}}; } while (@sMulti) { $sValuem = shift @sMulti; # # Properties without Values must be Optional # if (($sValuem eq '') and (!$$rBlobParam{Optional})) { my $sError = ACTINIC::GetPhrase(-1, 328, $$rBlobParam{Label}); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } # # A checked checkbox without selection text generates default 'on' # if (($sValuem eq 'on') and ($$rBlobParam{UIType} == $UI_CHECKBOX)) { $sValuem = ''; #? $filelog .= "Checkbox default 'on' converted to '' (Any)\n"; } # # Properties must have single value unless Multiselect # if (exists $UsedValues{$sParam1}) { # A Property may be repeated if it specifies the same value # Needed for custom searches # if (($UsedValues{$sParam1} != $sValuem) and (!$$rBlobParam{MultiSelect})) { my $sError = ACTINIC::GetPhrase(-1, 329, $$rBlobParam{Label}); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } } $UsedValues{$sParam1} = $sValuem; # if (($sCmd eq 'Integer') && ($sValuem ne '')) # # validate non-optional integers, treat "Any" as optional, zero is ok. { unless ($sValuem =~ /^[-+]?\d+$/o) { my $sError = ACTINIC::GetPhrase(-1, 330, $sValuem, $$rBlobParam{Label}); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } } elsif (($sCmd eq 'Date') && $sValuem) # # validate non-optional dates, treat "Any" as optional { unless ($sValuem =~ /\d{8}/o) { my $sError = ACTINIC::GetPhrase(-1, 331, $sValuem, $$rBlobParam{Label}); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } } else { # Text Property, reserve for future use } # if ($sValuem ne '') { # need at least one valid Cmd # $bValidSearch = 1; # # Do the Property search # ($status, $sError) = SearchProperty($sPath, $sParam1, $sValuem, $rPropertyHits); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } #? $filelog .= "Property search $sParam1 = $sValuem\n"; } else { # "Any" exits "while" with $sValuem eq '' # @sMulti = (); } } # end of MultiSelect while loop # if ($sValuem eq '') { push @ResultsStack, [$SKIP_RESULT]; #? $filelog .= "SearchProperty was skipped\n"; } elsif (scalar (keys %$rPropertyHits)) { push @ResultsStack, [$TRUE_RESULT, $rPropertyHits]; #? if ($DEBUG) #? { #? my @PropertyResults = keys %$rPropertyHits; #? my $nPropertyResults = scalar (@PropertyResults); #? my $sPropertyResults = join (';', @PropertyResults); #? $filelog .= "Property search yielded $nPropertyResults hits\n"; #? $filelog .= " $sPropertyResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "PropertyResults were FALSE\n"; } } elsif ($sCmd eq 'Section') { my $rSectionHits = {}; # if ($sValue1) { # need at least one valid Cmd # $bValidSearch = 1; # # Do the section search # ($status, $sError) = SearchSection($sPath, $sValue1, $rSectionHits); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } if (scalar (keys %$rSectionHits)) { push @ResultsStack, [$TRUE_RESULT, $rSectionHits]; #? if ($DEBUG) #? { #? my @SectionResults = keys %$rSectionHits; #? my $nSectionResults = scalar (@SectionResults); #? my $sSectionResults = join (';', @SectionResults); #? $filelog .= "Section search yielded $nSectionResults hits\n"; #? $filelog .= " $sSectionResults\n"; #? } } else { push @ResultsStack, [$FALSE_RESULT]; #? $filelog .= "SectionResults were FALSE\n"; } } else { push @ResultsStack, [$SKIP_RESULT]; #? $filelog .= "SearchSection was skipped\n"; } } else { my $sError = ACTINIC::GetPhrase(-1, 332, $sCmd, $sSearchFile); SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } } # # Convert @ResultsStack to single hash reference (%NullSet by default) # my %NullSet; my $rhashResults = \%NullSet; if ($#ResultsStack == -1) { # This should never happen #? $filelog .= "Empty ResultsStack\n"; } elsif ($#ResultsStack == 0) { my $rArray = pop @ResultsStack; if (${$rArray}[0] == $TRUE_RESULT) { $rhashResults = ${$rArray}[1]; #? $filelog .= "Single result on stack was TRUE\n"; } else { # treat both single SKIP or FALSE as FALSE. #? $filelog .= "Single result on stack was FALSE\n"; } } else { my $pLine; my @ResultHashes; foreach $pLine (@ResultsStack) { my ($nStatus, $rhashtemp) = @{$pLine}; #? $filelog .= "Result was $nStatus\n"; if (($nStatus == $FALSE_RESULT) and ($::g_InputHash{GB} eq 'A')) { # any false will fail when AND requested # @ResultHashes = (); last; } elsif ($nStatus != $TRUE_RESULT) { # treat FALSE with OR the same as a SKIP next; } else { # only TRUE should get here push @ResultHashes, $rhashtemp; } # # now match up the result hashes # if ($#ResultHashes == -1) { $rhashResults = \%NullSet; #? $filelog .= "No stack results were TRUE\n"; } else { $rhashResults = shift @ResultHashes; my $bJoin = ($::g_InputHash{GB} eq 'A') ? $::INTERSECT : $::UNION; while (@ResultHashes) { #? $filelog .= "Multiple stack results were TRUE\n"; # while nothing should get here, be prepared # my $rPrevious = $rhashResults; my $rCurrent = shift @ResultHashes; JoinHashes($rPrevious, $rCurrent, $bJoin, $rhashResults); } } } } # # We need at least one search string, price band, section, or parameter # to search. # if (!$bValidSearch) { my $sError; my $sStart = ACTINIC::EncodeText2(ACTINIC::GetPhrase(-1, 113), $::FALSE); if ($bPriceSearch && # check if the message should refer to price range $::g_InputHash{ACTION}) { $sError = ACTINIC::GetPhrase(-1, 245); } else { $sError = ACTINIC::GetPhrase(-1, 2085); } SearchError($sError, $sPath); #? ACTINIC::RecordErrors("$filelog$sError", $sPath); exit; } # # retrieve the page number of the display # my $nPageNumber = $::g_InputHash{PN}; # # words for highlighting found in Text searches # my @StringTemp = keys %MatchWords; my $sWords = join (' ', @StringTemp); # # Display the results # ($status, $sError) = DisplayResults($sPath, $rhashResults, $nPageNumber, $sWords); if ($status != $::SUCCESS) { SearchError($sError, $sPath); #? if ($DEBUG) #? { #? ACTINIC::RecordErrors("$filelog$sError", $sPath); #? } exit; } #?if ($DEBUG) #? { #?ACTINIC::RecordErrors($filelog, $sPath); #? } exit; ################################################################ # # Init - Do the main script initialization. This function # terminates on error. # # Returns: 0 - path # # Expects: CGI environment # # Affects: %::g_InputHash - the CGI input hash # $::g_OriginalInputData - original CGI string # @::g_PageList - the history list # $::g_sWebSiteUrl - the URL of the catalog web site # $::g_sContentUrl - the URL of the image files # ################################################################ sub Init { # # Read the input strings. We expect the path, a text boolean flag and some text and/or a price band. # In the future, the path will come via a different (more secure) route, but let's leave it for now. # my ($status, $sError, $unused); ($status, $sError, $::g_OriginalInputData, $unused, %::g_InputHash) = ACTINIC::ReadAndParseInput(); if ($::SUCCESS != $status) { ACTINIC::TerminalError($sError); } # # Parse the ref page list # ($status, $sError, @::g_PageList) = ACTINIC::ProcessReferencePageData(%::g_InputHash); if ($status != $::SUCCESS) { ACTINIC::TerminalError($sError); } # # Validate the input. # my $sPath = ACTINIC::GetPath(); # retrieve the path ACTINIC::SecurePath($sPath); # make sure there is nothing funny going on if (!$sPath) # if the path is empty or undefined { ACTINIC::TerminalError("Path not found."); } if (!-e $sPath || # the path does not exist or !-d $sPath) # the path is not a directory { ACTINIC::TerminalError("Invalid path."); } # # Read the prompt blob # ($status, $sError) = ACTINIC::ReadPromptFile($sPath); if ($status != $::SUCCESS) { ACTINIC::ReportError($sError, $sPath); } # # Read the setup blob # ($status, $sError) = ACTINIC::ReadSetupFile($sPath); # read the setup if ($status != $::SUCCESS) { ACTINIC::ReportError($sError, $sPath); } # # Read the search setup blob # ($status, $sError) = ACTINIC::ReadSearchSetupFile($sPath); # read the search setup if ($status != $::SUCCESS) { ACTINIC::ReportError($sError, $sPath); } # # Read the catalog blob # ($status, $sError) = ACTINIC::ReadCatalogFile($sPath); # read the catalog blob if ($status != $::SUCCESS) { ACTINIC::ReportError($sError, $sPath); } # # Retrieve the web site url # ($status, $sError, $::g_sWebSiteUrl, $::g_sContentUrl) = ACTINIC::GetWebSiteURL(@::g_PageList); if ($status != $::SUCCESS) { ACTINIC::TerminalError($sError); } # # Check the B2B mode # my ($sUserDigest, $sBaseFile); $sUserDigest = $ACTINIC::B2B->Get('UserDigest'); if (!$sUserDigest) # No user { ($sUserDigest, $sBaseFile) = ACTINIC::CaccGetCookies(); # See if there is a user cookie after all $ACTINIC::B2B->Set('UserDigest',$sUserDigest); $ACTINIC::B2B->Set('BaseFile', $sBaseFile); } # # If someone is logged in with B2B mode, then we need to use the B2B referrer # if ($sUserDigest) { $sBaseFile = $ACTINIC::B2B->Get('BaseFile'); ($::g_sWebSiteUrl, $::g_sContentUrl) = ($sBaseFile, $sBaseFile); $::g_PageList[0] = $sBaseFile; } elsif( $::g_InputHash{BASE} ) # Otherwise check if there isn't a BASE directory passed on { ($::g_sWebSiteUrl, $::g_sContentUrl) = ($::g_InputHash{BASE}, $::g_InputHash{BASE}); $::g_PageList[0] = $::g_InputHash{BASE}; } return ($sPath); } ####################################################### # # ParseSearchInput - re-parse the original input into an hash of arrays that # detects repeated CGI Parameters for MultiSelect # # Expects: $::g_OriginalInputData from ACTINIC::ReadAndParseInput() # Does not repeat all the error checking from that routine. # # Output: 0 - reference to a hash to fill # # Returns: 0 - status # 1 - error message if any # ####################################################### sub ParseSearchInput { my ($rhashResults) = @_; # # parse and decode the input # my @EncodedInput = split (/[&=]/, $::g_OriginalInputData); # check the input line if ($#EncodedInput % 2 != 1) { return ($::FAILURE, "Bad input string \"" . $::g_OriginalInputData . "\". Argument count " . $#EncodedInput . ".\n", '', '', 0, 0); } my ($key, $value); while (@EncodedInput) { # decode the entry as an array to handle duplicates # $key = ACTINIC::DecodeText(shift @EncodedInput, $ACTINIC::FORM_URL_ENCODED); $value = ACTINIC::DecodeText(shift @EncodedInput, $ACTINIC::FORM_URL_ENCODED); # if (exists $$rhashResults{$key}) { push @{$$rhashResults{$key}}, $value; } else { $$rhashResults{$key} = [$value]; } } return ($::SUCCESS, ''); } ################################################################ # # SearchText - search the catalog for the specified text # # Input: 0 - the path to the data files # 1 - a reference to the space separated search strings # 2 - join operation # Output: 1 - a reference to the modified string - stop words # are stripped and non-word characters are replaced # by breaks (spaces) # 3 - reference to a hash to fill # # Returns: 0 - status # 1 - error message if any # ################################################################ sub SearchText { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count SearchText(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $psSearchString, $bJoin, $rhashResults) = @_; # # Split words on the same boundaries as the C++ does # my $sWordCharacters = ACTINIC::GetPhrase(-1, 239); my $sSplitString = "[^\Q$sWordCharacters\E]"; # form a regular expression for replacing non-word characters with spaces which we split on $$psSearchString =~ s/$sSplitString/ /g; # now break up the search strings the same as the C++ did (here the breaks are represented by spaces) my $sStopList = ACTINIC::GetPhrase(-1, 238); # get the stop list from the prompt file # # Combine any multiple-white-spaces into single space # $$psSearchString =~ s/\s+/ /go; # # The index is stored in lower case # $$psSearchString = lc $$psSearchString; # lc the search string so it has a chance to match the index $sStopList = lc $sStopList; # lc the stop list so we can skip the stop words # # And it can be extended character which is not handled by lc # so lets convert them here. - zmagyar - 01 Feb 2001 # $$psSearchString =~ tr/[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ]/[àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ]/; # complete the lc for the search string $sStopList =~ tr/[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ]/[àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþ]/; # ditto the stop list # # Retrieve the list of words to look for. They are space delimited. # my @listPreliminarySearchWords = split(/ +/, $$psSearchString); # do the actual split # # Throw out blank entries (if any - there shouldn't be) and words in the stop list # my ($sWord, @listSearchWords); foreach $sWord (@listPreliminarySearchWords) { if ($sWord eq '' || # blank entry or $sStopList =~ /\b$sWord\b/) # this word is in the stop list { next; # toss it } push (@listSearchWords, $sWord); # this is a good word, add it to the search list } # # Patch up the search string so highlights work properly - this value is used by the calling function # $$psSearchString = join(' ', @listSearchWords); # # Check for the special case of having nothing to search for # if (!@listSearchWords) { return ($::SUCCESS); } # # Open the index. # my ($status, $sError) = OpenTextIndex($sPath, \*INDEX); if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } # # Now loop through the words and retrieve the lists of hits building an array of lists. The lists # are stored as hashes to guarantee uniqueness. # my (@HitLists, $rhash); foreach $sWord (@listSearchWords) { #? ACTINIC::ASSERT($sWord ne '', "Empty search string.", __LINE__, __FILE__); $rhash = {}; # allocate a new hash ($status, $sError) = WordSearch($sWord, 2, \*INDEX, $rhash); # do the search - the 2 is the number of bytes into the file where the index begins (after the 2 byte version number) if ($status != $::SUCCESS) { close (INDEX); return ($status, $sError); } push (@HitLists, $rhash); # add the hash to the list of hits } close (INDEX); # close the index file # # Now join the results # my ($rhashCurrent, $rhashNext, $rhashLast); $rhashLast = shift @HitLists; foreach $rhashCurrent (@HitLists) { $rhashNext = {}; # allocate a hash for the results JoinHashes($rhashLast, $rhashCurrent, $bJoin, $rhashNext); # do the join $rhashLast = $rhashNext; # use the results for the next join } %$rhashResults = %$rhashLast; # copy the results to the output hash return ($::SUCCESS); } ################################################################ # # SearchSection - find all products in the given section # # Input: 0 - the path to the data files # 1 - section ID in question - if undef, return immediately # Output: 2 - reference to a hash to fill # # Returns: 0 - status # 1 - error message if any # ################################################################ sub SearchSection { #? ACTINIC::ASSERT($#_ == 2, "Incorrect parameter count SearchSection (" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $nSectionID, $rhashResults) = @_; undef %$rhashResults; # clear the results hash # # Check for the special case of having nothing to search for # if (!$nSectionID) { return ($::SUCCESS); } # # Open the index. # my ($status, $sError) = OpenTextIndex($sPath, \*INDEX); if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } # # Look for the products related to this section (and child sections) # my $sWord = sprintf('!@%8.8x', $nSectionID); ($status, $sError) = WordSearch($sWord, 2, \*INDEX, $rhashResults); # do the search - the 2 is the number of bytes into the file where the index begins (after the 2 byte version number) if ($status != $::SUCCESS) { close (INDEX); return ($status, $sError); } close (INDEX); # close the index file return ($::SUCCESS); } ################################################################ # # SearchProperty - find all products with given Property and Value # # Input: 0 - the path to the data files # 1 - CGI Property Name in question - if undef, return immediately # 2 - Specified Property value - if undef, return immediately # Output: 3 - reference to a hash to fill # # Returns: 0 - status # 1 - error message if any # ################################################################ sub SearchProperty { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count SearchProperty (" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $sPropertyName, $sPropertyValue, $rhashResults) = @_; # # Convert CGI Property Name to Index Property Name # $sPropertyName =~ s/^S_(.*)_\d+$/$1/; # # Check for the special case of having nothing to search for # if ((!$sPropertyName) or ($sPropertyValue eq '')) { return ($::SUCCESS); } # # Open the index. # my ($status, $sError) = OpenTextIndex($sPath, \*INDEX); if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } # # Look for the products with this value for this property # my $sWord = "!!$sPropertyName!$sPropertyValue"; ($status, $sError) = WordSearch($sWord, 2, \*INDEX, $rhashResults); # do the search - the 2 is the number of bytes into the file where the index begins (after the 2 byte version number) if ($status != $::SUCCESS) { close (INDEX); return ($status, $sError); } close (INDEX); # close the index file return ($::SUCCESS); } ################################################################ # # OpenTextIndex - open the text index # # Input: 0 - the path to the data files # Output: 1 - reference to the file handle # # Returns: 0 - status # 1 - error message if any # ################################################################ sub OpenTextIndex { #? ACTINIC::ASSERT($#_ == 1, "Incorrect parameter count OpenTextIndex (" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $rFile) = @_; # # Open the index. Retry a couple of times on failure just incase an update is in progress. # my ($status, $sError); my $nRetryCount = $::MAX_RETRY_COUNT; $status = $::SUCCESS; my $sFileName = $sPath . "oldtext.fil"; my $nExpected = 257; # expected version number while ($nRetryCount--) { unless (open ($rFile, "<$sFileName")) { $sError = $!; sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; $sError = ACTINIC::GetPhrase(-1, 246, $sFileName, $sError); next; } binmode $rFile; # # Check the file version number # my $sBuffer; unless (read($rFile, $sBuffer, 4) == 4) # read the blob version number (a short) { $sError = $!; close ($rFile); return ($::FAILURE, ACTINIC::GetPhrase(-1, 252, $sError)); } my ($nVersion) = unpack("n", $sBuffer); # convert to a number if ($nVersion != $nExpected) { close($rFile); sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; $sError = ACTINIC::GetPhrase(-1, 259, $nExpected, $nVersion); next; } last; } if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } return ($::SUCCESS); } ############################################################### # # WordSearch - search an index for a word. The results of this # recursive function is a hash where the keys are product # references. # # Input: 0 - string to look for # 1 - point to start in the file # 2 - file handle # Output: 3 - reference to product reference hash table # # Returns: 0 - status # 1 - error message # ############################################################### sub WordSearch { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count WordSearch(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sWord, $nLocation, $rFile, $rhashProdRefs) = @_; my ($nDependencies, $nCount, $nRefs, $sRefs, $sBuff, $sFragment, $sAnchor); my ($nIndex, $sSeek, $nHere, $nLength, $sNext, $nRead); # # At the start of the file, we have an (empty) anchor list # followed by a list of dependency records # unless (seek($rFile, $nLocation, 0)) # Seek to node { return ($::FAILURE, ACTINIC::GetPhrase(-1, 247, $!)); } # # Read the anchors (if any) # unless (read($rFile, $sBuff, 2) == 2) # Read the count { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } ($nCount) = unpack("n", $sBuff); # Turn into an integer for ($nIndex = 0; $nIndex < $nCount; $nIndex++) { unless (read($rFile, $sBuff, 2) == 2) # Get anchor length { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } ($nLength) = unpack("n", $sBuff); # unpack the anchor length unless (read ($rFile, $sAnchor, $nLength) == $nLength) # read the anchor { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } unless (read($rFile, $sBuff, 1) == 1) # read the reference count { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } ($nRefs) = unpack("C", $sBuff); # Unpack it $sRefs = ""; # Kill left-over references if ($nRefs > 0) { unless (read($rFile, $sRefs, $nRefs) == $nRefs) # Read and ignore the actual refs { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } } if ($sWord eq "") # If this is a match { $$rhashProdRefs{$sAnchor} = $$rhashProdRefs{$sAnchor} . $sRefs; # Add anchor reference list to hash } } # # Now search the dependencies # unless (read($rFile, $sBuff, 2) == 2) # Read count { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } $nDependencies = unpack("n", $sBuff); # Count of dependencies (network short) for ($nIndex = 0; $nIndex < $nDependencies; $nIndex++) { unless (read($rFile, $sBuff, 1) == 1) # Read fragment length { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } $nLength = unpack("C", $sBuff); # Unpack it unless (read($rFile, $sFragment, $nLength) == $nLength) # Read the string fragment { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } unless (read($rFile, $sSeek, 4) == 4) # Read the link (convert later, if we need it) { return ($::FAILURE, ACTINIC::GetPhrase(-1, 248, $!)); } unless ($::MATCH_WHOLE_WORDS_ONLY) { # # We only care about the fragment length as far as # the length of the word we're looking for # $sFragment = substr($sFragment, 0, length($sWord)); # Reduce fragment to useful length } # # Allow special regex characters in $sFragment # my $sQuotedFragment = quotemeta($sFragment); # # If the fragment partially matches our word then we # continue down the tree. It only needs to match as much # of the word as we have - it's perfectly possible for # the fragment to be longer than the word # if ($sWord =~ m/^$sQuotedFragment/i) # Does it match? { $sNext = $'; # Get part after match $nHere = tell($rFile); # Save where we are my ($status, $sError) = WordSearch($sNext, unpack("N", $sSeek), $rFile, $rhashProdRefs); # Look down tree if ($status != $::SUCCESS) { return ($status, $sError); } unless (seek($rFile, $nHere, 0)) # Back to where we were { return ($::FAILURE, ACTINIC::GetPhrase(-1, 247, $!)); } } if ($sFragment gt $sWord) # If we've passed the point in the list { last; # Don't look further } } return ($::SUCCESS); } ############################################################### # # SearchPrice - search an index for the list of products within # the given price band. The result of this function is a # hash containing the unique product references. Note that # this function returns immediately if the price band is # set to $::ANY_PRICE_BAND. # # Input: 0 - path # 1 - price band # Output: 2 - reference to product reference hash table # # Returns: 0 - status # 1 - error message # ############################################################### sub SearchPrice { #? ACTINIC::ASSERT($#_ == 2, "Incorrect parameter count SearchPrice(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $nPriceBand, $rhashProdRefs) = @_; # # Check for the "any" band case. # if ($nPriceBand == $::ANY_PRICE_BAND) { return ($::SUCCESS); } # # Load the price band blob. Use a patient method to load the file and validate it because the web site may be in mid update. # my $nRetryCount = $::MAX_RETRY_COUNT; my ($status, $sError); my $nExpectedVersion = 0; while ($nRetryCount--) { ($status, $sError) = ACTINIC::ReadConfigurationFile($sPath . "priceband.fil"); # load the file if ($status != $::SUCCESS) # on error, { sleep $::RETRY_SLEEP_DURATION; # pause a moment $sError .= ACTINIC::GetPhrase(-1, 256); next; # and try again } if ($nPriceBand >= $#$::g_pPriceBand) # the price band is out of range { sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; # record the problem $sError = ACTINIC::GetPhrase(-1, 249); next; # and try again } if ($::gnPriceBandVersion != $nExpectedVersion) # verify the file format version { sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; # record the problem $sError = ACTINIC::GetPhrase(-1, 257, $nExpectedVersion, $::gnPriceBandVersion); next; # and try again } last; # success, exit } if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } # # Now find the bounding regions in the price index # my $nLowerBound = $$::g_pPriceBand[$nPriceBand]; my $nUpperBound = $$::g_pPriceBand[$nPriceBand + 1]; # # Open the price index. Again - be patient. # $nRetryCount = $::MAX_RETRY_COUNT; $status = $::SUCCESS; my $sFileName = $sPath . "oldprice.fil"; my $nExpected = 0; # the anticipated version number while ($nRetryCount--) { unless (open (INDEX, "<$sFileName")) { sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; $sError = ACTINIC::GetPhrase(-1, 250, $sFileName, $!); next; } binmode INDEX; # # Check the file version number # my $sBuffer; unless (read(INDEX, $sBuffer, 2) == 2) # read the blob version number { $sError = $!; close (INDEX); return ($::FAILURE, ACTINIC::GetPhrase(-1, 252, $sError)); } my ($nVersion) = unpack("N", $sBuffer); # convert to a number if ($nVersion != $nExpected) { close(INDEX); sleep $::RETRY_SLEEP_DURATION; # pause a moment $status = $::FAILURE; $sError = ACTINIC::GetPhrase(-1, 258, $nExpected, $nVersion); next; } last; } if ($status != $::SUCCESS) # file never loaded { return($status, $sError); } # # Now read the matching product references # unless (seek (INDEX, $nLowerBound, 0)) # find the beginning of the matching region { $sError = $!; close (INDEX); return ($::FAILURE, ACTINIC::GetPhrase(-1, 251, $sError)); } my $nBytesToRead = $nUpperBound - $nLowerBound; my $sBuffer; unless (read(INDEX, $sBuffer, $nBytesToRead) == $nBytesToRead) # read the product references { $sError = $!; close (INDEX); return ($::FAILURE, ACTINIC::GetPhrase(-1, 252, $sError)); } close (INDEX); # # The data read from the index is a list of product references in the range separated by !'s # %$rhashProdRefs = map {$_ => 0} split(/!/, $sBuffer); # parse the product references and dump them to the hash return ($::SUCCESS); } ############################################################### # # ProductSearch - search an index for a product. The result of # this recursive function is a hash containing the product # definition. # # Input: 0 - product reference (or remaining fragment on # recursive call) # 1 - file handle # 2 - file name # Output: 3 - reference to product hash table # # Returns: 0 - status # 1 - error message # ############################################################### sub ProductSearch { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count ProductSearch(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sProductReference, $rFile, $sFilename, $rhashProduct) = @_; undef %$rhashProduct; # clear the existing product hash my ($Status, $sMessage, $sValue) = ACTINIC::IndexSearch($sProductReference, 2, $rFile, $sFilename); if ($Status != $::SUCCESS) { return ($Status, $sMessage); } # # Now parse the product definition string. The string is in the following format: # # CUR DIGITS PRICE ANCHOR NAME_LENGTH NAME DESCRIPTION_LENGTH DESCRIPTION SECTION_NAME_LENGTH SECTION_NAME # PROPERTY1_LENGTH PROPERTY1 PROPERTY2_LENGTH PROPERTY2 ... PROPERTYN_LENGTH PROPERTYN # # CUR - 3 character ISO currency code or equivalent # DIGITS - number of digits in the fractional part of the price # PRICE - product unit price in Actinic internal format # ANCHOR - HTML anchor of product. Anchor includes filename. # NAME_LENGTH - product name length # NAME - product name (may contain spaces) # DESCRIPTION_LENGTH - product description length # DESCRIPTION - product description (may contain spaces) # SECTION_LENGTH - section name length # SECTION - section name (may contain spaces) # PROPERTY_LENGTH - property length # PROPERTY - property packed as ! # unless ($sValue =~ /^(\S+) (\d+) (\d+) (\S+) (\d+) (.+)/s) { return ($::FAILURE, ACTINIC::GetPhrase(-1, 255, $sValue)); } $$rhashProduct{CURRENCY} = $1; # record the currency $$rhashProduct{DECIMALS} = $2; # record the currency decimal count $$rhashProduct{PRICE} = $3; # record the price $$rhashProduct{ANCHOR} = $4; # record the HTML anchor my $nLength = $5; # get the name length my $sBuffer = $6; # the remainder of the string # # Now extract the name # $$rhashProduct{NAME} = substr($sBuffer, 0, $nLength); substr($sBuffer, 0, $nLength + 1) = ''; # strip the used part # # Now extract the description # unless ($sBuffer =~ /^(\d+) (.+)/s) { return ($::FAILURE, ACTINIC::GetPhrase(-1, 255, $sValue)); } $nLength = $1; $sBuffer = $2; $$rhashProduct{DESCRIPTION} = substr($sBuffer, 0, $nLength); substr($sBuffer, 0, $nLength + 1) = ''; # strip the used part # # Now extract the section name # unless ($sBuffer =~ /^(\d+) (.+)/s) { return ($::FAILURE, ACTINIC::GetPhrase(-1, 255, $sValue)); } $nLength = $1; $sBuffer = $2; $$rhashProduct{SECTION} = substr($sBuffer, 0, $nLength); substr($sBuffer, 0, $nLength + 1) = ''; # strip the used part # # The rest is property information # my $rhashProperties = {}; # allocate a buffer for the hash properties my $sProperty; until ($sBuffer !~ /^(\d+) (.+)/s) # while properties still exist { $nLength = $1; $sBuffer = $2; $sProperty = substr($sBuffer, 0, $nLength); # get the next property from the list unless ($sProperty =~ /([^!]+)!(.*)/) # parse the property { return ($::FAILURE, ACTINIC::GetPhrase(-1, 255, $sValue)); } $$rhashProperties{$1} .= $2 . "!"; # = !!...! substr($sBuffer, 0, $nLength + 1) = ''; # strip the used part } $$rhashProduct{PROPERTIES} = $rhashProperties; # store the properties as a hash reference in the product hash return ($::SUCCESS); } ############################################################### # # JoinHashes - do a boolean join on the two supplied hashes # and store the results in a third hash. # # Input: 0 - reference to hash1 # 1 - reference to hash2 # 2 - join operation # Output: 3 - reference to output hash # ############################################################### sub JoinHashes { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count JoinHashes(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($rhash1, $rhash2, $bOperation, $rhashOutput) = @_; undef %$rhashOutput; # clear the output hash # # Now do the appropriate join operation on the hashes. (See Perl Cookbook p 147 (first edition)) # if ($bOperation == $::INTERSECT) # AND join (INTERSECTION) { foreach (keys %$rhash1) # check each key in hash1 { $$rhashOutput{$_} = 0 if exists $$rhash2{$_}; # add this key to the output hash if it exists in hash 2 } } else # OR join (UNION) { %$rhashOutput = %$rhash1; # copy all of the keys from hash1 to output foreach (keys %$rhash2) { $$rhashOutput{$_} = 0; # copy the keys from hash2 to output } } } ############################################################### # # SearchError - Report an error doing the search operation # # Input: 0 - error message # 1 - path # ############################################################### sub SearchError { #? ACTINIC::ASSERT($#_ == 1, "Incorrect parameter count SearchError(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sMessage, $sPath) = @_; # # Dup the last entry in the page list to help the bounce # my $sLastPage = $::g_PageList[0]; # store the first item of PageList as it will be empty after failed GetLastPage my $sLocalPage = ActinicOrder::GetLastPage(); # get the last item in the list if (!defined $sLocalPage) # If the GetLastPage didn't find any static page { # then recall the last page from the pagelist $sLocalPage = $sLastPage; # and use that one if (!defined $sLocalPage) # if still doesn't defined { $sLocalPage = ACTINIC::GetReferrer(); # then try to use referrer } } push (@::g_PageList, $sLocalPage); # put the item back push (@::g_PageList, $sLocalPage); # dup the item (this allows us to bounce properly using ReturnToLastPage) my ($status, $sError, $sHTML) = ACTINIC::BounceToPageEnhanced(5, ACTINIC::GetPhrase(-1, 1962) . $sMessage . ACTINIC::GetPhrase(-1, 1970) . ACTINIC::GetPhrase(-1, 2047), '', \@::g_PageList, $::g_sWebSiteUrl, $::g_sContentUrl, $::g_pSetupBlob, $sLocalPage, \%::g_InputHash, $::FALSE); if ($status != $::SUCCESS) { ACTINIC::ReportError($sMessage, ACTINIC::GetPath()); } ACTINIC::UpdateDisplay($sHTML, $::g_OriginalInputData, \@::g_PageList); } ############################################################### # # DisplayResults - output the search results # # Input: 0 - path # 1 - reference to hash containing matching product # references # 2 - the page number for the display # 3 - search strings # # Returns: 0 - status # 1 - error message if any # ############################################################### sub DisplayResults { #? ACTINIC::ASSERT($#_ == 3, "Incorrect parameter count DisplayResults(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $rhashResults, $nPageNumber, $sSearchStrings) = @_; my @Results = sort keys %$rhashResults; # get the product references in a fixed order # # Check for the "no matches" case # if ($#Results == -1) { return ($::NOTFOUND, ACTINIC::GetPhrase(-1, 267)); } # # Now read the template # my $sFilename = $sPath . "results.html"; unless (open (TFFILE, "<$sFilename")) { return ($::FAILURE, ACTINIC::GetPhrase(-1, 21, $sFilename, $!)); } my ($sHTML); { local $/; $sHTML = ; # read the entire file } close (TFFILE); # # Add location of catalog to checkout URL - this helps bounce pages to locate images # my $sUrl = $::g_PageList[0]; # Catalog directory if( $sUrl ) { my $sReferer = $sUrl; $sUrl =~ s/\/[^\/]*$/\//; # Keep only directory my $sStart = ACTINIC::EncodeText2(ACTINIC::GetPhrase(-1, 113), $::FALSE); # Get encoded ACTION name for checkout $sHTML =~ s/\?ACTION\=$sStart/\?ACTION\=$sStart\&BASE\=$sUrl/g; # Insert it into checkout link # # Add referrer otherwise it will be missing on next search # my ($status, $sMessage, $sPrevQuery, $sPageHistory) = ACTINIC::PrepareRefPageData($::g_OriginalInputData, \@::g_PageList, $::FALSE); my $sReplace = "\n" . "\n"; # add the query string in case it $sHTML =~ s/(]*>)/$1$sReplace/gi; } unless ($sHTML =~ /(.*?)<\/Actinic:SEARCH_RESULTS>/si) # extract the repeated section of markup { return ($::FAILURE, ACTINIC::GetPhrase(-1, 262)); } my $sRepeatXML = $1; # record the repeating portion # # Now flesh out the results sections # my ($nMin, $nMax); my $nResultsLimit = $$::g_pSearchSetup{SEARCH_RESULTS_PER_PAGE}; my $bResultsLimited = (0 != $nResultsLimit); if (!$bResultsLimited) # There is no limit on the number of items displayed on a page { $nMin = 0; # display all of the results $nMax = $#Results + 1; } else { $nMin = $nPageNumber * $nResultsLimit; # display all of the results $nMax = ($nPageNumber + 1) * $nResultsLimit; } if ($nMax > $#Results + 1) # ensure that we don't overflow { $nMax = $#Results + 1; } my ($status, $sError, $sResults) = SearchResultsParser($sPath, $sRepeatXML, \@Results, $nMin, $nMax, $sSearchStrings); if ($status != $::SUCCESS) { return ($status, $sError); } # # Now stick the results into the HTML # $sHTML =~ s/.*?<\/Actinic:SEARCH_RESULTS>/$sResults/si; # # Now handle the results summary # my $sSummary = ACTINIC::GetPhrase(-1, 264, $nMin + 1, $nMax, ($#Results + 1)); # # Finally build the list of continuation links # my $sContinue; if ($bResultsLimited) # if the results are limited { # Pass on customsearch file used # my $sCustomNumber = ''; if (exists $::g_InputHash{SN}) { $sCustomNumber = "&SN=$::g_InputHash{SN}"; } # # Pass on section selected # my $sCustomSection = ''; if (exists $::g_InputHash{SX}) { $sCustomSection = "&SX=$::g_InputHash{SX}"; } # # Build the basic search script URL # my $sScript = sprintf('%s?TB=%s&SS=%s%s%s&PR=%s', $::g_sSearchScript, $::g_InputHash{TB}, ACTINIC::EncodeText2($::g_InputHash{SS}, $::FALSE), $sCustomNumber, $sCustomSection, $::g_InputHash{PR}); my $sPathAndHistory = "&REFPAGE=" . ACTINIC::EncodeText2($::g_PageList[0], $::FALSE) . ($::g_InputHash{SHOP} ? "&SHOP=" . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : ""); my $sLinkFormat = ''; my $sLink; if (0 != $nPageNumber) # we are not on page 0 { $sLink = $sScript . "&PN=" . ($nPageNumber - 1) . $sPathAndHistory; $sLink = sprintf($sLinkFormat, $sLink); $sContinue .= $sLink . ACTINIC::GetPhrase(-1, 265, $nResultsLimit) . ""; # add the "Last 20" link } my $nPage; my $nMaxPageCount = ActinicOrder::RoundTax(($#Results + 1) / $nResultsLimit, $ActinicOrder::CEILING); my $sPageLabel; for ($nPage = 0; $nPage < $nMaxPageCount; $nPage++) # enumerate the result pages { $sPageLabel = ($nPage * $nResultsLimit + 1) . '-' . ((($nPage + 1) * $nResultsLimit) > ($#Results + 1) ? $#Results + 1 : ($nPage + 1) * $nResultsLimit); $sLink = $sScript . "&PN=" . $nPage . $sPathAndHistory; $sLink = sprintf($sLinkFormat, $sLink); if ($nPage == $nPageNumber) # the current page { $sContinue .= " " . $sPageLabel; # add the page number (no link) } else # anything other than the current page { $sContinue .= " " . $sLink . $sPageLabel . ""; # add the page link } } if ($nMaxPageCount != $nPageNumber + 1) # we are not on page MAX (the + 1 is because the page number index is from 0 -> max - 1) { $sLink = $sScript . "&PN=" . ($nPageNumber + 1) . $sPathAndHistory; $sLink = sprintf($sLinkFormat, $sLink); $sContinue .= " " . $sLink . ACTINIC::GetPhrase(-1, 266, $nResultsLimit) . ""; # add the "Next 20" link } if (1 == $nMaxPageCount) # if there is only one page { undef $sContinue; # don't post any message - it is just ugly } } # # Now stich it all together # $ACTINIC::B2B->ClearXML(); # clear the tag hash $ACTINIC::B2B->SetXML('S_SUMMARY',$sSummary); $ACTINIC::B2B->SetXML('S_CONTINUE',$sContinue); $sHTML = ACTINIC::ParseXML($sHTML); # do the insert ####### # make the file references point to the correct directory ####### if( !$ACTINIC::B2B->Get('UserDigest') ) { # # make CGI scripts refer back to the static search page # my $sURL = ACTINIC::EncodeText2($::g_sWebSiteUrl . "search.html", $::FALSE); $sHTML =~ s/(\<\s*A\s*HREF[^>?]+\?)/$1ACTINIC_REFERRER=$sURL&/gi; ($status, $sError, $sHTML) = ACTINIC::MakeLinksAbsolute($sHTML, $::g_sWebSiteUrl, $::g_sContentUrl); } else { # # Build the correct referer link # my $sBaseFile = $ACTINIC::B2B->Get('BaseFile'); my $smPath = ($sBaseFile) ? $sBaseFile : $::g_sContentUrl; my $sCgiUrl = $::g_sAccountScript; $sCgiUrl .= ($::g_InputHash{SHOP} ? '?SHOP=' . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) . '&' : '?'); $sCgiUrl .= 'PRODUCTPAGE='; # # make CGI scripts refer back to the static search page # my $sURL = ACTINIC::EncodeText2($sCgiUrl . "search.html", $::FALSE); $sHTML =~ s/(\<\s*A\s*HREF[^>?]+\?)/$1ACTINIC_REFERRER=$sURL&/gi; ($status, $sError, $sHTML) = ACTINIC::MakeLinksAbsolute($sHTML, $sCgiUrl, $smPath); } if ($status != $::SUCCESS) { return ($status, $sError); } # # Check section jump referrers # my $sRef = ACTINIC::EncodeText2(ACTINIC::GetLastNonScript(\@::g_PageList), $::FALSE); $sHTML =~ s/(['"]\&ACTINIC_REFERRER\=["']\s*\+)\s*escape\(location\.href\)/$1\'$sRef\'/; ACTINIC::PrintPage($sHTML, undef, $::FALSE); return ($::SUCCESS); } ############################################################### # # SearchResultsParser - function to handle the parsing of the # search results # # Input: 0 - path # 1 - results markup string # 2 - results array (array of ordered prod refs) # 3 - minimum count to display # 4 - maximum count to display # 5 - list of search words separated by spaces (used # for highlighting) # # Returns: 0 - status # 1 - error message # 2 - markup for all results # ############################################################### sub SearchResultsParser { #? ACTINIC::ASSERT($#_ == 5, "Incorrect parameter count SearchResultsParser(" . join(', ', @_) . ").", __LINE__, __FILE__); my ($sPath, $sResultMarkup, $rarrResults, $nMin, $nMax, $sSearchStrings) = @_; #? ACTINIC::ASSERT($#$rarrResults + 1 >= $nMax, "Max is greater than results size (" . ($#$rarrResults + 1) . " >= $nMax)", __LINE__, __FILE__); #? ACTINIC::ASSERT(-1 < $nMin, "Min < 0.", __LINE__, __FILE__); # # Prepare the product index # my $rFile = \*PRODUCTINDEX; my $sFilename = $sPath . "oldprod.fil"; my ($status, $sError) = ACTINIC::InitIndex($sFilename, $rFile, 257); if ($status != $::SUCCESS) { return($status, $sError); } # # Build the basic highlight script URL # my $sScript; if ($$::g_pSearchSetup{SEARCH_SHOW_HIGHLIGHT}) { my ($sPrevQuery, $sPageHistory); ($status, $sError, $sPrevQuery, $sPageHistory) = ACTINIC::PrepareRefPageData($::g_OriginalInputData, \@::g_PageList, $::FALSE); if ($status != $::SUCCESS) { # ignore errors since the PrevQuery value is not used in 99% of the cases - the remaining 1% will not have a terminal error } if ($ACTINIC::B2B->Get('UserDigest')) # B2B mode { $sScript = sprintf('%s?REFPAGE=%s&WD=%s%s&PRODUCTPAGE=', $::g_sAccountScript, ACTINIC::EncodeText2($::g_PageList[0], $::FALSE), ACTINIC::EncodeText2($sSearchStrings, $::FALSE), ($::g_InputHash{SHOP} ? "&SHOP=" . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : "")); } else # standard mode (or B2B with no login) { $sScript = sprintf('%s?REFPAGE=%s&WD=%s%s&PREVQUERY=%s&PN=', $::g_sSearchHighLightScript, ACTINIC::EncodeText2($::g_PageList[0], $::FALSE), ACTINIC::EncodeText2($sSearchStrings, $::FALSE), ($::g_InputHash{SHOP} ? "&SHOP=" . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : ""), ACTINIC::EncodeText2($sPrevQuery, $::FALSE)); } } else # no highlighting { if ($ACTINIC::B2B->Get('UserDigest')) # B2B mode { $sScript = sprintf('%s?REFPAGE=%s&WD=%s%s&PRODUCTPAGE=', $::g_sAccountScript, ACTINIC::EncodeText2($::g_PageList[0], $::FALSE), ACTINIC::EncodeText2($sSearchStrings, $::FALSE), ($::g_InputHash{SHOP} ? "&SHOP=" . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) : "")); } } # # Now loop over the results and build an HTML fragment for each result # my $nCount; my $sHTML; my $sTemp; # my $Parser = new ACTINIC_PXML(); my %Product; for ($nCount = $nMin; $nCount < $nMax; $nCount++) # process the range of product references in the results set { # $Parser->{CurrentLoop} = 0; # since we are looping outside of the parser, reset the loop flag # # Do the product lookup # ($status, $sError) = ProductSearch($$rarrResults[$nCount], $rFile, $sFilename, \%Product); if ($status == $::FAILURE) { ACTINIC::CleanupIndex($rFile); return($status, $sError); } if ($status == $::NOTFOUND) { ACTINIC::CleanupIndex($rFile); return($status, ACTINIC::GetPhrase(-1, 263)); } # # Build the replacement tags # $ACTINIC::B2B->SetXML('S_ITEM', ($nCount + 1)); if ($$::g_pSearchSetup{SEARCH_SHOW_HIGHLIGHT} && # the words are to be highlighted $sSearchStrings) # and there are some words { $Product{ANCHOR} =~ /([^\#]*)(.*)/; # break the page into the file and anchor my $sAnchor = $2; $ACTINIC::B2B->SetXML('S_LINK', sprintf('', $sScript . ACTINIC::EncodeText2($Product{ANCHOR}, $::FALSE) . $sAnchor)); } else # the links to the products are direct (no highlighting) { $ACTINIC::B2B->SetXML('S_LINK', sprintf('', $Product{ANCHOR})); } $sTemp = ""; if ($$::g_pSearchSetup{SEARCH_SHOW_NAME}) # only display the name if it is on { ($status, $sTemp) = ACTINIC::ProcessEscapableText($Product{NAME}); # properly encode the text if ($status != $::SUCCESS) { ACTINIC::CleanupIndex($rFile); return($status, $sTemp); } } else # otherwise, use the default text { $sTemp = ACTINIC::GetPhrase(-1, 278); } $ACTINIC::B2B->SetXML('S_PNAME', $sTemp); $sTemp = ""; if ($$::g_pSearchSetup{SEARCH_SHOW_SECTION}) # only display the section name if it is on { ($status, $sTemp) = ACTINIC::ProcessEscapableText($Product{SECTION}); # properly encode the text if ($status != $::SUCCESS) { ACTINIC::CleanupIndex($rFile); return($status, $sTemp); } $sTemp = "($sTemp)"; } $ACTINIC::B2B->SetXML('S_SNAME', $sTemp); $sTemp = ""; if ($$::g_pSearchSetup{SEARCH_SHOW_DESCRIPTION}) # only display the DESCRIPTION if it is on { ($status, $sTemp) = ACTINIC::ProcessEscapableText($Product{DESCRIPTION}); # properly encode the text if ($status != $::SUCCESS) { ACTINIC::CleanupIndex($rFile); return($status, $sTemp); } } $ACTINIC::B2B->SetXML('S_DESCR', $sTemp); # # The price formatting is a little more complex # $sTemp = ""; if ($$::g_pSearchSetup{SEARCH_SHOW_PRICE} && # only display the price if it is on and prices are displayed $$::g_pSetupBlob{PRICES_DISPLAYED} && $Product{PRICE} != 0) { ($status, $sError, $sTemp) = ActinicOrder::FormatPrice($Product{PRICE}, $::TRUE, $::g_pCatalogBlob); if ($status != $::SUCCESS) { ACTINIC::CleanupIndex($rFile); return($status, $sError); } } $ACTINIC::B2B->SetXML('S_PRICE', $sTemp); # # Display the searchable properties when requested. # 12/02/2001 zmagyar # $sTemp = ""; if ($$::g_pSearchSetup{SEARCH_SHOW_PROPERTY}) # does the user want it? { my ($sKey, $sValue); my $Props = $Product{PROPERTIES}; # take the properties while (($sKey, $sValue) = each(%$Props)) # check all of them { $sKey =~ s/\d$//; # remove the attached numbers $sValue =~ s/!$//g; # remove ! from the end of string $sValue =~ s/!/, /g; # convert all ! (used as separator) to , $sTemp .= "$sKey: $sValue
"; # add to display } } $ACTINIC::B2B->SetXML('S_PROP', $sTemp); # make XML substitution # $sHTML .= $Parser->Parse($sResultMarkup); # parse the XML $sHTML .= ACTINIC::ParseXML($sResultMarkup); # parse the XML } ACTINIC::CleanupIndex($rFile); return ($::SUCCESS, undef, $sHTML); } ############################################################### # # DirectLinkToProduct - look up the product for a given product # reference and display it directly # # Input: 0 - path # 1 - product reference to look for # # Returns: 0 - result HTML (the page or error message) # # Author: Zoltan Magyar # ############################################################### sub DirectLinkToProduct { my ($sPath, $sProdRef) = @_; my %Product; my $rFile = \*PRODUCTINDEX; my $sFilename = $sPath . "oldprod.fil"; my ($status, $sError) = ACTINIC::InitIndex($sFilename, $rFile, 257); if ($status != $::SUCCESS) { ACTINIC::TerminalError($sError); } # # Do the product lookup # ($status, $sError) = ProductSearch($sProdRef, $rFile, $sFilename, \%Product); if ($status == $::FAILURE) # search engine error { ACTINIC::CleanupIndex($rFile); SearchError($sError, $sPath); # report it } if ($status == $::NOTFOUND) # there wasn't any match { ACTINIC::CleanupIndex($rFile); # bounce back my ($status, $sError, $sHTML) = ACTINIC::BounceToPageEnhanced(5, ACTINIC::GetPhrase(-1, 1962) . ACTINIC::GetPhrase(-1, 1965, $sProdRef) . ACTINIC::GetPhrase(-1, 1970) . ACTINIC::GetPhrase(-1, 2048), '', \@::g_PageList, $::g_sWebSiteUrl, $::g_sContentUrl, $::g_pSetupBlob, ACTINIC::GetReferrer(), \%::g_InputHash, $::FALSE); return($sHTML); } # # Determine the product link to be used # my $sLink = $Product{ANCHOR}; ACTINIC::CleanupIndex($rFile); return(DisplayDirectLinkPage($sLink)); } ############################################################### # # DirectLinkToProduct - look up the product for a given product # reference and display it directly # # Input: 0 - path # 1 - product reference to look for # # Returns: 0 - result HTML (the page or error message) # # Author: Zoltan Magyar # ############################################################### sub DisplayDirectLinkPage { my $sLink = shift @_; # # Build the correct referer link # my $sBaseFile = $ACTINIC::B2B->Get('BaseFile'); my $sReferrer = ACTINIC::GetReferrer(); my $sCgiUrl = $::g_sAccountScript; $sCgiUrl .= ($::g_InputHash{SHOP} ? '?SHOP=' . ACTINIC::EncodeText2($::g_InputHash{SHOP}, $::FALSE) . '&' : '?'); # # If ACTINIC_REFERRER is not defined then it must be a call inside Catalog or Brochure # we should check the case when it comes from the Brochure index page which is # not in the /acatalog directory # if (!defined $::g_InputHash{ACTINIC_REFERRER}) { $sReferrer =~ s/\/[^\/]*$/\//; # strip file name if any $sReferrer =~ /[^\/]\/([^\/]+)\/$/; # get the last dir of the URL # # We don't check for matches above because the unmatch is used below # my $sLastDir = $1; if ($$::g_pSetupBlob{CATALOG_URL} !~ /$sLastDir\/$/ || # is it the same as the catalog URL (http://server/path/)? !defined $sLastDir) # if the last dir is not defined (http://server/) { # it is different so we should check for brochure index page # # Check that the referrer's last dir is the same as the directory above # /acatalog. If so then modify it to get the correct URL of catalog # if ($$::g_pSetupBlob{CATALOG_URL} =~ /$sLastDir\/([^\/]+)\/$/) { $sReferrer .= $1 . "/"; # fix the referrer $::g_sContentUrl = $sReferrer; # update the content URL (must point to acatalog directory) push (@::g_PageList, $sReferrer); } } } # # See if there is a logged in customer # if ($ACTINIC::B2B->Get('UserDigest')) # is there logged in business customer? { # if so then make the link to point bb script $sLink = !$$::g_pSetupBlob{USE_FRAMES} ? $sCgiUrl . "PRODUCTPAGE=" . $sLink . "&ACTINIC_REFERRER=" . ACTINIC::EncodeText2($sReferrer, $::FALSE): $sCgiUrl . "MAINFRAMEURL=" . ACTINIC::EncodeText2($sLink, $::FALSE) . "&ACTINIC_REFERRER=" . ACTINIC::EncodeText2($sReferrer, $::FALSE); } else # there isn't business customer logged in? { if ($$::g_pSetupBlob{B2B_MODE} && # but it is B2B version !$::g_InputHash{NOLOGIN}) # and login is not disallowed { # # Then create a special login page which takes to the specified product # my @Response = ACTINIC::TemplateFile(ACTINIC::GetPath() . $$::g_pSetupBlob{B2B_LOGONPAGE}); # make the substitutions if ($Response[0] != $::SUCCESS) { ACTINIC::TerminalError($Response[1]); } $sLink =~ /([^\#]*)(.*)/; # break the page into the file and anchor my $sAnchor = $2; # # Replace links to the specific product # my $sReplace = $$::g_pSetupBlob{USE_FRAMES} ? "" : ""; # # Replace ACTINIC_REFERRER tag to point to the right URL # $sReplace .= ""; $Response[2] =~ s/]+ACTION\s*=\s*["|'])($::g_sAccountScript)(["|'][^>]+>)/)/$1$sReplace$2/gi; return($Response[2]); # return the login page (don't generate redirect page here) } else # it is a plain catalog version { $sLink = $::g_sContentUrl . $sLink; # then use the link what was determined before } } # # Generate the redirect page # my $sHTML = ""; $sHTML .= ""; return($sHTML); }