first commit

This commit is contained in:
2024-11-10 21:08:49 +01:00
commit 0d932ce5ee
14455 changed files with 2567501 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 TechCrunch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,124 @@
# TechCrunch WP Asynchronous Tasks
TechCrunch WP Asynchronous Tasks plugin for TechCrunch.com
## Quick Start
WP Async Task can be installed as a plugin or bundled in other plugins or a theme. The class definition is wrapped in a `class_exists` check, so it will never run the risk of being accidentally defined twice. Just make sure that the plugin file is being included somehow.
Next, you need to extend the class with your own implementation. Implementations of the class act on an arbitrary action (e.g., `'save_post'`, etc). There are three parts that **must** be present in any class extending `WP_Async_Task`:
1. A protected `$action` property
2. A protected `prepare_data()` method
3. A protected `run_action()` method
```php
<?php
class JPB_Async_Task extends WP_Async_Task {
protected $action = 'save_post';
/**
* Prepare data for the asynchronous request
*
* @throws Exception If for any reason the request should not happen
*
* @param array $data An array of data sent to the hook
*
* @return array
*/
protected function prepare_data( $data ) {}
/**
* Run the async task action
*/
protected function run_action() {}
}
```
#### `$action`
The protected `$action` property should be set to the action to which you wish to attach the asynchronous task. For example, if you want to spin off an asynchronous task whenever a post gets saved, you would set this to `save_post`.
#### `prepare_data( $data )`
Use this method to prepare the action's data for use in the asynchronous process. Data will be given to `prepare_data()` as an indexed array, just as it would if you used `func_get_args()` to get a function's arguments. This method needs to return an array containing the data in a more useful format. Since these values will be sent in a `POST` request, it's advisable to stick to scalar values for the most part. For example, on `'save_post'`, the action provides `$post_id` and the `$post` object, so we might do this:
```php
protected function prepare_data($data){
$post_id = $data[0];
return array( 'post_id' => $post_id );
}
```
If for any reason the asynchronous task needs to be canceled, you will need to throw an exception:
```php
protected function prepare_data($data){
$post_id = $data[0];
$post = $data[1];
if( 'post' !== $post->post_type ) {
throw new Exception( 'We only want async tasks for posts' );
}
return array( 'post_id' => $post_id );
}
```
The library will handle catching the exception and will prevent the request from running if it catches an Exception.
#### `run_action()`
This method is responsible for running whatever action should trigger the functions that need to run inside the asynchronous request. The convention is to use `"wp_async_$this->action"`, but that is up to the implementation.
```php
protected function run_action() {
$post_id = $_POST['post_id'];
$post = get_post( $post_id );
if ( $post ) {
// Assuming $this->action is 'save_post'
do_action( "wp_async_$this->action", $post->ID, $post );
}
}
```
Make sure that you instantiate your asynchronous task once. Do this no earlier than the `'plugins_loaded'` action.
Finally, update the action of any tasks that you wish to move to the asynchronous task.
For example, you might change this:
```php
add_action( 'save_post', 'really_slow_process', 10, 2 );
```
to this:
```php
add_action( 'wp_async_save_post', 'really_slow_process', 10, 2 );
```
## Contributing
To contribute, please fork the github repository and submit a pull request.
When submitting pull requests, please make sure your changes do not cause any unit tests to fail. To run the unit test suite, make sure you've [installed composer](https://getcomposer.org/doc/00-intro.md) and install the test tools by running
```sh
composer install
```
After you've installed the dev tools, run the unit tests by running
```sh
vendor/bin/phpunit
```
## Copyright
© TechCrunch 2014
## License
This library is licensed under the [MIT](http://opensource.org/licenses/MIT) license. See LICENSE.md for more details.

View File

@@ -0,0 +1,6 @@
<?php
require_once __DIR__ . '/vendor/autoload.php';
WP_Mock::setUsePatchwork( false );
WP_Mock::bootstrap();

View File

@@ -0,0 +1,37 @@
{
"name" : "techcrunch/wp-async-task",
"description" : "Run asynchronous tasks for long-running operations in WordPress",
"minimum-stability": "dev",
"license" : "MIT",
"type" : "wordpress-plugin",
"authors" : [
{
"name": "Alex Khadiwala",
"role": "developer"
},
{
"name": "Nicolas Vincent",
"role": "developer"
},
{
"name" : "Eric Mann",
"email": "eric.mann@10up.com",
"role" : "developer"
},
{
"name" : "John P. Bloch",
"email": "john.bloch@10up.com",
"role" : "developer"
}
],
"require-dev" : {
"10up/wp_mock" : "dev-master",
"phpunit/phpunit": "*@stable"
},
"autoload" : {
"classmap": ["wp-async-task.php"]
},
"autoload-dev" : {
"classmap": ["tests/phpunit/mocks/"]
}
}

View File

@@ -0,0 +1,14 @@
<phpunit
bootstrap="./bootstrap.php.dist"
colors="true">
<testsuites>
<testsuite>
<directory suffix="Test.php">./tests/phpunit/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<file>./wp-async-task.php</file>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,438 @@
<?php
use WP_Mock\Tools\TestCase;
class WP_Async_Task_Tests extends TestCase {
/**
* Set up some test mocks
*/
public function setUp() {
parent::setUp();
$_COOKIE = array();
$_POST = array();
}
public function tearDown() {
parent::tearDown();
$_COOKIE = array();
$_POST = array();
}
/**
* Test that the correct actions are registered based on the auth level
*/
public function test_auth_level_both() {
$async = new Async( false );
WP_Mock::expectActionAdded( 'async', array( $async, 'launch' ), 10, 20 );
WP_Mock::expectActionAdded( 'admin_post_wp_async_async', array( $async, 'handle_postback' ) );
WP_Mock::expectActionAdded( 'admin_post_nopriv_wp_async_async', array( $async, 'handle_postback' ) );
$async->__construct( WP_Async_Task::BOTH );
$this->assertConditionsMet();
}
/**
* Test that the correct actions are registered based on the auth level
*/
public function test_auth_level_logged_in_only() {
$async = new Async( false );
WP_Mock::expectActionAdded( 'async', array( $async, 'launch' ), 10, 20 );
WP_Mock::expectActionAdded( 'admin_post_wp_async_async', array( $async, 'handle_postback' ) );
$async->__construct( WP_Async_Task::LOGGED_IN );
$this->assertConditionsMet();
}
/**
* Test that the correct actions are registered based on the auth level
*/
public function test_auth_level_logged_out_only() {
$async = new Async( false );
WP_Mock::expectActionAdded( 'async', array( $async, 'launch' ), 10, 20 );
WP_Mock::expectActionAdded( 'admin_post_nopriv_wp_async_async', array( $async, 'handle_postback' ) );
$async->__construct( WP_Async_Task::LOGGED_OUT );
$this->assertConditionsMet();
}
/**
* Test that the constructor throws an Exception if action is undefined
*
* @expectedException \Exception
*/
public function test_empty_action() {
new EmptyAsync();
}
/**
* Test that throwing an Exception in prepare_data stops a postback from firing
*/
public function test_exception_stops_launch_sequence() {
$async = $this->getMockAsync( 'Async', array( 'prepare_data', 'create_async_nonce' ) );
$arg1 = rand( 0, 9 );
$arg2 = rand( 10, 99 );
$async->shouldReceive( 'prepare_data' )
->once()
->with( array( $arg1, $arg2 ) )
->andThrow( 'Exception' );
$async->shouldReceive( 'create_async_nonce' )->never();
/** @var Async $async */
$async->launch( $arg1, $arg2 );
$this->assertConditionsMet();
}
/**
* Test that launch sets the correct action and _nonce values in the body
*/
public function test_launch() {
$async = $this->getMockAsync( 'Async', array( 'prepare_data', 'create_async_nonce' ) );
$arg = 'arg' . rand( 0, 9 );
$async->shouldReceive( 'prepare_data' )
->once()
->with( array( $arg ) )
->andReturn( array( 'foo' => $arg ) );
$nonce = substr( md5( 'async' . rand( 0, 9 ) ), - 12, 10 );
$async->shouldReceive( 'create_async_nonce' )
->once()
->with()
->andReturn( $nonce );
$body_data = new ReflectionProperty( 'Async', '_body_data' );
$body_data->setAccessible( true );
WP_Mock::wpFunction( 'has_action', array(
'times' => 1,
'args' => array( 'shutdown', array( $async, 'launch_on_shutdown' ) ),
'return' => false,
) );
WP_Mock::expectActionAdded( 'shutdown', array( $async, 'launch_on_shutdown' ) );
/** @var Async $async */
$async->launch( $arg );
$data = $body_data->getValue( $async );
$this->assertArrayHasKey( 'action', $data );
$this->assertEquals( 'wp_async_async', $data['action'] );
$this->assertArrayHasKey( '_nonce', $data );
$this->assertEquals( $nonce, $data['_nonce'] );
$this->assertConditionsMet();
}
public function test_launch_on_shutdown() {
$async = $this->getMockAsync( 'Async', array( 'prepare_data', 'create_async_nonce' ) );
$async->shouldReceive( 'prepare_data' )->andReturn( array() );
$async->shouldReceive( 'create_async_nonce' )->andReturn( 'asdf' );
WP_Mock::wpFunction( 'maybe_serialize', array(
'return' => function ( $thing ) {
return is_scalar( $thing ) ? $thing : serialize( $thing );
}
) );
$_COOKIE = array(
'_some_cookie' => 'Value',
'foo' => 'bar',
'random' => rand( 0, 999999 ),
'array' => array( 'not', 'scalar' ),
);
$cookie_header = '';
array_walk( $_COOKIE, function ( $value, $key ) use ( &$cookie_header ) {
if ( ! empty( $cookie_header ) ) {
$cookie_header .= '; ';
}
if ( ! is_scalar( $value ) ) {
$value = serialize( $value );
}
$cookie_header .= "$key=" . urlencode( $value );
} );
$verify_ssl = (bool) rand( 0, 1 );
WP_Mock::onFilter( 'https_local_ssl_verify' )->with( true )->reply( $verify_ssl );
WP_Mock::wpFunction( 'admin_url', array(
'times' => 1,
'args' => array( 'admin-post.php' ),
'return' => $url = 'https://tctechcrunch2011.wordpress.com/wp-admin/admin-post.php'
) );
WP_Mock::wpFunction( 'wp_remote_post', array(
'times' => 1,
'args' => array(
$url,
array(
'timeout' => 0.01,
'blocking' => false,
'sslverify' => $verify_ssl,
'body' => array(
'action' => 'wp_async_async',
'_nonce' => 'asdf',
),
'headers' => array(
'cookie' => $cookie_header,
),
)
),
) );
/** @var Async $async */
$async->launch(); // to set up body data, etc.
$async->launch_on_shutdown();
$this->assertConditionsMet();
}
public function test_launch_on_shutdown_empty_body() {
WP_Mock::wpFunction( 'wp_remote_post', array( 'times' => 0, ) );
/** @var Async $async */
$async = $this->getMockAsync();
$async->launch_on_shutdown();
$this->assertConditionsMet();
}
public function test_handle_postback_nonce_not_set() {
$async = $this->getMockAsync( 'Async', array( 'verify_async_nonce', 'run_action' ) );
$async->shouldReceive( 'verify_async_nonce' )->never();
$async->shouldReceive( 'run_action' )->never();
WP_Mock::expectFilterAdded( 'wp_die_handler', function () {
die();
} );
WP_Mock::wpFunction( 'wp_die', array( 'times' => 1 ) );
/** @var Async $async */
$async->handle_postback();
$this->assertConditionsMet();
}
public function test_handle_postback_invalid_nonce() {
$async = $this->getMockAsync( 'Async', array( 'verify_async_nonce', 'run_action' ) );
$nonce = 'asdfasdf';
$_POST['_nonce'] = $nonce;
$async->shouldReceive( 'verify_async_nonce' )
->once()
->with( $nonce )
->andReturn( false );
$async->shouldReceive( 'run_action' )->never();
WP_Mock::expectFilterAdded( 'wp_die_handler', function () {
die();
} );
WP_Mock::wpFunction( 'wp_die', array( 'times' => 1 ) );
/** @var Async $async */
$async->handle_postback();
$this->assertConditionsMet();
}
public function test_handle_postback_anon() {
$async = $this->getMockAsync( 'Async', array( 'verify_async_nonce', 'run_action' ) );
$nonce = 'asdfasdf';
$_POST['_nonce'] = $nonce;
$async->shouldReceive( 'verify_async_nonce' )
->once()
->with( $nonce )
->andReturn( true );
WP_Mock::wpFunction( 'is_user_logged_in', array( 'times' => 1, 'return' => false, ) );
$async->shouldReceive( 'run_action' )
->once()
->with();
WP_Mock::expectFilterAdded( 'wp_die_handler', function () {
die();
} );
WP_Mock::wpFunction( 'wp_die', array( 'times' => 1 ) );
/** @var Async $async */
$async->handle_postback();
$action = new ReflectionProperty( 'Async', 'action' );
$action->setAccessible( true );
$this->assertEquals( 'nopriv_async', $action->getValue( $async ) );
$this->assertConditionsMet();
}
public function test_handle_postback() {
$async = $this->getMockAsync( 'Async', array( 'verify_async_nonce', 'run_action' ) );
$nonce = 'asdfasdf';
$_POST['_nonce'] = $nonce;
$async->shouldReceive( 'verify_async_nonce' )
->once()
->with( $nonce )
->andReturn( true );
WP_Mock::wpFunction( 'is_user_logged_in', array( 'times' => 1, 'return' => true, ) );
$async->shouldReceive( 'run_action' )
->once()
->with();
WP_Mock::expectFilterAdded( 'wp_die_handler', function () {
die();
} );
WP_Mock::wpFunction( 'wp_die', array( 'times' => 1 ) );
/** @var Async $async */
$async->handle_postback();
$action = new ReflectionProperty( 'Async', 'action' );
$action->setAccessible( true );
$this->assertEquals( 'async', $action->getValue( $async ) );
$this->assertConditionsMet();
}
public function test_create_async_nonce() {
$async = $this->getMockAsync();
$nonce_tick = rand( 10, 99 );
WP_Mock::wpFunction( 'wp_nonce_tick', array(
'times' => 1,
'args' => array(),
'return' => $nonce_tick,
) );
$create_nonce = new ReflectionMethod( 'Async', 'create_async_nonce' );
$create_nonce->setAccessible( true );
$expected_hash = md5( $nonce_tick . 'wp_async_async' . get_class( $async ) );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 1,
'args' => array( $nonce_tick . 'wp_async_async' . get_class( $async ), 'nonce' ),
'return' => $expected_hash,
) );
$this->assertEquals( substr( $expected_hash, - 12, 10 ), $create_nonce->invoke( $async ) );
$this->assertConditionsMet();
}
public function test_verify_async_nonce_invalid() {
$async = $this->getMockAsync();
$nonce_tick = rand( 10, 99 );
WP_Mock::wpFunction( 'wp_nonce_tick', array(
'times' => 1,
'args' => array(),
'return' => $nonce_tick,
) );
$verify_nonce = new ReflectionMethod( 'Async', 'verify_async_nonce' );
$verify_nonce->setAccessible( true );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 2,
'return' => md5( rand( 100, 999 ) ),
) );
$this->assertFalse( $verify_nonce->invoke( $async, md5( $nonce_tick ) ) );
$this->assertConditionsMet();
}
public function test_verify_async_nonce_recent() {
$async = $this->getMockAsync();
$nonce_tick = rand( 10, 99 );
WP_Mock::wpFunction( 'wp_nonce_tick', array(
'times' => 1,
'args' => array(),
'return' => $nonce_tick,
) );
$verify_nonce = new ReflectionMethod( 'Async', 'verify_async_nonce' );
$verify_nonce->setAccessible( true );
$hash = md5( $nonce_tick . 'wp_async_async' . get_class( $async ) );
$nonce = substr( $hash, - 12, 10 );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 1,
'args' => array( $nonce_tick . 'wp_async_async' . get_class( $async ), 'nonce' ),
'return' => function ( $thing ) {
return md5( $thing );
}
) );
$this->assertSame( 1, $verify_nonce->invoke( $async, $nonce ) );
$this->assertConditionsMet();
}
public function test_verify_async_nonce_old_but_valid() {
$async = $this->getMockAsync();
$nonce_tick = rand( 10, 99 );
$real_tick = $nonce_tick - 1;
WP_Mock::wpFunction( 'wp_nonce_tick', array(
'times' => 1,
'args' => array(),
'return' => $nonce_tick,
) );
$verify_nonce = new ReflectionMethod( 'Async', 'verify_async_nonce' );
$verify_nonce->setAccessible( true );
$hash = md5( $real_tick . 'wp_async_async' . get_class( $async ) );
$nonce = substr( $hash, - 12, 10 );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 1,
'args' => array( $nonce_tick . 'wp_async_async' . get_class( $async ), 'nonce' ),
'return' => function ( $thing ) {
return md5( $thing );
}
) );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 1,
'args' => array( $real_tick . 'wp_async_async' . get_class( $async ), 'nonce' ),
'return' => function ( $thing ) {
return md5( $thing );
}
) );
$this->assertSame( 2, $verify_nonce->invoke( $async, $nonce ) );
$this->assertConditionsMet();
}
public function test_verify_async_nonce_nopriv() {
$async = $this->getMockAsync();
$nonce_tick = rand( 10, 99 );
WP_Mock::wpFunction( 'wp_nonce_tick', array(
'times' => 1,
'args' => array(),
'return' => $nonce_tick,
) );
$action = new ReflectionProperty( 'Async', 'action' );
$action->setAccessible( true );
$action->setValue( $async, 'nopriv_async' );
$verify_nonce = new ReflectionMethod( 'Async', 'verify_async_nonce' );
$verify_nonce->setAccessible( true );
$hash = md5( $nonce_tick . 'wp_async_async' . get_class( $async ) );
$nonce = substr( $hash, - 12, 10 );
WP_Mock::wpFunction( 'wp_hash', array(
'times' => 1,
'args' => array( $nonce_tick . 'wp_async_async' . get_class( $async ), 'nonce' ),
'return' => function ( $thing ) {
return md5( $thing );
}
) );
$this->assertSame( 1, $verify_nonce->invoke( $async, $nonce ) );
$this->assertConditionsMet();
}
/**
* Get a mock object for the async task class
*
* @param string $class The name of the class to mock
* @param array $methods Which methods to mock
* @param mixed $auth The auth level to simulate
*
* @return \Mockery\Mock
*/
private function getMockAsync( $class = 'Async', array $methods = array(), $auth = false ) {
$stub = '';
if ( ! empty( $methods ) ) {
$stub = '[' . implode( ',', $methods ) . ']';
}
$mockClass = "$class$stub";
/** @var \Mockery\Mock $mock */
$mock = Mockery::mock( $mockClass, array( $auth ) );
$mock->makePartial();
$mock->shouldAllowMockingProtectedMethods();
return $mock;
}
}

View File

@@ -0,0 +1,11 @@
<?php
class Async extends BaseAsync {
public function __construct( $init = parent::BOTH ) {
if ( $init ) {
parent::__construct( $init );
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
class BaseAsync extends WP_Async_Task {
protected $action = 'async';
protected function prepare_data( $data ) {
}
protected function run_action() {
}
}

View File

@@ -0,0 +1,7 @@
<?php
class EmptyAsync extends BaseAsync {
protected $action = null;
}

View File

@@ -0,0 +1,266 @@
<?php
/**
* Plugin Name: WP Asynchronous Tasks
* Version: 1.0
* Description: Creates an abstract class to execute asynchronous tasks
* Author: 10up, Eric Mann, Luke Gedeon, John P. Bloch
* License: MIT
*/
if ( ! class_exists( 'WP_Async_Task' ) ) {
abstract class WP_Async_Task {
/**
* Constant identifier for a task that should be available to logged-in users
*
* See constructor documentation for more details.
*/
const LOGGED_IN = 1;
/**
* Constant identifier for a task that should be available to logged-out users
*
* See constructor documentation for more details.
*/
const LOGGED_OUT = 2;
/**
* Constant identifier for a task that should be available to all users regardless of auth status
*
* See constructor documentation for more details.
*/
const BOTH = 3;
/**
* This is the argument count for the main action set in the constructor. It
* is set to an arbitrarily high value of twenty, but can be overridden if
* necessary
*
* @var int
*/
protected $argument_count = 20;
/**
* Priority to fire intermediate action.
*
* @var int
*/
protected $priority = 10;
/**
* @var string
*/
protected $action;
/**
* @var array
*/
protected $_body_data;
/**
* Constructor to wire up the necessary actions
*
* Which hooks the asynchronous postback happens on can be set by the
* $auth_level parameter. There are essentially three options: logged in users
* only, logged out users only, or both. Set this when you instantiate an
* object by using one of the three class constants to do so:
* - LOGGED_IN
* - LOGGED_OUT
* - BOTH
* $auth_level defaults to BOTH
*
* @throws Exception If the class' $action value hasn't been set
*
* @param int $auth_level The authentication level to use (see above)
*/
public function __construct( $auth_level = self::BOTH ) {
if ( empty( $this->action ) ) {
throw new Exception( 'Action not defined for class ' . __CLASS__ );
}
add_action( $this->action, array( $this, 'launch' ), (int) $this->priority, (int) $this->argument_count );
if ( $auth_level & self::LOGGED_IN ) {
add_action( "admin_post_wp_async_$this->action", array( $this, 'handle_postback' ) );
}
if ( $auth_level & self::LOGGED_OUT ) {
add_action( "admin_post_nopriv_wp_async_$this->action", array( $this, 'handle_postback' ) );
}
}
/**
* Add the shutdown action for launching the real postback if we don't
* get an exception thrown by prepare_data().
*
* @uses func_get_args() To grab any arguments passed by the action
*/
public function launch() {
$data = func_get_args();
try {
$data = $this->prepare_data( $data );
} catch ( Exception $e ) {
return;
}
$data['action'] = "wp_async_$this->action";
$data['_nonce'] = $this->create_async_nonce();
$this->_body_data = $data;
if ( ! has_action( 'shutdown', array( $this, 'launch_on_shutdown' ) ) ) {
add_action( 'shutdown', array( $this, 'launch_on_shutdown' ) );
}
}
/**
* Launch the request on the WordPress shutdown hook
*
* On VIP we got into data races due to the postback sometimes completing
* faster than the data could propogate to the database server cluster.
* This made WordPress get empty data sets from the database without
* failing. On their advice, we're moving the actual firing of the async
* postback to the shutdown hook. Supposedly that will ensure that the
* data at least has time to get into the object cache.
*
* @uses $_COOKIE To send a cookie header for async postback
* @uses apply_filters()
* @uses admin_url()
* @uses wp_remote_post()
*/
public function launch_on_shutdown() {
if ( ! empty( $this->_body_data ) ) {
$cookies = array();
foreach ( $_COOKIE as $name => $value ) {
$cookies[] = "$name=" . urlencode( is_array( $value ) ? serialize( $value ) : $value );
}
$request_args = array(
'timeout' => 0.01,
'blocking' => false,
'sslverify' => apply_filters( 'https_local_ssl_verify', true ),
'body' => $this->_body_data,
'headers' => array(
'cookie' => implode( '; ', $cookies ),
),
);
$url = admin_url( 'admin-post.php' );
wp_remote_post( $url, $request_args );
}
}
/**
* Verify the postback is valid, then fire any scheduled events.
*
* @uses $_POST['_nonce']
* @uses is_user_logged_in()
* @uses add_filter()
* @uses wp_die()
*/
public function handle_postback() {
if ( isset( $_POST['_nonce'] ) && $this->verify_async_nonce( $_POST['_nonce'] ) ) {
if ( ! is_user_logged_in() ) {
$this->action = "nopriv_$this->action";
}
$this->run_action();
}
add_filter( 'wp_die_handler', function() { die(); } );
wp_die();
}
/**
* Create a random, one time use token.
*
* Based entirely on wp_create_nonce() but does not tie the nonce to the
* current logged-in user.
*
* @uses wp_nonce_tick()
* @uses wp_hash()
*
* @return string The one-time use token
*/
protected function create_async_nonce() {
$action = $this->get_nonce_action();
$i = wp_nonce_tick();
return substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 );
}
/**
* Verify that the correct nonce was used within the time limit.
*
* @uses wp_nonce_tick()
* @uses wp_hash()
*
* @param string $nonce Nonce to be verified
*
* @return bool Whether the nonce check passed or failed
*/
protected function verify_async_nonce( $nonce ) {
$action = $this->get_nonce_action();
$i = wp_nonce_tick();
// Nonce generated 0-12 hours ago
if ( substr( wp_hash( $i . $action . get_class( $this ), 'nonce' ), - 12, 10 ) == $nonce ) {
return 1;
}
// Nonce generated 12-24 hours ago
if ( substr( wp_hash( ( $i - 1 ) . $action . get_class( $this ), 'nonce' ), - 12, 10 ) == $nonce ) {
return 2;
}
// Invalid nonce
return false;
}
/**
* Get a nonce action based on the $action property of the class
*
* @return string The nonce action for the current instance
*/
protected function get_nonce_action() {
$action = $this->action;
if ( substr( $action, 0, 7 ) === 'nopriv_' ) {
$action = substr( $action, 7 );
}
$action = "wp_async_$action";
return $action;
}
/**
* Prepare any data to be passed to the asynchronous postback
*
* The array this function receives will be a numerically keyed array from
* func_get_args(). It is expected that you will return an associative array
* so that the $_POST values used in the asynchronous call will make sense.
*
* The array you send back may or may not have anything to do with the data
* passed into this method. It all depends on the implementation details and
* what data is needed in the asynchronous postback.
*
* Do not set values for 'action' or '_nonce', as those will get overwritten
* later in launch().
*
* @throws Exception If the postback should not occur for any reason
*
* @param array $data The raw data received by the launch method
*
* @return array The prepared data
*/
abstract protected function prepare_data( $data );
/**
* Run the do_action function for the asynchronous postback.
*
* This method needs to fetch and sanitize any and all data from the $_POST
* superglobal and provide them to the do_action call.
*
* The action should be constructed as "wp_async_task_$this->action"
*/
abstract protected function run_action();
}
}