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.)
Pingback: Digression: Using OAuth 2.0 at WordPress.com | Words from Cheshire Engineering Corp.
Pingback: Trick: Create a post from Lua, Part 4 | Words from Cheshire Engineering Corp.