Repository: ansible/ansible-modules-extras

Issue:
Slack does not escape/encode ampersand (&) correctly
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE

<!--- Pick one below and delete the rest: -->
- Bug Report
##### COMPONENT NAME

Slack module
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

```
ansible 2.1.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

N/A
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->

Mac OS X 10.11.4 (but probably N/A)
##### SUMMARY

<!--- Explain the problem briefly -->

Whenever the `msg` or a `text` inside `attachments` contains an ampersand (`&`), the Slack module fails with `HTTP Error 400: Bad Request`.
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

Simply try to send a slack notification and make sure there is a `"&"` somewhere in the `msg` parameter (or inside a `attachments[].text`).

<!--- Paste example playbooks or commands between quotes below -->

```
- name: Send slack test message
  hosts: MyHost
  tasks:
    - slack:
        token: "xxx/yyy/zzz"
        channel: "#my-channel"
        username: "Ansible"
        msg: "one & two"
```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

You'd expect to correctly receive the slack notification containing `"one & two"` as the message.
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with high verbosity (-vvvv) -->

The module fails.

<!--- Paste verbatim command output between quotes below -->

```
fatal: [MyHost]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_args": {"attachments": null, "channel": "#my-channel", "color": "normal", "domain": null, "icon_emoji": null, "icon_url": "http://www.ansible.com/favicon.ico", "link_names": 1, "msg": "one & two", "parse": null, "token": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER", "username": "Ansible", "validate_certs": true}, "module_name": "slack"}, "msg": " failed to send payload={\"username\": \"Ansible\", \"text\": \"one & two\", \"link_names\": 1, \"channel\": \"#my-channel\", \"icon_url\": \"http://www.ansible.com/favicon.ico\"} to https://hooks.slack.com/services/[obscured]: HTTP Error 400: Bad Request"}
```
##### Temporary workaround

I found that when I manually replace all `&` with `%26` (URL escaped ampersand), I receive the correct notification. For people wanting a temporary fix: `- text: "{{ originalText | replace('&', '%26') }}"`



Discussion:
<!-- @Lapixx: Thanks for filing this bug report! -->

@ramondelafuente ping, this issue is waiting for your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->

this also happens for quotes. for example, I dump commit messages out to Slack on deploy and if the commit message has quotes, it breaks the JSON we pass to Slack and we get 400: Bad Request

@ramondelafuente, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

This is bug happens because https://github.com/ansible/ansible-modules-extras/blob/devel/notification/slack.py#L202 does a plain ".jsonify". According to slack's own docs at https://api.slack.com/docs/message-formatting we need to convert 3 characters into HTML entities:

> There are three characters you must convert to HTML entities and only three: &, <, and >.

Looks like it should be an easy fix (just perform string substitution of those 3 symbols and then afterwards feed it to `.jsonify`).

I saw the same thing as well, but as camflan mentioned, this problem also occurs with quotes. 

Sorry, missed camflan's comment. I suppose there is more digging to be done.


Resolving patch:
diff --git a/notification/slack.py b/notification/slack.py
--- a/notification/slack.py
+++ b/notification/slack.py
@@ -1,6 +1,7 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 
+# (c) 2016, René Moser <mail@renemoser.net>
 # (c) 2015, Stefan Berggren <nsg@nsg.cc>
 # (c) 2014, Ramon de la Fuente <ramon@delafuente.nl>
 #
@@ -154,7 +155,7 @@
             value: "load average: 5,16, 4,64, 2,43"
             short: "true"
 
-- name: Send notification message via Slack (deprecated API using domian)
+- name: Send notification message via Slack (deprecated API using domain)
   local_action:
     module: slack
     domain: future500.slack.com
@@ -166,13 +167,27 @@
 OLD_SLACK_INCOMING_WEBHOOK = 'https://%s/services/hooks/incoming-webhook?token=%s'
 SLACK_INCOMING_WEBHOOK = 'https://hooks.slack.com/services/%s'
 
+# See https://api.slack.com/docs/message-formatting#how_to_escape_characters
+# Escaping quotes and apostrophe however is related to how Ansible handles them.
+html_escape_table = {
+    '&': "&amp;",
+    '>': "&gt;",
+    '<': "&lt;",
+    '"': "\"",
+    "'": "\'",
+}
+
+def html_escape(text):
+    '''Produce entities within text.'''
+    return "".join(html_escape_table.get(c,c) for c in text)
+
 def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments):
     payload = {}
     if color == "normal" and text is not None:
-        payload = dict(text=text)
+        payload = dict(text=html_escape(text))
     elif text is not None:
         # With a custom color we have to set the message as attachment, and explicitely turn markdown parsing on for it.
-        payload = dict(attachments=[dict(text=text, color=color, mrkdwn_in=["text"])])
+        payload = dict(attachments=[dict(text=html_escape(text), color=color, mrkdwn_in=["text"])])
     if channel is not None:
         if (channel[0] == '#') or (channel[0] == '@'):
             payload['channel'] = channel
@@ -194,12 +209,24 @@ def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoj
             payload['attachments'] = []
 
     if attachments is not None:
+        keys_to_escape = [
+            'title',
+            'text',
+            'author_name',
+            'pretext',
+            'fallback',
+        ]
         for attachment in attachments:
+            for key in keys_to_escape:
+                if key in attachment:
+                    attachment[key] = html_escape(attachment[key])
+
             if 'fallback' not in attachment:
                 attachment['fallback'] = attachment['text']
+
             payload['attachments'].append(attachment)
 
-    payload="payload=" + module.jsonify(payload)
+    payload=module.jsonify(payload)
     return payload
 
 def do_notify_slack(module, domain, token, payload):
@@ -211,7 +238,12 @@ def do_notify_slack(module, domain, token, payload):
             module.fail_json(msg="Slack has updated its webhook API.  You need to specify a token of the form XXXX/YYYY/ZZZZ in your playbook")
         slack_incoming_webhook = OLD_SLACK_INCOMING_WEBHOOK % (domain, token)
 
-    response, info = fetch_url(module, slack_incoming_webhook, data=payload)
+    headers = {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+    }
+    response, info = fetch_url(module=module, url=slack_incoming_webhook, headers=headers, method='POST', data=payload)
+
     if info['status'] != 200:
         obscured_incoming_webhook = SLACK_INCOMING_WEBHOOK % ('[obscured]')
         module.fail_json(msg=" failed to send %s to %s: %s" % (payload, obscured_incoming_webhook, info['msg']))


Test patch: