Repository: plotly/plotly.py
Pull request #997 (2018-04-25 18:35:19)

validate plotly_(api)_domain - HTTPS, not HTTP
Addresses https://github.com/plotly/plotly.py/issues/996

Notes:
1. This error message does not get triggered if the original url in the request starts with `zhttp://...`. A separate error message is thrown for that.
2. The original url sent by a user is getting coerced into all lowercase, so a lowercase to lowercase comparison is not necessary.


Patch:
diff --git a/plotly/tools.py b/plotly/tools.py
index b24c39183..00daae4c1 100644
--- a/plotly/tools.py
+++ b/plotly/tools.py
@@ -192,6 +192,7 @@ def set_config_file(plotly_domain=None,
     ensure_local_plotly_files()  # make sure what's there is OK
     utils.validate_world_readable_and_sharing_settings({
         'sharing': sharing, 'world_readable': world_readable})
+
     settings = get_config_file()
     if isinstance(plotly_domain, six.string_types):
         settings['plotly_domain'] = plotly_domain
@@ -218,6 +219,11 @@ def set_config_file(plotly_domain=None,
     elif auto_open is not None:
         raise TypeError('auto_open should be a boolean')
 
+    # validate plotly_domain and plotly_api_domain
+    utils.validate_plotly_domains(
+        {'plotly_domain': plotly_domain, 'plotly_api_domain': plotly_api_domain}
+    )
+
     if isinstance(world_readable, bool):
         settings['world_readable'] = world_readable
         settings.pop('sharing')
diff --git a/plotly/utils.py b/plotly/utils.py
index 7d623014b..b39426bf6 100644
--- a/plotly/utils.py
+++ b/plotly/utils.py
@@ -7,11 +7,12 @@ Low-level functionality NOT intended for users to EVER use.
 """
 from __future__ import absolute_import
 
+import decimal
 import os.path
 import re
 import sys
 import threading
-import decimal
+import warnings
 from collections import deque
 
 import pytz
@@ -32,6 +33,30 @@ sage_all = get_module('sage.all')
 lock = threading.Lock()
 
 
+http_msg = (
+    "The plotly_domain and plotly_api_domain of your config file must start "
+    "with 'https', not 'http'. If you are not using On-Premise then run the "
+    "following code to ensure your plotly_domain and plotly_api_domain start "
+    "with 'https':\n\n\n"
+    "import plotly\n"
+    "plotly.tools.set_config_file(\n"
+    "    plotly_domain='https://plot.ly',\n"
+    "    plotly_api_domain='https://api.plot.ly'\n"
+    ")\n\n\n"
+    "If you are using On-Premise then you will need to use your company's "
+    "domain and api_domain urls:\n\n\n"
+    "import plotly\n"
+    "plotly.tools.set_config_file(\n"
+    "    plotly_domain='https://plotly.your-company.com',\n"
+    "    plotly_api_domain='https://plotly.your-company.com'\n"
+    ")\n\n\n"
+    "Make sure to replace `your-company.com` with the URL of your Plotly "
+    "On-Premise server.\nSee "
+    "https://plot.ly/python/getting-started/#special-instructions-for-plotly-onpremise-users "
+    "for more help with getting started with On-Premise."
+)
+
+
 ### general file setup tools ###
 
 def load_json_dict(filename, *args):
@@ -297,6 +322,7 @@ class PlotlyJSONEncoder(_json.JSONEncoder):
         else:
             raise NotEncodable
 
+
 ### unicode stuff ###
 def decode_unicode(coll):
     if isinstance(coll, list):
@@ -436,6 +462,16 @@ def validate_world_readable_and_sharing_settings(option_set):
         )
 
 
+def validate_plotly_domains(option_set):
+    domains_not_none = []
+    for d in ['plotly_domain', 'plotly_api_domain']:
+        if d in option_set and option_set[d]:
+            domains_not_none.append(option_set[d])
+
+    if not all(d.lower().startswith('https') for d in domains_not_none):
+        warnings.warn(http_msg, category=UserWarning)
+
+
 def set_sharing_and_world_readable(option_set):
     if 'world_readable' in option_set and 'sharing' not in option_set:
         option_set['sharing'] = (


Test patch:
diff --git a/plotly/tests/test_core/test_stream/test_stream.py b/plotly/tests/test_core/test_stream/test_stream.py
index 190f18a9f..869fd8d2d 100644
--- a/plotly/tests/test_core/test_stream/test_stream.py
+++ b/plotly/tests/test_core/test_stream/test_stream.py
@@ -18,7 +18,7 @@ ak = 'ubpiol2cve'
 tk = 'vaia8trjjb'
 config = {'plotly_domain': 'https://plot.ly',
           'plotly_streaming_domain': 'stream.plot.ly',
-          'plotly_api_domain': 'https://api.plot.ly', 
+          'plotly_api_domain': 'https://api.plot.ly',
           'plotly_ssl_verification': False}
 
 
diff --git a/plotly/tests/test_core/test_tools/test_file_tools.py b/plotly/tests/test_core/test_tools/test_file_tools.py
index f28aff058..c0a434ca7 100644
--- a/plotly/tests/test_core/test_tools/test_file_tools.py
+++ b/plotly/tests/test_core/test_tools/test_file_tools.py
@@ -1,6 +1,8 @@
 from plotly import tools, session
 from plotly.tests.utils import PlotlyTestCase
 
+import warnings
+
 
 class FileToolsTest(PlotlyTestCase):
 
@@ -43,7 +45,6 @@ class FileToolsTest(PlotlyTestCase):
         self.assertEqual(config['plotly_streaming_domain'], streaming_domain)
         tools.reset_config_file()
 
-
     def test_set_config_file_world_readable(self):
 
         # Return TypeError when world_readable type is not a bool
@@ -51,6 +52,30 @@ class FileToolsTest(PlotlyTestCase):
         kwargs = {'world_readable': 'True'}
         self.assertRaises(TypeError, tools.set_config_file, **kwargs)
 
+    def test_set_config_expected_warning_msg(self):
+
+        # Check that UserWarning is being called with http plotly_domain
+
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            kwargs = {'plotly_domain': 'http://www.foo-bar.com'}
+            tools.set_config_file(**kwargs)
+            assert len(w) == 1
+            assert issubclass(w[-1].category, UserWarning)
+            assert "plotly_domain" in str(w[-1].message)
+
+
+    def test_set_config_no_warning_msg_if_plotly_domain_is_https(self):
+
+        # Check that no UserWarning is being called with https plotly_domain
+
+        with warnings.catch_warnings(record=True) as w:
+            warnings.simplefilter("always")
+            kwargs = {'plotly_domain': 'https://www.foo-bar.com'}
+            tools.set_config_file(**kwargs)
+            assert len(w) == 0
+
+
     def test_reset_config_file(self):
 
         # Check reset_config and get_config return the same values