Commit eb433d7d authored by alain's avatar alain 🐙
Browse files

add admin plugins

parent 450fa78b
<?php
if (!class_exists('Ajaw_v1_ActionBuilder', false)):
class Ajaw_v1_ActionBuilder {
private $action;
private $callback = '__return_null';
private $params = array();
private $httpMethod = null;
private $capability = null;
private $permissionCheckCallback = null;
private $mustBeLoggedIn = true;
private $checkNonce = true;
public function __construct($action) {
$this->action = $action;
}
/**
* @param callable $callback
* @return $this
*/
public function handler($callback) {
$this->callback = $callback;
return $this;
}
public function requiredParam($name, $type = null, $validateCallback = null) {
return $this->addParameter($name, $type, true, null, $validateCallback);
}
public function optionalParam($name, $defaultValue = null, $type = null, $validateCallback = null) {
return $this->addParameter($name, $type, false, $defaultValue, $validateCallback);
}
private function addParameter($name, $type, $required, $defaultValue, $validateCallback) {
if (isset($type) && !isset(Ajaw_v1_Action::$defaultValidators[$type])) {
throw new LogicException(sprintf(
'Unknown parameter type "%s". Supported types are: %s.',
$type,
implode(', ', array_keys(Ajaw_v1_Action::$defaultValidators[$type]))
));
}
$this->params[$name] = array(
'required' => $required,
'defaultValue' => $defaultValue,
'type' => $type,
'validateCallback' => $validateCallback,
);
return $this;
}
public function method($httpMethod) {
$this->httpMethod = strtoupper($httpMethod);
return $this;
}
public function requiredCap($capability) {
$this->capability = $capability;
return $this;
}
public function permissionCallback($callback) {
$this->permissionCheckCallback = $callback;
return $this;
}
public function allowUnprivilegedUsers() {
$this->mustBeLoggedIn = false;
return $this;
}
public function withoutNonce() {
$this->checkNonce = false;
return $this;
}
public function build() {
$instance = new Ajaw_v1_Action($this->action, $this->callback, $this->params);
$instance->mustBeLoggedIn = $this->mustBeLoggedIn;
$instance->requiredCap = $this->capability;
$instance->nonceCheckEnabled = $this->checkNonce;
$instance->method = $this->httpMethod;
$instance->permissionCallback = $this->permissionCheckCallback;
return $instance;
}
public function register() {
$instance = $this->build();
$instance->register();
return $instance;
}
}
endif;
if (!class_exists('Ajaw_v1_Action', false)):
class Ajaw_v1_Action {
public $action;
public $callback;
public $params = array();
public $method = null;
public $requiredCap = null;
public $mustBeLoggedIn = false;
public $nonceCheckEnabled = true;
public $permissionCallback = null;
private $isScriptRegistered = false;
public $get = array();
public $post = array();
public $request = array();
public static $defaultValidators = array(
'int' => array(__CLASS__, 'validateInt'),
'float' => array(__CLASS__, 'validateFloat'),
'boolean' => array(__CLASS__, 'validateBoolean'),
'string' => array(__CLASS__, 'validateString'),
);
public function __construct($action, $callback, $params) {
$this->action = $action;
$this->callback = $callback;
$this->params = $params;
if (empty($this->action)) {
throw new LogicException(sprintf(
'AJAX action name is missing. You must either pass it to the %1$s constructor '
. 'or give the %1$s::$action property a valid default value.',
get_class($this)
));
}
}
/**
* Set up hooks for AJAX and helper scripts.
*/
public function register() {
//Register the AJAX handler(s).
$hookNames = array('wp_ajax_' . $this->action);
if (!$this->mustBeLoggedIn) {
$hookNames[] = 'wp_ajax_nopriv_' . $this->action;
}
foreach($hookNames as $hook) {
if (has_action($hook)) {
throw new RuntimeException(sprintf('The action name "%s" is already in use.', $this->action));
}
add_action($hook, array($this, 'processAjaxRequest'));
}
//Register the utility JS library after WP is fully loaded.
if (did_action('wp_loaded')) {
$this->registerScript();
} else {
add_action('wp_loaded', array($this, 'registerScript'), 2);
}
}
/**
* @access protected
*/
public function processAjaxRequest() {
$result = $this->handleAction();
if (is_wp_error($result)) {
$statusCode = $result->get_error_data();
if (isset($statusCode) && is_int($statusCode) ) {
status_header($statusCode);
}
$errorResponse = array(
'error' => array(
'message' => $result->get_error_message(),
'code' => $result->get_error_code()
)
);
$result = $errorResponse;
}
if (isset($result)) {
$this->outputJSON($result);
}
exit;
}
protected function handleAction() {
$method = strtoupper(filter_input(INPUT_SERVER, 'REQUEST_METHOD'));
if (isset($this->method) && ($method !== $this->method)) {
return new WP_Error(
'http_method_not_allowed',
'The HTTP method is not supported by the request handler.',
405
);
}
$isAuthorized = $this->checkAuthorization();
if ($isAuthorized !== true) {
return $isAuthorized;
}
$params = $this->parseParameters();
if ($params instanceof WP_Error) {
return $params;
}
//Call the user-specified action handler.
if (is_callable($this->callback)) {
return call_user_func($this->callback, $params);
} else {
return new WP_Error(
'missing_ajax_handler',
sprintf(
'There is no request handler assigned to the "%1$s" action. '
. 'Either pass a valid callback to $builder->request() or override the %2$s::%3$s method.',
$this->action,
__CLASS__,
__METHOD__
),
500
);
}
}
/**
* Check if the current user is authorized to perform this action.
*
* @return bool|WP_Error
*/
protected function checkAuthorization() {
if ($this->mustBeLoggedIn && !is_user_logged_in()) {
return new WP_Error('login_required', 'You must be logged in to perform this action.', 403);
}
if (isset($this->requiredCap) && !current_user_can($this->requiredCap)) {
return new WP_Error('capability_missing', 'You don\'t have permission to perform this action.', 403);
}
if ($this->nonceCheckEnabled && !check_ajax_referer($this->action, false, false)) {
return new WP_Error('nonce_check_failed', 'Invalid or missing nonce.', 403);
}
if (isset($this->permissionCallback)) {
$result = call_user_func($this->permissionCallback);
if ($result === false) {
return new WP_Error(
'permission_callback_failed',
'You don\'t have permission to perform this action.',
403
);
} else if (is_wp_error($result)) {
return $result;
}
}
return true;
}
protected function parseParameters() {
$method = strtoupper(filter_input(INPUT_SERVER, 'REQUEST_METHOD'));
//Retrieve request parameters.
if ($method === 'GET') {
$rawParams = $_GET;
} else if ($method === 'POST') {
$rawParams = $_POST;
} else {
$rawParams = $_REQUEST;
}
//Remove magic quotes. WordPress applies them in wp-settings.php.
//There's no hook for wp_magic_quotes, so we use one that's closest in execution order.
if (did_action('sanitize_comment_cookies') && function_exists('wp_magic_quotes')) {
$rawParams = wp_unslash($rawParams);
}
//Validate all parameters.
$inputParams = $rawParams;
foreach($this->params as $name => $settings) {
//Verify that all of the required parameters are present.
//Empty strings are treated as missing parameters.
if (isset($inputParams[$name]) && ($inputParams[$name] !== '')) {
$value = $this->validateParameter($settings, $inputParams[$name], $name);
if (is_wp_error($value)) {
return $value;
} else {
$inputParams[$name] = $value;
}
} else if (empty($settings['required'])) {
//It's an optional parameter. Use the default value.
$inputParams[$name] = $settings['defaultValue'];
} else {
return new WP_Error(
'missing_required_parameter',
sprintf('Required parameter is missing or empty: "%s".', $name),
400
);
}
}
return $inputParams;
}
protected function validateParameter($settings, $value, $name) {
if (isset($settings['type'])) {
$value = call_user_func(self::$defaultValidators[$settings['type']], $value, $name);
if (is_wp_error($value)) {
return $value;
}
}
if (isset($settings['validateCallback'])) {
$success = call_user_func($settings['validateCallback'], $value);
if (is_wp_error($success)) {
return $success;
} else if ($success === false) {
return new WP_Error(
'invalid_parameter_value',
sprintf('The value of the parameter "%s" is invalid.', $name),
400
);
}
}
return $value;
}
private static function validateInt($value, $name) {
$result = filter_var($value, FILTER_VALIDATE_INT);
if ($result === false) {
return new WP_Error(
'invalid_parameter_value',
sprintf('The value of the parameter "%s" is invalid. It must be an integer.', $name),
400
);
}
return $result;
}
private static function validateFloat($value, $name) {
$result = filter_var($value, FILTER_VALIDATE_FLOAT);
if ($result === false) {
return new WP_Error(
'invalid_parameter_value',
sprintf('The value of the parameter "%s" is invalid. It must be a float.', $name),
400
);
}
return $result;
}
private static function validateBoolean($value, $name) {
$result = filter_var($value, FILTER_VALIDATE_BOOLEAN, array('flags' => FILTER_NULL_ON_FAILURE));
if ($result === null) {
return new WP_Error(
'invalid_parameter_value',
sprintf('The value of the parameter "%s" is invalid. It must be a boolean.', $name),
400
);
}
return $result;
}
private static function validateString($value, $name) {
if (!is_string($value)) {
return new WP_Error(
'invalid_parameter_value',
sprintf('The value of the parameter "%s" is invalid. It must be a string.', $name),
400
);
}
return $value;
}
protected function outputJSON($response) {
@header('Content-Type: application/json; charset=' . get_option('blog_charset'));
echo json_encode($response);
}
public function registerScript() {
if ($this->isScriptRegistered) {
return;
}
$this->isScriptRegistered = true;
//There could be multiple instances of this class, but we only need to register the script once.
$handle = $this->getScriptHandle();
if (!wp_script_is($handle, 'registered')) {
wp_register_script(
$handle,
plugins_url('ajax-action-wrapper.js', __FILE__),
array('jquery'),
'20161105'
);
}
//Pass the action to the script.
if (function_exists('wp_add_inline_script')) {
wp_add_inline_script($handle, $this->generateActionJs(), 'after'); //WP 4.5+
} else {
add_filter('script_loader_tag', array($this, 'addRegistrationScript'), 10, 2); //WP 4.1+
}
}
/**
* Backwards compatibility for older versions of WP that don't have wp_add_inline_script().
* @internal
*
* @param string $tag
* @param string $handle
* @return string
*/
public function addRegistrationScript($tag, $handle) {
if ($handle === $this->getScriptHandle()) {
$tag .= '<script type="text/javascript">' . $this->generateActionJs() . '</script>';
}
return $tag;
}
protected function generateActionJs() {
$properties = array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'method' => $this->method,
'nonce' => $this->nonceCheckEnabled ? wp_create_nonce($this->action) : null,
);
return sprintf(
'AjawV1.actionRegistry.add("%s", %s);' . "\n",
esc_js($this->action),
json_encode($properties)
);
}
public function getScriptHandle() {
return 'ajaw-v1-ajax-action-wrapper';
}
/**
* Capture $_GET, $_POST and $_REQUEST without magic quotes.
*/
function captureRequestVars() {
$this->post = $_POST;
$this->get = $_GET;
$this->request = $_REQUEST;
if (
version_compare(phpversion(), '7.4.0alpha1', '<')
&& function_exists('get_magic_quotes_gpc')
&& get_magic_quotes_gpc()
) {
$this->post = stripslashes_deep($this->post);
$this->get = stripslashes_deep($this->get);
}
}
}
endif;
if (!function_exists('ajaw_v1_CreateAction')) {
function ajaw_v1_CreateAction($action) {
return new Ajaw_v1_ActionBuilder($action);
}
}
# AJAX Action Wrapper
This helper library makes it easier to handle AJAX requests in WordPress plugins. Mainly for personal use.
### Example
Define action:
```php
$exampleAction = ajaw_v1_CreateAction('ws_do_something')
->handler(array($this, 'myAjaxCallback'))
->requiredCap('manage_options')
->method('post')
->requiredParam('foo')
->optionalParam('bar', 'default value')
->register();
```
Call from JavaScript:
```javascript
AjawV1.getAction('ws_do_something').post(
{
'foo': '...'
},
function(response) {
console.log(response);
}
);
```
### Features
- Automate common, boring stuff.
- [x] Automatically pass the `admin-ajax.php` URL and nonce to JS.
- [x] Define required parameters.
```php
$builder->requiredParam('foo', 'int')
```
- [x] Define optional parameters with default values.
```php
$builder->optionalParam('meaningOfLife', 42, 'int')
```
- [x] Automatically remove "magic quotes" that WordPress adds to `$_GET`, `$_POST` and `$_REQUEST`.
- [x] Encode return values as JSON.
- Security should be the default.
- [x] Generate and verify nonces. Nonce verification is on by default, but can be disabled.
```php
$builder->withoutNonce()
```
- [x] Check capabilities.
```php
$builder->requiredCap('manage_options');
```
- [x] Verify that all required parameters are set.
- [x] Validate parameter values.
```php
$builder->optionalParam('things', 1, 'int', function($value) {
if ($value > 10) {
return new WP_Error(
'excessive_things',
'Too many things!',
400 //HTTP status code.
);
}
})
```
- [x] Set the required HTTP method.
```php
$builder->method('post')
```
- Resilience.
- [ ] Lenient response parsing to work around bugs in other plugins. For example, deal with extraneous whitespace and PHP notices in AJAX responses.
- [x] Multiple versions of the library can coexist on the same site.
### Why not use the REST API instead?
Backwards compatibility. In theory, this library should be compatible with WP 4.1+.
\ No newline at end of file
// Basic type definitions for the Ajaw AJAX wrapper library 1.0
declare namespace AjawV1 {
interface RequestParams { [name: string]: any }
interface SuccessCallback { (data, textStatus: string, jqXHR): void }
interface ErrorCallback { (data, textStatus: string, jqXHR, errorThrown): void }
class AjawAjaxAction {
get(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback): void;
post(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback): void;
request(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback, method?: string): void;
}
function getAction(action: string): AjawAjaxAction;
}
\ No newline at end of file
<
var AjawV1 = window.AjawV1 || {};
AjawV1.AjaxAction = (function () {
"use strict";
function AjawAjaxAction(action, properties) {
this.action = action;
this.ajaxUrl = properties['ajaxUrl'];
this.nonce = properties['nonce'];
this.requiredMethod = (typeof properties['method'] !== 'undefined') ? properties['method'] : null;
}
/**