DEV Community

Erik Hofer
Erik Hofer

Posted on

Publish to DokuWiki programmatically without any API

In a project I am working on we use 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 but natively, there is only XML-RPC. 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.

โ„น Note that the following examples use the native fetch API that required Node 18+.

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))
Enter fullscreen mode Exit fullscreen mode

It printed the follwing response.

<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>
Enter fullscreen mode Exit fullscreen mode

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

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 but luckily, I came up with a way simpler solution.

It is based on cheerio, 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 ๐Ÿ™„). I did it by utilizing the package fetch-cookie. Then, I just looked how the login form of DokuWiki works and came up with the following request.

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 })
})
Enter fullscreen mode Exit fullscreen mode

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

const PAGE_PATH = 'path/to/page'

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

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.

const cheerio = require('cheerio')

const $ = cheerio.load(html)

const textarea =  $('textarea#wiki__text')

textarea.text('This is the new, //generated//, page content.')
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

We successfully published to DokuWiki programmatically without any API ๐ŸŽ‰

Full example code:

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()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)