Repository: web2py/web2py

Issue:
encoding issue with Python > 3.7 (amp; passed in request var strings)
**Describe the bug**
We have an onclick function using a web2py_component that passes parameters using vars=dict(a=x, b=y, c=z), that is:
`web2py_component("{{=URL('my_controller', 'my_view.load', vars=dict(a=x,b=y,c=z))}}");`
This compiles to
`web2py_component("/my_app/my_controller/my_view.load?a=xamp;b=yamp;c=z);`
See the "amp;" in there? It is expected because of the multiple parameters in the string yes? url?var1&var2&var3 etc.
In Python 3.7.9 and below the "amp;" causes no problems. In Python 3.9 and 3.10 (3.8 has not been tested), the 'amp' gets passed as part of the variable name, so variables "b" and "c" come through as "ampb;" and "ampc;" so the controller that receives them doesn't know what to do with them because it is expecting simply "b" and "c".

To Reproduce**
Steps to reproduce the behavior:
Using Python 3.7.9, if I copy and paste the URL from the network tab in the inspector console into a browser window and remove the 'amp;' then the view loads just fine.
Using Python 3.9 or 3.10 I receive either a 404 or a 403 error due to the variable not being recognized. Removing the 'amp;' loads the view successfully. In other words, in Python 3.9 or 3.10
`http://localhost:8000/my_app/my_controller/my_view.load?a=x&amp;b=y&amp;c=z`
changed to
`http://localhost:8000/my_app/my_controller/my_view.load?a=x&b=y&c=z`
loads perfectly.

**Expected behavior**
Code works in latest version of Python 3.x

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: Windows 10
 - Browser Chrome
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.



Discussion:
Here is another example. We have an api function that builds a database query using a request.vars passed from the view. In Python 3.7 and below the 'amp;' included in the passed variable name had no effect. In Python 3.8 and above (tested in 3.8 and 3.10) the 'amp;' is included.

This looks to be an encoding/decoding issue. Any ideas on how to fix or a workaround? We have a number of production applications this is effecting.

This screenshot is from Python 3.10. You can see there is no output of request.vars.sender_uuid.

![image](https://user-images.githubusercontent.com/17770216/156263750-e7bd0e41-cf19-4f4a-8419-a26f4953fbe4.png)

And here is the exact same thing in Python 3.7.9. The output of request.vars.sender_uuid is shown.
![image](https://user-images.githubusercontent.com/17770216/156264693-066020e2-7994-481e-b3c2-1a3001c3561b.png)

We found the problem. Turns out to be related to the use of the dictionary to build the url. If instead of 
`web2py_component("{{=URL('my_controller', 'my_view.load', vars=dict(a=x,b=y,c=z))}}");`
we use
`web2py_component("{{=URL('my_controller', 'my_view.load')}}?a=" + x + "&b=" + y + "&z=" + z);`
then there is no issue.

Of course, since x, y, and z were passed to the view as Python variables they'll need to be declared first as JavaScript variables for the second line of code to work.


Resolving patch:
diff --git a/gluon/html.py b/gluon/html.py
--- a/gluon/html.py
+++ b/gluon/html.py
@@ -171,6 +171,10 @@ def truncate_string(text, length, dots='...'):
         text = to_native(text[:length - len(dots)]) + dots
     return text
 
+class SafeString(str):
+    """ A string know to be safe that does not need to be escaped """
+    def xml(self):
+        return self
 
 def URL(a=None,
         c=None,
@@ -415,7 +419,7 @@ def URL(a=None,
 
     url = url_out(r, env, application, controller, function,
                   args, other, scheme, host, port, language=language)
-    return url
+    return SafeString(url)
 
 
 def verifyURL(request, hmac_key=None, hash_vars=True, salt=None, user_signature=None, hash_extension=True):


Test patch: