Possible encoding issue passing string to Javascript in TWebBrowser

Introduction

This is the HTML page that I load into TWebBrowser for accessing the Google maps API:

<html>
<head>
</head>
<body>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=%GMAPSAPIKEY%&callback=initMap"></script>

<div id="editMap" style="width:600px; height:500px; margin:0; margin-bottom:0; background-color:#F9F9F9; border-right:1px solid #999; float:left;"></div>

<input type='hidden' id='DelphiVan' value='' />
<input type='hidden' id='DelphiNaar' value='' />
<input type='hidden' id='DelphiDistance' value='' />
<input type='hidden' id='DelphiPolyLine' value='' />

<script type="text/javascript">
  function initMap() {
                  ttMapHelper.InitMap('editMap')
  }

var ttMapHelper = (function () {
    var map;
    var directionsService, directionsDisplay;
    var polyLine;
    var markerA;
    var markerB;

    var routeChangedCallback;
    var routeOrigin;
    var routeDestination;
    var encodedLine;
    var totalDistance;
    var totalDistanceText;

    function getDirections(origin, destination, display) {
        directionsService.route({
            origin: origin,
            destination: destination,
            avoidTolls: true,
            travelMode: google.maps.TravelMode.DRIVING, //getTravelMode($('#travelMode').val()),
            unitSystem: google.maps.UnitSystem.METRIC //google.maps.UnitSystem.IMPERIAL
        }, function (response, status) {
            if (status === google.maps.DirectionsStatus.OK) {
                if (display)
                    directionsDisplay.setDirections(response);
                else
                    processDirections(response);
            }
            else {
                alert('Could not display directions due to: ' + status);
            }
        });
    }
    function getTravelMode(mode) {
        switch (mode) {
            case "DRIVING":
                return google.maps.TravelMode.DRIVING;
            case "BICYCLING":
                return google.maps.TravelMode.BICYCLING;
            case "WALKING":
                return google.maps.TravelMode.WALKING;
        }
    }
    function processDirections(result) {
        var route = result.routes[0];
        var leg = route.legs[0];

        routeOrigin = leg.start_address
        routeDestination = leg.end_address
        totalDistance = leg.distance.value;
        totalDistanceText = leg.distance.text;
        encodedLine = route.overview_polyline;

        if (routeChangedCallback)
            routeChangedCallback();
    }
    function displayPolyLine(encodedLine) {
        if (polyLine) {
            polyLine.setMap(null);
            markerA.setMap(null);
            markerB.setMap(null);
            polyLine = undefined;
            markerA = undefined;
            markerB = undefined;
        }

        var coordinates = google.maps.geometry.encoding.decodePath(encodedLine);
        polyLine = new google.maps.Polyline({
            path: coordinates,
            strokeColor: '#0000FF',
            strokeOpacity: 1.0,
            strokeWeight: 2
        });
        polyLine.setMap(map);

        markerA = new google.maps.Marker({
            position: coordinates[0],
            label: 'A',
            map: map
        });

        markerB = new google.maps.Marker({
            position: coordinates[coordinates.length - 1],
            label: 'B',
            map: map
        });

        var bounds = new google.maps.LatLngBounds();
        for (var i = 0; i < coordinates.length; i++) {
            bounds.extend(coordinates[i]);
        }
        map.fitBounds(bounds);
    }

    var initMap = function (mapId) {
        map = new google.maps.Map(document.getElementById(mapId), {
            zoom: 7,
            center: { lat: 52.2169918, lng: 5.6460789 },
            mapTypeId: google.maps.MapTypeId.ROADMAP
        });
        directionsService = new google.maps.DirectionsService;
        directionsDisplay = new google.maps.DirectionsRenderer({
            draggable: false,
            suppressBicyclingLayer: true,
            map: map
        });
        directionsDisplay.addListener('directions_changed', function () {
            processDirections(directionsDisplay.getDirections());
        });
    }
    var calculateRoute = function (origin, destination, callback) {
        routeChangedCallback = callback;
        routeOrigin = origin;
        routeDestination = destination;
        getDirections(origin, destination);
    }
    var editRoute = function (origin, destination, callback) {
        routeChangedCallback = callback;
        routeOrigin = origin;
        routeDestination = destination;
        directionsDisplay.setOptions({ draggable: true });
        getDirections(origin, destination, true);
    }
    var showRoute = function (encodedLine) {
        displayPolyLine(encodedLine);
    }

    var getOrigin = function () { return routeOrigin; }
    var getDestination = function () { return routeDestination; }
    var getDistance = function () { return totalDistance; }
    var getDistanceText = function () { return totalDistanceText; }
    var getEncodedLine = function () { return encodedLine; }

    return {
        InitMap: initMap,
        CalculateRoute: calculateRoute,
        EditRoute: editRoute,
        ShowRoute: showRoute,
        GetOrigin: getOrigin,
        GetDestination: getDestination,
        GetDistance: getDistance,
        GetDistanceText: getDistanceText,
        GetEncodedLine: getEncodedLine
    }
})();

  function PrepareDelphiVars() {
    document.getElementById('DelphiVan').value = ttMapHelper.GetOrigin();
    document.getElementById('DelphiNaar').value = ttMapHelper.GetDestination();
    document.getElementById('DelphiDistance').value = ttMapHelper.GetDistance();
    document.getElementById('DelphiPolyLine').value = ttMapHelper.GetEncodedLine();
  };

</script>
</body>
</html>

Initially, this works fine for determining a route from A to B. From the WebBrowserDocumentComplete handler, I execute:

procedure TFrmGoogleMaps.ToonRoute;
begin
  FHTMLWindow.execScript('ttMapHelper.EditRoute("' + FVan + '","' + FNaar + '",function (){PrepareDelphiVars();})', 'JavaScript')
end;

The PrepareDelphiVars puts the generated data in the hidden input fields, and I retrieve them with

lElement := WebBrowser.OleObject.Document.getElementById('DelphiVan');
if not VarIsNull(lElement) then
   FVan := lElement.getAttribute('value');

One of the things I retrieve is the generated polyline. It is of the form

i|r~Hi{y\[email protected]`@[email protected]@[email protected]][email protected]{BsFoG[eAC[[email protected]@`@[email protected]~^{[email protected]@[email protected]@[email protected]@wHIiDJ}....

If I copy/paste this string from a TMemo into Google's Interactive Polyline Encode/Decoder I get the correct route from Rotterdam to Amsterdam:

enter image description here

Issue

I want to display this generated polyline the next time I start TWebBrowser again (Note: it is on a form that is created run-time every time).
So I now call from the WebBrowserDocumentComplete:

procedure TFrmGoogleMaps.ToonPolyLine;
begin
  FHTMLWindow.execScript('ttMapHelper.ShowRoute("' + FPolyLine + '")', 'JavaScript')
end;

with the exact same string from the TMemo. I now end up in Germany (and I get Javascript errors in https://maps.googleapis.com/maps-api-v3/api/js/24/10/intl/nl_ALL/onion.js seconds later when moving the mouse):

enter image description here

This looks like an encoding issue, but I cannot find a solution yet. This is what I've tried:

  • Specify an encoding in the HTML: <meta charset="utf-8"> or <meta http-equiv="Content-Type" content="text/html; charset=windows-1251"> or <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
    This shows that encoding present when I query (WebBrowser.Document as IHTMLDocument2).charset in the WebBrowserDocumentComplete, but does not solve the issue.
  • Forcing TWebBrowser to use IE edge mode with <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  • Wrapping FPolyLine with TidURI.ParamsEncode or its siblings - sometimes I end up in the Gulf of Guinea 300 km south of Accra, Ghana ;-).
  • Combinations like specifying utf-8 in the HTML and using UTF8Encode(FPolyLine)

Note: My title says possible. That is because a comparison between the TMemo contents and a JavaScript alert() seems to show no difference in the strings apart from a minor display issue with a \b substring:

enter image description here

Moreover, the Javascript code is copied from a cloud solution where it works fine.

What can be going wrong here, and what is the solution?

(Delphi 10 Upgrade 1)

FWIW, this is how I load the HTML for a file into the TWebBrowser:

procedure TFrmGoogleMaps.FormCreate(Sender: TObject);
var
   lMemStream : TMemoryStream;
   lFileStream: TFileStream;
   lData      : ANSIString;
   lFileName  : String;
   p,l        : Integer;
begin
   lFileName := ExtractFilePath(ParamStr(0)) + cMapsHTMLFile;
   lFileStream := TFileStream.Create(lFileName,fmOpenRead);
   SetLength(lData, lFileStream.Size);
   lFileStream.ReadBuffer(Pointer(lData)^, Length(lData));
   lFileStream.Free;
   // Replace the API key marker with the actual key:
   p := System.AnsiStrings.PosEx(cAPIKeyMarker,lData);
   l := Length(cAPIKeyMarker);
   Delete(lData,p,l);
   Insert(cGoogleMapsAPIKey,lData,p);
   WebBrowser.Navigate('about:blank');  // Nodig voor initialisatie
   if Assigned(WebBrowser.Document) then
   begin
      lMemStream := TMemoryStream.Create;
      try
         lMemStream.WriteBuffer(Pointer(lData)^, Length(lData));
         lMemStream.Seek(0, soFromBeginning);
         (WebBrowser.Document as IPersistStreamInit).Load(TStreamAdapter.Create(lMemStream));
      finally
         lMemStream.Free;
      end;
      FHTMLWindow := (WebBrowser.Document as IHTMLDocument2).parentWindow;
    end;
end;

Answers:

Answer

The problem is in this line:

FHTMLWindow.execScript('ttMapHelper.ShowRoute("' + FPolyLine + '")', 'JavaScript')

and is very much similar to SQL injection: you're silently assuming the value if FPolyLine is in JavaScript format, which it is not. An easy fix is perhaps using

StringReplace(StringReplace(FPolyLine'\','\\',[rfReplaceAll]),'"','\"',[rfReplaceAll])

but a better fix would be using an extra <input type="hidden", set it's value via objects, and call it by name from JavaScript.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.