Using the restrict_manage_users Action Hook in WordPress 4.4

I recently participated in an interesting discussion on Stack Exchange about how to add a filter button just above the Users list table on the Users page in WordPress Admin. To sum up the whole issue, if you used the restrict_manage_users hook to do this in an earlier version of WordPress, your code for filtering the users list will probably not work correctly in WordPress 4.4 without making a small change.

Examining The Issue

In WordPress 4.4, the restrict_manage_users hook is called twice when loading the Users page.   (It is only called once in WordPress 4.3.1)  This means that if you use that hook to add a drop down select and filter button (or any other input field or html code), your additions will actually show up on the Users page twice, both above the table and below it.

The restrict_manage_users action hook is called twice in WordPress 4.4
The restrict_manage_users action hook is called twice in WordPress 4.4

Now, normally you would think that this addition is a great convenience. However, unless you change your code, when the user makes a selection in one of the dropdowns, the other dropdown remains the same. Then when the Filter button is clicked, the form is submitted with two values for that input. One value will contain the selection and the other will be blank. If you use Chrome Developer tools, you can inspect the query vars sent with the get request and you will see what I mean:

Analyzing the Query Vars sent with the GET request
Analyzing the Query Vars sent with the GET request

 

Tip:  Follow these instructions to inspect the query string parameters:

  • Open the page in Google Chrome Web Browser and press CTRL+SHIFT+I to open Developer Tools
  • At the top of the tools, select the Network tab.
  • With the developer tools still open, click on the filter button or submit button or whatever other button that you want to examine the request for.  You should see a whole bunch of requests appear.
  • Click on the request you are interested in.  In this case, it’s going to start with users.php.
  • Click on the Headers tab and scroll down to “Query String Parameters.”

How this Effects Your Code

If you wrote a WordPress plugin that uses both the restrict_manage_users hook and the pre_get_users filter, you likely looked at the $_GET global variable to alter the WP_User_Query.  Because of this change, the $_GET variable will be different, and your code might only work if the user uses the bottom dropdown menu and not the top one.  This is a very big bug, but fortunately there are several different ways to fix this issue.

The Solution

There are at least 3 different solutions to handling this problem.  Visit this WordPress Stack Exchange discussion for example code on how to fix this using PHP.

Here I’m going to present a solution that uses Javascript.  Simply add this to your theme’s functions.php file and change the NAME_OF_YOUR_INPUT_FIELD to the name of your input field!  Since WordPress automatically loads jQuery on the admin side, you do not have to enqueue any scripts.  This snippet of code simply adds a change listener to the dropdown inputs and then automatically updates the other dropdown to match the same value.


add_action( 'in_admin_footer', function() {
	?>
	<script type="text/javascript">
		var el = jQuery("[name='NAME_OF_YOUR_INPUT_FIELD']");
		el.change(function() {
			el.val(jQuery(this).val());
		});
	</script>
	<?php
} );

Questions?

If you tried this solution and are still having an issue, please comment below.  There may be use cases where this doesn’t work but I can’t think of any right now.  I’m interested in learning and your feedback is important.

Even if you use my javascript solution, I suggest that you read over the other solutions on Stack Exchange because they provide a lot of insight into how to use a static variable and/or your own custom action hook to deal with the issue of WordPress core calling a hook multiple times.   Using a static variable is a great way to manage how often your plugin performs certain actions, and doing so could help you future-proof your code.

 

WordPress Plugin or Theme Development with Ajax and jQuery

A little knowledge of Javascript, or the popular Javascript library jQuery, can go a long way in making your plugin or theme “do” things.  As you may know, WordPress is written in PHP.  However, for client-side code, Javascript is the typical solution.  How much you do in Javascript and how much you do in PHP depends on your particular task, but at the very least you will likely use Javascript to send POST requests and parse the data received from the XMLHttpRequest object.

WordPress has an action hook called wp_ajax that allows you to access the WordPress core functions when handling HTTP post requests.

The jQuery functions jQuery.post and jQuery.ajax can be used to send the requests. The requests need to be sent to the ajaxurl, which is a javascript variable already defined on the admin side. On the front end, you need to define this variable yourself to point to the location of admin-ajax.php.

For a complete tutorial on how to use jQuery & Ajax with WordPress, see my article: How to use Ajax with your WordPress Plugin or Theme?

Another option for using Javascript in your theme or plugin is to use WordPress’s Customizer API, which is something I plan to address in a future tutorial.

How to End an Ebay Listing Automatically with a PHP Script

One programming project I completed included a custom backend order management system.  The client had a website where she sold items that were also listed on Ebay.   She had been manually removing the items when they sold by logging into her Ebay account and going to the Ebay End Your Listing Early Page (this can be found on the Ebay Site Map).  Of course, doing this manually has a few issues.  Not only is it time consuming, but it also runs the risk of an out-of-stock item selling.  For best ecommerce practices, store inventory should be updated immediately.  So automating this task can really save a lot of headaches!

This backend sales automation task is fairly simple.  It involves sending an XML document to Ebay and then parsing the XML response that they return.  It transfers the data to and from Ebay’s server using the php cURL library, which is very commonplace.  You can check to see if your server has cURL enabled by using

function_exists('curl_version')

If you’re working with a WordPress plugin, I suggest that you use the WordPress HTTP API instead, as it has alternative ways of completing the HTTP request if cURL doesn’t exist.

If you’re writing a program to simply handle one Ebay seller’s inventory, you can get the necessary authentication token by signing up for the Ebay Developer Program.  The only other piece of data you need is the Ebay Id, which in my case was stored in a custom MySQL Database table.

This is the PHP script that will remove the Ebay listing:



/**
 * This function ends an Ebay listing.  Returns true if successful.
 *
 * @param $ebay_id String Ebay Item Id
 * @param $auth_token String EbayAuthToken
 */
function remove_item_from_ebay( $ebay_id, $auth_token ) {
	$endpoint = "https://api.ebay.com/ws/api.dll";

	$xmlbody = '
<?xml version="1.0" encoding="utf-8"?>
<EndItemsRequest xmlns="urn:ebay:apis:eBLBaseComponents">
	<RequesterCredentials>
		<eBayAuthToken>' . $auth_token . '</eBayAuthToken>
	</RequesterCredentials>

	<!-- Call-specific Input Fields -->
	<EndItemRequestContainer>
		<EndingReason>NotAvailable</EndingReason>
		<ItemID>' . $ebay_id . '</ItemID>
		<MessageID>' . $ebay_id . '</MessageID>
	</EndItemRequestContainer>
	<!-- ... more EndItemRequestContainer nodes allowed here ... -->
	<!-- Standard Input Fields -->
	<ErrorLanguage>en_US</ErrorLanguage>
	<WarningLevel>High</WarningLevel>
</EndItemsRequest>
';
	
	$headers = array(
//Regulates versioning of the XML interface for the API
		'X-EBAY-API-COMPATIBILITY-LEVEL: 861',
//the name of the call we are requesting
		'X-EBAY-API-CALL-NAME: EndItems',
//SiteID must also be set in the Request's XML
//SiteID = 0  (US) - UK = 3, Canada = 2, Australia = 15, ....
//SiteID Indicates the eBay site to associate the call with
		'X-EBAY-API-SITEID: 0 ',
	);


//initialise a CURL session
	$connection = curl_init();
//set the server we are using (could be Sandbox or Production server)
	curl_setopt( $connection, CURLOPT_URL, $endpoint );

//stop CURL from verifying the peer's certificate
	curl_setopt( $connection, CURLOPT_SSL_VERIFYPEER, 0 );
	curl_setopt( $connection, CURLOPT_SSL_VERIFYHOST, 0 );

//set the headers using the array of headers
	curl_setopt( $connection, CURLOPT_HTTPHEADER, $headers );

//set method as POST
	curl_setopt( $connection, CURLOPT_POST, 1 );

//set the XML body of the request
	curl_setopt( $connection, CURLOPT_POSTFIELDS, $xmlbody );

//set it to return the transfer as a string from curl_exec
	curl_setopt( $connection, CURLOPT_RETURNTRANSFER, 1 );

	curl_setopt( $connection, CURLOPT_TIMEOUT, 5 );
//Send the Request
	$response = curl_exec( $connection );

//close the connection
	curl_close( $connection );

	$r = simplexml_load_string( $response );

	return $r->Ack == 'Success';
}

You can also grab this code from my Github account.

I may improve this code in the future so that it’s more informative.  I could have it throw an exception on failure, or I might create a class that allows the error to be retrieved.  The response from Ebay also contains the ebay ID (in CorrelationID XML Tag), so for further validation you could verify that the ID is what you expected.

When parsing this response, remember that the root element “becomes” the SimpleXML Object.  In this case, the root element is EndItemsRequest.

A common error is to try to refer to this root element:

$r->EndItemsRequest->Ack   // WRONG
$r->Ack // CORRECT

For quick reference, here’s what the response XML looks like:

    <?xml version="1.0" encoding="UTF-8"?>
<EndItemsResponse xmlns="urn:ebay:apis:eBLBaseComponents">
    <Timestamp>2015-12-16T15:36:29.791Z</Timestamp>
    <Ack>Success</Ack>
    <Version>949</Version>
    <Build>E949_UNI_API5_17774433_R1</Build>
    <EndItemResponseContainer>
        <EndTime>2015-12-16T15:36:29.000Z</EndTime>
        <CorrelationID>222253987187</CorrelationID>
    </EndItemResponseContainer>
</EndItemsResponse>

 

And, also for easy reference, here is an example of what an Ebay failure response looks like:

<EndItemsResponse xmlns="urn:ebay:apis:eBLBaseComponents">
    <Timestamp>2014-09-25T18:52:41.125Z</Timestamp>
    <Ack>Failure</Ack>
    <Errors>
        <ShortMessage>Errors in Input Data.</ShortMessage>
        <LongMessage>Errors in Input Data.Please try again.</LongMessage>
        <ErrorCode>400</ErrorCode>
        <SeverityCode>Error</SeverityCode>
        <ErrorClassification>RequestError</ErrorClassification>
    </Errors>
    <Version>891</Version>
    <Build>E891_UNI_API5_17049963_R1</Build>
    <EndItemResponseContainer>
        <CorrelationID>290345600169</CorrelationID>
        <Errors>
            <ShortMessage>The auction has been closed.</ShortMessage>
            <LongMessage>The auction has already been closed.</LongMessage>
            <ErrorCode>1047</ErrorCode>
            <SeverityCode>Error</SeverityCode>
            <ErrorClassification>RequestError</ErrorClassification>
        </Errors>
    </EndItemResponseContainer>
</EndItemsResponse>

 

Hopefully, this little bit of code will help someone else struggling with how to implement this task.   Since my client was getting payment by Paypal, I used it in a IPN script (Instant Payment Notification).  I also had my IPN script send email notifications with the order information.  Paypal only automatically sends payment notifications to one email address, so if you want to have email notifications sent to multiple address, implementing Paypal’s IPN is one solution.

Improving on This: From PHP Script to PHP Class

Although procedural-style code is common for the PHP programming language, object-oriented programming for the web with PHP is becoming more popular.  Regardless of whether you write OOP style code, creating classes is a good idea for organization.  You can put similar functions together.  Also, using classes really decreases the chance that you’ll run into a naming conflict.  You only have to make sure your function name is unique to the class and not to the whole codebase.

From an object oriented perspective, you want to name your classes after nouns.  So I named my class EbayConnection, although EbayConnector would also work, but naming your class GetEbayConnection would be incorrect.  getEbayConnection (with a lowercase g, by standard conventions) would be a function name, or, when in a class, a method name.   Methods and functions are pretty much the same thing.  It’s just that we only call them methods when they are in a class.  And Methods should usually contain verbs.  At least that’s that I learned — it makes reading code easier and it helps in understanding how the program works!

So, I wrote the class EbayConnection (Grab it on Github).  I expanded on the above function not only by refactoring it a bit, but I also added some error reporting so that the calling program can now get the last error message and last error code.

class EbayConnection {
	private $auth = null;
	private $error_msg = null;
	private $error_code = null;
	function __construct($auth) {
		$this->auth = $auth;
	}
	/**
	 * This function ends an Ebay listing.  Returns true if successful.
	 *
	 * @param $ebay_id String Ebay Item Id
	 */
	public function endItem($ebay_id) {
		$endpoint = "https://api.ebay.com/ws/api.dll";
		$xmlbody = '
		<?xml version="1.0" encoding="utf-8"?>
		<EndItemsRequest xmlns="urn:ebay:apis:eBLBaseComponents">
		<RequesterCredentials>
		<eBayAuthToken>' . $this->auth . '</eBayAuthToken>
		</RequesterCredentials>
		<!-- Call-specific Input Fields -->
		<EndItemRequestContainer>
		<EndingReason>NotAvailable</EndingReason>
		<ItemID>' . $ebay_id . '</ItemID>
		<MessageID>' . $ebay_id . '</MessageID>
		</EndItemRequestContainer>
		<!-- ... more EndItemRequestContainer nodes allowed here ... -->
		<!-- Standard Input Fields -->
		<ErrorLanguage>en_US</ErrorLanguage>
		<WarningLevel>High</WarningLevel>
		</EndItemsRequest>';
		$headers = array(
			//Regulates versioning of the XML interface for the API
			'X-EBAY-API-COMPATIBILITY-LEVEL: 861',
			//the name of the call we are requesting
			'X-EBAY-API-CALL-NAME: EndItems',
			//SiteID must also be set in the Request's XML
			//SiteID = 0  (US) - UK = 3, Canada = 2, Australia = 15, ....
			//SiteID Indicates the eBay site to associate the call with
			'X-EBAY-API-SITEID: 0 ',
			);
		//initialise a CURL session
		$connection = curl_init();
		//set the server we are using (could be Sandbox or Production server)
		curl_setopt( $connection, CURLOPT_URL, $endpoint );
		//stop CURL from verifying the peer's certificate
		curl_setopt( $connection, CURLOPT_SSL_VERIFYPEER, 0 );
		curl_setopt( $connection, CURLOPT_SSL_VERIFYHOST, 0 );
		//set the headers using the array of headers
		curl_setopt( $connection, CURLOPT_HTTPHEADER, $headers );
		//set method as POST
		curl_setopt( $connection, CURLOPT_POST, 1 );
		//set the XML body of the request
		curl_setopt( $connection, CURLOPT_POSTFIELDS, $xmlbody );
		//set it to return the transfer as a string from curl_exec
		curl_setopt( $connection, CURLOPT_RETURNTRANSFER, 1 );
		curl_setopt( $connection, CURLOPT_TIMEOUT, 5 );
		//Send the Request
		$response = curl_exec( $connection );
		//close the connection
		curl_close( $connection );
		$r = simplexml_load_string( $response );
		if($r->Ack == 'Success') {
			return true;
		} else {
			$this->recordError($r);
			return false;
		}
	}
	private function recordError($response) {
		if(isset($response->Errors->LongMessage)) {
			$this->error_msg = $response->Errors->LongMessage;
		} elseif(isset($response->Error->ShortMessage)) {
			$this->error_msg = $response->Errors->ShortMessage;
		}
		if(isset($response->Errors->ErrorCode)) {
			$this->error_code = $response->Errors->ErrorCode;
		}
	}
	/**
	 * @return null|String
	 */
	public function getErrorMsg() {
		return $this->error_msg;
	}
	/**
	 * @return null|String
	 */
	public function getErrorCode() {
		return $this->error_code;
	}
}