File: /home/slfopp7cb1df/public_html/shaneconrad.me/wp-content/plugins/zapier/trunk/zapier.php
<?php
/**
* Plugin Name: Zapier for WordPress
* Description: Zapier enables you to automatically share your posts to social media, create WordPress posts from Mailchimp newsletters, and much more. Visit https://zapier.com/apps/wordpress/integrations for more details.
* Version: 1.5.3
* Author: Zapier
* Author URI: https://zapier.com
* License: Expat (MIT License)
* License URI: https://spdx.org/licenses/MIT.html
*/
require_once dirname(__FILE__) . '/vendor/autoload.php';
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
class Zapier_Auth_Loader
{
protected $actions;
protected $filters;
public function __construct()
{
$this->actions = array();
$this->filters = array();
}
public function add_plugin_action($hook, $component, $callback, $priority = 10, $accepted_args = 1)
{
$this->actions = $this->add($this->actions, $hook, $component, $callback, $priority, $accepted_args);
}
public function add_plugin_filter($hook, $component, $callback, $priority = 10, $accepted_args = 1)
{
$this->filters = $this->add($this->filters, $hook, $component, $callback, $priority, $accepted_args);
}
private function add($hooks, $hook, $component, $callback, $priority, $accepted_args)
{
$hooks[] = array(
'hook' => $hook,
'component' => $component,
'callback' => $callback,
'priority' => $priority,
'accepted_args' => $accepted_args,
);
return $hooks;
}
public function run()
{
foreach ($this->filters as $hook) {
add_filter($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
}
foreach ($this->actions as $hook) {
add_action($hook['hook'], array($hook['component'], $hook['callback']), $hook['priority'], $hook['accepted_args']);
}
}
}
class Zapier_Auth
{
private $error = null;
protected $namespace;
protected $loader;
public function __construct()
{
$this->namespace = 'zapier/v1';
$this->loader = new Zapier_Auth_Loader();
$this->define_public_hooks();
}
private function define_public_hooks()
{
$this->loader->add_plugin_action('rest_api_init', $this, 'add_api_routes');
$this->loader->add_plugin_filter('rest_pre_dispatch', $this, 'rest_pre_dispatch');
$this->loader->add_plugin_filter('determine_current_user', $this, 'determine_current_user');
// Webhooks
$this->loader->add_plugin_action('wp_update_user', $this, 'updated_user');
$this->loader->add_plugin_action('post_updated', $this, 'updated_post', 10, 3);
}
public function run()
{
$this->loader->run();
}
/**
* Verify user has proper authorization for webhook management
*
* @param WP_REST_Request $request
* @return bool|WP_Error
*/
private function verify_webhook_authorization($request)
{
// Check if user is logged in
if (!is_user_logged_in()) {
return new WP_Error(
'not_logged_in',
'You are not logged in',
array('status' => 401)
);
}
// Check if user has proper capabilities - only administrators should manage webhooks
if (!current_user_can('manage_options')) {
return new WP_Error(
'insufficient_permissions',
'You do not have sufficient permissions to manage webhooks',
array('status' => 403)
);
}
// CSRF protection: Different approaches for different auth methods
$is_jwt_auth = isset($_SERVER['HTTP_X_ZAPIER_AUTH']) && isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'Zapier';
if ($is_jwt_auth) {
// For JWT: Verify the token includes proper origin validation
// JWT tokens should only be used by Zapier's servers, not browsers
// The User-Agent check provides additional CSRF protection
if (!isset($_SERVER['HTTP_USER_AGENT']) || $_SERVER['HTTP_USER_AGENT'] !== 'Zapier') {
return new WP_Error(
'invalid_user_agent',
'Invalid request source',
array('status' => 403)
);
}
} else {
// For browser-based requests: Use WordPress nonce
$nonce = $request->get_header('X-WP-Nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error(
'invalid_nonce',
'Invalid security token',
array('status' => 403)
);
}
}
return true;
}
/**
* Verify user authorization for read-only operations
*
* @param WP_REST_Request $request
* @return bool|WP_Error
*/
private function verify_read_authorization($request)
{
// Check if user is logged in
if (!is_user_logged_in()) {
return new WP_Error(
'not_logged_in',
'You are not logged in',
array('status' => 401)
);
}
// For read operations, we can allow users with edit_posts capability
if (!current_user_can('edit_posts')) {
return new WP_Error(
'insufficient_permissions',
'You do not have sufficient permissions to access this resource',
array('status' => 403)
);
}
return true;
}
public function add_api_routes()
{
register_rest_route($this->namespace, '/token', array(
'methods' => "POST",
'callback' => array($this, 'generate_token'),
'permission_callback' => '__return_true'
));
register_rest_route($this->namespace, '/(?P<type>[a-zA-Z0-9_-]+)/supports', array(
'methods' => "GET",
'callback' => array($this, 'get_custom_type_supports'),
'permission_callback' => array($this, 'check_read_permission')
));
register_rest_route($this->namespace, '/roles', array(
'methods' => "GET",
'callback' => array($this, 'get_roles'),
'permission_callback' => array($this, 'check_read_permission')
));
register_rest_route($this->namespace, '/webhook', array(
'methods' => "POST",
'callback' => array($this, 'add_webhook'),
'permission_callback' => array($this, 'check_webhook_permission')
));
register_rest_route($this->namespace, '/webhook', array(
'methods' => "DELETE",
'callback' => array($this, 'remove_webhook'),
'permission_callback' => array($this, 'check_webhook_permission')
));
}
/**
* Permission callback for webhook operations
*/
public function check_webhook_permission($request)
{
$auth_result = $this->verify_webhook_authorization($request);
return !is_wp_error($auth_result);
}
/**
* Permission callback for read operations
*/
public function check_read_permission($request)
{
// Check if user is logged in (Application Password authentication should work here)
if (!is_user_logged_in()) {
return false;
}
// For read operations, allow users with edit_posts capability (editors and admins)
if (!current_user_can('edit_posts')) {
return false;
}
return true;
}
public function generate_token($request)
{
$secret_key = get_option('zapier_secret');
$username = $request->get_param('username');
$password = $request->get_param('password');
$user = wp_authenticate($username, $password);
if (is_wp_error($user)) {
$error_code = $user->get_error_code();
return new WP_Error(
$error_code,
$user->get_error_message($error_code),
array(
'status' => 401,
)
);
}
$issuedAt = time();
$token = array(
'iss' => get_bloginfo('url'),
'iat' => $issuedAt,
'nbf' => $issuedAt,
'exp' => $issuedAt + (60 * 20),
'data' => array(
'user_id' => $user->data->ID,
),
);
return array(
'token' => JWT::encode($token, $secret_key, 'HS256'),
);
}
public function get_custom_type_supports($request)
{
// Authorization is handled by permission_callback
$type = $request['type'];
$types = get_post_types(array());
if(!in_array($type, $types)) {
return new WP_Error(
'invalid_post_type',
'Invalid post type',
array(
'status' => 404,
)
);
}
return array('supports' => get_all_post_type_supports($type));
}
public function get_roles($request = null)
{
// Authorization is handled by permission_callback
$roles = array();
foreach (wp_roles()->roles as $key => $role) {
$roles[] = (array('id' => $key, 'name' => $role['name']));
}
return array('roles' => $roles);
}
public function add_webhook($request) {
// Authorization is handled by permission_callback
$auth_result = $this->verify_webhook_authorization($request);
if (is_wp_error($auth_result)) {
return $auth_result;
}
$ALLOWED_ACTIONS = array('wp_update_user','post_updated');
$action = $request->get_param("action");
$endpoint_url = $request->get_param("endpoint_url");
if(!in_array($action, $ALLOWED_ACTIONS)) {
return new WP_Error(
'invalid_action',
'Invalid action',
array(
'status' => 400,
)
);
}
if(empty($endpoint_url)) {
return new WP_Error(
'invalid_endpoint_url',
'Invalid endpoint url',
array(
'status' => 400,
)
);
}
// Enhanced URL validation
if (!$this->is_safe_url($endpoint_url)) {
return new WP_Error(
'unsafe_endpoint_url',
'The provided endpoint URL is not allowed for security reasons',
array(
'status' => 400,
)
);
}
$option_key = "zapier_hooks_$action";
$hooks = get_option($option_key, []);
if(!in_array($endpoint_url, $hooks)) {
$hooks[] = $endpoint_url;
update_option($option_key, $hooks);
}
return array('success' => true);
}
public function remove_webhook($request) {
// Authorization is handled by permission_callback
$auth_result = $this->verify_webhook_authorization($request);
if (is_wp_error($auth_result)) {
return $auth_result;
}
$action = $request->get_param("action");
$endpoint_url = $request->get_param("endpoint_url");
$option_key = "zapier_hooks_$action";
$hooks = get_option($option_key, []);
if(($key = array_search($endpoint_url, $hooks)) !== false) {
unset($hooks[$key]);
update_option($option_key, $hooks);
}
return array('success' => true);
}
public function updated_user($user_id) {
$option_key = "zapier_hooks_wp_update_user";
$hooks = get_option($option_key, []);
foreach ($hooks as $hook) {
// Validate the URL
if (!$this->is_safe_url($hook)) {
continue; // Skip unsafe URLs
}
// Use wp_safe_remote_post to ensure secure requests
$response = wp_safe_remote_post($hook, [
'body' => json_encode(['user_id' => $user_id]),
'headers' => ['Content-Type' => 'application/json'],
]);
}
}
public function updated_post($post_id, $post_after, $post_before) {
$option_key = "zapier_hooks_post_updated";
$rest_base = get_post_type_object($post_after->post_type)->rest_base;
$changed_properties = $this->compareObjects($post_after, $post_before);
$hooks = get_option($option_key, []);
foreach($hooks as $hook) {
$response = wp_remote_post($hook, array(
'body' => json_encode(array(
'post_id' => $post_id,
'rest_base' => $rest_base,
'post_after_status' => $post_after->post_status,
'post_before_status' => $post_before->post_status,
'post_changed_properties' => $changed_properties
)),
'headers' => array('Content-Type' => 'application/json'),
));
}
}
public function get_user_from_token()
{
try {
JWT::$leeway = 240; // $leeway in seconds
$token = JWT::decode(
$_SERVER['HTTP_X_ZAPIER_AUTH'],
new Key(get_option('zapier_secret'), 'HS256')
);
if ($token->iss != get_bloginfo('url')) {
$this->error = new WP_Error(
'bad_issuer',
'The issuer does not match with this server',
array(
'status' => 401,
)
);
} elseif (!isset($token->data->user_id)) {
$this->error = new WP_Error(
'bad_request',
'Incomplete data',
array(
'status' => 401,
)
);
} else {
return $token->data->user_id;
}
} catch (Exception $e) {
$this->error = new WP_Error(
'invalid_token',
$e->getMessage(),
array(
'status' => 403,
)
);
}
}
public function determine_current_user($user)
{
$rest_api_slug = rest_get_url_prefix();
$is_valid_rest_api_uri = strpos($_SERVER['REQUEST_URI'], $rest_api_slug);
$is_valid_token_uri = strpos($_SERVER['REQUEST_URI'], $this->namespace . '/token');
$is_zapier_request = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] === 'Zapier' && isset($_SERVER['HTTP_X_ZAPIER_AUTH']);
if ($is_zapier_request && $is_valid_rest_api_uri && !$is_valid_token_uri) {
$user_id = $this->get_user_from_token();
if ($user_id) {
return $user_id;
}
}
return $user;
}
public function rest_pre_dispatch($request)
{
if (is_wp_error($this->error)) {
return $this->error;
}
return $request;
}
private function compareObjects($obj1, $obj2) {
$reflect1 = new ReflectionClass($obj1);
$reflect2 = new ReflectionClass($obj2);
if ($reflect1->getName() !== $reflect2->getName()) {
throw new Exception('Objects must be instances of the same class');
}
$properties1 = $reflect1->getProperties();
$changed_properties = [];
foreach ($properties1 as $property) {
// Make property accessible if it's private or protected
$property->setAccessible(true);
$value1 = $property->getValue($obj1);
$value2 = $property->getValue($obj2);
if ($value1 !== $value2) {
$changed_properties[] = str_replace('post_', '', $property->getName());
}
}
return $changed_properties;
}
// Helper function to ensure only safe URLs are used
private function is_safe_url($url) {
// Validate URL format
if (!wp_http_validate_url($url)) {
return false;
}
// Ensure the URL is not internal (localhost, private IPs)
if ($this->is_internal_url($url)) {
return false;
}
// Allow only sanctioned domains (modify this list as needed)
$allowed_domains = ['zapier.com', 'hooks.zapier.com'];
$parsed_url = parse_url($url);
if (!isset($parsed_url['host'])) {
return false;
}
foreach ($allowed_domains as $allowed_domain) {
if (str_ends_with($parsed_url['host'], $allowed_domain)) {
return true;
}
}
return false;
}
// Helper function to block internal/private addresses
private function is_internal_url($url) {
$parsed = parse_url($url);
if (!$parsed || empty($parsed['host'])) {
return true;
}
$ip = gethostbyname($parsed['host']);
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
}
}
register_activation_hook(__FILE__, 'zapier_add_secret_key');
register_deactivation_hook(__FILE__, 'zapier_delete_secret_key');
function zapier_add_secret_key() {
// the resulting value for the zapier_secret is 256 in length
add_option('zapier_secret', bin2hex(random_bytes(128)));
}
function zapier_delete_secret_key() {
delete_option('zapier_secret');
}
$plugin = new Zapier_Auth();
$plugin->run();