Tags

, , ,

This is an article in a series of unknown length discussing a tool written in Lua to publish posts on a WordPress blog from a PC. The source code is available in a public fossil repository.

In this installment we will describe the wplist.lua utility that makes it easy to capture lists of posts, categories, tags, media items, and similar things made available from the REST API.

Why?

The WordPress REST API provides a number of lists of entities that are part of each blog. These include the obvious list of all posts, as well as lists of categories, tags, users, comments, and more. In each case, the API provides details about each item, as well as query parameters to effectively search for a specific item in the larger list.

Implementing support for these lists will be useful if only as a quick way to itemize what appears on the blog. Our implementation will provide a terse list by default, or the complete table of details if asked.

Implementation

The actual dirty work is found in cli/wp.lua in the function getList().

---
-- Retrieve a list from a WordPress blog found at args.baseurl,
-- passing the OAuth token.
function M.getList(list, noenvelope)
  local E = urlescape    -- local alias for easy typing
  local q = {            -- collect all query parameters in a list
    "context=edit",
    "http_envelope="..E(not noenvelope),
    "status="..E(args.status and  args.status or "any")
  }
  -- add additional query parameters based on command line arguments
  if args.before then q[#q+1] =  "before="..E(args.before) end
  if args.after then q[#q+1] =  "after="..E(args.after) end
  if args.order then q[#q+1] =  "order="..E(args.order) end
  if args.orderby then q[#q+1] =  "order_by="..E(args.orderby) end
  if args.page then q[#q+1] = "page="..E(args.page) end
  if args.number then q[#q+1] =  "number="..E(args.number) end
  if args.type then q[#q+1] =  "type="..E(args.type) end
  if args.tag then q[#q+1] =  "tag="..E(args.tag) end
  if args.category then q[#q+1] =  "category="..E(args.category) end

  return M.REST(args.baseurl..'/'..list..'?'..table.concat(q,"&"))
end

The command line arguments are passed in to the API endpoint (with suitable URL escaping just in case a category or tag name contains a space or other problematic character), using the table q to assemble them and table.concat to collect them all into a string separated by ampersand characters.

The new tool wplist.lua has the usual collection of modules included, and declares its usage and command line arguments with pl.lapp as usual.

local args = lapp [[
List information from a WordPress blog to a file or stdout. Part of the WP CLI Tools.
https://curiouser.cheshireeng.com/applications/wp-cli-tools/

These options are related to the config file, with the ones marked * actually
stored in the file. Either --blog or --site and --token must be available and 
consistent for posting to be allowed. 
  --authenticate              Do web-based authentication and write config
  --blog (default "")         *The blog at which to post.
  --token (default "")        *The OAuth token from the redirect URL.
  --expires (default "")      *The OAuth token expiration date.  
  --site (default "")         *The WP Site ID for the token's blog.  
  --tokenurl (default "")     The full URL containing the token
  --showconfig                Just display the config file
  --writeconfig               Write the config file with the options  

General options:  
  -v,--verbose                Be more chatty about the process
  --keepraw (default "")      Name a file to fill with raw logging  
  --debug                     Don't use this.

Options identifying what to list:
  --list (default "posts")    List: posts, categories, tags, media, users, 
                              comments, stats. Some need --container to be 
                              useful.

Options for the listing:  
  -n,--number (default 20)    Items per page
  -p,--page (default 1)       Page to retrieve
  --before (default "")       Posted before ISO8601 date
  --after (default "")        Posted after ISO8601 date
  --order (default "DESC")    Sort order: DESC or ASC
  --orderby (default "date")  Column to sort by: date, modified, comment_count, ID.
  --type (default "post")     Type: post, page, any.
  --tag (default "")          Tag name to list
  --category (default "")     Category name to list
  --status (default "")       Status to list
  --out (default stdout)      The file to write.
  --container                 Save the whole JSON container, not just a simple list.
]]

-- erase some optional fields from the args table completely
common.clearoptional(args, {'keepraw','before','after','order','orderby','type',
    'tag','category','status'} )

We make a number of the options that take strings completely optional by letting lapp see them has defaulting to zero-length strings, then removing them from the args table after the command line is parsed. This makes code like we wrote in wp.getList() safer, since it can just say if args.tag then ... end rather than also needing to qualify it with #args.tag > 0.

The --list option takes a keyword like posts, pages, or anything else that will fit into the API’s URL at that position in the path. This version does not restrict the option to just the documented API endpoints, but it is possible that it should.

Similarly, no attempt is made to validate the query parameter values. --after and --before should be ISO8601 date strings. --type comes from a controlled list of post types. And so forth.

After dealing with the stock options, config files, authentication setup, and so forth, the main body of the script is straightforward. We call the API, then dump a few fields from its response.

---
-- Actual main body of the script. 
--
local ok,res
local list = args.list
ok,res = wp.getList(args.list)
if not ok then
  lapp.error(res)
end
if args.debug then
  print(pretty.write(ok))
end
local body = ok.body or ok
if body.error then
  lapp.error(body.error ..  "n" .. body.message, true)
end

We try to print an error if the API call failed. There may be (read: are) cases where failure gets past this point. Understanding and resolving them is left as an exercise. The otherwise undocumented --debug option will help here by dumping the complete response packet if execution gets to that point.

if args.container then
  args.out:write(pretty.write(body))
  args.out:close()

If asked for the list’s container, we simply output it as is. Use --out to put it in a file if stdout isn’t your taste. Otherwise, we behave more like DIR or ls and try to list just enough info about each item to be useful and not over-fill a single line.

else
  if body.found then
    args.out:write("Found "..body.found.." items.n")
  end
  local i0 = (args.page-1)*args.number
  for idx,item in ipairs(body[list]) do
    args.out:write(tostring(idx + i0), ": ", 
      item.ID and ("(ID="..item.ID..") ") or "",
      item.id and ("(id="..item.id..") ") or "",
      (item.name or item.title or item.content or "?"):sub(1,50), 
      'n')
  end
  args.out:close()
end

One thing to note is that the list API endpoints generally take parameters for a count of items per page and a page number to output. Many of them include a field found that identifies how many total items match the list and query parameters. This output numbers the lines according to the page and item counts. Also note that most of the endpoints have silent limits on the maximum number of items that can be requested per call.

TODO: A future version could automate the capture of all items from a list by noticing that found is greater than the number of items actually received in the array, and making additional calls with args.page and args.number set accordingly.

What Next?

The next big step is to think about integrating a fossil repository full of posts written in Markdown with a blog, using the WP CLI tools as the communications channel. Doing this successfully will likely drive changes in the tools so that PC side scripting can avoid reposting document that have not been edited on the PC. Allowing for edits made in the WP web UI would be bonus.

Another big step to consider is some amount of handling for media other than the post itself. WP supports pictures, audio, and video. A WP post can also have a “featured image” associated. Being able to handle posts with media and featured images in a useful way could also have value.

Repository and Checkins

All of the code supporting this tool is in a public fossil repository. See the discussion at the tools page for how to get started using this repository. This post documents work that was checked in as [681cb168dd].

The Rest of the Series

  • Part 1 showed how to use XML-RPC and wp.newPost
  • Part 2 added file reading to make a minimal working utility.
  • Part 3 added amenities and made the utility useful.
  • We digressed to talk about OAuth and using it at WordPress.com.
  • Part 4 switched to cURL, REST, and OAuth, but can’t yet post.
  • Part 5 improved the token handling and user display and still can’t post.
  • Part 6 implemented posting and added the ability to set title, tags, and categories from command line options.
  • Part 7 added a new utility and created modules common to all our utilities.
  • We digressed to build a tiny embedded web server to make authentication easier.
  • Part 8 used the Spoon server to make authentication go smoothly.

(Written with StackEdit.)