Tags

, , , ,

In Part 1 we introduced XML-RPC and the wp.newPost method that allows a new post to be created in a WordPress blog. In Part 2 we fleshed out the script to the point where it could make a post at all.

In this part, we will add a simple INI file and improved handling of the command line options all based on capabilities provided in the Penlight module. The result is a complete script that remembers a single blog, username, and password in a configuration file stored under the user’s home directory, and can create new posts without further user interaction or requiring that batch files, cron jobs or scheduled tasks contain the user’s password directly.

Warning: The user’s password is stored in the clear in the .wppost\settings.ini file. Protect that file appropriately. On Windows, it will be in C:\Users\YourAccountName\.wppost\settings.ini, and assuming you haven’t messed with your Windows configuration too much it will have an ACL that restricts access to just your account, or any administrator. A future article will describe how to have access without storing a password anywhere at all.

Better…

Options

The Penlight module pl.lapp provides a slick command line options parser. It takes a description of the module formatted like help text, and teases out from it a description of what options are supported. For this script, the declaration looks like:

Require the module at the top:

local lapp = require "pl.lapp"

Call it to process the arg table created by lua.exe containing the original command line, to produce a new args table containing named fields for each possible command line argument:

local args = lapp [[
Post a file on a WordPres blog as a draft post.

  -u,--username  (default "")    The WP User
  -p,--password  (default "")    The WP Password
  --blog (default "")            The blog at which to post.
  -v,--verbose                   Be more chatty about the process  
  --showconfig                   Just display the config file
  --writeconfig                  Write the config file with the options  
  <filename> (default "")        The file to post.  
]]

The pl.lapp module uses a clever trick of Lua syntax to make its module table be callable as if it were a function. Using the Lua multi-line string literal syntax makes it easy to lay out the help text to contain the expected table of options and their default values.

The rest of the code will refer to names like args.filename instead of arg[1] as was done in the previous version. This is clearer, and it makes it easier to have options with default values that can be seen throughout the script.

The --showconfig and --writeconfig options are discussed in the next section.

Configuration

Two additional Penlight features are combined to easily name and use a configuration file. The pl.app module supplies the app.appfile() function which locates a folder named after this script and stored somewhere in the user’s home directory. The pl.config module supplies the config.readfile() function which parses a variety of simple configuration file formats into a simple Lua table. Combined, these give us all we need to remember and use a saved blog, username, and password credential.

We make sure we’ve required our modules at the top:

local app = require "pl.app"
local config = require "pl.config"

Our configuration file name is settings.ini in the folder located (and created if missing) by app.appfile():

local configfile = app.appfile "settings.ini"

We read the configuration file, and silently supply a table of default values if it was not found:

local conf = config.read(configfile) or {
    username = "",
    password = "",
    blog = "blog.example.com",
}

We then simply merge values from the configuration file into the args table, but only if a command line argument is not present:

-- Add missing configuration fields to args
for k,v in pairs(conf) do
    if (not args[k]) or (#args[k] < 1) then args[k] = v end
end

If the --writeconfig option was given, we overwrite any existing configuration with only those fields from the args table that we want preserved:

-- Possibly write back the config file
if args.writeconfig then
    local f = io.open(configfile, "w")
    f:write"# configuration written by the --writeconfig optionnn"
    for _,k in ipairs({"blog","username","password"}) do
        f:write(k,"=",args[k],"n")
    end
    f:close()
end

The last bit of cleanup is to exit the script if either --showconfig or --writeconfig were specified, and if not to verify that the user really did name a file to be uploaded:

-- exit before actually doing anything if requested.
if args.showconfig or args.writeconfig then os.exit(0) end

-- verify if we got this far that we really do have a file to process
if not (args.filename and #args.filename > 0) then 
    lapp.error("Must name a file to post.", true) 
end

Other changes

Aside from some cosmetic rearrangement and tweaking of comments, the only actual substantive changes from the previous version are to use the named fields of the args table in place of explicit positional parameters from the arg array.

What next?

The full list of interesting features from Part 2 still applies.

In addition, it turns out that WordPress does support OAuth2 and adapting the script into a proper OAuth client is certainly the right thing to do. The key advantage of OAuth is that it allows the script to only store a limited access token rather than the keys to the kingdom in the form of a complete username and password.

The Complete Code

Here is the complete script in a single block for easy copy and paste to a folder in your PATH, and named wppost.lua. Don’t forget to install Lua for Windows to get all the modules it needs but one, and to get the remaining module from Lua XML-RPC and copy that zip file’s src folder into a folder named xmlrpc in the same folder where you put wppost.lua.

--- 
-- Utility to post a file to WordPress as a Draft of a Post.
--
-- Written as sample code for the curioser.cheshireeng.com blog. See:
--   https://curiouser.cheshireeng.com/2014/08/28/trick-create-a-post-from-lua-part-1/
--   https://curiouser.cheshireeng.com/2014/09/02/trick-create-a-post-from-lua-part-2/

require "pl.app".require_here()  --package.path = [[.?init.lua;]]..package.path
local xmlrpc = require "xmlrpc"
xmlrpc.http = require "xmlrpc.http"
local utils = require "pl.utils"
local app = require "pl.app"
local lapp = require "pl.lapp"
local config = require "pl.config"

-- Put the pl.lapp based options handling near the top for easy visibility
local args = lapp [[
Post a file on a WordPres blog as a draft post.

  -u,--username  (default "")    The WP User
  -p,--password  (default "")    The WP Password
  --blog (default "")            The blog at which to post.
  -v,--verbose                   Be more chatty about the process  
  --showconfig                   Just display the config file
  --writeconfig                  Write the config file with the options  
  <filename> (default "")        The file to post.  
]]

-- Also read a config file stored in a "home directory" folder
local configfile = app.appfile"settings.ini"
if args.showconfig then
    print("config:", configfile)
    local s = utils.readfile(configfile)
    io.write(s and s:gsub("password=%S+","password=*****") or "--config file empty--n")
end
local conf = config.read(configfile) or {
    username = "",
    password = "",
    blog = "blog.example.com",
}

-- Add missing configuration fields to args
for k,v in pairs(conf) do
    if (not args[k]) or (#args[k] < 1) then args[k] = v end
end

-- Possibly write back the config file
if args.writeconfig then
    local f = io.open(configfile, "w")
    f:write"# configuration written by the --writeconfig optionnn"
    for _,k in ipairs({"blog","username","password"}) do
        f:write(k,"=",args[k],"n")
    end
    f:close()
end


local rpcurl = "https://"..args.blog.."/xmlrpc.php"
if args.verbose then
    print("file:",args.filename)
    print("user:", #args.username > 0 and args.username or "-none-")
    print("pass:", #args.password > 0 and "-present-" or "-none-")
    print("blog:",args.blog)
    print("url:",rpcurl)
end

-- exit before actually doing anything if requested.
if args.showconfig or args.writeconfig then os.exit(0) end

-- verify if we got this far that we really do have a file to process
if not (args.filename and #args.filename > 0) then 
    lapp.error("Must name a file to post.", true) 
end

local function htmlentities(s)
    local e = s:gsub("[%&%<%>]",{
          ["&"]="&",
          ["<"]="<",
          [">"]=">",
        })
    return e
end

---
-- Make a new post on a WordPress blog found at rpcurl, using credentials
-- from username and password, with the given title and body. The new post
-- is always a draft and will have no Tags and the default Category.
local function wp_newPost(title, body, rpcurl, username, password)
    title = htmlentities(title)
    local content = {
        post_type = 'post',   -- post, page, link...
        post_status = 'draft',    -- draft, publish, pending, future, private...
        --post_date = '2014-04-01 03:14:16'   -- post date, required if status is 'future'
        post_title = title, -- Title
        post_content = body,    -- full content, including <!--more--> if desired
    }

    local ok, res = xmlrpc.http.call(
        rpcurl,
        'wp.newPost', 0, username, password, content
    )
    return ok, res
end


---
-- Make a new post from the body of a file, using the filename as
-- the title. The file is posted as-is, with no substitutions or 
-- other changes. 
local function postFile(filename, rpcurl, username, password)
    local title = filename
    local content = utils.readfile(filename, false)
    if not content then return nil, "No such file" end
    local ok, res = wp_newPost(
        title,
        content,
        rpcurl,
        username,
        password)
    return ok, res
end

---
-- Actual main body of the script. Read the command line for file name, user, and password.
-- Make the post as requested and show the result.

local ok, res = postFile(args.filename, rpcurl, args.username, args.password)
if ok then
    print("wppost success: ".. tostring(res))
else
    print("wppost failed:", res)
    os.exit(1)
end

(Written with StackEdit.)