Revision: 5462
Author:   ihab.awad
Date:     Thu Jun 27 13:45:40 2013
Log:      Fix various bugs in API taming
https://codereview.appspot.com/10648043

Fixes the following bugs:

  https://code.google.com/p/google-caja/issues/detail?id=1766
  https://code.google.com/p/google-caja/issues/detail?id=1767
  https://code.google.com/p/google-caja/issues/detail?id=1775
  https://code.google.com/p/google-caja/issues/detail?id=1776
  https://code.google.com/p/google-caja/issues/detail?id=1777

Issue 1777 was "fixed" by simply realizing that whatever issue was causing
garbled data many, many CL's ago was no longer an issue.

[email protected]

http://code.google.com/p/google-caja/source/detail?r=5462

Added:
/trunk/tests/com/google/caja/apitaming/visualization/chart-range-filter.html
 /trunk/tests/com/google/caja/apitaming/visualization/formatters-table.html
 /trunk/tests/com/google/caja/plugin/apitaming-tests.json
Modified:
 /trunk/src/com/google/caja/apitaming/cajaTamingGoogleLoader.js
 /trunk/src/com/google/caja/apitaming/google.visualization.policyFactory.js
 /trunk/src/com/google/caja/plugin/guest-manager.js
 /trunk/tests/com/google/caja/apitaming/visualization/table.html
 /trunk/tests/com/google/caja/apitaming/visualization-list.json
 /trunk/tests/com/google/caja/plugin/test-index.js

=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/apitaming/visualization/chart-range-filter.html Thu Jun 27 13:45:40 2013
@@ -0,0 +1,111 @@
+<!--
+You are free to copy and use this sample in accordance with the terms of the
+Apache license (http://www.apache.org/licenses/LICENSE-2.0.html)
+-->
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd";>
+<html xmlns="http://www.w3.org/1999/xhtml";>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
+    <title>
+      Google Visualization API Sample
+    </title>
+ <script type="text/javascript" src="http://www.google.com/jsapi";></script>
+    <script type="text/javascript">
+ google.load('visualization', '1.1', {packages: ['corechart', 'controls']});
+    </script>
+    <script type="text/javascript">
+      function drawVisualization() {
+        var dashboard = new google.visualization.Dashboard(
+             document.getElementById('dashboard'));
+
+         debugger;
+
+         var control = new google.visualization.ControlWrapper({
+           'controlType': 'ChartRangeFilter',
+           'containerId': 'control',
+           'options': {
+             // Filter by the date axis.
+             'filterColumnIndex': 0,
+             'ui': {
+               'chartType': 'LineChart',
+               'chartOptions': {
+                 'chartArea': {'width': '90%'},
+                 'hAxis': {'baselineColor': 'none'}
+               },
+ // Display a single series that shows the closing value of the stock. + // Thus, this view has two columns: the date (axis) and the stock value (line series).
+               'chartView': {
+                 'columns': [0, 3]
+               },
+               // 1 day in milliseconds = 24 * 60 * 60 * 1000 = 86,400,000
+               'minRangeSize': 86400000
+             }
+           },
+           // Initial range: 2012-02-09 to 2012-03-20.
+ 'state': {'range': {'start': new Date(2012, 1, 9), 'end': new Date(2012, 2, 20)}}
+         });
+
+      debugger;
+
+         var chart = new google.visualization.ChartWrapper({
+           'chartType': 'CandlestickChart',
+           'containerId': 'chart',
+           'options': {
+ // Use the same chart area width as the control for axis alignment.
+             'chartArea': {'height': '80%', 'width': '90%'},
+             'hAxis': {'slantedText': false},
+             'vAxis': {'viewWindow': {'min': 0, 'max': 2000}},
+             'legend': {'position': 'none'}
+           },
+           // Convert the first column from 'date' to 'string'.
+           'view': {
+             'columns': [
+               {
+                 'calc': function(dataTable, rowIndex) {
+                   return dataTable.getFormattedValue(rowIndex, 0);
+                 },
+                 'type': 'string'
+               }, 1, 2, 3, 4]
+           }
+         });
+
+        debugger;
+
+         var data = new google.visualization.DataTable();
+         data.addColumn('date', 'Date');
+         data.addColumn('number', 'Stock low');
+         data.addColumn('number', 'Stock open');
+         data.addColumn('number', 'Stock close');
+         data.addColumn('number', 'Stock high');
+
+         // Create random stock values, just like it works in reality.
+         var open, close = 300;
+         var low, high;
+         for (var day = 1; day < 121; ++day) {
+ var change = (Math.sin(day / 2.5 + Math.PI) + Math.sin(day / 3) - Math.cos(day * 0.7)) * 150;
+           change = change >= 0 ? change + 10 : change - 10;
+           open = close;
+           close = Math.max(50, open + change);
+           low = Math.min(open, close) - (Math.cos(day * 1.7) + 1) * 15;
+           low = Math.max(0, low);
+           high = Math.max(open, close) + (Math.cos(day * 1.3) + 1) * 15;
+           var date = new Date(2012, 0 ,day);
+ data.addRow([date, Math.round(low), Math.round(open), Math.round(close), Math.round(high)]);
+         }
+
+         dashboard.bind(control, chart);
+         dashboard.draw(data);
+      }
+
+
+      google.setOnLoadCallback(drawVisualization);
+    </script>
+  </head>
+  <body>
+    <div id="dashboard">
+        <div id="chart" style='width: 915px; height: 300px;'></div>
+        <div id="control" style='width: 915px; height: 50px;'></div>
+    </div>
+  </body>
+</html>
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/apitaming/visualization/formatters-table.html Thu Jun 27 13:45:40 2013
@@ -0,0 +1,75 @@
+<html>
+  <head>
+ <script type="text/javascript" src="https://www.google.com/jsapi";></script>
+    <script type="text/javascript">
+      google.load("visualization", "1", {packages:["corechart","table"]});
+      google.setOnLoadCallback(testFormatters);
+
+      function testFormatters() {
+        var data = new google.visualization.DataTable();
+
+        data.addColumn('number', 'Arrow');
+        data.addColumn('number', 'Bar');
+        data.addColumn('number', 'Color range');
+        data.addColumn('number', 'Color gradient');
+        data.addColumn('date', 'Date');
+        data.addColumn('number', 'Number');
+        data.addColumn('string', 'Pattern');
+
+        data.addRows([
+          [ +4, +250,  50,  50, new Date(1977, 1, 1), -1234.87654321, '' ],
+          [  1,  -50, 150, 150, new Date(1978, 2, 2),  1234.87654321, '' ],
+          [ +3, +200, 250, 250, new Date(1979, 3, 3), -8765.12345678, '' ],
+          [  0,    0, 350, 350, new Date(1980, 4, 4),  8765.12345678, '' ],
+        ]);
+
+        new google.visualization.TableArrowFormat({
+              base: 2
+            })
+            .format(data, 0);
+        new google.visualization.TableBarFormat({
+              base: 100,
+              colorNegative: 'blue',
+              colorPositive: 'red',
+              drawZeroLine: true,
+              min: -100,
+              max: +300,
+              showValue: false,
+              width: 50
+            })
+            .format(data, 1);
+        var cf1 = new google.visualization.TableColorFormat();
+        cf1.addRange(100, 300, '#9C2542', '#A1CAF1');
+        cf1.format(data, 2);
+        var cf2 = new google.visualization.TableColorFormat();
+        cf2.addGradientRange(0, 400, '#9C2542', '#ff0000', '#ffffff');
+        cf2.format(data, 3);
+        new google.visualization.TableDateFormat({
+              formatType: 'medium',
+              timeZone: -5
+            })
+            .format(data, 4);
+        new google.visualization.TableNumberFormat({
+              decimalSymbol: ',',
+              fractionDigits: 3,
+              groupingSymbol: '.',
+              negativeColor: 'red',
+              negativeParens: false,
+              prefix: '\'Bout ',
+              suffix: ' big \'uns.'
+            })
+            .format(data, 5);
+        new google.visualization.TablePatternFormat(
+ '\u003cstrong\u003e{0}\u003c/strong\u003e for \u003cem\u003e{2}\u003c/em\u003e!\u003cscript\u003ealert(42);\u003c/script\u003e' +
+              '\u003cbr\u003eGefurfifier!')
+            .format(data, [0, 1, 2, 3, 4, 5], 6);
+
+ var table = new google.visualization.Table(document.getElementById('chart_div'));
+        table.draw(data, {showRowNumber: true, allowHtml: true });
+      }
+    </script>
+  </head>
+  <body>
+    <div id="chart_div"></div>
+  </body>
+</html>
=======================================
--- /dev/null
+++ /trunk/tests/com/google/caja/plugin/apitaming-tests.json Thu Jun 27 13:45:40 2013
@@ -0,0 +1,33 @@
+[
+  {
+    "label": "apitaming",
+    "mode": "none",
+    "tests":
+      [
+        {
+          "bare": 
"../apitaming/tests-index.html?testsList=maps-list.json&cajaServer=/caja",
+          "label": "API taming Maps individual list"
+        },
+        {
+          "bare": 
"../apitaming/tests-index.html?testsList=picker-list.json&cajaServer=/caja",
+          "label": "API taming Picker individual list"
+        },
+        {
+          "bare": 
"../apitaming/tests-index.html?testsList=visualization-list.json&cajaServer=/caja",
+          "label": "API taming GViz individual list"
+        },
+        {
+          "bare": 
"../apitaming/tests-side-by-side.html?testsList=maps-list.json&cajaServer=/caja",
+          "label": "API taming Maps all (large/slow page)"
+        },
+        {
+          "bare": 
"../apitaming/tests-side-by-side.html?testsList=picker-list.json&cajaServer=/caja",
+          "label": "API taming Picker all (large/slow page)"
+        },
+        {
+          "bare": 
"../apitaming/tests-side-by-side.html?testsList=visualization-list.json&cajaServer=/caja",
+          "label": "API taming GViz all (large/slow page)"
+        }
+      ]
+  }
+]
=======================================
--- /trunk/src/com/google/caja/apitaming/cajaTamingGoogleLoader.js Thu Feb 28 09:33:35 2013 +++ /trunk/src/com/google/caja/apitaming/cajaTamingGoogleLoader.js Thu Jun 27 13:45:40 2013
@@ -174,7 +174,7 @@
       if (!frame.hasOwnProperty('opaqueNodeByIdCounter___')) {
         frame.opaqueNodeByIdCounter___ = 0;
       }
- var node = frame.untame(frame.imports.document.getElementById(origId));
+      var node = frame.getElementByGuestId(origId);
       var d = node.ownerDocument.createElement('div');
var opaqueId = 'opaqueNodeById__' + frame.opaqueNodeByIdCounter___++ + '__' + frame.idSuffix;
       d.setAttribute('id', opaqueId);
=======================================
--- /trunk/src/com/google/caja/apitaming/google.visualization.policyFactory.js Tue Jan 22 12:48:15 2013 +++ /trunk/src/com/google/caja/apitaming/google.visualization.policyFactory.js Thu Jun 27 13:45:40 2013
@@ -38,7 +38,7 @@
     if (args.length < 1) { return []; }
     var spec = args[0];
     if (typeof spec === 'string') { spec = JSON.parse(spec); }
-    spec = utils.copyJson(spec);
+    spec = utils.copyMixed(spec);
     if (spec.containerId) {
       tameContainerId = spec.containerId;
       spec.containerId = utils.opaqueNodeById(spec.containerId);
@@ -94,7 +94,7 @@
   // result has HTML in tooltips turned off

   function copyDrawOpts(opts) {
-    opts = utils.copyJson(opts);
+    opts = !!opts ? utils.copyJson(opts) : {};
     opts.allowHtml = false;
     opts.tooltip = { isHtml: false };
     return opts;
@@ -289,43 +289,63 @@
   ////////////////////////////////////////////////////////////////////////
   // Formatters

-  v.ArrowFormat = function(opt_options) {};
-  v.ArrowFormat.__super__ = Object;
-  v.ArrowFormat.__before__ = [ utils.mapArgs(utils.copyJson) ];
-  v.ArrowFormat.prototype.format = function(dataTable, columnIndex) {};
+  function formatter(name, cb) {
+    cb(name + 'Format');
+    cb('Table' + name + 'Format');
+  }

-  v.BarFormat = function(opt_options) {};
-  v.BarFormat.__super__ = Object;
-  v.BarFormat.__before__ = [ utils.mapArgs(utils.copyJson) ];
-  v.BarFormat.prototype.format = function(dataTable, columnIndex) {};
+  formatter('Arrow', function(name) {
+    v[name] = function(opt_options) {};
+    v[name].__super__ = Object;
+    v[name].__before__ = [ utils.mapArgs(utils.copyJson) ];
+    v[name].prototype.format = function(dataTable, columnIndex) {};
+  });

-  v.ColorFormat = function() {};
-  v.ColorFormat.__super__ = Object;
-  v.ColorFormat.__before__ = [ utils.mapArgs(utils.copyJson) ];
-  v.ColorFormat.prototype.addRange = function(from, to, color, bgcolor) {};
- v.ColorFormat.prototype.addGradientRange = function(from, to, color, fromBgColor, toBgColor) {};
-  v.ColorFormat.prototype.format = function(dataTable, columnIndex) {};
+  formatter('Bar', function(name) {
+    v[name] = function(opt_options) {};
+    v[name].__super__ = Object;
+    v[name].__before__ = [ utils.mapArgs(utils.copyJson) ];
+    v[name].prototype.format = function(dataTable, columnIndex) {};
+  });

-  v.DateFormat = function(opt_options) {};
-  v.DateFormat.__super__ = Object;
-  v.DateFormat.__before__ = [ utils.mapArgs(utils.copyJson) ];
-  v.DateFormat.prototype.format = function(dataTable, columnIndex) {};
-  v.DateFormat.prototype.formatValue = function(value) {};
+  formatter('Color', function(name) {
+    v[name] = function() {};
+    v[name].__super__ = Object;
+    v[name].__before__ = [ utils.mapArgs(utils.copyJson) ];
+    v[name].prototype.addRange = function(from, to, color, bgcolor) {};
+    v[name].prototype.addGradientRange =
+        function(from, to, color, fromBgColor, toBgColor) {};
+    v[name].prototype.format = function(dataTable, columnIndex) {};
+  });

-  v.NumberFormat = function(opt_options) {};
-  v.NumberFormat.__super__ = Object;
-  v.NumberFormat.__before__ = [ utils.mapArgs(utils.copyJson) ];
-  v.NumberFormat.prototype.format = function(dataTable, columnIndex) {};
-  v.NumberFormat.prototype.formatValue = function(value) {};
-  v.NumberFormat.DECIMAL_SEP = 1;
-  v.NumberFormat.GROUP_SEP = 1;
-  v.NumberFormat.DECIMAL_PATTERN = 1;
+  formatter('Date', function(name) {
+    v[name] = function(opt_options) {};
+    v[name].__super__ = Object;
+    v[name].__before__ = [ utils.mapArgs(utils.copyJson) ];
+    v[name].prototype.format = function(dataTable, columnIndex) {};
+    v[name].prototype.formatValue = function(value) {};
+  });

-  v.PatternFormat = function(pattern) {};
-  v.PatternFormat.__super__ = Object;
- v.PatternFormat.prototype.format = function(dataTable, srcColumnIndices, opt_dstColumnIndex) {}; - v.PatternFormat.prototype.format.__before__ = [ utils.mapArgs(utils.identity, utils.copyJson, utils.identity) ];
+  formatter('Number', function(name) {
+    v[name] = function(opt_options) {};
+    v[name].__super__ = Object;
+    v[name].__before__ = [ utils.mapArgs(utils.copyJson) ];
+    v[name].prototype.format = function(dataTable, columnIndex) {};
+    v[name].prototype.formatValue = function(value) {};
+    v[name].DECIMAL_SEP = 1;
+    v[name].GROUP_SEP = 1;
+    v[name].DECIMAL_PATTERN = 1;
+  });

+  formatter('Pattern', function(name) {
+    v[name] = function(pattern) {};
+    v[name].__super__ = Object;
+    v[name].prototype.format =
+        function(dataTable, srcColumnIndices, opt_dstColumnIndex) {};
+    v[name].prototype.format.__before__ = [
+        utils.mapArgs(utils.identity, utils.copyJson, utils.identity)
+      ];
+  });

   ////////////////////////////////////////////////////////////////////////
   // GadgetHelper
@@ -528,27 +548,24 @@
   v.CandlestickChart.prototype.getSelection = function() {};
   v.CandlestickChart.prototype.setSelection = function(selection) {};

- // TODO(ihab.awad): AnnotatedTimeLine data is garbled in testing under ES5.
-  // This is disabled until we fix this.
-
-  // v.AnnotatedTimeLine = function(container) {};
-  // v.AnnotatedTimeLine.__super__ = Object;
-  // v.AnnotatedTimeLine.__before__ = [
-  //   function(f, self, args) {
-  //     var outer = args[0];
-  //     var inner = utils.opaqueNode(outer);
-  //     inner.style.width = outer.style.width;
-  //     inner.style.height = outer.style.height;
-  //     return [ inner ];
-  //   }
-  // ];
-  // v.AnnotatedTimeLine.prototype.draw = function(data, opt_options) {};
- // v.AnnotatedTimeLine.prototype.draw.__before__ = [ utils.mapArgs(utils.identity, copyDrawOpts, utils.copyJson) ];
-  // v.AnnotatedTimeLine.prototype.getSelection = function() {};
-  // v.AnnotatedTimeLine.prototype.getVisibleChartRange = function() {};
- // v.AnnotatedTimeLine.prototype.setVisibleChartRange = function(firstDate, lastDate, opt_animate) {}; - // v.AnnotatedTimeLine.prototype.showDataColumns = function(columnIndexes) {}; - // v.AnnotatedTimeLine.prototype.hideDataColumns = function(columnIndexes) {};
+  v.AnnotatedTimeLine = function(container) {};
+  v.AnnotatedTimeLine.__super__ = Object;
+  v.AnnotatedTimeLine.__before__ = [
+     function(f, self, args) {
+       var outer = args[0];
+       var inner = utils.opaqueNode(outer);
+       inner.style.width = outer.style.width;
+       inner.style.height = outer.style.height;
+       return [ inner ];
+     }
+  ];
+  v.AnnotatedTimeLine.prototype.draw = function(data, opt_options) {};
+ v.AnnotatedTimeLine.prototype.draw.__before__ = [ utils.mapArgs(utils.identity, copyDrawOpts, utils.copyJson) ];
+  v.AnnotatedTimeLine.prototype.getSelection = function() {};
+  v.AnnotatedTimeLine.prototype.getVisibleChartRange = function() {};
+ v.AnnotatedTimeLine.prototype.setVisibleChartRange = function(firstDate, lastDate, opt_animate) {}; + v.AnnotatedTimeLine.prototype.showDataColumns = function(columnIndexes) {}; + v.AnnotatedTimeLine.prototype.hideDataColumns = function(columnIndexes) {};

   v.GeoMap = function(container) {};
   v.GeoMap.__super__ = Object;
@@ -648,9 +665,9 @@
v.ControlWrapper.prototype.setContainerId.__before__ = [ containerIdBeforeSet ];
   v.ControlWrapper.prototype.setOption = function(key, value) {};
   v.ControlWrapper.prototype.setOptions = function(options_obj) {};
- v.ControlWrapper.prototype.setOptions.__before__ = [ utils.mapArgs(utils.copyJson) ]; + v.ControlWrapper.prototype.setOptions.__before__ = [ utils.mapArgs(utils.copyMixed) ];
   v.ControlWrapper.prototype.setState = function(state_obj) {};
- v.ControlWrapper.prototype.setState.__before__ = [ utils.mapArgs(utils.copyJson) ]; + v.ControlWrapper.prototype.setState.__before__ = [ utils.mapArgs(utils.copyMixed) ];


   return {
=======================================
--- /trunk/src/com/google/caja/plugin/guest-manager.js Thu Apr 11 19:45:31 2013 +++ /trunk/src/com/google/caja/plugin/guest-manager.js Thu Jun 27 13:45:40 2013
@@ -62,6 +62,16 @@
     flash: true
   };

+  function copyStringMap(o) {
+    var r = {};
+    for (var k in o) {
+      if (Object.prototype.hasOwnProperty.call(o, k)) {
+        r[k] = o[k];
+      }
+    }
+    return r;
+  }
+
   var self = {
     // Public state
     div: divs.outer && divs.outer.parentNode,
@@ -69,6 +79,34 @@
     getUrl: function() { return args.url; },
     getUriPolicy: function() { return uriPolicy; },

+    getElementByGuestId: domicile
+        ? function(id) {
+          return self.untame(
+              Object.prototype.v___
+ ? self.imports.v___('document').m___('getElementById', [id])
+                  : self.imports.document.getElementById(id));
+        }
+        : function(_) {
+          return null;
+        },
+
+    rewriteUri: domicile
+        ? function(url, mime, opts) {
+          return self.untame(
+              Object.prototype.v___
+                  ? domicile.m___('rewriteUri', [
+                      url,
+                      mime,
+                      copyStringMap(opts)])
+                  : domicile.rewriteUri(
+                      url,
+                      mime,
+                      copyStringMap(opts)));
+        }
+        : function(_) {
+          return null;
+        },
+
     // deprecated; idSuffix in domado means '-' + idClass, but idSuffix
     // exposed here is without the leading '-'.  Future code should use the
     // idClass property instead.
@@ -90,8 +128,6 @@
     domicile: domicile,      // Currently exposed only for the test suite
     htmlEmitter: htmlEmitter,

- rewriteUri: domicile ? domicile.rewriteUri : function() { return null; },
-
     // Taming utilities
     tame: frameTamingMembrane.tame,
     untame: frameTamingMembrane.untame,
=======================================
--- /trunk/tests/com/google/caja/apitaming/visualization/table.html Tue Nov 6 10:03:09 2012 +++ /trunk/tests/com/google/caja/apitaming/visualization/table.html Thu Jun 27 13:45:40 2013
@@ -16,13 +16,23 @@
           ['Bob',   {v: 7000,  f: '$7,000'},  true]
         ]);

- var table = new google.visualization.Table(document.getElementById('table_div'));
-        table.draw(data, {showRowNumber: true});
+        // Show a regular table being drawn
+
+        new google.visualization.Table(
+            document.getElementById('table_plain_div'))
+            .draw(data, {showRowNumber: true});
+
+        // Demonstrate 'draw' being called with 'null' options (2nd arg)
+
+        new google.visualization.Table(
+            document.getElementById('table_nullopts_div'))
+            .draw(data, null);
       }
     </script>
   </head>

-  <body>
-    <div id='table_div'></div>
+  <body style="overflow: scroll;">
+    <div id="table_plain_div"></div>
+    <div id="table_nullopts_div"></div>
   </body>
 </html>
=======================================
--- /trunk/tests/com/google/caja/apitaming/visualization-list.json Tue Nov 6 10:03:09 2012 +++ /trunk/tests/com/google/caja/apitaming/visualization-list.json Thu Jun 27 13:45:40 2013
@@ -5,6 +5,7 @@
   "visualization/bubblechart.html",
   "visualization/candlestickchart.html",
   "visualization/charteditor.html",
+  "visualization/chart-range-filter.html",
   "visualization/chartwrapper.html",
   "visualization/columnchart.html",
   "visualization/combochart.html",
@@ -18,6 +19,7 @@
   "visualization/errrors-test.html",
   "visualization/events-test.html",
   "visualization/formatters.html",
+  "visualization/formatters-table.html",
   "visualization/gaugechart.html",
   "visualization/geochart-markers.html",
   "visualization/geochart-regions.html",
=======================================
--- /trunk/tests/com/google/caja/plugin/test-index.js Wed Jun 12 12:11:32 2013 +++ /trunk/tests/com/google/caja/plugin/test-index.js Thu Jun 27 13:45:40 2013
@@ -110,9 +110,14 @@
     };
     request.send();
   }
+
+  // TODO(kpreid): Refactor to avoid unnecessary latency and dependencies,
+  // perhaps using Q.

   loadCatalog('tests', 'browser-tests.json', function() {
-    loadCatalog('tests', '../ses/ses-tests.json');
+    loadCatalog('tests', '../ses/ses-tests.json', function() {
+      loadCatalog('tests', 'apitaming-tests.json');
+    });
   });
   loadCatalog('thirdparty', 'third-party-tests.json');

--

--- You received this message because you are subscribed to the Google Groups "Google Caja Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to