1

I have a page that I need to periodically update the file location for a link to point at a new file. Trying to do this via a Python script using requests.

I've got authentication set up via 'Application Passwords' plugin, and I'm able to download a json from the api for the page in question. I have a pre-built string that contains the new file location, ready to go. I extract the page content, specifically the 'rendered' version, as a string. I do a quick replace() on that string between two known tags (<h4>) and then reload the modified 'rendered' page content back into the json, and upload it back to the endpoint, again using requests().

When it's all said and done, I'm getting no errors from the script, a status code of 200... but nothing changes on the page.

Furthermore, I'm finding the following in the error log:

[21-Aug-2019 00:12:51 UTC] PHP Warning: Invalid argument supplied for foreach() in /home1/example/public_html/wp-includes/rest-api/class-wp-rest-request.php on line 778

Not exactly sure what that means, but I'm assuming it's not good.

import base64, json, requests

user = 'admin'
pythonapp = 'QUDM PYFn xfyK 9Hhe Ayf2 hk6k'
url = 'https://example.com/wp-json/wp/v2'

data_string = user + ':' + pythonapp

token = base64.b64encode(data_string.encode())

headers = {'Authorization':'Basic ' + token.decode('utf8')}

newsletterURL = 'http://www.example.com/wp-content/uploads/2019/08/lorem-ipsum.pdf'
oldH4 = '<h4><a href="http://www.example.com/wp-content/uploads/2019/08/NCWGC-2019-8.pdf" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">Click here to download the newsletter!!!</a></h4>'
newH4 = '<h4><a href="' + newsletterURL + '" target="_blank" rel="noreferrer noopener" aria-label=" (opens in a new tab)">Click here to download the newsletter!!!</a></h4>'

#  Download the current Newsletter page
r = requests.get(url + '/pages/6', headers=headers)

tmp = r.json()

Pretty sure the code up to this point works as intended/expected; obviously it's been somewhat modified to be a stand-alone example vs. part of the larger script.

oldPageContent = tmp['content']['rendered']
# print(oldPageContent.partition('</h4>')[0])

newPageContent = oldPageContent.replace(oldH4, newH4)
# print(newPageContent.partition('</h4>')[0])

tmp['content']['rendered'] = newPageContent
# print(tmp['content']['rendered'].partition('</h4>')[0])

r = requests.post(url + '/pages/6', headers=headers, json=tmp)

# print(json.loads(r.content.decode('utf8'))['content']['rendered'].partition('</h4>')

Here I'm attempting to replace the specific URL inside the body content, reassemble things, and upload the whole thing back to the

"content": {
    "rendered": "\n<h4><a href=\"http://www.example.com/wp-content/uploads/2019/08/NCWGC-2019-8.pdf\" target=\"_blank\" rel=\"noreferrer noopener\" aria-label=\" (opens in a new tab)\">Click here to download the newsletter!!!</a></h4>\n\n\n\n<p>Submissions for the newsletter are always welcome. Space is somewhat limited so keep \u2019em short and sweet!",
    "protected": false
}

This is an example of the specific field that I'm trying to extract from the content of the page, update, and upload.

Kinda thinking maybe I should be using json.dumps/loads to get at ['content']['rendered'] rather than just r.json()? Though when I print out the resulting strings, they sure look like right...

Any ideas/suggestions for updating a page via the REST api? Or am I going about this completely wrong?

Update:

After the excellent help from @SallyCJ, this is the final form of what I ended up with, in case it helps anyone else down the road:


import base64, json, re, requests

user = 'admin'
pythonapp = 'QUDM PYFn xfyK 9Hhe Ayf2 hk6k'
url = 'https://example.com/wp-json/wp/v2'

data_string = user + ':' + pythonapp

token = base64.b64encode(data_string.encode())

headers = {'Authorization':'Basic ' + token.decode('utf8')}

#  Temporary URL for the sake of testing
newsletterURL = 'http://www.example.com/wp-content/uploads/2019/08/lorem-ipsum.pdf'

#  Download the current Newsletter page
r = requests.get(url + '/pages/6', headers=headers)

#  Extract the actual page content from all the rest of the API data
data = r.json()
content = data['content']['rendered']

#  Find the URL for the download in 'content', and replace it with the new URL
regex = r"(http(s)?)://(www.)?(example.com\/wp-content/uploads\/).+(pdf)"
newContent = re.sub(regex, newsletterURL,content)

#  Create a new data payload using the updated 'content'
data = {"content": newContent}

#  Upload it back to the site
r = requests.post(url + '/pages/6', headers=headers, json=data)
4
  • 1
    Perhaps double-checking the relevant endpoint/route reference would help. But I'm guessing you're not sending the proper parameters when updating the page..
    – Sally CJ
    Commented Aug 22, 2019 at 12:39
  • Unfortunately the examples in that reference are a little... sparse... on details, at least for updating the page. That said, I think you're probably correct; I did some more poking around, and I can do other operations via the api without any difficulty, just not updating that page. Not sure what I'm not doing right, though...
    – memilanuk
    Commented Aug 23, 2019 at 19:14
  • 1
    Well, if you can show your code.. or the request parameters, then maybe I can help?
    – Sally CJ
    Commented Aug 24, 2019 at 14:25
  • @SallyCJ put the code and commentary in the original post. Thanks for the help!
    – memilanuk
    Commented Aug 25, 2019 at 21:53

1 Answer 1

1

The problem is with your POST request, where the content property should be a plain string and not an object. You should also send just the data that you want to change.

When updating a Page (i.e. a post of the type page), you should provide a JSON-encoded or URL-encoded string containing one or more of the properties/parameters as listed here.

Examples using cURL from the command line prompt:

  • URL-encoded (Content-Type header is application/x-www-form-urlencoded):

    curl -d "title=New%20Title!&content=New%20Content%3F" -X POST https://example.com/wp-json/wp/v2/pages/9 --user "admin:QUDM PYFn xfyK 9Hhe Ayf2 hk6k"
    
  • JSON-encoded

    curl -H "Content-Type: application/json" -d "{\"title\":\"New Title!\",\"content\":\"New Content?\"}" -X POST https://example.com/wp-json/wp/v2/pages/9 --user "admin:QUDM PYFn xfyK 9Hhe Ayf2 hk6k"
    

So in your code:

  • This line:

    tmp['content']['rendered'] = newPageContent
    
  • should be:

    tmp = { 'content': newPageContent }
    

    but you can of course add other properties that you want to change; e.g. title and status.

And (despite that I'm not a Python expert), you can use the example below as a reference when creating/retrieving/updating/deleting a Page:

import base64, json, requests

user = 'admin'
pythonapp = 'QUDM PYFn xfyK 9Hhe Ayf2 hk6k'
url = 'https://example.com/wp-json/wp/v2'

data_string = user + ':' + pythonapp
token = base64.b64encode( data_string.encode() )
headers = { 'Authorization': 'Basic ' + token.decode( 'utf8' ) }

# Create a Page.
data = { 'title': 'Testing from Python' }
r = requests.post( url + '/pages/', headers=headers, json=data )
data = r.json()
page_id = str( data['id'] )
print 'Page created. ID: ' + page_id

# Retrieve the Page.
r = requests.get( url + '/pages/' + page_id, headers=headers )
data = r.json()
title = data['title']['rendered']
print 'Page #' + page_id + ' retrieved. The title is: ' + title

# Update the Page.
data = { 'content': 'New content here' }
r = requests.post( url + '/pages/' + page_id, headers=headers, json=data )
data = r.json()
content = data['content']['rendered']
print 'Page #' + page_id + ' updated. Content set to: ' + content

# Delete the Page.
r = requests.delete( url + '/pages/' + page_id, headers=headers )
data = r.json()
print 'Page #' + page_id + ' moved to the Trash. (Well, it should be...)'
print 'The Page "status" is ' + data['status']
3
  • I had wondered about sending the whole payload back vs. just the specific field I'm concerned with. Good to know! One other question on sending the content back... when I retrieve it, it is in a structured/nested format, with "content" returning sub-levels of "rendered" and "protected". Do I need to just update to "content", or to "content": "rendered": "<h4>Foo</h4>"?
    – memilanuk
    Commented Aug 26, 2019 at 14:11
  • 1
    As I said, you just need to update to "content" :) - that structured thing indicates an object (e.g. "content":{"rendered":"Foo bar"}) which WordPress returns in the response when retrieving a Page/Post/etc. But when updating the resource, you'd use just "content":"New content here". I.e. Send "content" as a string.
    – Sally CJ
    Commented Aug 27, 2019 at 2:49
  • 1
    Finally got time to play with this tonight. Being able to extract the "content" as a string (rather than trying to manipulate nested dictionaries), update the specific URL using re.sub and then just upload that particular piece (rather than the whole payload) turns out to be way simpler than what I was trying to do before! Thanks!!!
    – memilanuk
    Commented Aug 29, 2019 at 9:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.