Verified Commit 5cd6e666 authored by Christian Wach's avatar Christian Wach Committed by Kevin Cristiano

First pass at adding REST API compatibility

Signed-off-by: Kevin Cristiano's avatarKevin Cristiano <kcristiano@kcristiano.com>
parent e2d98eca
......@@ -121,6 +121,17 @@ if ( file_exists( CIVICRM_SETTINGS_PATH ) ) {
// Prevent CiviCRM from rendering its own header
define( 'CIVICRM_UF_HEAD', TRUE );
/**
* Setting this to 'true' will replace all mailing URLs calls to 'extern/url.php'
* and 'extern/open.php' with their REST counterpart 'civicrm/v3/url' and 'civicrm/v3/open'.
*
* Use for test purposes, may affect mailing
* performance, see Plugin->replace_tracking_urls() method.
*/
if ( ! defined( 'CIVICRM_WP_REST_REPLACE_MAILING_TRACKING' ) ) {
define( 'CIVICRM_WP_REST_REPLACE_MAILING_TRACKING', false );
}
/**
* Define CiviCRM_For_WordPress Class.
......@@ -512,6 +523,9 @@ class CiviCRM_For_WordPress {
include_once CIVICRM_PLUGIN_DIR . 'includes/civicrm.basepage.php';
$this->basepage = new CiviCRM_For_WordPress_Basepage;
// Include REST API autoloader class
require_once( CIVICRM_PLUGIN_DIR . 'wp-rest/Autoloader.php' );
}
......@@ -636,6 +650,12 @@ class CiviCRM_For_WordPress {
// Register hooks for clean URLs.
$this->register_hooks_clean_urls();
// Set up REST API.
CiviCRM_WP_REST\Autoloader::add_source( $source_path = trailingslashit( CIVICRM_PLUGIN_DIR . 'wp-rest' ) );
// Init REST API.
new CiviCRM_WP_REST\Plugin;
}
......
<?php
/**
* Autoloader class.
*
* @since 0.1
*/
namespace CiviCRM_WP_REST;
class Autoloader {
/**
* Instance.
*
* @since 0.1
* @var string
*/
private static $instance = null;
/**
* Namespace.
*
* @since 0.1
* @var string
*/
private $namespace = 'CiviCRM_WP_REST';
/**
* Autoloader directory sources.
*
* @since 0.1
* @var array
*/
private static $source_directories = [];
/**
* Constructor.
*
* @since 0.1
*/
private function __construct() {
$this->register_autoloader();
}
/**
* Creates an instance of this class.
*
* @since 0.1
*/
private static function instance() {
if ( ! self::$instance ) self::$instance = new self;
}
/**
* Adds a directory source.
*
* @since 0.1
* @param string $source The source path
*/
public static function add_source( string $source_path ) {
// make sure we have an instance
self::instance();
if ( ! is_readable( trailingslashit( $source_path ) ) )
return \WP_Error( 'civicrm_wp_rest_error', __( 'The source ' . $source . ' is not readable.' ) );
self::$source_directories[] = $source_path;
}
/**
* Registers the autoloader.
*
* @since 0.1
* @return bool Wehather the autoloader has been registered or not
*/
private function register_autoloader() {
return spl_autoload_register( [ $this, 'autoload' ] );
}
/**
* Loads the classes.
*
* @since 0.1
* @param string $class_name The class name to load
*/
private function autoload( $class_name ) {
if ( false === strpos( $class_name, $this->namespace ) ) return;
$parts = explode( '\\', $class_name );
// remove namespace and join class path
$class_path = str_replace( '_', '-', implode( DIRECTORY_SEPARATOR, array_slice( $parts, 1 ) ) );
array_map( function( $source_path ) use ( $class_path ) {
$path = $source_path . $class_path . '.php';
if ( ! file_exists( $path ) ) return;
require $path;
}, static::$source_directories );
}
}
<?php
/**
* CiviCRM Mailing_Hooks class.
*
* @since 0.1
*/
namespace CiviCRM_WP_REST\Civi;
class Mailing_Hooks {
/**
* Mailing Url endpoint.
*
* @since 0.1
* @var string
*/
public $url_endpoint;
/**
* Mailing Open endpoint.
*
* @since 0.1
* @var string
*/
public $open_endpoint;
/**
* Constructor.
*
* @since 0.1
*/
public function __construct() {
$this->url_endpoint = rest_url( 'civicrm/v3/url' );
$this->open_endpoint = rest_url( 'civicrm/v3/open' );
}
/**
* Register hooks.
*
* @since 0.1
*/
public function register_hooks() {
add_filter( 'civicrm_alterMailParams', [ $this, 'do_mailing_urls' ], 10, 2 );
}
/**
* Filters the mailing html and replaces calls to 'extern/url.php' and
* 'extern/open.php' with their REST counterparts 'civicrm/v3/url' and 'civicrm/v3/open'.
*
* @uses 'civicrm_alterMailParams'
*
* @since 0.1
* @param array &$params Mail params
* @param string $context The Context
* @return array $params The filtered Mail params
*/
public function do_mailing_urls( &$params, $context ) {
if ( $context == 'civimail' ) {
$params['html'] = $this->replace_html_mailing_tracking_urls( $params['html'] );
$params['text'] = $this->replace_text_mailing_tracking_urls( $params['text'] );
}
return $params;
}
/**
* Replace html mailing tracking urls.
*
* @since 0.1
* @param string $contnet The mailing content
* @return string $content The mailing content
*/
public function replace_html_mailing_tracking_urls( string $content ) {
$doc = \phpQuery::newDocument( $content );
foreach ( $doc[ '[href*="civicrm/extern/url.php"], [src*="civicrm/extern/open.php"]' ] as $element ) {
$href = pq( $element )->attr( 'href' );
$src = pq( $element )->attr( 'src' );
// replace extern/url
if ( strpos( $href, 'civicrm/extern/url.php' ) ) {
$query_string = strstr( $href, '?' );
pq( $element )->attr( 'href', $this->url_endpoint . $query_string );
}
// replace extern/open
if ( strpos( $src, 'civicrm/extern/open.php' ) ) {
$query_string = strstr( $src, '?' );
pq( $element )->attr( 'src', $this->open_endpoint . $query_string );
}
unset( $href, $src, $query_string );
}
return $doc->html();
}
/**
* Replace text mailing tracking urls.
*
* @since 0.1
* @param string $contnet The mailing content
* @return string $content The mailing content
*/
public function replace_text_mailing_tracking_urls( string $content ) {
// replace extern url
$content = preg_replace( '/http.*civicrm\/extern\/url\.php/i', $this->url_endpoint, $content );
// replace open url
$content = preg_replace( '/http.*civicrm\/extern\/open\.php/i', $this->open_endpoint, $content );
return $content;
}
}
<?php
/**
* AuthorizeIPN controller class.
*
* Replacement for CiviCRM's 'extern/authorizeIPN.php'.
*
* @see https://docs.civicrm.org/sysadmin/en/latest/setup/payment-processors/authorize-net/#shell-script-testing-method
*
* @since 0.1
*/
namespace CiviCRM_WP_REST\Controller;
class AuthorizeIPN extends Base {
/**
* The base route.
*
* @since 0.1
* @var string
*/
protected $rest_base = 'authorizeIPN';
/**
* Registers routes.
*
* @since 0.1
*/
public function register_routes() {
register_rest_route( $this->get_namespace(), $this->get_rest_base(), [
[
'methods' => \WP_REST_Server::ALLMETHODS,
'callback' => [ $this, 'get_item' ]
]
] );
}
/**
* Get items.
*
* @since 0.1
* @param WP_REST_Request $request
*/
public function get_item( $request ) {
/**
* Filter request params.
*
* @since 0.1
* @param array $params
* @param WP_REST_Request $request
*/
$params = apply_filters( 'civi_wp_rest/controller/authorizeIPN/params', $request->get_params(), $request );
$authorize_IPN = new \CRM_Core_Payment_AuthorizeNetIPN( $params );
// log notification
\Civi::log()->alert( 'payment_notification processor_name=AuthNet', $params );
/**
* Filter AuthorizeIPN object.
*
* @param CRM_Core_Payment_AuthorizeNetIPN $authorize_IPN
* @param array $params
* @param WP_REST_Request $request
*/
$authorize_IPN = apply_filters( 'civi_wp_rest/controller/authorizeIPN/instance', $authorize_IPN, $params, $request );
try {
if ( ! method_exists( $authorize_IPN, 'main' ) || ! $this->instance_of_crm_base_ipn( $authorize_IPN ) )
return $this->civi_rest_error( get_class( $authorize_IPN ) . ' must implement a "main" method.' );
$result = $authorize_IPN->main();
} catch ( \CRM_Core_Exception $e ) {
\Civi::log()->error( $e->getMessage() );
\Civi::log()->error( 'error data ', [ 'data' => $e->getErrorData() ] );
\Civi::log()->error( 'REQUEST ', [ 'params' => $params ] );
return $this->civi_rest_error( $e->getMessage() );
}
return rest_ensure_response( $result );
}
/**
* Checks whether object is an instance of CRM_Core_Payment_AuthorizeNetIPN or CRM_Core_Payment_BaseIPN.
*
* Needed because the instance is being filtered through 'civi_wp_rest/controller/authorizeIPN/instance'.
*
* @since 0.1
* @param CRM_Core_Payment_AuthorizeNetIPN|CRM_Core_Payment_BaseIPN $object
* @return bool
*/
public function instance_of_crm_base_ipn( $object ) {
return $object instanceof \CRM_Core_Payment_BaseIPN || $object instanceof \CRM_Core_Payment_AuthorizeNetIPN;
}
/**
* Item schema.
*
* @since 0.1
* @return array $schema
*/
public function get_item_schema() {}
/**
* Item arguments.
*
* @since 0.1
* @return array $arguments
*/
public function get_item_args() {}
}
<?php
/**
* Base controller class.
*
* @since 0.1
*/
namespace CiviCRM_WP_REST\Controller;
use CiviCRM_WP_REST\Endpoint\Endpoint_Interface;
abstract class Base extends \WP_REST_Controller implements Endpoint_Interface {
/**
* Route namespace.
*
* @since 0.1
* @var string
*/
protected $namespace = 'civicrm/v3';
/**
* Gets the endpoint namespace.
*
* @since 0.1
* @return string $namespace
*/
public function get_namespace() {
return $this->namespace;
}
/**
* Gets the rest base route.
*
* @since 0.1
* @return string $rest_base
*/
public function get_rest_base() {
return '/' . $this->rest_base;
}
/**
* Retrieves the endpoint ie. '/civicrm/v3/rest'.
*
* @since 0.1
* @return string $rest_base
*/
public function get_endpoint() {
return '/' . $this->get_namespace() . $this->get_rest_base();
}
/**
* Checks whether the requested route is equal to this endpoint.
*
* @since 0.1
* @param WP_REST_Request $request
* @return bool $is_current_endpoint True if it's equal, false otherwise
*/
public function is_current_endpoint( $request ) {
return $this->get_endpoint() == $request->get_route();
}
/**
* Authorization status code.
*
* @since 0.1
* @return int $status
*/
protected function authorization_status_code() {
$status = 401;
if ( is_user_logged_in() ) $status = 403;
return $status;
}
/**
* Wrapper for WP_Error.
*
* @since 0.1
* @param string $message
* @param mixed $data Error data
* @return WP_Error $error
*/
protected function civi_rest_error( $message, $data = [] ) {
return new \WP_Error( 'civicrm_rest_api_error', $message, empty( $data ) ? [ 'status' => $this->authorization_status_code() ] : $data );
}
}
<?php
/**
* Cxn controller class.
*
* CiviConnect endpoint, replacement for CiviCRM's 'extern/cxn.php'.
*
* @since 0.1
*/
namespace CiviCRM_WP_REST\Controller;
class Cxn extends Base {
/**
* The base route.
*
* @since 0.1
* @var string
*/
protected $rest_base = 'cxn';
/**
* Registers routes.
*
* @since 0.1
*/
public function register_routes() {
register_rest_route( $this->get_namespace(), $this->get_rest_base(), [
[
'methods' => \WP_REST_Server::ALLMETHODS,
'callback' => [ $this, 'get_item' ]
]
] );
}
/**
* Get items.
*
* @since 0.1
* @param WP_REST_Request $request
*/
public function get_item( $request ) {
/**
* Filter request params.
*
* @since 0.1
* @param array $params
* @param WP_REST_Request $request
*/
$params = apply_filters( 'civi_wp_rest/controller/cxn/params', $request->get_params(), $request );
// init connection server
$cxn = \CRM_Cxn_BAO_Cxn::createApiServer();
/**
* Filter connection server object.
*
* @param Civi\Cxn\Rpc\ApiServer $cxn
* @param array $params
* @param WP_REST_Request $request
*/
$cxn = apply_filters( 'civi_wp_rest/controller/cxn/instance', $cxn, $params, $request );
try {
$result = $cxn->handle( $request->get_body() );
} catch ( Civi\Cxn\Rpc\Exception\CxnException $e ) {
return $this->civi_rest_error( $e->getMessage() );
} catch ( Civi\Cxn\Rpc\Exception\ExpiredCertException $e ) {
return $this->civi_rest_error( $e->getMessage() );
} catch ( Civi\Cxn\Rpc\Exception\InvalidCertException $e ) {
return $this->civi_rest_error( $e->getMessage() );
} catch ( Civi\Cxn\Rpc\Exception\InvalidMessageException $e ) {
return $this->civi_rest_error( $e->getMessage() );
} catch ( Civi\Cxn\Rpc\Exception\GarbledMessageException $e ) {
return $this->civi_rest_error( $e->getMessage() );
}
/**
* Bypass WP and send request from Cxn.
*/
add_filter( 'rest_pre_serve_request', function( $served, $response, $request, $server ) use ( $result ) {
// Civi\Cxn\Rpc\Message->send()
$result->send();
return true;
}, 10, 4 );
return rest_ensure_response( $result );
}
/**
* Item schema.
*
* @since 0.1
* @return array $schema
*/
public function get_item_schema() {}
/**
* Item arguments.
*
* @since 0.1
* @return array $arguments
*/
public function get_item_args() {}
}
<?php
/**
* Open controller class.
*
* @since 0.1
*/
namespace CiviCRM_WP_REST\Controller;
class Open extends Base {
/**
* The base route.
*
* @since 0.1
* @var string
*/
protected $rest_base = 'open';
/**
* Registers routes.
*
* @since 0.1
*/
public function register_routes() {
register_rest_route( $this->get_namespace(), $this->get_rest_base(), [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'args' => $this->get_item_args()
],
'schema' => [ $this, 'get_item_schema' ]
] );
}
/**
* Get item.
*
* @since 0.1
* @param WP_REST_Request $request
*/
public function get_item( $request ) {
$queue_id = $request->get_param( 'q' );
// track open
\CRM_Mailing_Event_BAO_Opened::open( $queue_id );
// serve tracker file
add_filter( 'rest_pre_serve_request', [ $this, 'serve_tracker_file' ], 10, 4 );
}
/**
* Serves the tracker gif file.
*
* @since 0.1
* @param bool $served Whether the request has been served