Publish to DokuWiki programmatically without any API
Tags: javascript, dokuwiki, tutorial, node
Published: 2022-09-20 17:16:57

In a project I am working on we use [DokuWiki](https://www.dokuwiki.org/dokuwiki) for our documentation. In addition to the manually written text, I wanted to integrate some automatically generated content that is updated every day.

There is a [Plugin that provides a REST API](https://www.dokuwiki.org/plugin:api) but natively, there is only [XML-RPC](https://www.dokuwiki.org/devel:xmlrpc). Not that fancy, but for simply pushing simple page content, this would be sufficient. So I tried a `fetch` version of the [sample jQuery client](https://www.dokuwiki.org/devel:xmlrpc:clients#sample_jquery_client).

> ℹ Note that the following examples use the [native `fetch` API](https://dev.to/andrewbaisden/the-nodejs-18-fetch-api-72m) that required Node 18+.

```js
const WIKI_URL = 'https://example.com/dokuwiki'

const xml=`
  <?xml version="1.0"?>
  <methodCall>
    <methodName>wiki.getRPCVersionSupported</methodName>
    <params></params>
  </methodCall>`

fetch(WIKI_URL + '/lib/exe/xmlrpc.php', {
  method: 'POST',
  body: xml,
  headers: { 'Content-Type': 'text/xml' }
})
  .then(response => response.text())
  .then(text => console.log(text))
```
It printed the follwing response.
```xml
<methodResponse>
  <fault>
    <value>
      <struct>
        <member>
          <name>faultCode</name>
          <value><int>-32605</int></value>
        </member>
        <member>
          <name>faultString</name>
          <value><string>XML-RPC server not enabled.</string></value>
        </member>
      </struct>
    </value>
  </fault>
</methodResponse>
```
Uh, bummer 😐
Now, the DokuWiki instance is shared among multiple teams. Installing a plugin or enabling the XML-RPC API would involve at least some bureaucracy.

Because of that, I tried to come up with a solution **without any API**. I mean, if I can do it in my browser, there must be a way for a script to do it, right?

So the first thing I did was looking at what DokuWiki does internally when I edit a page in the browser. In principle, it is a simple HTML form.


![DokuWiki page edit form data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rua1ywq3bi4g7jmsways.png)

The form data shows that crafting a corresponding HTTP request is not that simple. We probably need to read some values from the form or reverse-engineer how to produce them. Especialy `sectok` and `changecheck` look suspicious.

To save time on that, my first impulse was to use a UI automation tool like [Selenium](https://www.selenium.dev/) but luckily, I came up with a way simpler solution.

It is based on [cheerio](https://cheerio.js.org/), an "implementation of core jQuery designed specifically for the server". With this, crafting the request becomes as easy as submitting a form with jQuery (almost).

But first, we need to sign in. For this, we have to make `fetch` support cookies (side note, even with XML-RPC, [this would have been the case](https://www.dokuwiki.org/devel:xmlrpc#dokuwikilogin) 🙄). I did it by utilizing the package [fetch-cookie](https://www.npmjs.com/package/fetch-cookie). Then, I just looked how the login form of DokuWiki works and came up with the following request.

```js
const makeFetchCookie = require('fetch-cookie')
const fetchCookie = makeFetchCookie(fetch)

const WIKI_URL = 'https://example.com/dokuwiki'
const USERNAME = 'example'
const PASSWORD = 'example'

await fetchCookie(WIKI_URL + '/doku.php?do=login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({ u: USERNAME, p: PASSWORD })
})
```

After this, we can request the editing form for the page we want to update.

```js
const PAGE_PATH = 'path/to/page'

const html = await fetchCookie(`${WIKI_URL}/doku.php/${PAGE_PATH}?do=edit`)
      .then(response => response.text())
```

We load the HTML into cheerio and look for the text area with the ID `wiki__text`. Then we can update the content of the text area.

```js
const cheerio = require('cheerio')

const $ = cheerio.load(html)

const textarea =  $('textarea#wiki__text')

textarea.text('This is the new, //generated//, page content.')
```
Note that if you want to replace parts of the exsiting text, you can get the current content by calling `textarea.text()` without the argument.

We can now get the content body for our POST request in the following way.

```js
const formData = $('form#dw__editform').serialize() + '&do[save]='
```

The addition of `do[save]=` is needed because that would be added when clicking the submit button. Without this, no data will actually be saved.

Finally, we can craft and send the request.

```js
await fetchCookie(`${WIKI_URL}/doku.php/${PAGE_PATH}?do=edit`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: formData
})
```
We successfully published to DokuWiki programmatically without any API 🎉

Full example code:
```js
const cheerio = require('cheerio')
const makeFetchCookie = require('fetch-cookie')
const fetchCookie = makeFetchCookie(fetch)

const WIKI_URL = 'https://example.com/dokuwiki'
const PAGE_PATH = 'path/to/page'
const USERNAME = 'example'
const PASSWORD = 'example'

async function main() {

  await fetchCookie(WIKI_URL + '/doku.php?do=login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({ u: USERNAME, p: PASSWORD })
  })

  const html = await fetchCookie(`${WIKI_URL}/doku.php/${PAGE_PATH}?do=edit`)
      .then(response => response.text())

  const $ = cheerio.load(html)

  const textarea =  $('textarea#wiki__text')

  textarea.text('This is the new, //generated//, page content.')

  const formData = $('form#dw__editform').serialize() + '&do[save]='

  await fetchCookie(`${WIKI_URL}/doku.php/${PAGE_PATH}?do=edit`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: formData
  })
}

main()
```