diff --git a/action_plugins/moinmoin_page.py b/action_plugins/moinmoin_page.py
new file mode 100755
index 0000000000000000000000000000000000000000..dff9b56c65029ea4f2107f886910da1c3733d221
--- /dev/null
+++ b/action_plugins/moinmoin_page.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python3
+
+# Copyright: (c) 2019, Alexandre Iooss <erdnaxe@crans.org>
+#
+# GNU General Public License v3.0+
+
+import re
+import urllib.error
+import urllib.parse
+import urllib.request
+import difflib
+
+
+from ansible.errors import AnsibleError
+from ansible.plugins.action import ActionBase
+from ansible.utils.display import Display
+from ansible.module_utils._text import to_native
+
+display = Display()
+
+
+class ActionModule(ActionBase):
+
+    TRANSFERS_FILES = False
+    _VALID_ARGS = frozenset(('url', 'user', 'password', 'content', 'revision_comment'))
+
+    def login(self, url, user, password):
+        """
+        Log in and return session cookie or None if failed
+
+        :param url: random wiki url (not root page)
+        :param user: wiki user
+        :param password: user's password
+        :return: session cookie
+        """
+        # Send a HTTP POST request
+        data = urllib.parse.urlencode({
+            'action': 'login',
+            'login': 'Connexion',
+            'name': user,
+            'password': password
+        }).encode()
+        req = urllib.request.Request(url, data)
+        try:
+            response = urllib.request.urlopen(req)
+            cookie = response.getheader('set-cookie')
+        except urllib.error.HTTPError as e:
+            # If 404, then also return header
+            cookie = e.getheader('set-cookie')
+
+        # Check that authentication worked
+        if not cookie:
+            raise AnsibleError(to_native('server did not return a session cookie'))
+        return cookie
+
+    def craft_request(self, suffix):
+        """
+        Crafts a function that takes an url and a cookie,
+        and returns the content of the requested page with given action suffix.
+        """
+        def f(url, cookie):
+            req = urllib.request.Request(url + suffix)
+            req.add_header("Cookie", cookie)
+            content = urllib.request.urlopen(req).read().decode('utf-8')
+            return content
+        return f
+
+
+    def edit_ticket(self, url, cookie):
+        """
+        Return edition ticket of url
+
+        :param url: page to edit
+        :param cookie: session cookie
+        :return: edit ticket
+        """
+        # Send request with session cookie
+        content = self.craft_request("?action=edit&editor=text")(url, cookie)
+
+        # Search for ticket
+        search = re.search('name=\"ticket\" value=\"([^\"]*)\"', content)
+        if not search:
+            raise AnsibleError(to_native('no edit ticket was found'))
+        
+        return search.group(1)
+
+
+    def edit(self, url, user, password, content, revision_comment, cookie):
+        """
+        Edit a MoinMoin wiki page
+
+        :param url: page to edit
+        :param user: wiki user
+        :param password: user's password
+        :param content: content to place on this page
+        :param revision_comment: revision comment
+        """
+        # Connect and get edit ticket
+        ticket = self.edit_ticket(url, cookie)
+
+        # Create request and send
+        data = {
+            'button_save': 'Enregistrer les modifications',
+            'category': '',
+            'comment': revision_comment.encode("utf-8"),
+            'savetext': content.encode("utf-8"),
+            'action': 'edit',
+            'ticket': ticket
+        }
+        req = urllib.request.Request(url, urllib.parse.urlencode(data).encode())
+        req.add_header("Cookie", cookie)
+        urllib.request.urlopen(req)
+
+
+    def run(self, tmp=None, task_vars=None):
+        """
+        The run method is the main Action Plugin driver. All work is done from within this 	method.
+
+        tmp: Temporary directory. Sometimes an action plugin sets up
+             a temporary directory and then calls another module. This parameter
+             allows us to reuse the same directory for both.
+
+        task_vars: The variables (host vars, group vars, config vars, etc) associated with this task.
+                   Note that while this will contain Ansible facts from the host, they should be used
+                   with caution as a user running Ansible can disable their collection. If you want
+                   make sure that your Action Plugin always has access to the ones it needs, you may
+                   want to consider running the setup module directly in the run the method and getting
+                   the Ansible facts that way.
+                   The strategy plugin which manages running tasks on instances uses an ansible.vars.manager
+                   VariableManager instance to retrieve this context specific dict of variables.
+        """
+        if task_vars is None:
+            task_vars = dict()
+
+        
+        result = super(ActionModule, self).run(tmp, task_vars)
+        del tmp
+
+        result['changed'] = False
+
+
+        url = self._task.args.get("url")
+        user = self._task.args.get("user")
+        password = self._task.args.get("password")
+        content = self._task.args.get("content")
+        revision_comment = self._task.args.get("revision_comment")
+
+        cookie = self.login(url, user, password)
+
+        changed = False
+
+        try:
+            raw = self.craft_request("?action=raw")(url, cookie)
+            if raw != content:
+                changed = True
+        except urllib.error.HTTPError:  # We will create the page.
+            changed = True
+            raw = ""
+
+        # Display any change
+        if changed:
+            diff = difflib.unified_diff(raw.splitlines(), content.splitlines(), fromfile="old", tofile="new", lineterm="")
+            for line in diff:
+                if line.startswith("-"):
+                    display.display(line, "red")
+                elif line.startswith("+"):
+                    display.display(line, "green")
+                elif line.startswith("@"):
+                    display.display(line, "yellow")
+                else:
+                    display.display(line)
+
+            # Do apply the change if not in check mode
+            if not self._play_context.check_mode:
+                self.edit(url, user, password, content, revision_comment, cookie)
+
+        
+        self._supports_check_mode = True
+        self._supports_async = False
+
+        return result