This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1,645 @@
<?php
namespace Gravity_Forms\Gravity_Tools\API;
use Gravity_Forms\Gravity_Tools\Model\Form_Model;
use Gravity_Forms\Gravity_Tools\Utils\Common;
use WP_Error;
if ( ! defined( 'GRAVITY_API_URL' ) ) {
define( 'GRAVITY_API_URL', 'https://gravityapi.com/wp-json/gravityapi/v1' );
}
/**
* Client-side API wrapper for interacting with the Gravity APIs.
*
* @package Gravity Tools
* @since 1.0
*/
class Gravity_Api {
/**
* @var Common
*/
protected $common;
/**
* @var Form_Model
*/
protected $model;
/**
* @var string
*/
protected $namespace;
/**
* Constructor.
*
* @since 1.0
*
* @param $common
* @param $model
* @param $namespace
*/
public function __construct( $common, $model, $namespace ) {
$this->common = $common;
$this->model = $model;
$this->namespace = $namespace;
}
/**
* Retrieves site key and site secret key from remote API and stores them as WP options. Returns false if license key is invalid; otherwise, returns true.
*
* @since 1.0
*
* @param string $license_key License key to be registered.
* @param boolean $is_md5 Specifies if $license_key provided is an MD5 or unhashed license key.
*
* @return bool|WP_Error
*/
public function register_current_site( $license_key, $is_md5 = false ) {
$body = array();
$body['site_name'] = get_bloginfo( 'name' );
$body['site_url'] = get_bloginfo( 'url' );
if ( $is_md5 ) {
$body['license_key_md5'] = $license_key;
} else {
$body['license_key'] = $license_key;
}
$result = $this->request( 'sites', $body, 'POST', array( 'headers' => $this->get_license_auth_header( $license_key ) ) );
$result = $this->prepare_response_body( $result, true );
if ( is_wp_error( $result ) ) {
return $result;
}
update_option( 'gf_site_key', $result['key'] );
update_option( 'gf_site_secret', $result['secret'] );
return true;
}
/**
* Updates license key for a site that has already been registered.
*
* @since 1.0
*
* @param string $new_license_key_md5 Hash license key to be updated
*
* @return \Gravity_Forms\Gravity_Tools\License\License_API_Response|WP_Error
*/
public function update_current_site( $new_license_key_md5 ) {
$site_key = $this->get_site_key();
$site_secret = $this->get_site_secret();
if ( empty( $site_key ) || empty( $site_secret ) ) {
return false;
}
$body = $this->get_remote_post_params();
$body['site_name'] = get_bloginfo( 'name' );
$body['site_url'] = get_bloginfo( 'url' );
$body['site_key'] = $site_key;
$body['site_secret'] = $site_secret;
$body['license_key_md5'] = $new_license_key_md5;
$result = $this->request( 'sites/' . $site_key, $body, 'PUT', array( 'headers' => $this->get_site_auth_header( $site_key, $site_secret ) ) );
$result = $this->prepare_response_body( $result, true );
if ( is_wp_error( $result ) ) {
return $result;
}
return $result;
}
/***
* Removes a license key from a registered site. NOTE: It doesn't actually deregister the site.
*
* @deprecated Use gapi()->update_current_site('') instead.
*
* @return bool|WP_Error
*/
public function deregister_current_site() {
$site_key = $this->get_site_key();
$site_secret = $this->get_site_secret();
if ( empty( $site_key ) ) {
return false;
}
$body = array(
'license_key_md5' => '',
);
$result = $this->request( 'sites/' . $site_key, $body, 'PUT', array( 'headers' => $this->get_site_auth_header( $site_key, $site_secret ) ) );
$result = $this->prepare_response_body( $result, true );
if ( is_wp_error( $result ) ) {
return $result;
}
return true;
}
/**
* Check the given license key to get its information from the API.
*
* @since 1.0
*
* @param string $key The license key.
*
* @return array|false|WP_Error
*/
public function check_license( $key ) {
$params = array(
'site_url' => get_option( 'home' ),
'is_multisite' => is_multisite(),
);
/**
* Allow the params passed to check_license to be modified before sending.
*
* @since 1.0
*/
$params = apply_filters( 'gravity_api_check_license_params', $params );
$resource = 'licenses/' . $key . '/check?' . build_query( $params );
$result = $this->request( $resource, null );
$result = $this->prepare_response_body( $result, true );
if ( is_wp_error( $result ) ) {
return $result;
}
$response = $result;
if ( $this->common->rgar( $result, 'license' ) ) {
$response = $this->common->rgar( $result, 'license' );
}
// Set the license object to the transient.
set_transient( 'rg_gforms_license', $response, DAY_IN_SECONDS );
return $response;
}
/**
* Get GF core and add-on family information.
*
* @since 1.0
*
* @return false|array
*/
public function get_plugins_info() {
$version_info = $this->get_version_info();
if ( empty( $version_info['offerings'] ) ) {
return false;
}
return $version_info['offerings'];
}
/**
* Get version information from the Gravity Manager API.
*
* @since 1.0
*
* @param false $cache
*
* @return array
*/
private function get_version_info( $cache = false ) {
$version_info = null;
if ( $cache ) {
$cache_key = sprintf( '%s_version_info', $this->namespace );
$cached_info = get_option( $cache_key );
// Checking cache expiration
$cache_duration = DAY_IN_SECONDS; // 24 hours.
$cache_timestamp = $cached_info && isset( $cached_info['timestamp'] ) ? $cached_info['timestamp'] : 0;
// Is cache expired? If not, set $version_info to the cached data.
if ( $cache_timestamp + $cache_duration >= time() ) {
$version_info = $cached_info;
}
}
if ( is_wp_error( $version_info ) || isset( $version_info['headers'] ) ) {
// Legacy ( < 2.1.1.14 ) version info contained the whole raw response.
$version_info = null;
}
// If we reach this point with a $version_info array, it's from cache, and we can return it.
if ( $version_info ) {
return $version_info;
}
//Getting version number
$options = array(
'method' => 'POST',
'timeout' => 20,
);
$options['headers'] = array(
'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ),
);
$options['body'] = $this->get_remote_post_params();
$options['timeout'] = 15;
$nocache = $cache ? '' : 'nocache=1'; //disabling server side caching
$raw_response = $this->common->post_to_manager( 'version.php', $nocache, $options );
$version_info = array(
'is_valid_key' => '1',
'version' => '',
'url' => '',
'is_error' => '1',
);
if ( is_wp_error( $raw_response ) || $this->common->rgars( $raw_response, 'response/code' ) != 200 ) {
$version_info['timestamp'] = time();
return $version_info;
}
$decoded = json_decode( $raw_response['body'], true );
if ( empty( $decoded ) ) {
$version_info['timestamp'] = time();
return $version_info;
}
$decoded['timestamp'] = time();
// Caching response.
$cache_key = sprintf( '%s_version_info', $this->namespace );
update_option( $cache_key, $decoded, false ); //caching version info
return $decoded;
}
/**
* Get the parameters to use in a remote request.
*
* @since 1.0
*
* @return array
*/
public function get_remote_post_params() {
// This can get called in contexts where this file isn't loaded. Require it here to avoid fatals.
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
global $wpdb;
$plugin_list = get_plugins();
$plugins = array();
$active_plugins = get_option( 'active_plugins' );
foreach ( $plugin_list as $key => $plugin ) {
$is_active = in_array( $key, $active_plugins );
$slug = substr( $key, 0, strpos( $key, '/' ) );
if ( empty( $slug ) ) {
$slug = str_replace( '.php', '', $key );
}
$plugins[] = array(
'name' => str_replace( 'phpinfo()', 'PHP Info', $plugin['Name'] ),
'slug' => $slug,
'version' => $plugin['Version'],
'is_active' => $is_active,
);
}
$plugins = json_encode( $plugins );
//get theme info
$theme = wp_get_theme();
$theme_name = $theme->get( 'Name' );
$theme_uri = $theme->get( 'ThemeURI' );
$theme_version = $theme->get( 'Version' );
$theme_author = $theme->get( 'Author' );
$theme_author_uri = $theme->get( 'AuthorURI' );
$im = is_multisite();
$lang = get_locale();
$post = array(
'plugins' => $plugins,
'tn' => $theme_name,
'tu' => $theme_uri,
'tv' => $theme_version,
'ta' => $theme_author,
'tau' => $theme_author_uri,
'im' => $im,
'lang' => $lang,
);
/**
* Allows the remote post parameters to be filtered to add more data.
*
* @since 1.0
*
* @param array $post The current data array.
*
* @return array
*/
return apply_filters( 'gravity_api_remote_post_params', $post );;
}
/**
* Update the usage data (call version.php in Gravity Manager). We will replace it once we have statistics API endpoints.
*
* @since 1.0
*/
public function update_site_data() {
// Whenever we update the plugins info, we call the versions.php to update usage data.
$options = array( 'method' => 'POST' );
$options['headers'] = array(
'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ),
'Referer' => get_bloginfo( 'url' ),
);
$options['body'] = $this->common->get_remote_post_params();
// Set the version to 3 which lightens the burden of version.php, it won't return anything to us anymore.
$options['body']['version'] = '3';
$options['timeout'] = 15;
$nocache = 'nocache=1'; //disabling server side caching
$this->common->post_to_manager( 'version.php', $nocache, $options );
}
/**
* Send an email to Hubspot to add to the list.
*
* @since 1.0
*
* @param $email
*
* @return mixed
*/
public function send_email_to_hubspot( $email ) {
$body = array(
'email' => $email,
);
$result = $this->request( 'emails/installation/add-to-list', $body, 'POST', array( 'headers' => $this->get_license_info_header( $site_secret ) ) );
$result = $this->prepare_response_body( $result, true );
if ( is_wp_error( $result ) ) {
return $result;
}
return true;
}
// # HELPERS
/**
* Get the stored license key.
*
* @since 1.0
*
* @return mixed
*/
public function get_key() {
return $this->common->get_key();
}
/**
* Get the site auth header.
*
* @since 1.0
*
* @param $site_key
* @param $site_secret
*
* @return string[]
*/
private function get_site_auth_header( $site_key, $site_secret ) {
$auth = base64_encode( "{$site_key}:{$site_secret}" );
return array( 'Authorization' => 'GravityAPI ' . $auth );
}
/**
* Get the license info header.
*
* @since 1.0
*
* @param $site_secret
*
* @return string[]
*/
private function get_license_info_header( $site_secret ) {
$auth = base64_encode( "gravityforms.com:{$site_secret}" );
return array( 'Authorization' => 'GravityAPI ' . $auth );
}
/**
* Get the license auth header.
*
* @since 1.0
*
* @param $license_key_md5
*
* @return string[]
*/
private function get_license_auth_header( $license_key_md5 ) {
$auth = base64_encode( "license:{$license_key_md5}" );
return array( 'Authorization' => 'GravityAPI ' . $auth );
}
/**
* Prepare response body.
*
* @since 1.0
*
* @param WP_Error|WP_REST_Response $raw_response The API response.
* @param bool $as_array Whether to return the response as an array or object.
*
* @return array|object|WP_Error
*/
public function prepare_response_body( $raw_response, $as_array = false ) {
if ( is_wp_error( $raw_response ) ) {
return $raw_response;
}
$response_body = json_decode( wp_remote_retrieve_body( $raw_response ), $as_array );
$response_code = wp_remote_retrieve_response_code( $raw_response );
$response_message = wp_remote_retrieve_response_message( $raw_response );
if ( $response_code > 200 ) {
// If a WP_Error was returned in the body.
if ( $this->common->rgar( $response_body, 'code' ) ) {
// Restore the WP_Error.
$error = new WP_Error( $response_body['code'], $response_body['message'], $response_body['data'] );
} else {
$error = new WP_Error( 'server_error', 'Error from server: ' . $response_message );
}
return $error;
}
return $response_body;
}
/**
* Purge the site credentials.
*
* @since 1.0
*
* @return void
*/
public function purge_site_credentials() {
delete_option( 'gf_site_key' );
delete_option( 'gf_site_secret' );
delete_option( 'gf_site_registered' );
}
/**
* Making API requests.
*
* @since 1.0
*
* @param string $resource The API route.
* @param array $body The request body.
* @param string $method The method.
* @param array $options The options.
*
* @return array|WP_Error
*/
public function request( $resource, $body, $method = 'POST', $options = array() ) {
$body['timestamp'] = time();
// set default options
$options = wp_parse_args( $options, array(
'method' => $method,
'timeout' => 10,
'body' => in_array( $method, array( 'GET', 'DELETE' ) ) ? null : json_encode( $body ),
'headers' => array(),
'sslverify' => false,
) );
// set default header options
$options['headers'] = wp_parse_args( $options['headers'], array(
'Content-Type' => 'application/json; charset=' . get_option( 'blog_charset' ),
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ),
'Referer' => get_bloginfo( 'url' ),
) );
// WP docs say method should be uppercase
$options['method'] = strtoupper( $options['method'] );
$request_url = $this->get_gravity_api_url() . $resource;
return wp_remote_request( $request_url, $options );
}
/**
* Get the site key.
*
* @since 1.0
*
* @return false|mixed|void
*/
public function get_site_key() {
if ( defined( 'GRAVITY_API_SITE_KEY' ) ) {
return GRAVITY_API_SITE_KEY;
}
$site_key = get_option( 'gf_site_key' );
if ( empty( $site_key ) ) {
return false;
}
return $site_key;
}
/**
* Get the site secret.
*
* @since 1.0
*
* @return false|mixed|void
*/
public function get_site_secret() {
if ( defined( 'GRAVITY_API_SITE_SECRET' ) ) {
return GRAVITY_API_SITE_SECRET;
}
$site_secret = get_option( 'gf_site_secret' );
if ( empty( $site_secret ) ) {
return false;
}
return $site_secret;
}
/**
* Get the gravity URL.
*
* @since 1.0
*
* @return string
*/
public function get_gravity_api_url() {
return trailingslashit( GRAVITY_API_URL );
}
/**
* Check if the site has the gf_site_key and gf_site_secret options.
*
* @since 1.0
*
* @return bool
*/
public function is_site_registered() {
return $this->get_site_key() && $this->get_site_secret();
}
/**
* Check if the site has the gf_site_key, gf_site_secret and also the gf_site_registered options.
*
* @since 1.0
*
* @return bool
*/
public function is_legacy_registration() {
return $this->is_site_registered() && ! get_option( 'gf_site_registered' );
}
}
@@ -0,0 +1,96 @@
<?php
namespace Gravity_Forms\Gravity_Tools\API;
use Gravity_Forms\Gravity_Tools\Data\Oauth_Data_Handler;
abstract class Oauth_Handler {
protected $supports_refresh_token = false;
protected $response_payload_name = 'auth_payload';
protected $payload_access_token_name = 'access_token';
protected $payload_refresh_token_name = 'refresh_token';
protected $namespace = '';
/**
* @var Oauth_Data_Handler $data
*/
protected $data;
public function __construct( $data ) {
$this->data = $data;
}
public function handle_response() {
if ( ! $this->is_response() ) {
return;
}
$payload = filter_input( INPUT_POST, $this->response_payload_name );
if ( is_string( $payload ) ) {
$payload = json_decode( $payload, true );
}
if ( empty( $payload ) ) {
return;
}
if ( isset( $payload[ $this->payload_access_token_name ] ) ) {
$this->store_access_token( $payload[ $this->payload_access_token_name ] );
}
if ( isset( $payload[ $this->payload_refresh_token_name ] ) ) {
$this->store_refresh_token( $payload[ $this->payload_refresh_token_name ] );
}
}
public function get_access_token() {
$token = $this->data->get( 'access_token', $this->namespace );
if ( $this->is_valid_token( $token ) ) {
return $token;
}
if ( ! $this->supports_refresh_token ) {
return new \WP_Error( __( 'Token is invalid or expired. Please re-connect.', 'gravity-tools' ) );
}
return $this->refresh_expired_token();
}
public function get_refresh_token() {
return $this->data->get( 'refresh_token', $this->namespace );
}
protected function refresh_expired_token() {
return null;
}
protected function store_access_token( $token ) {
$this->data->save( 'access_token', $token, $this->namespace );
}
protected function store_refresh_token( $refresh_token ) {
$this->data->save( 'refresh_token', $refresh_token, $this->namespace );
}
protected function is_response() {
return false;
}
abstract public function get_return_url();
abstract public function get_oauth_url();
abstract public function get_refresh_url();
abstract protected function is_valid_token( $token );
abstract public function get_connection_details();
}
@@ -0,0 +1,99 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Apps;
use Gravity_Forms\Gravity_Tools\Providers\Config_Collection_Service_Provider;
use Gravity_Forms\Gravity_Tools\Config\App_Config;
trait Registers_Apps {
//----------------------------------------
//---------- App Registration ------------
//----------------------------------------
/**
* Register a JS app with the given arguments.
*
* @since 1.0
*
* @param array $args
*/
public function register_app( $args ) {
$config = new App_Config( $this->container->get( Config_Collection_Service_Provider::DATA_PARSER ) );
$config->set_data( $args );
$this->container->get( Config_Collection_Service_Provider::CONFIG_COLLECTION )->add_config( $config );
$should_display = is_callable( $args['enqueue'] ) ? call_user_func( $args['enqueue'] ) : $args['enqueue'];
if ( ! $should_display ) {
return;
}
if ( ! empty( $args['css'] ) ) {
$this->enqueue_app_css( $args );
}
if ( ! empty( $args['root_element'] ) ) {
$this->add_root_element( $args['root_element'] );
}
}
/**
* Enqueue the CSS assets for the app.
*
* @since 1.0
*
* @param $args
*/
protected function enqueue_app_css( $args ) {
$css_asset = $args['css'];
add_action( 'wp_enqueue_scripts', function () use ( $css_asset ) {
call_user_func_array( 'wp_enqueue_style', $css_asset );
} );
add_action( 'admin_enqueue_scripts', function () use ( $css_asset ) {
call_user_func_array( 'wp_enqueue_style', $css_asset );
} );
}
/**
* Add the root element to the footer output for bootstrapping.
*
* @since 1.0
*
* @param string $root
*/
protected function add_root_element( $root ) {
$self = $this;
add_action( $this->get_inject_action(), function() use ( $root, $self ) {
echo $self->get_root_markup( $root );
}, 10, 0 );
}
/**
* The markup for the root element of the app.
*
* @since 1.0
*
* @param $root
*
* @return string
*/
protected function get_root_markup( $root ) {
return '<div data-js="' . $root . '"></div>';
}
/**
* Get the action name for injecting app root.
*
* @since 1.0
*
* @return string
*/
protected function get_inject_action() {
return 'admin_footer';
}
}
@@ -0,0 +1,151 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Assets;
class Asset_Processor {
/**
* @var array $map - The Hash Map generated by our node scripts.
*/
private $js_map;
/**
* @var string $asset_path - The path to the js dist directory.
*/
private $js_asset_path;
/**
* @var string $match_pattern - The pattern to match against when identifying scripts to process.
*/
private $js_match_pattern;
/**
* @var array $map - The Hash Map generated by our node scripts.
*/
private $css_map;
/**
* @var string $asset_path - The path to the css dist directory.
*/
private $css_asset_path;
/**
* @var string $match_pattern - The pattern to match against when identifying scripts to process.
*/
private $css_match_pattern;
private $constant_name;
/**
* Constructor
*
* @since 1.0
*
* @param array $map
* @param string $asset_path
*
* @return void
*/
public function __construct( $js_map, $css_map, $js_asset_path, $css_asset_path, $js_match_pattern, $css_match_pattern, $constant_name ) {
$this->js_map = $js_map;
$this->js_asset_path = $js_asset_path;
$this->js_match_pattern = $js_match_pattern;
$this->css_map = $css_map;
$this->css_asset_path = $css_asset_path;
$this->css_match_pattern = $css_match_pattern;
$this->constant_name = $constant_name;
}
/**
* Perform processing actions on assets.
*
* @since 1.0
*
* @return void
*/
public function process_assets() {
$this->process_scripts();
$this->process_styles();
}
/**
* Process the ver values for all of the registered scripts in order to append a
* file hash (if it exists) or the filemtime (if required).
*
* @since 1.0
*
* @return void
*/
private function process_scripts() {
global $wp_scripts;
$registered = $wp_scripts->registered;
foreach ( $registered as &$asset ) {
// Bail if not one of our assets.
if ( strpos( $asset->src, $this->js_match_pattern ) === false ) {
continue;
}
$basename = basename( $asset->src );
$path = sprintf( '%s/%s', $this->js_asset_path, $basename );
// Asset doesn't exist in hash_map, skip.
if ( ! array_key_exists( $basename, $this->js_map ) ) {
continue;
}
// The hash is either the value from our map, or the filemtime for dev.
$hash = defined( $this->constant_name ) && constant( $this->constant_name ) ?
filemtime( $path ) :
$this->js_map[ $basename ]['version'];
$asset->ver = $hash;
}
$wp_scripts->registered = $registered;
return;
}
/**
* Process the ver values for all of the registered scripts in order to append a
* file hash (if it exists) or the filemtime (if required).
*
* @since 1.0
*
* @return void
*/
private function process_styles() {
global $wp_styles;
$registered = $wp_styles->registered;
foreach ( $registered as &$asset ) {
// Bail if not one of our assets.
if ( strpos( $asset->src, $this->css_match_pattern ) === false ) {
continue;
}
$basename = basename( $asset->src );
$path = sprintf( '%s/%s', $this->css_asset_path, $basename );
// Asset doesn't exist in hash_map, skip.
if ( ! array_key_exists( $basename, $this->css_map ) ) {
continue;
}
// The hash is either the value from our map, or the filemtime for dev.
$hash = defined( $this->constant_name ) && constant( $this->constant_name ) ?
filemtime( $path ) :
$this->css_map[ $basename ]['version'];
$asset->ver = $hash;
}
$wp_styles->registered = $registered;
return;
}
}
@@ -0,0 +1,806 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Background_Processing;
use Gravity_Forms\Gravity_Tools\Cache\Cache;
use Gravity_Forms\Gravity_Tools\Logging\Logger;
use Gravity_Forms\Gravity_Tools\Utils\Common;
/**
* Abstract Background_Process class.
*
* @since 1.0
*
* @abstract
* @extends WP_Async_Request
*/
abstract class Background_Process extends WP_Async_Request {
/**
* Action
*
* @since 1.0
*
* (default value: 'background_process')
*
* @var string
* @access protected
*/
protected $action = 'background_process';
/**
* Start time of current process.
*
* @since 1.0
*
* (default value: 0)
*
* @var int
* @access protected
*/
protected $start_time = 0;
/**
* Cron_hook_identifier
*
* @since 1.0
*
* @var mixed
* @access protected
*/
protected $cron_hook_identifier;
/**
* Cron_interval_identifier
*
* @since 1.0
*
* @var mixed
* @access protected
*/
protected $cron_interval_identifier;
/**
* Query_url
*
* @since 2.3
*
* @var string
* @access protected
*/
protected $query_url;
/**
* @var Common
*/
protected $utils;
/**
* @var Logger
*/
protected $logger;
/**
* @var Cache
*/
protected $cache;
/**
* Initiate new background process
*
* @since 1.0
*/
public function __construct( Common $utils, Logger $logger, Cache $cache ) {
parent::__construct();
$this->utils = $utils;
$this->logger = $logger;
$this->cache = $cache;
$this->query_url = admin_url( 'admin-ajax.php' );
$this->cron_hook_identifier = $this->identifier . '_cron';
$this->cron_interval_identifier = $this->identifier . '_cron_interval';
add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
}
/**
* Dispatches the queued tasks to Admin Ajax for processing and schedules a cron job in case processing fails.
*
* @since 1.0
*
* @access public
*
* @return array|WP_Error
*/
public function dispatch() {
// @todo implement logging
$this->logger->log_debug( sprintf( '%s(): Running for %s.', __METHOD__, $this->action ) );
if ( $this->is_queue_empty() ) {
$this->clear_scheduled_event();
$dispatched = new \WP_Error( 'queue_empty', 'Nothing left to process' );
} else {
// Schedule the cron healthcheck.
$this->schedule_event();
// Perform remote post.
$dispatched = parent::dispatch();
}
if ( is_wp_error( $dispatched ) ) {
$this->logger->log_debug( sprintf( '%s(): Unable to dispatch tasks to Admin Ajax: %s', __METHOD__, $dispatched->get_error_message() ) );
}
return $dispatched;
}
/**
* Get the dispatch request arguments.
*
* @since 2.3-rc-2
*
* @return array
*/
protected function get_post_args() {
$post_args = parent::get_post_args();
// Blocking prevents some issues such as cURL connection errors being reported.
unset( $post_args['blocking'] );
return $post_args;
}
/**
* Push to queue
*
* @since 1.0
*
* @param mixed $data Data.
*
* @return $this
*/
public function push_to_queue( $data ) {
$this->data[] = $data;
return $this;
}
/**
* Save queue
*
* @since 1.0
*
* @return $this
*/
public function save() {
$key = $this->generate_key();
if ( ! empty( $this->data ) ) {
$data = array(
'blog_id' => get_current_blog_id(),
'data' => $this->data,
);
update_site_option( $key, $data );
}
return $this;
}
/**
* Update queue
*
* @since 1.0
*
* @param string $key Key.
* @param array $data Data.
*
* @return $this
*/
public function update( $key, $data ) {
if ( ! empty( $data ) ) {
$old_value = get_site_option( $key );
if ( $old_value ) {
$this->logger->log_debug( sprintf( '%s(): Updating batch %s. Tasks remaining: %d.', __METHOD__, $key, count( $data ) ) );
$data = array(
'blog_id' => get_current_blog_id(),
'data' => $data,
);
update_site_option( $key, $data );
}
}
return $this;
}
/**
* Delete queue
*
* @param string $key Key.
*
* @return $this
*/
public function delete( $key ) {
$this->logger->log_debug( sprintf( '%s(): Deleting batch %s.', __METHOD__, $key ) );
delete_site_option( $key );
return $this;
}
/**
* Generate key
*
* Generates a unique key based on microtime. Queue items are
* given a unique key so that they can be merged upon save.
*
* @since 1.0
*
* @param int $length Length.
*
* @return string
*/
protected function generate_key( $length = 64 ) {
$unique = md5( microtime() . rand() );
$prepend = $this->identifier . '_batch_blog_id_' . get_current_blog_id() . '_';
return substr( $prepend . $unique, 0, $length );
}
/**
* Maybe process queue
*
* Checks whether data exists within the queue and that
* the process is not already running.
*
* @since 1.0
*/
public function maybe_handle() {
$this->logger->log_debug( sprintf( '%s(): Running for %s.', __METHOD__, $this->action ) );
// Don't lock up other requests while processing
session_write_close();
if ( $this->is_process_running() ) {
// Background process already running.
wp_die();
}
if ( $this->is_queue_empty() ) {
// No data to process.
wp_die();
}
check_ajax_referer( $this->identifier, 'nonce' );
$this->handle();
wp_die();
}
/**
* Is queue empty
*
* @since 1.0
*
* @return bool
*/
protected function is_queue_empty() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
$count = $wpdb->get_var( $wpdb->prepare( "
SELECT COUNT(*)
FROM {$table}
WHERE {$column} LIKE %s
", $key ) );
return ( $count > 0 ) ? false : true;
}
/**
* Is process running
*
* Check whether the current process is already running
* in a background process.
*
* @since 1.0
*/
protected function is_process_running() {
$running = false;
$lock_timestamp = get_site_option( $this->identifier . '_process_lock' );
if ( $lock_timestamp ) {
$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
if ( microtime( true ) - $lock_timestamp > $lock_duration ) {
$this->unlock_process();
} else {
$running = true;
}
}
return $running;
}
/**
* Lock process
*
* Lock the process so that multiple instances can't run simultaneously.
* Override if applicable, but the duration should be greater than that
* defined in the time_exceeded() method.
*
* @since 1.0
*/
protected function lock_process() {
$this->start_time = time(); // Set start time of current process.
update_site_option( $this->identifier . '_process_lock', microtime( true ) );
}
/**
* Unlock process
*
* Unlock the process so that other instances can spawn.
*
* @since 1.0
*
* @return $this
*/
public function unlock_process() {
delete_site_option( $this->identifier . '_process_lock' );
return $this;
}
/**
* Get batch
*
* @since 1.0
*
* @return stdClass Return the first batch from the queue
*/
protected function get_batch() {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
$key_column = 'option_id';
$value_column = 'option_value';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
$key_column = 'meta_id';
$value_column = 'meta_value';
}
$key = $wpdb->esc_like( $this->identifier . '_batch_blog_id_' . get_current_blog_id() . '_' ) . '%';
$sql = "
SELECT *
FROM {$table}
WHERE {$column} LIKE %s
ORDER BY {$key_column} ASC
LIMIT 1
";
$query = $wpdb->get_row( $wpdb->prepare( $sql, $key ) );
if ( empty( $query ) ) {
// No more batches for this blog ID. Get the next one in the queue regardless of the blog ID.
$key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%';
$query = $wpdb->get_row( $wpdb->prepare( $sql, $key ) );
}
$batch = new \stdClass();
$batch->key = $query->$column;
$value = maybe_unserialize( $query->$value_column );
$batch->data = $value['data'];
$batch->blog_id = $value['blog_id'];
return $batch;
}
/**
* Handle
*
* Pass each queue item to the task handler, while remaining
* within server memory and time limit constraints.
*
* @since 1.0
*/
protected function handle() {
$this->lock_process();
do {
$batch = $this->get_batch();
if ( is_multisite() ) {
$current_blog_id = get_current_blog_id();
if ( $current_blog_id !== $batch->blog_id ) {
$this->spawn_multisite_child_process( $batch->blog_id );
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
// Switch back to the current blog and return so the other tasks queued in this process can be run.
switch_to_blog( $current_blog_id );
return;
} else {
wp_die();
}
}
}
$this->logger->log_debug( sprintf( '%s(): Processing batch %s; Tasks: %d.', __METHOD__, $batch->key, count( $batch->data ) ) );
$task_num = 0;
foreach ( $batch->data as $key => $value ) {
$this->logger->log_debug( sprintf( '%s(): Processing task %d.', __METHOD__, ++ $task_num ) );
$task = $this->task( $value );
if ( $task !== false ) {
$this->logger->log_debug( sprintf( '%s(): Keeping task %d in batch.', __METHOD__, $task_num ) );
$batch->data[ $key ] = $task;
} else {
$this->logger->log_debug( sprintf( '%s(): Removing task %d from batch.', __METHOD__, $task_num ) );
unset( $batch->data[ $key ] );
}
if ( $task !== false || $this->time_exceeded() || $this->memory_exceeded() ) {
// Batch limits reached or task not complete.
break;
}
}
// Update or delete current batch.
if ( ! empty( $batch->data ) ) {
$this->update( $batch->key, $batch->data );
} else {
$this->delete( $batch->key );
}
} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
$this->logger->log_debug( sprintf( '%s(): Batch completed for %s.', __METHOD__, $this->action ) );
$this->unlock_process();
// Start next batch or complete process.
if ( ! $this->is_queue_empty() ) {
$this->dispatch();
} else {
$this->complete();
}
if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
// Return so the other tasks queued in this process can be run.
return;
} else {
wp_die();
}
}
/**
* Spawn a new background process on the multisite that scheduled the current task
*
* @param int $blog_id
*
* @since 2.3
*/
protected function spawn_multisite_child_process( $blog_id ) {
$this->logger->log_debug( sprintf( '%s(): Running for blog #%s.', __METHOD__, $blog_id ) );
switch_to_blog( $blog_id );
$this->query_url = admin_url( 'admin-ajax.php' );
$this->unlock_process();
$this->dispatch();
}
/**
* Memory exceeded
*
* Ensures the batch process never exceeds 90%
* of the maximum WordPress memory.
*
* @since 1.0
*
* @return bool
*/
protected function memory_exceeded() {
$memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory
$current_memory = memory_get_usage( true );
$return = false;
if ( $current_memory >= $memory_limit ) {
$return = true;
}
return apply_filters( $this->identifier . '_memory_exceeded', $return );
}
/**
* Get memory limit
*
* @since 1.0
*
* @return int
*/
protected function get_memory_limit() {
if ( function_exists( 'ini_get' ) ) {
$memory_limit = ini_get( 'memory_limit' );
} else {
// Sensible default.
$memory_limit = '128M';
}
if ( ! $memory_limit || - 1 === intval( $memory_limit ) ) {
// Unlimited, set to 32GB.
$memory_limit = '32G';
}
return $this->convert_hr_to_bytes( $memory_limit );
}
/**
* Converts a shorthand byte value to an integer byte value.
*
* @param string $value A (PHP ini) byte value, either shorthand or ordinary.
*
* @return int An integer byte value.
*/
protected function convert_hr_to_bytes( $value ) {
// Globally available in WordPress 4.6
if ( function_exists( 'wp_convert_hr_to_bytes' ) ) {
return wp_convert_hr_to_bytes( $value );
}
// Backwards compatible support for Wordpress 3.6 to 4.5
$value = strtolower( trim( $value ) );
$bytes = (int) $value;
if ( false !== strpos( $value, 'g' ) ) {
$bytes *= pow( 1024, 3 );
} elseif ( false !== strpos( $value, 'm' ) ) {
$bytes *= pow( 1024, 2 );
} elseif ( false !== strpos( $value, 'k' ) ) {
$bytes *= 1024;
}
// Deal with large (float) values which run into the maximum integer size.
return min( $bytes, PHP_INT_MAX );
}
/**
* Time exceeded.
*
* @since 1.0
*
* Ensures the batch never exceeds a sensible time limit.
* A timeout limit of 30s is common on shared hosting.
*
* @return bool
*/
protected function time_exceeded() {
$finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
$return = false;
if ( time() >= $finish ) {
$return = true;
}
return apply_filters( $this->identifier . '_time_exceeded', $return );
}
/**
* Complete.
*
* @since 1.0
*
* Override if applicable, but ensure that the below actions are
* performed, or, call parent::complete().
*/
protected function complete() {
// Unschedule the cron healthcheck.
$this->clear_scheduled_event();
}
/**
* Schedule cron healthcheck
*
* @since 1.0
*
* @access public
*
* @param mixed $schedules Schedules.
*
* @return mixed
*/
public function schedule_cron_healthcheck( $schedules ) {
$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
if ( property_exists( $this, 'cron_interval' ) ) {
$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval_identifier );
}
// Adds every 5 minutes to the existing schedules.
$schedules[ $this->identifier . '_cron_interval' ] = array(
'interval' => MINUTE_IN_SECONDS * $interval,
'display' => sprintf( __( 'Every %d Minutes', 'gravityforms' ), $interval ),
);
return $schedules;
}
/**
* Handle cron healthcheck
*
* Restart the background process if not already running
* and data exists in the queue.
*
* @since 1.0
*/
public function handle_cron_healthcheck() {
$this->logger->log_debug( sprintf( '%s(): Running for %s.', __METHOD__, $this->action ) );
$this->record_cron_event( $this->cron_hook_identifier );
if ( $this->is_process_running() ) {
// Background process already running.
return;
}
if ( $this->is_queue_empty() ) {
// No data to process.
$this->clear_scheduled_event();
return;
}
$this->handle();
}
/**
* Maintains a semipersistent record of the 3 most recent events for the specified hook.
*
* @since 2.7.1
*
* @param string $hook The cron hook name.
*/
public function record_cron_event( $hook ) {
if ( ! defined( 'DOING_CRON' ) || ! DOING_CRON ) {
return;
}
$events = $this->cache->get( Cache::KEY_CRON_EVENTS );
if ( ! is_array( $events ) ) {
$events = array();
}
if ( empty( $events[ $hook ] ) || ! is_array( $events[ $hook ] ) ) {
$events[ $hook ] = array( time() );
} else {
array_unshift( $events[ $hook ], time() );
array_splice( $events[ $hook ], 3 );
}
$this->cache->set( Cache::KEY_CRON_EVENTS, $events, true );
}
/**
* Schedule event
*
* @since 1.0
*/
protected function schedule_event() {
if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
$this->logger->log_debug( sprintf( '%s(): Scheduling cron event for %s.', __METHOD__, $this->action ) );
wp_schedule_event( time() + 10, $this->cron_interval_identifier, $this->cron_hook_identifier );
}
}
/**
* Clear scheduled event
*
* @since 1.0
*/
protected function clear_scheduled_event() {
$timestamp = wp_next_scheduled( $this->cron_hook_identifier );
if ( $timestamp ) {
$this->logger->log_debug( sprintf( '%s(): Clearing cron event for %s.', __METHOD__, $this->action ) );
wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
}
}
/**
* Clears all scheduled events.
*
* @since 2.3.1.x
*/
public function clear_scheduled_events() {
wp_clear_scheduled_hook( $this->cron_hook_identifier );
}
/**
* Cancel Process
*
* Stop processing queue items, clear cronjob and delete batch.
*
* @since 1.0
*/
public function cancel_process() {
if ( ! $this->is_queue_empty() ) {
$batch = $this->get_batch();
$this->delete( $batch->key );
$this->clear_scheduled_events();
}
}
/**
* Clears all batches from the queue.
*
* @since 2.3
*
* @param bool $all_blogs_in_network
*
* @return false|int
*/
public function clear_queue( $all_blogs_in_network = false ) {
global $wpdb;
$table = $wpdb->options;
$column = 'option_name';
if ( is_multisite() ) {
$table = $wpdb->sitemeta;
$column = 'meta_key';
}
$key = $this->identifier . '_batch_';
if ( ! $all_blogs_in_network ) {
$key .= 'blog_id_' . get_current_blog_id() . '_';
}
$key = $wpdb->esc_like( $key ) . '%';
$result = $wpdb->query( $wpdb->prepare( "
DELETE FROM {$table}
WHERE {$column} LIKE %s
", $key ) );
$this->data = array();
return $result;
}
/**
* Task
*
* Override this method to perform any actions required on each
* queue item. Return the modified item for further processing
* in the next pass through. Or, return false to remove the
* item from the queue.
*
* @since 1.0
*
* @param mixed $item Queue item to iterate over.
*
* @return mixed
*/
abstract protected function task( $item );
}
@@ -0,0 +1,157 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Background_Processing;
/**
* Abstract WP_Async_Request class.
*
* @abstract
*/
abstract class WP_Async_Request {
/**
* Prefix
*
* (default value: 'wp')
*
* @var string
* @access protected
*/
protected $prefix = 'wp';
/**
* Action
*
* (default value: 'async_request')
*
* @var string
* @access protected
*/
protected $action = 'async_request';
/**
* Identifier
*
* @var mixed
* @access protected
*/
protected $identifier;
/**
* Data
*
* (default value: array())
*
* @var array
* @access protected
*/
protected $data = array();
/**
* Initiate new async request
*/
public function __construct() {
$this->identifier = $this->prefix . '_' . $this->action;
add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) );
add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) );
}
/**
* Set data used during the request
*
* @param array $data Data.
*
* @return $this
*/
public function data( $data ) {
$this->data = $data;
return $this;
}
/**
* Dispatch the async request
*
* @return array|WP_Error
*/
public function dispatch() {
$url = add_query_arg( $this->get_query_args(), $this->get_query_url() );
$args = $this->get_post_args();
return wp_remote_post( esc_url_raw( $url ), $args );
}
/**
* Get query args
*
* @return array
*/
protected function get_query_args() {
if ( property_exists( $this, 'query_args' ) ) {
return $this->query_args;
}
return array(
'action' => $this->identifier,
'nonce' => wp_create_nonce( $this->identifier ),
);
}
/**
* Get query URL
*
* @return string
*/
protected function get_query_url() {
if ( property_exists( $this, 'query_url' ) ) {
return $this->query_url;
}
return admin_url( 'admin-ajax.php' );
}
/**
* Get post args
*
* @return array
*/
protected function get_post_args() {
if ( property_exists( $this, 'post_args' ) ) {
return $this->post_args;
}
return array(
'timeout' => 0.01,
'blocking' => false,
'body' => $this->data,
'cookies' => $_COOKIE,
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
}
/**
* Maybe handle
*
* Check for correct nonce and pass to handler.
*/
public function maybe_handle() {
// Don't lock up other requests while processing
session_write_close();
check_ajax_referer( $this->identifier, 'nonce' );
$this->handle();
wp_die();
}
/**
* Handle
*
* Override this method to perform any actions required
* during the async request.
*/
abstract protected function handle();
}
@@ -0,0 +1,262 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Cache;
use Gravity_Forms\Gravity_Tools\Utils\Common;
/**
*
* Notes:
* 1. The WordPress Transients API does not support boolean
* values so boolean values should be converted to integers
* or arrays before setting the values as persistent.
*
* 2. The transients API only deletes the transient from the database
* when the transient is accessed after it has expired. WordPress doesn't
* do any garbage collection of transients.
*
*/
class Cache {
/**
* @var Common
*/
protected $common;
/**
* Constructor
*
* @param Common $common
*/
public function __construct( $common ) {
$this->common = $common;
}
const KEY_CRON_EVENTS = 'cron_events_log';
private static $_transient_prefix = 'GFCache_';
private static $_cache = array();
/**
* Get a value from cache.
*
* @since 1.0
*
* @param $key
* @param $found
* @param $is_persistent
*
* @return false|mixed|string|null
*/
public function get( $key, &$found = null, $is_persistent = true ) {
global $blog_id;
if ( is_multisite() ) {
$key = $blog_id . ':' . $key;
}
if ( isset( self::$_cache[ $key ] ) ) {
$found = true;
$data = $this->common->rgar( self::$_cache[ $key ], 'data' );
return $data;
}
//If set to not persistent, do not check transient for performance reasons
if ( ! $is_persistent ) {
$found = false;
return false;
}
$data = self::get_transient( $key );
if ( false === ( $data ) ) {
$found = false;
return false;
} else {
self::$_cache[ $key ] = array( 'data' => $data, 'is_persistent' => true );
$found = true;
return $data;
}
}
/**
* Set a value in the cache.
*
* @since 1.0
*
* @param $key
* @param $data
* @param $is_persistent
* @param $expiration_seconds
*
* @return bool
*/
public function set( $key, $data, $is_persistent = false, $expiration_seconds = 0 ) {
global $blog_id;
$success = true;
if ( is_multisite() ) {
$key = $blog_id . ':' . $key;
}
if ( $is_persistent ) {
$success = self::set_transient( $key, $data, $expiration_seconds );
}
self::$_cache[ $key ] = array( 'data' => $data, 'is_persistent' => $is_persistent );
return $success;
}
/**
* Delete a value from cache.
*
* @since 1.0
*
* @param $key
*
* @return bool
*/
public function delete( $key ) {
global $blog_id;
$success = true;
if ( is_multisite() ) {
$key = $blog_id . ':' . $key;
}
if ( isset( self::$_cache[ $key ] ) ) {
if ( self::$_cache[ $key ]['is_persistent'] ) {
$success = self::delete_transient( $key );
}
unset( self::$_cache[ $key ] );
} else {
$success = self::delete_transient( $key );
}
return $success;
}
/**
* FLush the cache.
*
* @since 1.0
*
* @param $flush_persistent
*
* @return bool
*/
public function flush( $flush_persistent = false ) {
global $wpdb;
self::$_cache = array();
if ( false === $flush_persistent ) {
return true;
}
if ( is_multisite() ) {
$sql = $wpdb->prepare( "
DELETE FROM $wpdb->sitemeta
WHERE meta_key LIKE %s OR
meta_key LIKE %s
",
'\_site\_transient\_timeout\_GFCache\_%',
'_site_transient_GFCache_%'
);
} else {
$sql = $wpdb->prepare( "
DELETE FROM $wpdb->options
WHERE option_name LIKE %s OR
option_name LIKE %s
",
'\_transient\_timeout\_GFCache\_%',
'_transient_GFCache_%'
);
}
$rows_deleted = $wpdb->query( $sql );
$success = $rows_deleted !== false ? true : false;
return $success;
}
/**
* Delete a transient by its key.
*
* @since 1.0
*
* @param $key
*
* @return false
*/
private function delete_transient( $key ) {
if ( ! function_exists( 'wp_hash' ) ) {
return false;
}
$key = self::$_transient_prefix . wp_hash( $key );
if ( is_multisite() ) {
$success = delete_site_transient( $key );
} else {
$success = delete_transient( $key );
}
return $success;
}
/**
* Set a transient by its key.
*
* @since 1.0
*
* @param $key
* @param $data
* @param $expiration
*
* @return false
*/
private function set_transient( $key, $data, $expiration ) {
if ( ! function_exists( 'wp_hash' ) ) {
return false;
}
$key = self::$_transient_prefix . wp_hash( $key );
if ( is_multisite() ) {
$success = set_site_transient( $key, $data, $expiration );
} else {
$success = set_transient( $key, $data, $expiration );
}
return $success;
}
/**
* Get a transient by its key.
*
* @since 1.0
*
* @param $key
*
* @return false
*/
private function get_transient( $key ) {
if ( ! function_exists( 'wp_hash' ) ) {
return false;
}
$key = self::$_transient_prefix . wp_hash( $key );
if ( is_multisite() ) {
$data = get_site_transient( $key );
} else {
$data = get_transient( $key );
}
return $data;
}
}
@@ -0,0 +1,94 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Config;
use Gravity_Forms\Gravity_Tools\Config_Collection;
use Gravity_Forms\Gravity_Tools\Config;
/**
* Config items for app registration, including helper methods.
*/
class App_Config extends Config {
/**
* The name of the app, in slug form.
*
* @var string $app_name
*/
private $app_name;
/**
* The condition for enqueuing the app data.
*
* @var bool|callable $display_condition
*/
private $display_condition;
/**
* The CSS assets to enqueue.
*
* @var array $css_asset
*/
private $css_asset;
/**
* The relative path to the chunk in JS.
*
* @var string $chunk
*/
private $chunk;
/**
* The root element to use to load the app.
*
* @var string $root_element
*/
private $root_element;
/**
* Set the data for this app config.
*
* @since 1.0
*
* @param $data
*/
public function set_data( $data ) {
$this->name = $data['object_name'];
$this->script_to_localize = $data['script_name'];
$this->app_name = $data['app_name'];
$this->display_condition = $data['enqueue'];
$this->chunk = $data['chunk'];
$this->root_element = $data['root_element'];
}
/**
* Whether we should enqueue this data.
*
* @since 1.0
*
* @return bool|mixed
*/
public function should_enqueue() {
return is_admin();
}
/**
* Config data.
*
* @since 1.0
*
* @return array[]
*/
public function data() {
return array(
'apps' => array(
$this->app_name => array(
'should_display' => is_callable( $this->display_condition ) ? call_user_func( $this->display_condition ) : $this->display_condition,
'chunk_path' => $this->chunk,
'root_element' => $this->root_element,
),
),
);
}
}
@@ -0,0 +1,48 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data;
class Transient_Strategy {
/**
* Get transient value.
*
* @since 1.0
*
* @param $key
*
* @return mixed
*/
public function get( $key ) {
return get_transient( $key );
}
/**
* Set transient value.
*
* @since 1.0
*
* @param $key
* @param $value
* @param $timeout
*
* @return bool
*/
public function set( $key, $value, $timeout ) {
return set_transient( $key, $value, $timeout );
}
/**
* Delete transient value.
*
* @since 1.0
*
* @param $key
*
* @return bool
*/
public function delete( $key ) {
return delete_transient( $key );
}
}
@@ -0,0 +1,11 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data;
interface Oauth_Data_Handler {
public function get( $key, $namespace = 'config' );
public function save( $key, $value, $namespace = 'config' );
}
@@ -0,0 +1,104 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data_Import\Sources;
use Gravity_Forms\Gravity_Tools\Data_Import\Record;
use Gravity_Forms\Gravity_Tools\Data_Import\Source;
class CSV_Source implements Source {
private $raw_data;
private $data;
/**
* Parse and store the passed CSV data.
*
* @param string $data - The raw CSV string data
*
* @return void
*/
public function set_data( $data ) {
$this->raw_data = $data;
$rows = array_map( 'str_getcsv', $data );
$header = array_shift( $rows );
$csv = array();
foreach ( $rows as $row ) {
$csv[] = array_combine( $header, $row );
}
$this->data = $csv;
}
/**
* Get all of the available keys from the CSV headers.
*
* @param array $args - An array of additional args, info, or data needed
*
* @return array
*/
public function keys( $args = array() ) {
$data = $this->data;
if ( empty( $data ) ) {
return array();
}
$entry = array_shift( $data );
return array_keys( $entry );
}
/**
* Determine if this CSV has a given key.
*
* @param string $key
*
* @return bool
*/
public function has_key( $key ) {
return in_array( $key, $this->keys() );
}
/**
* Get the rows for this CSV.
*
* @param array $args
*
* @return Record[]
*/
public function records( $args = array() ) {
$records = array();
foreach ( $this->data as $row ) {
$records[] = new Record( $row );
}
return $records;
}
/**
* Get the row count for this CSV.
*
* @param array $args
*
* @return int
*/
public function count( $args = array() ) {
return count( $this->data );
}
/**
* Get a specific slice of rows from this CSV.
*
* @param int $count
* @param int $offset
* @param array $additional_args
*
* @return Record[]
*/
public function slice( $count, $offset, $additional_args = array() ) {
return array_slice( $this->data, $offset, $count, true );
}
}
@@ -0,0 +1,52 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data_Import;
class Import_Handler {
/**
* @var Source
*/
protected $source;
/**
* @var Destination
*/
protected $destination;
public function __construct( Source $source, Destination $destination ) {
$this->source = $source;
$this->destination = $destination;
}
public function handle( $data, $map, $additional_args = array(), $count = -1, $offset = -1 ) {
$this->source->set_data( $data );
$records = $count === -1 ? $this->source->records( $additional_args ) : $this->source->slice( $count, $offset, $additional_args );
$to_be_inserted = array();
foreach ( $records as $record ) {
$to_be_inserted[] = $this->map_single_record( $record, $map );
}
$this->destination->store( $to_be_inserted );
}
protected function map_single_record( Record $record, $map ) {
$insertion_data = array();
foreach ( $map as $source_key => $destination_key ) {
if ( ! $this->destination->has_key( $destination_key ) ) {
throw new \InvalidArgumentException( sprintf( 'Attempting to map key %s to invalid destination field %s.', $source_key, $destination_key ) );
}
if ( ! $this->source->has_key( $source_key ) ) {
throw new \InvalidArgumentException( sprintf( 'Attempting to map invalid source key %s.', $source_key ) );
}
$insertion_data[ $destination_key ] = $record->value_for_key( $source_key );
}
return $insertion_data;
}
}
@@ -0,0 +1,73 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data_Import;
/**
* Record
*
* A Record represents a single piece of information from a Source. In a CSV import
* it would represent a row, in a WP Post import it would represent a single Post object,
* and so on.
*/
class Record {
/**
* @var array
*/
protected $data;
/**
* Record constructor
*
* @param array $data - The data for this record.
*
* @return void
*/
public function __construct( $data ) {
$this->data = $data;
}
/**
* Getter for the $data property.
*
* @return array
*/
public function data() {
return $this->data;
}
/**
* Get a value from this record for the given key.
*
* @param string $key
*
* @return mixed
*/
public function value_for_key( $key ) {
if ( ! $this->has_key( $key ) ) {
throw new \InvalidArgumentException( sprintf( 'Key %s does not exist on current record.', $key ) );
}
return $this->data[ $key ];
}
/**
* Retrieve the keys for this record.
*
* @return string[]
*/
public function keys() {
return array_keys( $this->data );
}
/**
* Determine if this record has the given key.
*
* @param string $key
*
* @return bool
*/
public function has_key( $key ) {
return array_key_exists( $key, $this->data );
}
}
@@ -0,0 +1,48 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data_Import;
/**
* Destination Interface
*
* A Destination is any system/context to which the imported data can be sent.
*/
interface Destination {
/**
* Get the valid keys for this Destination.
*
* @param array @args
*
* @return string[]
*/
public function keys( $args = array() );
/**
* Determine if this Destination has the given key.
*
* @param string $key
*
* @return bool
*/
public function has_key( $key );
/**
* Store the given array of records into this Destination.
*
* @param Record[] $records
*
* @return void
*/
public function store( $records );
/**
* Check for duplicate records already existing within this Destination.
*
* @param Record $record
* @param string $check_key
*
* @return bool
*/
public function check_for_duplicates( $record, $check_key );
}
@@ -0,0 +1,72 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Data_Import;
/**
* Source Interface
*
* A Source is any collection of data from which values can be mapped. In the case of a CSV import,
* this would represent the CSV, for instance. This interface provides the contract for all of the functionality
* required of a valid Source.
*/
interface Source {
/**
* Set the internal $data property to whatever is passed.
*
* @param mixed $data
*
* @return void
*/
public function set_data( $data );
/**
* Get all of the valid/available keys for this source.
*
* In something like a CSV, this would be the column headers. For a WP Post,
* it would be the various field key names, etc.
*
* @param array $args - An array of additional args, info, or data needed.
*
* @return array
*/
public function keys( $args = array() );
/**
* Determine if this source has a given key.
*
* @param string $key
*
* @return bool
*/
public function has_key( $key );
/**
* Get the records for this source.
*
* @param array $args
*
* @return Record[]
*/
public function records( $args = array() );
/**
* Get the record count for this source.
*
* @param array $args
*
* @return int
*/
public function count( $args = array() );
/**
* Get a specific slice of records from this source.
*
* @param int $count
* @param int $offset
* @param array $additional_args
*
* @return Record[]
*/
public function slice( $count, $offset, $additional_args = array() );
}
@@ -0,0 +1,189 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Emails;
use Gravity_Forms\Gravity_Tools\Utils\Bettarray;
/**
* Email_Templatizer
*
* For the given email markup, take find any placeholder tokens and replace them with values from
* the provided $data. Can be used for any markup.
*/
class Email_Templatizer {
protected $email_template = '';
protected $open_delin;
protected $close_delin;
/**
* @param string $email_template The markup to parse
* @param string $open_delin The string to use for the opening delimiter for tokens. Defaults to '{{'.
* @param string $close_delin The string to use for the closing delimiter for tokens. Defaults to '}}'.
*
* @return void
*/
public function __construct( $email_template, $open_delin = '{{', $close_delin = '}}' ) {
$this->email_template = $email_template;
$this->open_delin = $open_delin;
$this->close_delin = $close_delin;
}
/**
* Take the stored markup and render it with the given placeholder values.
*
* @param array|Bettarray $data the data to use with the placeholders
* @param int $max_length the maximum length, in characters, for the markup
* @param bool $echo whether to echo the markup (if false, markup will be returned)
*
* @return string|void
*/
public function render( $data, $max_length = false, $echo = false ) {
// Cast to Bettarray to allow dot navigation.
if ( ! is_a( $data, Bettarray::class ) ) {
$data = new Bettarray( $data );
}
$rendered = $this->handle_loops( $data );
$rendered = $this->handle_conditionals( $data, $rendered );
$rendered = $this->handle_placeholders( $data, $rendered );
if ( $max_length ) {
$rendered = $this->truncate_markup( $rendered, $max_length );
}
if ( $echo ) {
echo $rendered;
return;
}
return $rendered;
}
private function handle_loops( $data, $markup = false ) {
if ( ! $markup ) {
$markup = $this->email_template;
}
$pattern = sprintf( '/(%s\|for[^\|]+\|%s)([^\|]+)(%s\|endfor\|%s)/', preg_quote( $this->open_delin ), preg_quote( $this->close_delin ), preg_quote( $this->open_delin ), preg_quote( $this->close_delin ) );
return preg_replace_callback( $pattern, function ( $matches ) use ( $data ) {
// Something has gone terribly awry, just return the original text.
if ( count( $matches ) !== 4 ) {
return $matches[0];
}
$opening = $matches[1];
$contents = $matches[2];
$loop = str_replace( $this->open_delin . '|for', '', $opening );
$loop = str_replace( '|' . $this->close_delin, '', $loop );
$loop = trim( $loop );
$loop_parts = explode( ' ', $loop );
// Loop has invalid syntax. Bail.
if ( count( $loop_parts ) !== 3 ) {
return $matches[0];
}
$item_name = $loop_parts[0];
$item_value_string = $loop_parts[2];
$item_values = $data->get_raw( $item_value_string );
if ( ! is_array( $item_values ) ) {
return $matches[0];
}
$loop_data = new Bettarray( $data->all() );
$loop_data->delete( $item_value_string );
$return = '';
foreach( $item_values as $item_value ) {
$loop_data->set( $item_name, $item_value );;
$template = new Email_Templatizer( $contents );
$return .= $template->render( $loop_data );
}
return $return;
}, $markup );
}
/**
* Process the markup to replace placeholders with data.
*
* @param Bettarray $data the data containing the placeholder values
* @param string $markup if passed, the markup to act upon
*
* @return string
*/
private function handle_placeholders( $data, $markup = false ) {
if ( ! $markup ) {
$markup = $this->email_template;
}
$pattern = sprintf( '/%s[^%s]*%s/', preg_quote( $this->open_delin ), preg_quote( $this->close_delin ), preg_quote( $this->close_delin ) );
return preg_replace_callback( $pattern, function ( $matches ) use ( $data ) {
$cleaned = str_replace( $this->open_delin, '', $matches[0] );
$cleaned = str_replace( $this->close_delin, '', $cleaned );
$search = trim( $cleaned );
$replacement = $data->get_raw( $search );
if ( ! is_string( $replacement ) && ! is_numeric( $replacement ) ) {
return $matches[0];
}
$sanitized = wp_kses_post( $replacement );
return $sanitized;
}, $markup );
}
private function handle_conditionals( $data, $markup = false ) {
if ( ! $markup ) {
$markup = $this->email_template;
}
$pattern = sprintf( '/(%s\|if[^\|]+\|%s)([^\|]+)(%s\|endif\|%s)/', preg_quote( $this->open_delin ), preg_quote( $this->close_delin ), preg_quote( $this->open_delin ), preg_quote( $this->close_delin ) );
return preg_replace_callback( $pattern, function ( $matches ) use ( $data ) {
// Something has gone terribly awry, just return the original text.
if ( count( $matches ) !== 4 ) {
return $matches[0];
}
$opening = $matches[1];
$contents = $matches[2];
$condition = str_replace( $this->open_delin . '|if', '', $opening );
$condition = str_replace( '|' . $this->close_delin, '', $condition );
$condition = trim( $condition );
$check_val = $data->get_raw( $condition );
if ( empty( $check_val ) ) {
return '';
}
return $contents;
}, $markup );
}
/**
* A basic method to truncate the markup to $max_length characters.
*
* @todo Update to handle tags more gracefully to ensure none get truncated.
*
* @param string $markup the markup to modify
* @param int $max_length the number of characters to which the markup should be limited
*
* @return string
*/
private function truncate_markup( $markup, $max_length ) {
return substr( $markup, 0, $max_length );
}
}
@@ -0,0 +1,47 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Endpoints;
abstract class Endpoint {
protected $required_params = array();
protected $minimum_cap = 'manage_options';
abstract public function handle();
/**
* Default nonce.
*
* @since 1.0
*
* @return int
*/
protected function get_nonce_name() {
return -1;
}
/**
* Default validation, checks ajax referer and verifies required params.
*
* @since 1.0
*
* @return bool
*/
protected function validate() {
check_ajax_referer( $this->get_nonce_name(), 'security' );
if ( ! current_user_can( $this->minimum_cap ) ) {
return false;
}
foreach( $this->required_params as $param ) {
if ( ! isset( $_REQUEST[ $param ] ) ) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,212 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Enum;
use Gravity_Forms\Gravity_Tools\Utils\Booliesh;
/**
* Field Type Validation Enum
*
* This class provides defined, explicit methods for validating and sanitizing values
* before storing them in the database.
*
* Example usage:
*
* $validated_value = Field_Type_Validation_Enum::validate( Field_Type_Validation_Enum::STRING, 'foobar' );
*
* Actual validation methods can be called directly as well:
*
* $validated_string = Field_Type_Validation_Enum::validate_string( 'foobar' );
*/
class Field_Type_Validation_Enum {
const STRING = 'string';
const INT = 'int';
const FLOAT = 'float';
const BOOLEAN = 'boolean';
const DATE = 'date';
const OBJECT = 'object';
const ARR = 'array';
const EMAIL = 'email';
/**
* Returns an array mapping field types to their validation callbacks;
*
* @since 1.0
*
* @return array[]
*/
private static function validation_map() {
return array(
self::STRING => array( self::class, 'validate_string' ),
self::INT => array( self::class, 'validate_int' ),
self::FLOAT => array( self::class, 'validate_float' ),
self::BOOLEAN => array( self::class, 'validate_bool' ),
self::DATE => array( self::class, 'validate_date' ),
self::OBJECT => array( self::class, 'validate_object' ),
self::ARR => array( self::class, 'validate_array' ),
self::EMAIL => array( self::class, 'validate_email' ),
);
}
/**
* Validate a given value based on the field type.
*
* @since 1.0
*
* @param string $type The field type to be used for validation.
* @param mixed $value The actual value to validate.
*
* @return mixed
*/
public static function validate( $type, $value ) {
if ( ! is_string( $type ) && is_callable( $type ) ) {
return call_user_func( $type, $value );
}
if ( ! array_key_exists( $type, self::validation_map() ) ) {
$error_string = sprintf( 'Field type %s is not valid.', $type );
throw new \InvalidArgumentException( $error_string, 480 );
}
$validated_value = call_user_func( self::validation_map()[ $type ], $value );
return $validated_value;
}
/**
* Validates a string and adds slashes for DB storage.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return string|null
*/
public static function validate_string( $value ) {
if ( ! is_string( $value ) ) {
return null;
}
return addslashes( $value );
}
/**
* Validates an integer.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return int|null
*/
public static function validate_int( $value ) {
if ( ! is_numeric( $value ) ) {
return null;
}
return (integer) $value;
}
/**
* Validates a float.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return float|null
*/
public static function validate_float( $value ) {
if ( ! is_numeric( $value ) ) {
return null;
}
return (float) $value;
}
/**
* Validates a boolean.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return bool|null
*/
public static function validate_bool( $value ) {
return Booliesh::get( $value, null );
}
/**
* Validates a date string.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return string|null
*/
public static function validate_date( $value ) {
if ( ! is_string( $value ) ) {
return null;
}
$check = strtotime( $value );
if ( ! $check ) {
return null;
}
return $value;
}
/**
* Validates an object.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return object|null
*/
public static function validate_object( $value ) {
if ( ! is_object( $value ) ) {
return null;
}
return $value;
}
/**
* Validates an array.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return array|null
*/
public static function validate_array( $value ) {
if ( ! is_array( $value ) ) {
return null;
}
return $value;
}
/**
* Validates an email.
*
* @since 1.0
*
* @param mixed $value The value to validate.
*
* @return string|null
*/
public static function validate_email( $value ) {
return filter_var( $value, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE );
}
}
@@ -0,0 +1,213 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Models;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection;
/**
* Model
*
* This provides the base abstract contract for Models in Hermes. A Model is responsible
* for defining all of the fields, relationships, and permissions surrounding a given
* object/data type.
*/
abstract class Model {
/**
* The type of object this model represents.
*
* @since 1.0
*
* @var string
*/
protected $type = '';
/**
* The minimum capability required for a given user to be able to access
* this object's data. If defined, will override individual permissions.
*
* @var string
*/
protected $access_cap = '';
/**
* The minimum capability required for a given user to perform specific CRUD
* tasks against this model.
*
* @var string
*/
protected $access_cap_view = '';
protected $access_cap_edit = '';
protected $access_cap_create = '';
protected $access_cap_delete = '';
/**
* If a model needs to support ad-hoc defined meta fields (i.e., fields that
* are defined by the user and are not defined explicitly in the model), set
* this to true.
*
* @var bool
*/
protected $supports_ad_hoc_fields = false;
/**
* If your model needs to use an explicit/non-standard table, set it here.
* Do not include the DB prefix in the table name.
*
* @var bool|string
*/
protected $forced_table_name = false;
/**
* If your model supports Full Text search fields, list them here.
*
* @var array
*/
protected $searchable_fields = array();
/**
* Concrete Models must implement the public ::relationships() method
* in order to define any relationships this object type may have.
*
* @return Relationship_Collection
*/
abstract public function relationships();
/**
* Determine if this model supports ad hoc field
* definitions.
*
* @return bool
*/
public function supports_ad_hoc_fields() {
return $this->supports_ad_hoc_fields;
}
/**
* Accessor for forced_table_name.
*
* @return bool|string
*/
public function forced_table_name() {
return $this->forced_table_name;
}
/**
* Accessor for the type property
*
* @return string
*/
public function type() {
return $this->type;
}
/**
* Accessor for $searchable_fields
*
* @return array
*/
public function searchable_fields() {
return $this->searchable_fields;
}
/**
* Concrete Models must define the specific Fields and Field Types this
* object type has. Any request regarding this object type will use this
* array to determine if a given field should be evaluated, and how to
* validate it.
*
* In the Database design, tables for this object type should have columns
* that strictly match the field names outlined here. For instance, if a field
* called "first_name" is defined here, a column of "first_name" must exist in
* the DB table for this object.
*
* @return array
*/
public function fields() {
return array();
}
/**
* Concrete Models must define the specific Fields and Field Types this object
* type supports for *meta* fields. Meta fields differ from typical fields in that
* they do not require specific columns in the base Database table but are rather stored
* in a meta table. This allows for flexibility in adding new fields to a given object type
* after code has been deployed to production while avoiding having to modify DB tables.
*
* @return array
*/
public function meta_fields() {
return array();
}
/**
* Checks the current user's access for this object type.Typically this should
* not be overwritten in the concrete Model, except for cases in which custom
* access functionality is required.
*
* @param string $action - The specific CRUD action being checked.
*
* @return bool
*/
public function has_access( $action = 'view' ) {
if ( ! empty( $this->access_cap ) ) {
return current_user_can( $this->access_cap );
}
switch ( $action ) {
case 'view':
return current_user_can( $this->access_cap_view );
case 'edit':
return current_user_can( $this->access_cap_edit );
case 'create':
return current_user_can( $this->access_cap_create );
case 'delete':
return current_user_can( $this->access_cap_delete );
}
}
/**
* Concrete Models may define various transformations that can be applied to queried data. This
* is useful in situations where the data being stored for a given model's field needs to be returned
* in various formats. For instance, the data stored could be a post ID, and the transformation could return
* various aspects of that post, such as the Post Title, Description, etc. Other use-cases are things such as
* converting to all-caps, translating strings to other languages, or converting currencies.
*
* @return array
*/
public function transformations() {
return array();
}
/**
* Using the given transformation type, pass a value through a transformation and return the result. The
* given transformation must be present in the transformations() array of this model.
*
* @param string $transformation_name
* @param mixed $transformation_arg
* @param mixed $value
*
* @return mixed
*/
public function handle_transformation( $transformation_name, $transformation_arg, $value ) {
if ( ! isset( $this->transformations()[ $transformation_name ] ) ) {
throw new \InvalidArgumentException( 'Attempting to call invalid transformation type ' . $transformation_name . ' on object type ' . $this->type );
}
$transformation = $this->transformations()[ $transformation_name ];
if ( empty( $transformation_arg ) ) {
return call_user_func( $transformation, $value );
}
if ( is_array( $transformation_arg ) ) {
return call_user_func( $transformation, $value, ...$transformation_arg );
}
return call_user_func( $transformation, $value, $transformation_arg );
}
}
@@ -0,0 +1,554 @@
# Project Hermes
__A Query Architecture for dynamic, performant, client-controlled data handling and processing.__
## AJAX, GraphQL, and The Problem of Query Structures
In our typical product configuration, bespoke endpoints are setup and maintained for each unique type
of query that might come from the Client. Need to get a list of users? That's an endpoint. Get the companies users
belong to? Another endpoint.
Save a setting, get a setting, update a field, get a field - all individual endpoints.
Oftentimes, this paradigm is more than acceptable. The total number of query types being handled is typically low, and
the
need for net-new endpoints rarely occurs.
But what about situations in which there are many, many types of data to query, in all sorts of various configurations?
What if the
application is a single-page React app, where all the data comes view queries and nothing exists on page load? What if a
given data type
or object model requires a new piece of data? Suddenly we're not just spinning up unique endpoints for each situation,
but also having to
update/modify/drop database tables as well (something that has caused countless headaches for Plugin products like
ours).
Enter GraphQL, an alternative Query standard to REST/AJAX. GraphQL differs from traditional REST architecture primarily
in how
query structures and data shapes are controlled. In REST, you retrieve a specific object from a specific endpoint. "
Users" are queried via "/json/users".
"Companies" are queried via "json/companies". If you need both, or need to get Users categorized by a given Company,
it's two separate queries,
while necessitating that values from the first are saved and passed to the second. In simple contexts, this is fine, but
as complexity grows,
so do the headaches around this.
GraphQL tackles this by having the Server simply establish a structured schema of Object Types, along with their
relationships, fields, and other criteria. The Client can then use this schema to query *anything it needs*, all at a
single endpoint.
## That's great, but what are you talking about?
Let's look at a typical example: retrieving a list of companies, the departments at said company, and the employees
assigned to each department. In REST, that
would be 3 separate requests, like the following pseudo-code:
```js
const companies = request( { dataType: 'company' } );
for ( company in companies ) {
const departments = request( { dataType: 'department', companyId: company.id } );
for ( department in departments ) {
const employees = request( { dataType: 'employee', departmentId: department.id } );
department.employees = employees;
}
company.departments = departments;
}
return companies;
```
Not terrible, but if more nesting/relationships were required, things would quickly balloon out of control.
Not to mention we're making 3 distinct requests to the server, requiring more time and bandwidth.
Now let's look at how this would work in GraphQL:
```js
const query = `company {
id,
name,
address,
department {
id,
name,
employeeNames: employee {
id,
first_name,
last_name
}
}
}`;
const companies = request( { query } );
```
Not only do we reduce the total number of requests to one, we also have
complete, full control over the data shape returned to us. We can even alias the various
field values (like we do here by aliasing `employee` to `employeeNames` ). This results in substantially
decreased overhead for the server, as new data structures don't require any updates. It also makes architecting
the client-side system easier, as dynamic queries can be made simply by traversing the schema provided
by the server and selecting which fields/related objects to query.
Magic!
## Monkey wrench: GraphQL is enormé.
This is all good and well, but how do we take advantage of GraphQL? On the plus side, it's an open-source
system that is actively-maintained and provided by some incredible engineers at Facebook/Meta.
Unfortunately, it's also absolutely gigantic. Including the library required to run a functioning GraphQL server
in PHP is dozens of additional MB of file bloat, all replete with opportunities for conflicts with other plugins
which may use GraphQL. Includig it in a distributed plugin is a non-starter, full-stop.
So are we out of luck?
## Solution: just write our own
Given the limited file size constraints and variety in hosting platforms, we decided the best approach
would be to adopt GraphQL's syntax and functionality, but hand-roll our own server for parsing GraphQL
queries into SQL statements that we can execute against the database.
And thus, Project Hermes was born!
## The Bits and Bobs
Fundamentally, Hermes consists of four mechanisms: `Models`, `Handlers`, `Tokens`, and `Runners`.
During a given request, the `Models` registered to the system are referenced by a `Handler`, which takes the Query
String
and lexes the string into defined `Tokens`, each of which are classes holding the
structured data needed for the various `Runners` to execute the query.
That's a lot, but let's look at them one by one:
### Models: The Backbone of Hermes Data
`Models` are responsible for describing all of the various object types available for
querying and mutation. A given `Model` defines:
- The fields available for the object type
- The meta fields available to assign to the object type
- The minimum Capability required for they querying user to access the object type
- The relationships a given object type has to _other_ object types.
When a query is executed, the registered `Models` are referenced to ensure the data being
requested exists, that the data is available to the user, and the various tables required
for querying.
Consider the following:
```php
class FakeContactModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'contact';
protected $access_cap = 'manage_options';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::INT,
'first_name' => Field_Type_Validation_Enum::STRING,
'last_name' => Field_Type_Validation_Enum::STRING,
'email' => Field_Type_Validation_Enum::EMAIL,
'phone' => Field_Type_Validation_Enum::STRING,
'foobar' => function ( $value ) {
if ( $value === 'foo' ) {
return 'foo';
}
return null;
},
);
}
public function meta_fields() {
return array(
'secondary_phone' => Field_Type_Validation_Enum::STRING,
'alternate_website' => Field_Type_Validation_Enum::STRING,
);
}
public function relationships() {
return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection(
array(
new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship( 'group', 'contact', 'manage_options', true )
)
);
}
}
```
This code would register a new `Model` for an object type named `contact`. It defines several fields
that a `contact` can have, as well as any `meta` fields that might be assignable to it. The `fields`
should exist in the related DB table as columns, while `meta` fields can be added ad-hoc and are stored
in the separate `meta` lookup table.
Each field, whether meta or local, references a specific Field Validation Type, either by directly referencing
one of the Field Validation Type Enum values, or by providing a custom callback. This determines how values
will be validated and sanitized before being inserted into the database.
Finally, it establishes a relationship to another `Model`, `Group`. Since the table connects Groups _to_ Contacts,
this `Model` designates the relationship as `reversed`, telling the system to look in the `group_contact` table.
With this `Model` in-hand, you can register it to a `Model_Collection` by simply using:
```php
$contact = new FakeContactModel();
$collection = new Model_Collection();
$collection->add( 'contact', $contact );
```
### Handlers and Tokens: Hermes' traffic coordinator
When a Query is sent to the system (typically through a registered AJAX endpoint or REST route), it's passed to a
`Handler`, either the `Query_Handler` for _query_ calls or
the `Mutation_Handler` for mutation calls (Insert, Update, Delete, Connect).
This `Handler` then kicks off the process by passing the Query string to a set of `Tokens`, each of which take
the string (or portions of the string) and lex it into usable objects (or `Tokens`) holding all of the data needed
for the specific operation being handled.
To accomplish this, each `Token` defines a series of key/value pairs, where the `key` represents a regex `MARK` for
identifying the
path taken in the regular expression, and the `value` is the pattern to match against while running the expression.
These are passed to a call to
`preg_match_all()` against the Query string.
This results in an array of `MARK` designators along with a matching array containing the matched results. These pairs
are evaluated
and either stored to the `Token` as data or (when the `Token` is identified as requiring further lexing) passed along to
another `Token` for
processing.
### Runners: Hermes' Bravest Little Soldiers
Once all the various `Tokens` have been generated, we're left with a structured set of PHP Objects, each containing the
data
necessary for performing the Query. With these in hand, the system then passes them on to a series of `Runners`. These
clases
are responsible for processing the stack of `Tokens`, taking their various bits of data, and converting it into SQL
statements
which can then be executed against the Databse.
Once these SQL statements are executed, the results are collated and - when applicable - the structure outlined
in the original Query request is returned. (Note: Delete and Connect mutations don't have a bespoke return structure.
The response from
such operations will be consistent).
## Implementing Hermes in New Projects
Let's go over a sample of how to implement Hermes in a new project. This example will assume that you are
using the standard Service Provider pattern we use in other products, but if not you simple need to move the
code to whatever bootstrapping/provider paradigm you are using.
### Setting up Models
First, you need to register a `Model_Collection` to your provider and add some `Models` to it based on what your
data needs are. We'll register a single `Contact` model.
```php
$container->add( self::CONTACT_MODEL, function() {
// Assume this Model exists elsewhere in the codebase.
return new Contact_Model;
});
$container->add( self::MODEL_COLLECTION, function() use ( $container ) {
$collection = new Model_Collection();
$collection->add( 'contact', $container->get( self::CONTACT_MODEL ) );
});
```
### Configuring the Handlers
Next, we need to configure the `Handlers` for use in this system. To do this,
we'll need a couple pieces: a string representing the `db_namespace` used in this product,
and the `Model_Collection` we registered earlier. The `db_namespace` should be unique to
the product, and is used when generating table names.
```php
$container->add( self::QUERY_HANDLER, function() use ( $container ) {
$db_namespace = 'gravitytools';
$model_collection = $container->get( self::MODEL_COLLECTION );
return new Query_Handler( $db_namespace, $model_collection );
});
```
The `Mutation_Handler` is a bit more involved, as we'll also need to determine
which Mutation types we want to support, and pass the related `Runners` to our `Handler`.
Typically you'll need to support Insert, Update, Connect, and Delete operations.
```php
$container->add( self::MUTATION_HANDLER, function() use ( $container ) {
$db_namespace = 'gravitytools';
$model_collection = $container->get( self::MODEL_COLLECTION );
$query_handler = $container->get( self::QUERY_HANDLER );
$runners = array(
'insert' => new Insert_Runner( $db_namespace, $query_handler ),
'delete' => new Delete_Runner( $db_namespace, $query_handler ),
'connect' => new Connect_Runner( $db_namespace, $query_handler ),
'update' => new Update_Runner( $db_namespace, $query_handler ),
);
return new Mutation_Handler( $db_namespace, $model_collection, $query_handler, $runners );
});
```
With that, the system now has all the information it needs to start handling Queries!
### Adding Endpoints
Finally, you'll need to add endpoints to handle the Query requests. Typically you'll want to
register 2 endpoints, one for Query and one for Mutation, but depending on your needs you may also
use a single endpoint and handle routing internally. For this example, we'll register 2 endpoints via
WordPress's AJAX system.
```php
add_action( 'wp_ajax_hermes_query', function() use ( $container ) {
$query_string = filter_input( INPUT_POST, 'query', FILTER_DEFAULT );
// The handlers call wp_send_json() at the end, so no exit is required.
$container->get( self::QUERY_HANDLER )->handle_query( $query_string );
} );
add_action( 'wp_ajax_hermes_mutation', function() use ( $container ) {
$query_string = filter_input( INPUT_POST, 'mutation', FILTER_DEFAULT );
// The handlers call wp_send_json() at the end, so no exit is required.
$container->get( self::MUTATION_HANDLER )->handle_mutation( $query_string );
} );
```
With that in place, `POST` requests to `/wp-admin/admin-ajax.php?action=hermes_query` will trigger a Query
request, and `POST` requests to `/wp-admin/admin-ajax.php?action=hermes_mutation` will trigger a Mutation.
In actual production environments, you'll want to make sure to add in authentication via nonces/keys/some
other mechanism, but at this point you have a functioning Hermes system!
## Database Table Setup
In the future, the sytem will reference any registered `Models` and automatically generate DB tables based
on their information. For now, however, the table setup is a manual process.
The most-important aspect of table setup is that you follow the explicit naming convention required. The convention
is as follows:
#### For Objects
`$wpdb->prefix + '_' + $db_namespace + '_' + $object_type`
An object type named `contact` in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_contact`.
#### For Lookup Tables
`$wpdb->prefix + '_' + $db_namespace + '_' + $from_object_type + '_' + $to_object_type`
A table connecting a `contact` to `group` in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_contact_group`.
Columns should be `id`, `$from_object_id`, and `$to_object_id`.
So our `contact` to `group` table would have columns of `id`, `contact_id`, and `group_id`.
#### For the Meta Table
`$wpdb->prefix + '_' + $db_namespace + '_' + 'meta`
A meta table in a product with a namespace of `gravitytools` will need a table named `wp_gravitytools_meta`.
## Using Hermes
Once you've gotten Hermes set up in your environment (and created the related DB tables), you're ready to start
querying it.
There are two fundamental types of interactions you can have with Hermes: `query` and `mutation`.
### Queries
Queries are the mechanism used to _retrieve_ data from the server. In a REST paradigm, this would be your
`GET` requests. They don't modify any existing data, they simply retrieve data from the server in the shape
sent in the query.
#### Basic Query
Let's look at a very basic example. Imagine you need to retrieve all of the `company` records from the server,
and that you want to have the `id`, `name`, and `address` fields returned for each record. The query would look
something like this:
```graphql
{
company {
id,
name,
address
}
}
```
#### Adding Arguments
That works great, but it's rare that you need to retrieve every single record of a given type in a single query.
Instead, let's add `limit` as an `argument` so we only retreive the first 10 records:
```graphql
{
company( limit: 10 ) {
id,
name,
address
}
}
```
`Arguments` can be combined by passing multiple comma-delineated pairs:
```graphql
{
company( limit: 10, offset: 5, is_operational: true ) {
id,
name,
address
}
}
```
Here we provided three `arguments`: we limite the results to 10, offset the results by 5, and restrict
the results to only those records in which the field `is_operational` is `true`.
#### Argument Operators
By default, an argument will be evaluated as `=` (e.g. `limit: 10` becomes `limit = 10` ). Other operator types
can be indicated by suffixing the argument's field name with the operator type you wish to use. See the following table
for all the supported operators.
| Operator Type | Suffix | Example |
|---------------|-------------|---------------------------|
| `=` | `No Suffix` | `length: 10` |
| `!=` | `_ne` | `length_ne: 10` |
| `>` | `_gt` | `length_gt: 10` |
| `<` | `_lt` | `length_lt: 10` |
| `>=` | `_gte` | `length_gte: 10` |
| `<=` | `_lte` | `length_lte: 10` |
| `IN/CONTAINS` | `_in` | `length_in: 10\|20\|30\|` |
#### Related Object Records
We can also query for records related to the main object type we're retrieving. For instance, if we wanted to
retrieve all of the `departments` in each `company`, we can simply add it like so:
```graphql
{
company( limit: 10 ) {
id,
name,
address,
department {
id,
name,
}
}
}
```
Queries can be infinitely-nested, and each level can utilize arguments and fields just like a non-nested query. Do note, however,
that higher levels of nesting will result in more-complex queries, and on larger datasets the query time can balloon. Using `limits` and
other `argument` types can help alleviate this, but always use caution when heavily-nesting your queries.
#### Aliasing
Sometimes, the field names we use in the database don't work particularly well in client applications. Rather than forcing
the client application to traverse the resulting records and rename fields as needed, we can simply alias the field names directly
in the query:
```graphql
{
first_ten_companies: company( limit: 10 ) {
id,
business_name: name,
address
}
}
```
This would alias `company` to `first_ten_companies`, and the field `name` to `business_name` in the resulting data. Any level of the query
can be aliased, whether it's a field or a nested query.
### Mutations
In addition to retrieving data, we can also use Hermes to modify, create, or remove existing data. These types of operations
are called `mutations`, and come in four varieties. The type of mutation can be defined by prefixing the `object type` with the type
of `mutation` you wish to run.
#### Insert
An `insert` mutation inserts a new record into the database. It consists of two parts: the objects to insert, and the values to return once
they have been inserted:
```graphql
{
insert_company( objects: [{ name: "My Business", address: "1234 Pine Drive" }]) {
returning {
id,
name,
address
}
}
}
```
The above `mutation` would result in our record being added to the database, and the `id`, `name`, and `address` fields being returned for our new record.
Multiple records can be inserted at a time, and the returned values will include every record created.
#### Update
An `update` mutation modifies an existing record with new values. Similar to the `insert` mutation, we also define what data shape we want to be returned
once the update is complete. Unlike `insert` mutations, only a single record can be updated at a time.
```graphql
{
update_company( id: 5, name: "My Updated Company Name" ){
returning {
id,
name,
address
}
}
}
```
The above mutation would update the record with an `id` of `5` with the new name `My Updated Company Name`.
**Note**: every `update` mutation _must_ include an `id` so that the server knows which record to update. Updates
based on other column values are not currently supported.
#### Delete
A `delete` mutation simply removes the provided record from the database. No returning data shape is defined, as there
is no data to return.
```graphql
{
delete_company( id: 10 ){}
}
```
The above `mutation` would result in the company record with an id of `10` being deleted from the database.
**Note**: as with `update` mutations, you must pass the `id` of the record you wish to delete.
#### Connect
A `connect` mutation creates a relationship between two `object types` (if such a relationship is defined in
the appropriate object `models`). We don't define a resulting data shape; instead the server simply returns a
basic success/failure response.
```graphql
{
connect_company_department( from : 1, to: 3 ){}
}
```
The above `mutation` would result in the `company` with an `id` of `1` to be related to the `department` with
an `id` of `3`.
@@ -0,0 +1,94 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect\Connect_Mutation_Token;
/**
* A concrete implementation of Runner which handles database operations connecting
* two object types to one another via a lookup table.
*/
class Connect_Runner extends Runner {
/**
* Using the data provided by the Mutation_Token, this determines the correct
* DB table for the connection and inserts the appropriate IDs into the table.
*
* This also checks permissions for both object types to ensure that the user has
* the appropriate capabilities for running this connect mutation.
*
* @param Connect_Mutation_Token $mutation
* @param Model $object_model
*
* @return void
*/
public function run( $mutation, $object_model, $return = false ) {
$from_object = $mutation->from_object();
$to_object = $mutation->to_object();
$pairs = $mutation->pairs();
foreach ( $pairs as $pair ) {
$to_id = $pair['to'];
$from_id = $pair['from'];
$this->run_single( $from_object, $to_object, $from_id, $to_id, $object_model );
}
$response = sprintf( '%s connections from %s to %s created.', count( $pairs ), $from_object, $to_object );
if ( $return ) {
return $response;
}
wp_send_json_success( $response );
}
public function run_single( $from_object, $to_object, $from_id, $to_id, $object_model ) {
global $wpdb;
if ( ! $object_model->relationships()->has( $to_object ) ) {
$error_message = sprintf( 'Relationship from %s to %s does not exist.', $from_object, $to_object );
throw new \InvalidArgumentException( $error_message, 455 );
}
if ( ! $object_model->relationships()->get( $to_object )->has_access( 'edit' ) ) {
$error_message = sprintf( 'Attempting to access forbidden object type %s.', $to_object );
throw new \InvalidArgumentException( $error_message, 403 );
}
$relationship = $object_model->relationships()->get( $to_object );
if ( $relationship->is_one_to_many() ) {
$this->handle_otm_connection( $from_object, $to_object, $from_id, $to_id, $relationship );
return;
}
$table_name = sprintf( '%s%s_%s_%s', $wpdb->prefix, $this->db_namespace, $from_object, $to_object );
$check_sql = sprintf( 'SELECT * FROM %s WHERE %s_id = "%s" AND %s_id = "%s"', $table_name, $from_object, $from_id, $to_object, $to_id );
$existing = $wpdb->get_results( $check_sql );
if ( empty( $existing ) ) {
$connect_sql = sprintf( 'INSERT INTO %s ( %s_id, %s_id ) VALUES( "%s", "%s" )', $table_name, $from_object, $to_object, $from_id, $to_id );
}
$wpdb->query( $connect_sql );
}
private function handle_otm_connection( $from_object, $to_object, $from_id, $to_id, $relationship ) {
global $wpdb;
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $relationship->is_reverse() ? $from_object : $to_object );
$id_string = sprintf( '%sId', $relationship->is_reverse() ? $to_object : $from_object );
$check_sql = sprintf( 'SELECT * FROM %s WHERE %s = "%s" AND id = "%s"', $table_name, $id_string, $relationship->is_reverse() ? $to_id : $from_id, $relationship->is_reverse() ? $from_id : $to_id );
$existing = $wpdb->get_results( $check_sql );
if ( ! empty( $existing ) ) {
return;
}
$connect_sql = sprintf( 'UPDATE %s SET %s = "%s" WHERE id = "%s"', $table_name, $id_string, $relationship->is_reverse() ? $to_id : $from_id, $relationship->is_reverse() ? $from_id : $to_id );
$wpdb->query( $connect_sql );
}
}
@@ -0,0 +1,54 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Delete\Delete_Mutation_Token;
/**
* A concrete implementation of Runner which handles database operations for
* deleting an object from the appropriate tables.
*/
class Delete_Runner extends Runner {
/**
* Using the data provided by the Mutation_Token, this determines the correct
* DB table for the deletion action and removes the record.
*
*
* @param Delete_Mutation_Token $mutation
* @param Model $object_model
*
* @return void
*/
public function run( $mutation, $object_model, $return = false ) {
global $wpdb;
$ids_to_delete = $mutation->ids_to_delete();
$ids_to_delete = array_map( function( $id ) {
return esc_sql( $id );
}, $ids_to_delete );
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() );
do_action( 'gt_hermes_before_delete', $ids_to_delete, $table_name, $object_model->type() );
if ( in_array( 'all', $ids_to_delete ) ) {
$delete_sql = sprintf( 'DELETE FROM %s', $table_name );
$ids_to_delete = array( 'all' );
} else {
$in_clause = sprintf( '(%s)', implode( ', ', $ids_to_delete ) );
$delete_sql = sprintf( 'DELETE FROM %s WHERE id IN %s', $table_name, $in_clause );
}
$wpdb->query( $delete_sql );
if( $return ) {
return $ids_to_delete;
}
wp_send_json_success( array( 'deleted_ids' => $ids_to_delete ) );
}
}
@@ -0,0 +1,82 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect\Connect_Mutation_Token;
/**
* A concrete implementation of Runner which handles database operations disconnecting
* two object types from one another via a lookup table.
*/
class Disconnect_Runner extends Runner {
/**
* Using the data provided by the Mutation_Token, this determines the correct
* DB table for the connection and inserts the appropriate IDs into the table.
*
* This also checks permissions for both object types to ensure that the user has
* the appropriate capabilities for running this connect mutation.
*
* @param Connect_Mutation_Token $mutation
* @param Model $object_model
*
* @return void
*/
public function run( $mutation, $object_model, $return = false ) {
$from_object = $mutation->from_object();
$to_object = $mutation->to_object();
$pairs = $mutation->pairs();
foreach ( $pairs as $pair ) {
$to_id = $pair['to'];
$from_id = $pair['from'];
$this->run_single( $from_object, $to_object, $from_id, $to_id, $object_model );
}
$response = sprintf( '%s connections from %s to %s removed.', count( $pairs ), $from_object, $to_object );
if ( $return ) {
return $response;
}
wp_send_json_success( $response );
}
public function run_single( $from_object, $to_object, $from_id, $to_id, $object_model ) {
global $wpdb;
if ( ! $object_model->relationships()->has( $to_object ) ) {
$error_message = sprintf( 'Relationship from %s to %s does not exist.', $from_object, $to_object );
throw new \InvalidArgumentException( $error_message, 455 );
}
if ( ! $object_model->relationships()->get( $to_object )->has_access( 'edit' ) ) {
$error_message = sprintf( 'Attempting to access forbidden object type %s.', $to_object );
throw new \InvalidArgumentException( $error_message, 403 );
}
$relationship = $object_model->relationships()->get( $to_object );
if ( $relationship->is_one_to_many() ) {
$this->run_otm_single( $from_object, $to_object, $from_id, $to_id, $relationship );
return;
}
$table_name = sprintf( '%s%s_%s_%s', $wpdb->prefix, $this->db_namespace, $from_object, $to_object );
$disconnect_sql = sprintf( 'DELETE FROM %s WHERE %s_id = "%s" AND %s_id = "%s"', $table_name, $from_object, $from_id, $to_object, $to_id );
$wpdb->query( $disconnect_sql );
}
public function run_otm_single( $from_object, $to_object, $from_id, $to_id, $relationship ) {
global $wpdb;
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $relationship->is_reverse() ? $from_object : $to_object );
$id_string = sprintf( '%sId', $to_object );
$disconnect_sql = sprintf( 'UPDATE %s SET %s = "0" WHERE id = "%s"', $table_name, $id_string, $relationship->is_reverse() ? $to_id : $from_id );
$wpdb->query( $disconnect_sql );
}
}
@@ -0,0 +1,129 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Insert\Insert_Mutation_Token;
/**
* A concrete implementation of Runner which handles database operations required
* to insert a set of objects into the appropriate tables.
*/
class Insert_Runner extends Runner {
/**
* @var Connect_Runner
*/
protected $connect_runner;
public function __construct( $db_namespace, $query_handler, $model_collection, $connect_runner ) {
parent::__construct( $db_namespace, $query_handler, $model_collection );
$this->connect_runner = $connect_runner;
}
/**
* Using the data provided by the Mutation_Token, this determines the correct
* DB table for the insertion and adds the records one-by-one.
*
* This also checks permissions for the object type to ensure that the user has
* the appropriate capabilities for running this insert mutation. If any meta fields
* are defined in the mutation, those values are inserted into the meta table with
* the appropriate values.
*
* @param Insert_Mutation_Token $mutation
* @param Model $object_model
*
* @return void
*/
public function run( $mutation, $object_model, $return = false ) {
$insertion_objects = $mutation->children();
$inserted_ids = array();
$child_ids = array();
foreach ( $insertion_objects->children() as $idx => $object ) {
if ( ! $this->models->has( $object->object_type() ) ) {
throw new \InvalidArgumentException( sprintf( 'Object type %s does not exist.', $object->object_type() ) );
}
$fields = $object->children();
$object_model = $this->models->get( $object->object_type() );
$categorized_fields = $this->categorize_fields( $object_model, $fields );
// Make sure to set the correct timestamps for created and updated.
$categorized_fields['local']['dateCreated'] = gmdate( 'Y-m-d H:i:s', time() );
$categorized_fields['local']['dateUpdated'] = gmdate( 'Y-m-d H:i:s', time() );
$inserted_id = $this->handle_single_insert( $object_model, $categorized_fields );
if ( $object->is_child() ) {
if ( ! isset( $child_ids[ $object->parent_object_type() ] ) ) {
$child_ids[ $object->parent_object_type() ] = array();
}
if ( ! isset( $child_ids[ $object->parent_object_type() ][ $object->object_type() ] ) ) {
$child_ids[ $object->parent_object_type() ][ $object->object_type() ] = array();
}
$child_ids[ $object->parent_object_type() ][ $object->object_type() ][] = $inserted_id;
}
if ( ! empty( $child_ids[ $object->object_type() ] ) ) {
foreach ( $child_ids[ $object->object_type() ] as $child_type => $children ) {
foreach( $children as $child_id ) {
$this->connect_runner->run_single( $object->object_type(), $child_type, $inserted_id, $child_id, $object_model );
}
}
$child_ids[ $object->object_type() ] = array();
}
if ( ! $object->is_child() ) {
$inserted_ids[] = $inserted_id;
}
}
$objects_gql = sprintf( '{ %s: %s(id_in: %s){ %s }', $object_model->type(), $object_model->type(), implode( '|', $inserted_ids ), $mutation->return_fields() );
$data = $this->query_handler->handle_query( $objects_gql, $return );
if ( $return ) {
return $data;
}
wp_send_json_success( $data );
}
/**
* Helper method to handle an individual insertion action from the array of objects to insert.
*
* @param Model $object_model
* @param array $categorized_fields
*
* @return int
*/
private function handle_single_insert( $object_model, $categorized_fields ) {
global $wpdb;
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() );
$field_list = $this->get_field_name_list_from_fields( $categorized_fields['local'] );
$values_list = $this->get_field_values_list_from_fields( $categorized_fields['local'] );
$sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $table_name, $field_list, $values_list );
$wpdb->query( $sql );
$object_id = $wpdb->insert_id;
if ( ! empty( $categorized_fields['meta'] ) ) {
foreach ( $categorized_fields['meta'] as $key => $value ) {
$meta_table_name = sprintf( '%s%s_meta', $wpdb->prefix, $this->db_namespace );
$insert_fields_string = 'object_type, object_id, meta_name, meta_value';
$insert_values_string = sprintf( '"%s", "%s", "%s", "%s"', $object_model->type(), $object_id, $key, $value );
$meta_sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $meta_table_name, $insert_fields_string, $insert_values_string );
$wpdb->query( $meta_sql );
}
}
return $object_id;
}
}
@@ -0,0 +1,197 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Enum\Field_Type_Validation_Enum;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Query_Handler;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Model_Collection;
/**
* Runner
*
* This provides the abstract contract for a Runner.
*
* Runners are the classes responsible for actually running a given mutation type.
* The majority of the logic already exists in this abstract class, so all a concrete
* need do is implement the public ::run() method to take the mutation values and
* handle the database interactions.
*/
abstract class Runner {
/**
* The namespace to use for DB tables. This is used between the
* $wpdb->prefix value and the DB table name.
*
* Example: passing 'gravitytools' here would result in tables such
* as 'wp_gravitytools_meta', etc.
*
* This is typically defined a single time in a service provider and passed
* to the various classes which need it, such as this Runner.
*
* @var string
*/
protected $db_namespace;
/**
* The Query Handler is used to form response objects for Insert and Update operations.
* Using it instead of direct SQL calls ensures that aliases, field validation, etc, are
* all applied to the response just like a normal Query would.
*
* @var Query_Handler
*/
protected $query_handler;
/**
* The collection of models currently available to the system.
*
* @var Model_Collection
*/
protected $models;
/**
* See property descriptions for more information about these arguments and their usage.
*
* @param string $db_namespace
* @param Query_Handler $query_handler
* @param Model_Collection $model_collection
*
* @return void
*/
public function __construct( $db_namespace, $query_handler, $model_collection ) {
$this->db_namespace = $db_namespace;
$this->query_handler = $query_handler;
$this->models = $model_collection;
}
/**
* Run the actual mutation action against the database.
*
* Concrete Runners will implement this method in order to handle all
* of the database transactions for this mutation type.
*
* This method should have no return value, as it is intended to be the final
* process called before echoing the JSON to return to the client. As such, this method
* should always end with either wp_json_send_success() or wp_json_send_error() to ensure
* that values are echoed correctly and that the request doesn't continue after processing.
*
* @param Mutation_Token $mutation
* @param Model $object_model
* @param bool $return
*
* @return void
*/
abstract public function run( $mutation, $object_model, $return = false );
/**
* Helper method to take an array of fields and return a comma-delimited
* list for use in INSERT column statements.
*
* @param array $fields
*
* @return string
*/
protected function get_field_name_list_from_fields( $fields ) {
return implode( ', ', array_keys( $fields ) );
}
/**
* Helper method to take an array of fields and return a comma-delimited
* list of their values for use in an INSERT VALUES() statement.
*
* @param array $fields
*
* @return string
*/
protected function get_field_values_list_from_fields( $fields ) {
$values = array_values( $fields );
foreach ( $values as $key => $value ) {
$values[ $key ] = sprintf( '"%s"', $value );
}
return implode( ', ', $values );
}
/**
* Helper method to take an array of fields and return a set of
* comma-delimited pairs of 'key = value' strings for usage in
* UPDATE statements.
*
* @param array $fields
*
* @return string
*/
protected function get_update_field_list( $fields ) {
$pairs = array();
foreach ( $fields as $key => $value ) {
if ( $key === 'id' ) {
continue;
}
$pairs[] = sprintf( '%s = "%s"', $key, $value );
}
return implode( ', ', $pairs );
}
/**
* Takes an object model and array of fields to process and categorizes them into
* 'local' (aka, existing in the database as columns) fields and 'meta' fields. This
* also uses the Model to check permissions for the given object type, and ensures
* that all referenced fields are properly defined in the Model.
*
* @param Model $object_model
* @param array $fields_to_process
*
* @return array|array[]
*/
protected function categorize_fields( $object_model, $fields_to_process ) {
$categorized = array(
'meta' => array(),
'local' => array(),
);
foreach ( $fields_to_process as $field_name => $value ) {
// Sometimes parsed empty arrays get misidentified as columnar fields. We need to disregard them.
if ( is_array( $value ) && empty( $value ) ) {
continue;
}
if ( ! $object_model->supports_ad_hoc_fields() && ! array_key_exists( $field_name, $object_model->fields() ) && ! array_key_exists( $field_name, $object_model->meta_fields() ) ) {
$error_string = sprintf( 'Attempting to access invalid field %s on object type %s', $field_name, $object_model->type() );
throw new \InvalidArgumentException( $error_string, 450 );
}
if ( array_key_exists( $field_name, $object_model->fields() ) ) {
$field_validation_type = $object_model->fields()[ $field_name ];
$validated = Field_Type_Validation_Enum::validate( $field_validation_type, $value );
if ( ! is_null( $value ) && is_null( $validated ) ) {
$field_type_string = is_string( $field_validation_type ) ? $field_validation_type : 'callback';
$error_string = sprintf( 'Invalid field value %s sent to field %s with a type of %s.', $value, $field_name, $field_type_string );
throw new \InvalidArgumentException( $error_string, 451 );
}
$categorized['local'][ $field_name ] = $validated;
}
if ( array_key_exists( $field_name, $object_model->meta_fields() ) ) {
$field_validation_type = $object_model->meta_fields()[ $field_name ];
$validated = Field_Type_Validation_Enum::validate( $field_validation_type, $value );
if ( ! is_null( $value ) && is_null( $validated ) ) {
$error_string = sprintf( 'Invalid field value %s sent to field %s with a type of %s.', $value, $field_name, (string) $field_validation_type );
throw new \InvalidArgumentException( $error_string, 451 );
}
$categorized['meta'][ $field_name ] = $validated;
}
}
return $categorized;
}
}
@@ -0,0 +1,158 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Data_Object_From_Array_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Field_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Model_Collection;
class Schema_Runner {
/**
* @var Model_Collection
*/
protected $models;
private $available_fields = array(
'name',
'fields',
'metaFields',
'relationships',
);
private $sub_fields_map = array(
'fields' => array(
'name',
'type',
),
'meta_fields' => array(
'name',
'type',
),
'relationships' => array(
'to',
'accessCap',
),
);
public function __construct( $models ) {
$this->models = $models;
}
/**
* Run the process for gathering info about the schema.
*
* @var Data_Object_From_Array_Token $object
*
* @return array
*/
public function run( $object ) {
$data = array();
foreach ( $this->models->all() as $model ) {
$data[] = $this->get_data_for_model( $object->children(), $model );
}
return $data;
}
/**
* Get model data for a single model using the given fields.
*
* @param array $fields
* @param Model $model
*
* @return array
*/
private function get_data_for_model( $fields, $model ) {
$data = array();
foreach ( $fields as $field ) {
$check_val = is_a( $field, Field_Token::class ) ? $field->name() : $field->object_type();
if ( ! in_array( $check_val, $this->available_fields ) ) {
throw new \InvalidArgumentException( sprintf( 'Attempting to access invalid schema field %s', $check_val ) );
}
if ( is_a( $field, Data_Object_From_Array_Token::class ) ) {
$subfields_to_include = $this->get_field_names_from_token( $field );
}
switch ( $check_val ) {
case 'name':
$data[ $check_val ] = $model->type();
break;
case 'fields':
$row_data = array();
foreach( $model->fields() as $field_name => $field_type ) {
if ( ! is_string( $field_type ) ) {
$field_type = 'custom';
}
$row_subdata = array();
if ( in_array( 'name', $subfields_to_include ) ) {
$row_subdata['name'] = $field_name;
}
if ( in_array( 'type', $subfields_to_include ) ) {
$row_subdata['type'] = $field_type;
}
$row_data[] = $row_subdata;
}
$data[ $check_val ] = $row_data;
break;
case 'metaFields':
$row_data = array();
foreach( $model->meta_fields() as $field_name => $field_type ) {
if( ! is_string( $field_type ) ) {
$field_type = 'custom';
}
$row_subdata = array();
if ( in_array( 'name', $subfields_to_include ) ) {
$row_subdata['name'] = $field_name;
}
if ( in_array( 'type', $subfields_to_include ) ) {
$row_subdata['type'] = $field_type;
}
$row_data[] = $row_subdata;
}
$data[ $check_val ] = $row_data;
break;
case 'relationships':
$row_data = array();
foreach( $model->relationships()->all() as $relationship ) {
$row_subdata = array();
if ( in_array( 'to', $subfields_to_include ) ) {
$row_subdata['to'] = $relationship->to();
}
if( in_array( 'accessCap', $subfields_to_include ) ) {
$row_subdata['accessCap'] = $relationship->cap();
}
$row_data[] = $row_subdata;
}
$data[ $check_val ] = $row_data;
break;
default:
break;
}
}
return $data;
}
/**
* Get individual field names from a given token.
*
* @param Data_Object_From_Array_Token $token
*
* @return array
*/
private function get_field_names_from_token( $token ) {
$fields = array();
foreach( $token->fields() as $field ) {
$fields[] = $field->name();
}
return $fields;
}
}
@@ -0,0 +1,91 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Runners;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Update\Update_Mutation_Token;
/**
* A concrete implementation of Runner which handles database operations required
* to insert a set of objects into the appropriate tables.
*/
class Update_Runner extends Runner {
/**
* Using the data provided by the Mutation_Token, this determines the correct
* DB table for the update and updates the record identified by the ID column.
*
* This also checks permissions for the object type to ensure that the user has
* the appropriate capabilities for running this update mutation. If any meta fields
* are defined in the mutation, those values are inserted into the meta table with
* the appropriate values.
*
* @param Update_Mutation_Token $mutation
* @param Model $object_model
*
* @return void
*/
public function run( $mutation, $object_model, $return = false ) {
$fields_to_update = $mutation->children()->children();
if ( ! array_key_exists( 'id', $fields_to_update ) ) {
$error_string = sprintf( 'Update mutations must contain an id in the fields list. Fields provided: %s', json_encode( array_keys( $fields_to_update ) ) );
throw new \InvalidArgumentException( $error_string, 452 );
}
$object_id = $fields_to_update['id'];
$categorized_fields = $this->categorize_fields( $object_model, $fields_to_update );
// Set dateUpdated with current time
$categorized_fields['local']['dateUpdated'] = gmdate( 'Y-m-d H:i:s', time() );
$this->handle_single_update( $object_model, $categorized_fields, $object_id );
$objects_gql = sprintf( '{ %s: %s(id: %s){ %s }', $object_model->type(), $object_model->type(), $object_id, $mutation->return_fields() );
$data = $this->query_handler->handle_query( $objects_gql, $return );
if ( $return ) {
return $data;
}
wp_send_json_success( $data );
}
/**
* Helper method to handle an individual update action from the array of objects to update.
*
* @param Model $object_model
* @param array $categorized_fields
*
* @return int
*/
private function handle_single_update( $object_model, $categorized_fields, $object_id ) {
global $wpdb;
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_model->type() );
$field_list = $this->get_update_field_list( $categorized_fields['local'] );
$sql = sprintf( 'UPDATE %s SET %s WHERE id = "%s"', $table_name, $field_list, $object_id );
$wpdb->query( $sql );
if ( ! empty( $categorized_fields['meta'] ) ) {
foreach ( $categorized_fields['meta'] as $key => $value ) {
$meta_table_name = sprintf( '%s%s_meta', $wpdb->prefix, $this->db_namespace );
$delete_sql = sprintf( 'DELETE FROM %s WHERE meta_name = "%s" AND object_id = "%s"', $meta_table_name, $key, $object_id );
$wpdb->query( $delete_sql );
$insert_fields_string = 'object_type, object_id, meta_name, meta_value';
$insert_values_string = sprintf( '"%s", "%s", "%s", "%s"', $object_model->type(), $object_id, $key, $value );
$meta_sql = sprintf( 'INSERT INTO %s (%s) VALUES (%s)', $meta_table_name, $insert_fields_string, $insert_values_string );
$wpdb->query( $meta_sql );
}
}
return $object_id;
}
}
@@ -0,0 +1,168 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
/**
* Connect Mutation Token
*
* A token containing information for perfoming a Connect Mutation.
*/
class Connect_Mutation_Token extends Mutation_Token {
protected $operation = 'connect';
/**
* A token containing the IDs to connect.
*
* @var Connection_Values_Token
*/
protected $connection_ids;
/**
* The object type to connect from.
*
* @var string
*/
protected $from_object;
/**
* The object type to connect to.
*
* @var string
*/
protected $to_object;
/**
* Public $from_object accessor.
*
* @return string
*/
public function from_object() {
return $this->from_object;
}
/**
* Public $to_object accessor.
*
* @return string
*/
public function to_object() {
return $this->to_object;
}
/**
* Public $pairs accessor.
*
* $return array
*/
public function pairs() {
return $this->connection_ids->pairs();
}
/**
* Return the class properties as an array when children() is called.
*
* @return array
*/
public function children() {
return array(
'from_object' => $this->from_object(),
'to_object' => $this->to_object(),
'pairs' => $this->pairs(),
);
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$this->tokenize( $results );
return;
}
/**
* Use the given REGEX matches to generate the appropriate tokens for this mutation.
*
* @param array $parts - An array resulting from preg_match_all() with this class's regex values.
*
* @return void
*/
public function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$data = array();
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'operation_alias':
$objects = str_replace( 'connect_', '', $value );
$object_parts = explode( '_', $objects );
if ( count( $object_parts ) !== 2 ) {
throw new \InvalidArgumentException( 'Error parsing connection object types.', 480 );
}
$data['from_object'] = $object_parts[0];
$data['to_object'] = $object_parts[1];
$data['alias'] = $value;
break;
case 'arg_group':
$data['connection_ids'] = new Connection_Values_Token( $value );
break;
case 'alias':
$has_alias = $value;
break;
}
}
if ( empty( $data['connection_ids'] ) || empty( $data['from_object'] ) ) {
throw new \InvalidArgumentException( 'Connect payload malformed. Check values and try again.', 485 );
}
$this->set_properties( $data );
}
/**
* Use the parsed tokens data to set the class properties.
*
* @param array $data
*
* @return void
*/
protected function set_properties( $data ) {
$this->alias = $data['alias'];
$this->connection_ids = $data['connection_ids'];
$this->object_type = $data['from_object'];
$this->from_object = $data['from_object'];
$this->to_object = $data['to_object'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
protected function regex_types() {
return array(
'operation_alias' => 'connect_[^\(]*',
'arg_group' => '\[[^\]]+\]',
'alias' => '[_A-Za-z][_0-9A-Za-z]*:',
'open_bracket' => '{',
'close_bracket' => '}',
);
}
}
@@ -0,0 +1,107 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token;
/**
* Connection Values Token
*
* Used to hold the IDs to use for a given Connect mutation.
*/
class Connection_Values_Token extends Token {
protected $type = 'Connection_Values';
/**
* An array of sub-arrays, each consisting of a `to` and `from` index.
*
* @var array[]
*/
protected $pairs = array();
public function pairs() {
return $this->pairs;
}
/**
* Return the $to and $from values as children.
*
* @return array
*/
public function children() {
return $this->pairs;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$matches = $results[0];
$marks = $results['MARK'];
$state = array();
$data = array();
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'argument_pair':
$parts = explode( ':', $value );
if ( count( $parts ) < 2 ) {
break;
}
$key = trim( $parts[0] );
$this_value = trim( $parts[1] );
if ( ! in_array( $key, array( 'to', 'from' ) ) ) {
break;
}
$state[ $key ] = trim( $this_value, ' "' );
break;
case 'splitter':
// We don't have a to and from, bail.
if ( empty( $state['to'] ) || empty( $state['from'] ) ) {
$state = array();
break;
}
$data[] = $state;
$state = array();
break;
}
}
if ( ! empty( $state['to'] ) && ! empty( $state['from'] ) ) {
$data[] = $state;
$state = array();
}
$this->pairs = $data;
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
return array(
'argument_pair' => '([a-zA-z0-9_-]*):([^,\}\)]+)',
'splitter' => '\},',
);
}
}
@@ -0,0 +1,9 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect;
class Disconnect_Mutation_Token extends Connect_Mutation_Token {
protected $operation = 'disconnect';
}
@@ -0,0 +1,115 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Delete;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
/**
* Delete Mutation Token
*
* Contains the values for executing a Delete mutation.
*/
class Delete_Mutation_Token extends Mutation_Token {
protected $operation = 'delete';
/**
* Token holding the ID to delete.
*
* @var ID_To_Delete_Token
*/
protected $id_to_delete;
/**
* Public accessor for the ID to delete.
*
* @return string
*/
public function ids_to_delete() {
return $this->id_to_delete->ids();
}
/**
* Pass through the children from the $id_to_delete token.
*
* @return array
*/
public function children() {
return $this->id_to_delete->children();
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$this->tokenize( $results );
return;
}
/**
* Use the given REGEX matches to generate the appropriate tokens for this mutation.
*
* @param array $parts - An array resulting from preg_match_all() with this class's regex values.
*
* @return void
*/
public function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$data = array();
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'operation_alias':
$data['object_type'] = str_replace( 'delete_', '', $value );
$data['alias'] = $value;
break;
case 'arg_group':
$data['id_to_delete'] = new ID_To_Delete_Token( $value );
break;
case 'alias':
$has_alias = $value;
break;
}
}
if ( empty( $data['id_to_delete'] ) || empty( $data['object_type'] ) ) {
throw new \InvalidArgumentException( 'Delete payload malformed. Check values and try again.', 490 );
}
$this->set_properties( $data );
}
protected function set_properties( $data ) {
$this->alias = $data['alias'];
$this->object_type = $data['object_type'];
$this->id_to_delete = $data['id_to_delete'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
protected function regex_types() {
return array(
'operation_alias' => 'delete_[^\(]*',
'arg_group' => '\([^\)]+\)',
'alias' => '[_A-Za-z][_0-9A-Za-z]*:',
'open_bracket' => '{',
'close_bracket' => '}',
);
}
}
@@ -0,0 +1,85 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Delete;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token;
/**
* ID To Delete Token
*
* Stores the ID to delete for a Delete mutation.
*/
class ID_To_Delete_Token extends Token {
protected $type = 'IDs_To_Delete';
/**
* The IDs to delete.
*
* @var string
*/
protected $ids;
/**
* Public accessor for $id.
*
* @return string
*/
public function ids() {
return $this->ids;
}
/**
* Return the ID as an array as children.
*
* @return array
*/
public function children() {
return $this->ids;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$matches = $results[0];
$marks = $results['MARK'];
$data = array();
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'ids':
$data['ids'] = json_decode( $value );
break;
}
}
if ( empty( $data['ids'] ) ) {
throw new \InvalidArgumentException( 'Delete payload malformed. Check values and try again.', 490 );
}
$this->ids = $data['ids'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
return array(
'ids' => '\[[^\]]+\]',
);
}
}
@@ -0,0 +1,148 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Insert;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
/**
* Insert Mutation Token
*
* Holds the values for an Insert mutation.
*/
class Insert_Mutation_Token extends Mutation_Token {
protected $operation = 'insert';
/**
* An string of fields to return after the mutation is performed.
*
* @var string
*/
protected $return_fields = '';
/**
* A token holding the various objects to insert during this mutation.
*
* @var Insertion_Objects_Token
*/
protected $objects_to_insert;
/**
* Public accessor for $return_fields.
*
* @return array
*/
public function return_fields() {
return $this->return_fields;
}
/**
* Return $objects_to_insert as children.
*
* @return Insertion_Objects_Token
*/
public function children() {
return $this->objects_to_insert;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
$contents = preg_replace( "/\r|\n|\t/", '', $contents );
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$this->tokenize( $results );
return;
}
/**
* Use the given REGEX matches to generate the appropriate tokens for this mutation.
*
* @param array $parts - An array resulting from preg_match_all() with this class's regex values.
*
* @return void
*/
public function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$data = array();
$next_is_return = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'returning_def':
$cleaned_return_def = preg_replace( "/\r|\n|\t/", '', $value );
$cleaned_return_def = str_replace( 'returning {', '', $cleaned_return_def );
$cleaned_return_def = substr( $cleaned_return_def, 0, -3 );
$data['return_fields'] = $cleaned_return_def;
break;
case 'operation_alias':
$data['object_type'] = str_replace( 'insert_', '', $value );
$data['alias'] = $value;
break;
case 'arg_group':
$data['objects_to_insert'] = new Insertion_Objects_Token( $value, array( 'object_type' => $data['object_type'] ) );
break;
case 'alias':
$has_alias = $value;
break;
case 'identifier':
break;
case 'close_bracket':
if ( $next_is_return ) {
$next_is_return = false;
break;
} else {
$this->set_properties( $data );
return;
}
}
}
$this->set_properties( $data );
}
/**
* Use the parsed tokens data to set the class properties.
*
* @param array $data
*
* @return void
*/
protected function set_properties( $data ) {
$this->alias = $data['alias'];
$this->object_type = $data['object_type'];
$this->objects_to_insert = $data['objects_to_insert'];
$this->return_fields = $data['return_fields'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
protected function regex_types() {
return array(
'returning_def' => 'returning {[^\%]+',
'operation_alias' => 'insert_[^\(]*',
'arg_group' => '\(\s*objects:.*\)\s*\{',
'alias' => '[_A-Za-z][_0-9A-Za-z]*:',
'identifier' => '[_A-Za-z][_0-9A-Za-z]*',
'open_bracket' => '{',
'close_bracket' => '}',
);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Insert;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token_From_Array;
/**
* Insertion Object Token
*
* A token holding the values for an object to be inserted into the Database.
*/
class Insertion_Object_Token extends Token_From_Array {
protected $type = 'insertion_object';
/**
* The fields to insert for this object.
*
* @var array
*/
protected $items;
protected $object_type;
protected $is_child;
protected $parent_object_type;
/**
* Return $items as children.
*
* @return array
*/
public function children() {
return $this->items;
}
public function object_type() {
return $this->object_type;
}
public function is_child() {
return $this->is_child;
}
public function parent_object_type() {
return $this->parent_object_type;
}
/**
* Parse the string contents to values.
*
* @return void
*/
public function parse( &$matches, &$marks, $additional_args = array() ) {
$fields = $additional_args['fields'];
$this->items = $fields;
$this->object_type = $additional_args['object_type'];
$this->is_child = $additional_args['is_child'];
$this->parent_object_type = $additional_args['parent_object_type'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
return array(
'relatedObject' => '',
'value' => ':[^,\}]+',
'key' => '[a-zA-Z0-9_\-]+',
);
}
}
@@ -0,0 +1,169 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Insert;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token;
/**
* Insertion Objects Token
*
* A token holding a series of Insertion Object Tokens containing the various items to insert during this mutation.
*/
class Insertion_Objects_Token extends Token {
protected $type = 'insertion_objects';
/**
* @var Insertion_Object_Token[]
*/
protected $objects;
/**
* Return the array of Insertion Object Tokens as children.
*
* @return Insertion_Object_Token[]
*/
public function children() {
return $this->objects;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
$contents = preg_replace( "/\r|\n|\t/", '', $contents );
preg_match_all( $this->get_parsing_regex(), $contents, $parts );
$matches = $parts[0];
$marks = $parts['MARK'];
$current_object_type = $args['object_type'];
$organized_data[ $current_object_type ] = $this->recursively_organize_tokens( $matches, $marks, $current_object_type );
$organized_objects = array();
$this->recursively_convert_tokens_to_objects( $organized_objects, $organized_data[ $current_object_type ]['objects'], $current_object_type, false, false );
$this->objects = $organized_objects;
}
private function recursively_organize_tokens( &$matches, &$marks, $object_type ) {
$data = array();
$current_field_key = false;
$is_child = false;
$is_array_value = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'closingArray':
if ( $is_array_value ) {
$is_array_value = false;
}
break;
case 'key':
if ( $is_array_value ) {
$data[ $current_field_key ][] = $value;
break;
}
if ( $marks[0] === 'openingArray' ) {
$object_type = $value;
if ( $marks[1] !== 'openingObject' ) {
$is_array_value = true;
$current_field_key = $value;
}
if ( ! isset( $data[ $value ] ) ) {
$data[ $value ] = array();
if ( $marks[1] === 'openingObject' && $object_type !== 'objects' ) {
$data[ $value ]['isChild'] = true;
}
$is_child = ( $marks[1] === 'openingObject' );
}
break;
}
$current_field_key = $value;
break;
case 'openingObject':
if ( $is_child ) {
$data[ $object_type ][] = $this->recursively_organize_tokens( $matches, $marks, $object_type );
break;
}
break;
case 'closingObject':
return $data;
case 'value':
$field_value = trim( $value, ': ' );
if ( $current_field_key ) {
$data[ $current_field_key ] = $field_value;
$current_field_key = false;
}
break;
default:
break;
}
}
return $data;
}
private function recursively_convert_tokens_to_objects( &$organized_objects, $data, $current_object_type, $parent_object_type = false, $is_child = false ) {
$fields = array();
$marks = array();
$matches = array();
foreach ( $data as $object_idx => $object_data ) {
// Avoid bad data.
if ( ! is_array( $object_data ) || empty( $object_data ) ) {
continue;
}
foreach ( $object_data as $key => $value ) {
if ( is_array( $value ) && isset( $value['isChild'] ) ) {
unset( $value['isChild'] );
$this->recursively_convert_tokens_to_objects( $organized_objects, $value, $key, $current_object_type, true );
continue;
}
$field_key = $key;
$fields[ $field_key ] = is_string( $value ) ? trim( $value, ': "' ) : $value;
}
$organized_objects[] = new Insertion_Object_Token(
$marks,
$matches,
array(
'object_type' => $current_object_type,
'fields' => $fields,
'is_child' => $is_child,
'parent_object_type' => $parent_object_type,
)
);
}
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
// (?|(*MARK:openingArray)\[|(*MARK:openingObject)\{|(*MARK:closingObject)\}|(*MARK:closingArray)\]|(*MARK:value):\s*"[^"]+\s*"|(*MARK:key)[a-zA-Z0-9_\-]+)
return array(
'openingArray' => '\[',
'openingObject' => '\{',
'closingObject' => '\}',
'closingArray' => '\]',
'value' => ':\s*"[^"]+\s*"',
'key' => '[a-zA-Z0-9_\-]+',
);
}
}
@@ -0,0 +1,189 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Update;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Token;
/**
* Fields to Update Token
*
* A token holding the fields to update during this Mutation.
*/
class Fields_To_Update_Token extends Token {
protected $type = 'Fields_To_Update';
/**
* The fields to update.
*
* @var array
*/
protected $items = array();
/**
* Public accessor for $items.
*
* @return array
*/
public function items() {
return $this->items;
}
/**
* Return $items as children.
*
* @return array
*/
public function children() {
return $this->items();
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$this->tokenize( $results );
}
private function reset_state( &$state ) {
$state = array(
'is_text_block' => false,
'key_found' => '',
'value_found' => '',
);
}
protected function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$state = array(
'is_text_block' => false,
'key_found' => '',
'value_found' => '',
);
$data = array();
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'string':
if ( ! $state['key_found'] && ! $state['is_text_block'] ) {
$state['key_found'] = trim( $value, ': )' );
break;
}
if ( $state['key_found'] && ! $state['is_text_block'] && ! $state['value_found'] ) {
$state['value_found'] = trim( $value, ': )' );
break;
}
if ( $state['is_text_block'] ) {
$state['value_found'] = $state['value_found'] . $value;
}
break;
case 'comma':
if ( $state['is_text_block'] ) {
$state['value_found'] = $state['value_found'] . $value;
break;
}
// End of key/value pair
$data[ $state['key_found'] ] = $state['value_found'];
$this->reset_state( $state );
break;
case 'colon':
case 'comma':
if ( $state['is_text_block'] ) {
$state['value_found'] = $state['value_found'] . $value;
break;
}
break;
case 'quote':
// Start of text block.
if ( ! $state['is_text_block'] ) {
$state['is_text_block'] = true;
break;
}
// End of text block.
$data[ $state['key_found'] ] = $state['value_found'];
$this->reset_state( $state );
break;
case 'num':
if ( $state['key_found'] && ! $state['is_text_block'] ) {
$state['value_found'] = $value;
break;
}
if ( $state['is_text_block'] ) {
$state['value_found'] = $state['value_found'] . $value;
}
break;
case 'esc_quote':
if ( $state['is_text_block'] ) {
$state['value_found'] = $state['value_found'] . '"';
}
break;
}
}
if ( $state['key_found'] && $state['value_found'] ) {
$data[ $state['key_found'] ] = $state['value_found'];
}
$data = array_filter( $data, function ( $item, $key ) {
if ( $item === 0 || $item === '0' ) {
return true;
}
if ( empty( $key ) ) {
return false;
}
return $item !== false && $item !== null;
}, ARRAY_FILTER_USE_BOTH );
$this->set_properties( $data );
}
private function set_properties( $data ) {
$this->items = $data;
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
return array(
'opening_par' => '\(',
'comma' => ',',
'bool' => 'true|false',
'colon' => ':',
'comma' => ',',
'string' => '[a-zA-Z_\s\'\.$&+;=?@#|<>.\^*()%!-]+',
'num' => '[0-9]+',
'quote' => '[\"\\\']',
'esc_quote' => '\\\\"',
'closing_para' => '\\)',
);
}
}
@@ -0,0 +1,163 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Update;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
/**
* Update Mutation Token
*
* Contains the values needed for executing an Update Mutation.
*/
class Update_Mutation_Token extends Mutation_Token {
protected $operation = 'update';
/**
* The fields to return after the Update has been performed.
*
* @var string
*/
protected $return_fields = '';
/**
* The fields to update during the mutation.
*
* @var Fields_To_Update_Token
*/
protected $fields_to_update;
/**
* Public accessor for $return_fields
*
* @return string
*/
public function return_fields() {
return $this->return_fields;
}
/**
* Public acessor for $fields_to_update.
*
* @return Fields_To_Update_Token
*/
public function fields_to_update() {
return $this->fields_to_update;
}
/**
* Return $fields_to_update as children.
*
* @return Fields_To_Update_Token
*/
public function children() {
return $this->fields_to_update;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$this->tokenize( $results );
return;
}
/**
* Use the given REGEX matches to generate the appropriate tokens for this mutation.
*
* @param array $parts - An array resulting from preg_match_all() with this class's regex values.
*
* @return void
*/
public function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$data = array(
'return_fields' => '',
);
$next_is_return = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'returning_def':
$cleaned_return_def = preg_replace( "/\r|\n|\t/", '', $value );
$cleaned_return_def = str_replace( 'returning {', '', $cleaned_return_def );
$cleaned_return_def = substr( $cleaned_return_def, 0, -3 );
$data['return_fields'] = $cleaned_return_def;
break;
case 'operation_alias':
$data['object_type'] = str_replace( 'update_', '', $value );
$data['alias'] = $value;
break;
case 'arg_group':
$data['fields_to_update'] = new Fields_To_Update_Token( $value );
break;
case 'alias':
$has_alias = $value;
break;
case 'identifier':
if ( ! $next_is_return ) {
break;
}
$data['return_fields'][] = $value;
break;
case 'close_bracket':
if ( $next_is_return ) {
$next_is_return = false;
break;
} else {
$this->set_properties( $data );
return;
}
}
}
$this->set_properties( $data );
}
/**
* Use the parsed tokens data to set the class properties.
*
* @param array $data
*
* @return void
*/
protected function set_properties( $data ) {
$this->alias = $data['alias'];
$this->object_type = $data['object_type'];
$this->fields_to_update = $data['fields_to_update'];
$this->return_fields = $data['return_fields'];
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
protected function regex_types() {
return array(
'returning_def' => 'returning {[^\%]+',
'operation_alias' => 'update_[^\(]*',
'arg_group' => '\([^\)]+\)\s?{',
'alias' => '[_A-Za-z][_0-9A-Za-z]*:',
'identifier' => '[_A-Za-z][_0-9A-Za-z]*',
'open_bracket' => '{',
'close_bracket' => '}',
);
}
}
@@ -0,0 +1,141 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Insert\Insert_Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Update\Update_Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Delete\Delete_Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect\Connect_Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Connect\Disconnect_Mutation_Token;
/**
* Generic Mutation Token
*
* A token used to take a given Mutation string and determing the specific Mutatin type to execute.
*
* This is the top-level entry point when dealing with a Mutation.
*/
class Generic_Mutation_Token extends Mutation_Token {
protected $type = 'generic_mutation';
/**
* The type of this mutation.
*
* @var string
*/
protected $mutation_type;
/**
* A specific Mutation_Token for the type of mutation being executed.
*
* @var Mutation_Token
*/
protected $typed_token;
/**
* Public accessor for $mutation_type;
*
* @return string
*/
public function mutation_type() {
return $this->mutation_type;
}
/**
* Public accessor for $typed_token.
*
* @return Mutation_Token
*/
public function mutation() {
return $this->typed_token;
}
/**
* Return the $typed_token as children.
*
* @return Mutation_Token
*/
public function children() {
return $this->typed_token;
}
/**
* Parse the string contents to values.
*
* @param string $contents
*
* @return void
*/
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $parts );
$matches = $parts[0];
$marks = $parts['MARK'];
$typed_token = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
if ( $mark_type !== 'operation' ) {
continue;
}
$cleaned = trim( $value, '{ (' );
if ( strpos( $cleaned, 'insert_' ) !== false ) {
$typed_token = new Insert_Mutation_Token( $contents );
$this->mutation_type = 'insert';
continue;
}
if ( strpos( $cleaned, 'update_' ) !== false ) {
$typed_token = new Update_Mutation_Token( $contents );
$this->mutation_type = 'update';
continue;
}
if ( strpos( $cleaned, 'delete_' ) !== false ) {
$typed_token = new Delete_Mutation_Token( $contents );
$this->mutation_type = 'delete';
continue;
}
if ( strpos( $cleaned, 'disconnect_' ) !== false ) {
$typed_token = new Disconnect_Mutation_Token( $contents );
$this->mutation_type = 'disconnect';
continue;
}
if ( strpos( $cleaned, 'connect_' ) !== false ) {
$typed_token = new Connect_Mutation_Token( $contents );
$this->mutation_type = 'connect';
continue;
}
$typed_token = apply_filters( 'gravitytools_hermes_mutation_type_token', $typed_token, $cleaned, $contents );
$this->mutation_type = apply_filters( 'gravitytools_hermes_mutation_type', false, $typed_token );
}
if ( empty( $typed_token ) ) {
throw new \InvalidArgumentException( 'Invalid operation type passed to mutation.', 475 );
}
$this->typed_token = $typed_token;
}
/**
* The regex types to use while parsing.
*
* $key represents the MARK type, while the $value represents the REGEX string to use.
*
* @return string[]
*/
public function regex_types() {
return array(
'operation' => '\{[^\(]+\(',
);
}
}
@@ -0,0 +1,149 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
class Arguments_Token extends Token {
protected $type = 'Arguments';
protected $items = array();
private $comparator_strings = array(
'_gte' => '>=',
'_lte' => '<=',
'_ne' => '!=',
'_gt' => '>',
'_lt' => '<',
'_in' => 'in',
);
public function items() {
return $this->items;
}
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $parts );
$matches = $parts[0];
$marks = $parts['MARK'];
// Tracking values.
$in_text = false;
$current_value = '';
$current_key = '';
$is_key = true;
$is_value = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'quote':
$in_text = ! $in_text;
if ( ! $in_text ) {
$is_key = true;
$is_value = false;
}
break;
case 'colon':
if ( $in_text ) {
$current_value .= $value;
break;
}
$is_key = false;
$is_value = true;
break;
case 'comma':
if ( $in_text ) {
$current_value .= $value;
break;
}
$condition_data = $this->get_condition_data_from_key( $current_key );
$data[] = array(
'key' => trim( $condition_data['key'], '" \'' ),
'comparator' => $condition_data['comparator'],
'value' => $this->parse_value( $current_value ),
);
$current_key = '';
$current_value = '';
$is_key = true;
$is_value = false;
break;
case 'value':
if ( $is_key ) {
$current_key .= $value;
break;
}
if ( $is_value ) {
$current_value .= $value;
break;
}
break;
}
}
if ( ! empty( $current_value ) && ! empty( $current_value ) ) {
$condition_data = $this->get_condition_data_from_key( $current_key );
$data[] = array(
'key' => trim( $condition_data['key'], '" \'' ),
'comparator' => $condition_data['comparator'],
'value' => $this->parse_value( $current_value ),
);
}
$this->items = $data;
}
private function parse_value( $value ) {
$value = trim( $value, '" \'' );
if ( strpos( $value, '|' ) === false ) {
return $value;
}
return explode( '|', $value );
}
public function regex_types() {
return array(
'quote' => '[\'"]',
'comma' => ',',
'colon' => ':',
'value' => '[\|\sa-zA-Z0-9_-]',
);
}
public function children() {
return $this->items();
}
private function get_condition_data_from_key( $key_to_check ) {
$comparator = '=';
$key = $key_to_check;
foreach ( $this->comparator_strings as $check => $result ) {
if ( strpos( $key_to_check, $check ) !== false ) {
$key = str_replace( $check, '', $key_to_check );
$comparator = $result;
return array(
'comparator' => $comparator,
'key' => $key,
);
}
}
return array(
'comparator' => $comparator,
'key' => $key,
);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
abstract class Base_Token {
protected $type;
protected $parent;
public function type() {
return $this->type;
}
public function parent() {
return $this->parent;
}
public function set_parent( Base_Token $parent ) {
$this->parent = $parent;
}
abstract public function children();
}
@@ -0,0 +1,109 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
class Data_Object_From_Array_Token extends Token_From_Array {
protected $type = 'Data_Object';
protected $object_type;
protected $fields;
protected $arguments;
protected $alias;
public function object_type() {
return $this->object_type;
}
public function arguments() {
if ( ! empty( $this->arguments ) ) {
return $this->arguments->items();
}
return array();
}
public function fields() {
return $this->fields;
}
public function alias() {
return $this->alias;
}
public function parse( &$matches, &$marks, $additional_args = array() ) {
$data = array(
'object_type' => $additional_args['object_type'],
'alias' => trim( $additional_args['alias'], ' :' ),
'arguments' => false,
'fields' => array(),
);
$has_alias = false;
while ( ! empty( $matches ) ) {
$value = array_shift( $matches );
$mark_type = array_shift( $marks );
switch ( $mark_type ) {
case 'arg_group':
$data['arguments'] = new Arguments_Token( $value );
$data['arguments']->set_parent( $this );
break;
case 'alias':
$has_alias = $value;
break;
case 'identifier':
if ( $marks[0] === 'open_bracket' || ( $marks[0] === 'arg_group' && $marks[1] === 'open_bracket' ) ) {
$data_object = new self( $matches, $marks, array(
'object_type' => $value,
'alias' => $has_alias ? $has_alias : $value,
) );
$data_object->set_parent( $this );
$data['fields'][] = $data_object;
$has_alias = false;
break;
}
$field_data = array(
'name' => $value,
'alias' => trim( $has_alias, ' :' ),
);
if ( $marks[0] === 'arg_group' ) {
$args = array_shift( $matches );
$mark = array_shift( $marks );
$field_data['arguments'] = new Arguments_Token( $args );
$field_data['arguments']->set_parent( $this );
}
$field_token = new Field_Token( $matches, $marks, $field_data );
$field_token->set_parent( $this );
$data['fields'][] = $field_token;
$has_alias = false;
break;
case 'close_bracket':
$this->set_properties( $data );
return;
}
}
$this->set_properties( $data );
}
private function set_properties( $data ) {
$this->fields = $data['fields'];
$this->arguments = $data['arguments'];
$this->object_type = $data['object_type'];
$this->alias = $data['alias'];
}
public function children() {
return $this->fields();
}
}
@@ -0,0 +1,44 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
class Field_Token extends Token_From_Array {
protected $type = 'Field';
protected $name;
protected $alias;
protected $arguments;
public function name() {
return $this->name;
}
public function alias() {
return $this->alias;
}
public function object_type() {
return $this->name;
}
public function arguments() {
return $this->arguments;
}
public function parse( &$matches, &$marks, $additional_args = array() ) {
$this->name = $additional_args['name'];
$this->alias = $additional_args['alias'];
if ( isset( $additional_args['arguments'] ) ) {
$this->arguments = $additional_args['arguments'];
}
}
public function children() {
return $this->arguments();
}
}
@@ -0,0 +1,27 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
abstract class Mutation_Token extends Token {
protected $type = 'Mutation';
protected $object_type;
protected $operation;
protected $alias;
public function object_type() {
return $this->object_type;
}
public function operation() {
return $this->operation;
}
public function alias() {
return $this->alias;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
class Query_Token extends Token {
protected $type = 'Query';
protected $arguments;
protected $object_type;
protected $items;
protected $alias;
public function object_type() {
return $this->object_type;
}
public function items() {
return $this->items;
}
public function alias() {
return $this->alias;
}
public function parse( $contents, $args = array() ) {
preg_match_all( $this->get_parsing_regex(), $contents, $results );
$values = $this->tokenize( $results );
$this->object_type = 'query';
$this->items = $values->fields();
}
private function tokenize( $parts ) {
$matches = $parts[0];
$marks = $parts['MARK'];
$data = array();
$data = new Data_Object_From_Array_Token( $matches, $marks, array( 'object_type' => $this->type, 'alias' => false ) );
$data->set_parent( $this );
return $data;
}
protected function regex_types() {
//(?| (*MARK:arg_group)\([^\)]+\) | (*MARK:identifier)[_A-Za-z][_0-9A-Za-z]* | (*MARK:open_bracket){ | (*MARK:close_bracket)} )
return array(
'arg_group' => '\([^\)]+\)',
'alias' => '[_A-Za-z][_0-9A-Za-z]*:',
'identifier' => '[_A-Za-z][_0-9A-Za-z]*',
'open_bracket' => '{',
'close_bracket' => '}',
);
}
/**
* @return Data_Object_From_Array_Token[]
*/
public function children() {
return $this->items();
}
}
@@ -0,0 +1,12 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
abstract class Token_From_Array extends Base_Token {
public function __construct( &$matches, &$marks, $additional_args = array() ) {
$this->parse( $matches, $marks, $additional_args );
}
abstract public function parse( &$matches, &$marks, $additional_args = array() );
}
@@ -0,0 +1,30 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Tokens;
abstract class Token extends Base_Token {
protected $args = array();
public function __construct( $contents, $args = array() ) {
$this->parse( $contents, $args );
}
protected function regex_types() {
return array();
}
protected function get_parsing_regex() {
$clauses = array();
foreach ( $this->regex_types() as $type => $pattern ) {
$clauses[] = sprintf( '(*MARK:%s)%s', $type, $pattern );
}
$clauses_concat = implode( '|', $clauses );
return sprintf( '/(?|%s)/m', $clauses_concat );
}
abstract public function parse( $contents, $args = array() );
}
@@ -0,0 +1,78 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Utils;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
/**
* Model Collection
*
* A simple collection class for storing registered Models in the system.
*/
class Model_Collection {
/**
* @var Model[]
*/
protected $models = array();
/**
* Register a model type.
*
* @param string $type
* @param Model $model
*
* @return void
*/
public function add( $type, Model $model ) {
$this->models[ $type ] = $model;
}
/**
* Unregister/remove a Model type.
*
* @param string $type
*
* @return void
*/
public function remove( $type ) {
unset( $this->models[ $type ] );
}
/**
* Check if a Model of a given type exists in the collection.
*
* @param string $type
*
* @return bool
*/
public function has( $type ) {
return array_key_exists( $type, $this->models );
}
/**
* Get a Model from the collection by its type.
*
* @param string $type
*
* @return Model|null
*/
public function get( $type ) {
if ( ! $this->has( $type ) ) {
return null;
}
return $this->models[ $type ];
}
/**
* Get all registered models from collection.
*
* @return Model[]
*/
public function all() {
return $this->models;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Utils;
/**
* Relationshp Collection
*
* A simple collection handling Relationship objects.
*/
class Relationship_Collection {
/**
* @var Relationship[]
*/
protected $relationships;
/**
* @param Relationship[] $relationships
*/
public function __construct( $relationships = array() ) {
$this->relationships = $relationships;
}
/**
* Add a Relationship to the collection.
*
* @param Relationship $relationship
*
* @return void
*/
public function add( Relationship $relationship ) {
$this->relationships[] = $relationship;
}
/**
* Get a given Relationship by the related object.
*
* @param string $related_object
*
* @return Relationship|mixed|null
*/
public function get( $related_object ) {
if ( ! $this->has( $related_object ) ) {
return null;
}
$relationship = array_filter( $this->relationships, function ( $relationship ) use ( $related_object ) {
return $relationship->to() === $related_object;
} );
return array_shift( $relationship );
}
/**
* Check if the collection contains a relationship for the given object type.
*
* @param string $related_object
*
* @return bool
*/
public function has( $related_object ) {
$relationship = array_filter( $this->relationships, function ( $relationship ) use ( $related_object ) {
return $relationship->to() === $related_object;
} );
return ! empty( $relationship );
}
/**
* Get all the relationships currently registered to collection.
*
* @return array
*/
public function all() {
return $this->relationships;
}
}
@@ -0,0 +1,162 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes\Utils;
/**
* Relationship
*
* Defines relationships between two object types.
*
* $from - The object to connect from
* $to = The object to connect to
* $cap - The minimum capability required for accessing this relationship.
* $is_reverse - used to indicate that the relationship should use a reversed table name
*
* Example:
*
* $group_contact = new Relationship( 'group', 'contact', 'manage_options', false )
*
* This would result in a relationship from groups to contacts, and ensure that only
* users with the `manage_options` capability are able to access it. When the relationship
* is queried, the system will look in a table with `_group_contact` as the suffix.
*
*
* Example 2:
*
* $contact_group = new Relationship( 'contact', 'group', 'manage_options', true )
*
* This would result in a relationship from contacts to groups, again restricted to users
* with the `manage_options` capability. Since $is_reverse is `true`, queries would continue
* to look for a table with `_group_contact` as the suffix, when normally it would attempt to
* locate a table with the suffix of `_contact_group`. This setup allows you to use a single
* lookup table for both directions of the relationship.
*/
class Relationship {
/**
* @var string The object type to connect from.
*/
protected $from;
/**
* @var string The object type to connect to.
*/
protected $to;
/**
* @var string The minimum WordPress role or capability required for accessing this relationship
* from within Queries and Mutations.
*/
protected $cap;
/**
* @var boolean Indicates if this relationship is the reversal of another relationship and should
* use the original's lookup table for queries.
*/
protected $is_reverse;
/**
* @var string Indicates the type of relationship - default is many_to_many, but can be one_to_many instead.
*/
protected $relationship_type;
/**
* Constructor
*
* @param $from
* @param $to
* @param $cap
* @param $is_reverse
*/
public function __construct( $from, $to, $cap, $is_reverse = false, $relationship_type = 'many_to_many' ) {
$this->from = $from;
$this->to = $to;
$this->cap = $cap;
$this->is_reverse = $is_reverse;
$this->relationship_type = $relationship_type;
}
/**
* Public $from accessor.
*
* @return string
*/
public function from() {
return $this->from;
}
/**
* Public $to accessor.
*
* @return string
*/
public function to() {
return $this->to;
}
/**
* Public $cap accessor.
*
* @return string
*/
public function cap() {
return $this->cap;
}
/**
* Public $is_reverse accessor
*
* @return string
*/
public function is_reverse() {
return $this->is_reverse;
}
/**
* Whether the current user can access this relationship. (Uses current_user_can() by default).
*
* @return bool
*/
public function has_access() {
return current_user_can( $this->cap );
}
/**
* The type of relationship this is (many_to_many or one_to_many)
*
* return string
*/
public function relationship_type() {
return $this->relationship_type;
}
/**
* Determine if this relationship is one-to-many
*
* @return bool
*/
public function is_one_to_many() {
return $this->relationship_type === 'one_to_many';
}
/**
* Determines the correct table suffix when querying lookup tables.
*
* When $is_reverse is `true`, the suffix has the object type slugs swapped.
*
* @return string
*/
public function get_table_suffix() {
if ( $this->relationship_type() === 'one_to_many' ) {
return $this->is_reverse ? $this->to : $this->from;
}
if ( $this->is_reverse ) {
return sprintf( '%s_%s', $this->to, $this->from );
}
return sprintf( '%s_%s', $this->from, $this->to );
}
}
@@ -0,0 +1,187 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Connect_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Delete_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Disconnect_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Insert_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Update_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Mutations\Generic_Mutation_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Model_Collection;
/**
* The initial entry point for handling Mutation requests. For Query handling, see Query_Handler.
*/
class Mutation_Handler {
/**
* The namespace to use when querying DB tables. The namespace is used after the $wpdb->prefix
* value and before the actual table name.
*
* Example:
*
* Passing `gravitytools` would result in a meta table name of `wp_gravitytools_meta`..
*
* @var string
*/
protected $db_namespace;
/**
* The collection of models supported for queries.
*
* @var Model_Collection
*/
protected $models;
/**
* A valid Query Handler for retrieving the resulting objects after an insert/update.
*
* @var Query_Handler
*/
protected $query_handler;
/**
* The Runner for handling Insert mutations.
*
* @var Insert_Runner
*/
protected $insert_runner;
/**
* The Runner for handling Delete mutations.
*
* @var Delete_Runner
*/
protected $delete_runner;
/**
* The Runner for handling Update mutations.
*
* @var Update_Runner
*/
protected $update_runner;
/**
* The Runner for handling Connect mutations.
*
* @var Connect_Runner
*/
protected $connect_runner;
/**
* The Runner for handling Disconnect mutations.
*
* @var Disconnect_Runner
*/
protected $disconnect_runner;
/**
* All runners registered to handler.
*
* @var Runner[]
*/
protected $all_runners;
/**
* Constructor
*
* @param string $db_namespace
* @param Model_Collection $models
* @param Query_Handler $query_handler
* @param Runner[] $runners
*/
public function __construct( $db_namespace, $models, $query_handler, $runners ) {
$this->db_namespace = $db_namespace;
$this->models = $models;
$this->query_handler = $query_handler;
$this->insert_runner = $runners['insert'];
$this->delete_runner = $runners['delete'];
$this->update_runner = $runners['update'];
$this->connect_runner = $runners['connect'];
$this->disconnect_runner = $runners['disconnect'];
$this->all_runners = $runners;
}
/**
* Parse the provided Mutation string and execute the appropriate SQL queries to perform the
* mutation.
*
* @param string $mutation_string
* @param bool $return
*
* @return void
*/
public function handle_mutation( $mutation_string, $return = false ) {
global $wpdb;
// Pass the string to the Generic Mutation token to determine the specific mutation type.
$generic_mutation = new Generic_Mutation_Token( $mutation_string );
/**
* Mutation_Token $mutation
*/
$mutation = $generic_mutation->mutation();
// Ensure the object type in question is registered in our Model Collection.
if ( ! $this->models->has( $mutation->object_type() ) ) {
$error_message = sprintf( 'Mutation attempted with invalid object type: %s', $mutation->object_type() );
throw new \InvalidArgumentException( $error_message, 470 );
}
$object_model = $this->models->get( $mutation->object_type() );
switch( $mutation->operation() ) {
case 'insert':
$action_check = 'create';
break;
case 'update':
$action_check = 'edit';
break;
case 'delete':
$action_check = 'delete';
break;
case 'connect':
$action_check = 'edit';
break;
case 'disconnect':
$action_check = 'edit';
break;
default:
$action_check = 'view';
break;
}
// Ensure the querying user has the appropriate permissions to access data for this object.
if ( ! $object_model->has_access( $action_check ) ) {
$error_message = sprintf( 'Access not allowed for object type %s', $mutation->object_type() );
throw new \InvalidArgumentException( $error_message, 403 );
}
// Handle the actual mutation based on the identified mutation type by calling its appropriate Runner.
switch ( $mutation->operation() ) {
case 'insert':
return $this->insert_runner->run( $mutation, $object_model, $return );
break;
case 'update':
return $this->update_runner->run( $mutation, $object_model, $return );
break;
case 'delete':
return $this->delete_runner->run( $mutation, $object_model, $return );
break;
case 'connect':
return $this->connect_runner->run( $mutation, $object_model, $return );
break;
case 'disconnect':
return $this->disconnect_runner->run( $mutation, $object_model, $return );
break;
default:
if ( array_key_exists( $mutation->operation(), $this->all_runners ) ) {
return $this->all_runners[ $mutation->operation() ]->run( $mutation, $object_model, $return );
break;
}
break;
}
}
}
@@ -0,0 +1,920 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Hermes;
use Gravity_Forms\Gravity_Tools\Hermes\Models\Model;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Schema_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Data_Object_From_Array_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Query_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Model_Collection;
use InvalidArgumentException;
/**
* Query Handler
*
* The entry point for parsing queries made to Hermes. For Mutations, see Mutation_Handler.
*/
class Query_Handler {
/**
* The namespace to use when querying DB tables. The namespace is used after the $wpdb->prefix
* value and before the actual table name.
*
* Example:
*
* Passing `gravitytools` would result in a meta table name of `wp_gravitytools_meta`..
*
* @var string
*/
protected $db_namespace;
/**
* The collection of models supported for queries.
*
* @var Model_Collection
*/
protected $models;
/**
* @var Schema_Runner
*/
protected $schema_runner;
/**
* Any fields that are global to all queries.
*
* @var array
*/
private $global_fields = array(
'aggregate',
);
/**
* Tracks any objects which use filtered data.
*
* @var array
*/
private $overridden_objects = array();
/**
* Constructor
*
* @param string $db_namespace
*/
public function __construct( $db_namespace, Model_Collection $models, Schema_Runner $schema_runner ) {
$this->db_namespace = $db_namespace;
$this->models = $models;
$this->schema_runner = $schema_runner;
}
/**
* Parse the given query string text and perform the appropriate database queries to return
* the requested data structure.
*
* @param string $query_string
*
* @return array
*/
public function handle_query( $query_string, $return = false ) {
global $wpdb;
// Parse to token array
$query_token = new Query_Token( $query_string );
$data = array();
// Use the Token to generate recursive SQL.
foreach ( $query_token->children() as $object ) {
$object_name = ! empty( $object->alias() ) ? $object->alias() : $object->object_type();
$custom_data = apply_filters( 'gt_hermes_custom_object_data_' . $object->object_type(), null, $object );
if ( ! is_null( $custom_data ) ) {
$this->overridden_objects[] = $object_name;
$data[ $object_name ] = $custom_data;
continue;
}
if ( $object->object_type() === '__schema' ) {
$data[ $object_name ] = $this->get_schema_values_for_query( $object );
continue;
}
$sql = $this->recursively_generate_sql( $object );
$transformations = $this->recursively_get_transformations( $object );
$data[ $object_name ] = array(
'sql' => sprintf( 'SELECT %s', $sql ),
'transformations' => $transformations,
'object' => $object,
);
}
$results = array();
// Decode the results and set them up for return.
foreach ( $data as $data_group_name => $data_group_values ) {
// Schema values do not need to be queried; just return the rows as-is.
if ( isset( $data_group_values['schema'] ) ) {
$results[ $data_group_name ] = $data_group_values['schema'];
continue;
}
// Data was overwritten by a filter, return what is there.
if ( in_array( $data_group_name, $this->overridden_objects ) ) {
$results[ $data_group_name ] = $data_group_values;
continue;
}
$query_results = $wpdb->get_results( $data_group_values['sql'], ARRAY_A );
$rows = array();
foreach ( $query_results as $query_result ) {
$json_data = array_shift( $query_result );
$decoded_data = json_decode( $json_data, true );
$rows[] = $decoded_data;
}
$rows = $this->recursively_apply_transformations( $rows, $data_group_values['transformations'] );
/**
* Allows third-parties to modify the rows returned from a query.
*
* @param array $rows the current result for the query
* @param string $data_group_name the name of the object being queried
* @param array $data_group_values all of the various values relevant to this request
*
* @return array
*/
$rows = apply_filters( 'gt_hermes_query_results', $rows, $data_group_name, $data_group_values );
$results[ $data_group_name ] = $rows;
}
if ( $return ) {
return $results;
}
wp_send_json_success( $results );
}
/**
* Loops through the objects in the Query String and recursively generates the appropriate
* SQL for the related query. Supports an infinite level of nesting, but caution should be used when
* performing deeply-nested queries as performance may be impacted.
*
* @return string
*/
public function recursively_generate_sql( Data_Object_From_Array_Token $data, $idx_prefix = null, $parent_table = false, $parent_object_type = false ) {
$object_type = $data->object_type();
$object_name = ! empty( $data->alias() ) ? $data->alias() : $data->object_type();
// Ensure the object type being queried exists as a Model.
if ( ! $this->models->has( $object_type ) ) {
$error_message = sprintf( 'Attempting to access invalid object type %s', $object_type );
throw new InvalidArgumentException( $error_message, 460 );
}
$object_model = $this->models->get( $object_type );
// Ensure that the querying user has the appropriate permissions to access object.
if ( ! $object_model->has_access( 'view' ) ) {
$error_message = sprintf( 'Access not allowed for object type %s', $object_type );
throw new InvalidArgumentException( $error_message, 403 );
}
// Set up values for the table being queried.
$table_name = $this->compose_table_name( $object_type );
$table_alias = $this->compose_table_alias( $object_name, $parent_table );
// Categorized queried fields as either local or meta fields for future processing.
$fields_to_process = $data->fields();
$categorized_fields = $this->categorize_fields( $object_model, $fields_to_process, $table_alias );
$arguments = $data->arguments();
// Set up data arrays for holding pieces of the SQL statement for later concatenation.
$field_pairs = array();
$where_clauses = array();
$join_clauses = array();
$field_sql = null;
$from_sql = null;
$join_sql = null;
$where_sql = null;
$group_sql = null;
$limit_sql = null;
$order_sql = null;
$separator_sql = null;
// Arguments are present; parse them and add them to the appropriate SQL arrays.
if ( ! empty( $arguments ) ) {
$this->get_where_clauses_from_arguments( $where_clauses, $table_alias, $arguments, $object_model );
$limit_sql = $this->get_limit_from_arguments( $arguments );
$order_sql = $this->get_order_from_arguments( $arguments, $table_alias );
}
// Loop through each local field and generate the appropriate SQL chunks for retrieving the data.
foreach ( $categorized_fields['local'] as $field_name => $field_alias ) {
if ( is_a( $field_alias, Data_Object_From_Array_Token::class ) ) {
$this_alias = empty( $field_alias->alias() ) ? $field_alias->object_type() : $field_alias->alias();
$sub_sql = $this->recursively_generate_sql( $field_alias, null, $table_alias, $object_type );
$sub_sql_parts = explode( '|gsmtpfieldsseparator|', $sub_sql );
$sub_sql = sprintf( '( SELECT JSON_ARRAYAGG( %s ) %s )', $sub_sql_parts[0], $sub_sql_parts[1] );
$field_pairs[] = sprintf( '"%s", %s', $this_alias, $sub_sql );
continue;
}
$field_name = str_replace( $field_alias . '.', '', $field_name );
$field_pairs[] = sprintf( '"%s", %s.%s', $field_alias, $table_alias, $field_name );
}
$meta_table_name = $this->compose_table_name( 'meta' );
// Loop through each meta field and compose the appropriate JOIN query for gathering its data.
foreach ( $categorized_fields['meta'] as $field_name => $field_data ) {
$value_clause = $parent_table ? sprintf( '%s.meta_value', $field_data['lookup_table_alias'] ) : sprintf( 'MIN(%s.meta_value)', $field_data['lookup_table_alias'] );
$field_pairs[] = sprintf( '"%s", %s', $field_data['alias'], $value_clause, $field_name );
$join_clauses[] = sprintf(
'LEFT JOIN %s AS %s ON %s.object_type = "%s" AND %s.meta_name = "%s" AND %s.object_id = %s.id',
$meta_table_name,
$field_data['lookup_table_alias'],
$field_data['lookup_table_alias'],
$object_type,
$field_data['lookup_table_alias'],
$field_name,
$field_data['lookup_table_alias'],
$table_alias
);
}
// If an aggregate is being called, add it as a subquery with the existing where conditions applied.
if ( in_array( 'aggregate', $categorized_fields['global'] ) ) {
$agg_alias = $categorized_fields['global']['aggregate'];
$agg_sql = sprintf( '"%s", (SELECT COUNT(*) FROM %s', $agg_alias, $table_name );
if ( ! empty( $where_clauses ) ) {
$agg_sql .= sprintf( ' WHERE %s', str_replace( $table_alias, $table_name, implode( ' AND ', $where_clauses ) ) );
}
$agg_sql .= ')';
$field_pairs[] = $agg_sql;
}
// A parent table exists, meaning this is a nested query and requires a JOIN statement relating it
// to the parent table.
if ( $parent_table ) {
$this->populate_relationship_clauses( $join_clauses, $where_clauses, $parent_object_type, $object_type, $table_alias, $parent_table );
}
// Run all the query params through filters to allow modification.
$query_params = array(
'field_pairs' => $field_pairs,
'where_clauses' => $where_clauses,
'join_clauses' => $join_clauses,
'limit_sql' => $limit_sql,
'order_sql' => $order_sql,
);
$query_params = apply_filters( 'gt_hermes_query_params', $query_params, $object_type, $categorized_fields, $arguments, $table_alias, $parent_table );
$field_pairs = $query_params['field_pairs'];
$where_clauses = $query_params['where_clauses'];
$join_clauses = $query_params['join_clauses'];
$limit_sql = $query_params['limit_sql'];
$order_sql = $query_params['order_sql'];
// Concatenate each SQL array to generate the final SQL.
$field_sql = implode( ', ', $field_pairs );
$from_sql = sprintf( 'FROM %s AS %s', $table_name, $table_alias );
$join_sql = implode( ' ', $join_clauses );
if ( ! empty( $where_clauses ) ) {
$where_sql = sprintf( 'WHERE %s', implode( ' AND ', $where_clauses ) );
}
$group_sql = null;
if ( ! $parent_table ) {
$group_sql = sprintf( 'GROUP BY %s.id', $table_alias );
}
if ( $parent_table ) {
$separator_sql = '|gsmtpfieldsseparator|';
}
// Return the resulting SQL
return sprintf( 'JSON_OBJECT( %s ) %s %s %s %s %s %s %s', $field_sql, $separator_sql, $from_sql, $join_sql, $where_sql, $group_sql, $order_sql, $limit_sql );
}
private function populate_relationship_clauses( &$join_clauses, &$where_clauses, $parent_object_type, $object_type, $table_alias, $parent_table ) {
$parent_model = $this->models->get( $parent_object_type );
$relationship = $parent_model->relationships()->get( $object_type );
if ( ! $relationship->has_access() ) {
throw new \InvalidArgumentException( 'Attempting to access forbidden relationship ' . $relationship->to() );
}
if ( $relationship->relationship_type() === 'one_to_many' ) {
$this->populate_otm_relationship_clauses( $where_clauses, $relationship, $table_alias, $parent_table );
return;
}
$lookup_table_name = $this->compose_join_table_name( $relationship->get_table_suffix() );
$lookup_table_alias = sprintf( 'join_%s', $table_alias );
$id_string = sprintf( '%s_id', $object_type );
$parent_id_string = sprintf( '%s_id', $parent_object_type );
$join_clauses[] = sprintf( 'LEFT JOIN %s AS %s ON %s.id = %s.%s', $lookup_table_name, $lookup_table_alias, $table_alias, $lookup_table_alias, $id_string );
$where_clauses[] = sprintf( '%s.%s = %s.id', $lookup_table_alias, $parent_id_string, $parent_table );
}
private function populate_otm_relationship_clauses( &$where_clauses, $relationship, $table_alias, $parent_table ) {
$id_string = $relationship->is_reverse() ? sprintf( '%sId', $relationship->to() ) : sprintf( '%sId', $relationship->from() );
$where_clauses[] = sprintf( '%s.id = %s.%s', ! $relationship->is_reverse() ? $parent_table : $table_alias, ! $relationship->is_reverse() ? $table_alias : $parent_table, $id_string );
}
/**
* Categorizes fields as either local (i.e., existing as columns within the table for the object) or meta
* (i.e., existing as custom values in the meta table).
*
* @param Model $object_model
* @param array $fields_to_process
* @param string $table_alias
*
* @return array|array[]
*/
protected function categorize_fields( $object_model, $fields_to_process, $table_alias ) {
$categorized = array(
'meta' => array(),
'local' => array(),
'global' => array(),
);
foreach ( $fields_to_process as $field ) {
if ( is_a( $field, Data_Object_From_Array_Token::class ) ) {
$child_type = $field->object_type();
if ( ! $object_model->relationships()->has( $child_type ) ) {
$error_string = sprintf( 'Attempting to access invalid related object %s for object type %s', $child_type, $object_model->type() );
throw new InvalidArgumentException( $error_string, 455 );
}
$categorized['local'][ $field->alias() ] = $field;
continue;
}
$field_name = $field->name();
if ( ! in_array( $field_name, $this->global_fields ) && ! array_key_exists( $field_name, $object_model->fields() ) && ! array_key_exists( $field_name, $object_model->meta_fields() ) ) {
$error_string = sprintf( 'Attempting to access invalid field %s on object type %s', $field_name, $object_model->type() );
throw new InvalidArgumentException( $error_string, 450 );
}
$alias = $field->alias();
$identifier = $alias ? $alias : $field_name;
if ( in_array( $field_name, $this->global_fields ) ) {
$categorized['global'][ $field_name ] = $identifier;
}
if ( array_key_exists( $field_name, $object_model->fields() ) ) {
$categorized['local'][ $identifier . '.' . $field_name ] = $identifier;
}
if ( array_key_exists( $field_name, $object_model->meta_fields() ) ) {
$categorized['meta'][ $field_name ] = array(
'alias' => $identifier,
'lookup_table_alias' => sprintf( 'meta_%s_%s', $table_alias, $identifier ),
);
}
}
return $categorized;
}
/**
* From the given array of $field_names, build the properly-structured SQL for selecting them.
*
* If a field is detected as a related object, we treat it as a top-level query and recursively
* begin the SQL generation process for it.
*
* @param array $field_names
* @param string $table_alias
* @param string $object_type
*
* @return array
*/
protected function build_local_field_select_clauses( $field_names, $table_alias, $object_type ) {
$pairs = array();
foreach ( $field_names as $field_name => $alias ) {
if ( is_a( $alias, Data_Object_From_Array_Token::class ) ) {
$value = '(' . $this->recursively_generate_sql( $alias, $field_name, $table_alias, $object_type ) . ')';
$pairs[] = sprintf( '"%s", %s', $field_name, $value );
continue;
}
$pairs[] = sprintf( '"%s", %s.%s', $alias, $table_alias, $field_name );
}
return $pairs;
}
/**
* Generates the appropriate SQL clause(s) for selecting a Meta field. Meta fields exist in a lookup table, and thus require both a SELECT statement to properly
* query the fields as well as a JOIN clause to join the meta table with a specific alias.
*
* @param string $meta_name the name of the field being selected
* @param string $alias the alias to use when returning the selected field
* @param string $object_type the object type to grab the values from
* @param string $parent_table_alias if present, the parent table this nested query belongs to
* @param string $idx_prefix If present, the previous IDX prefix used for the parent table.
* (to be prenended to this table's alias)
*
* @return array
*/
protected function build_meta_query( $meta_name, $alias, $object_type, $parent_table_alias, $idx_prefix ) {
global $wpdb;
$meta_table_name = $wpdb->prefix . $this->db_namespace . '_' . 'meta';
$meta_table_alias = sprintf( 'meta_%s%s', $meta_name, is_null( $idx_prefix ) ? null : '_' . $idx_prefix );
$select_clause = sprintf(
'"%1$s", %2$s.meta_value',
$alias,
$meta_table_alias
);
$join_clause = sprintf(
'LEFT JOIN %1$s AS %2$s ON %3$s.object_id = %4$s.id AND %5$s.object_type = "%6$s" AND %7$s.meta_name = "%8$s"',
$meta_table_name,
$meta_table_alias,
$meta_table_alias,
$parent_table_alias,
$meta_table_alias,
$object_type,
$meta_table_alias,
$meta_name
);
return array(
'select_clause' => $select_clause,
'join_clause' => $join_clause,
);
}
/**
* Build a SQL WHERE clause from the given set of conditions.
*
* @param array $conditions
*
* @return void
*/
protected function build_where_clauses( $conditions ) {
$clauses = array();
foreach ( $conditions as $condition ) {
$column_name = $condition['key'];
$column_value = $condition['value'];
$comparator = $condition['comparator'];
$clauses[] = sprintf( '%1$s %2$s %3$s', $column_name, $comparator, $column_value );
}
return $clauses;
}
/**
* Compose the proper table name for a given object type.
*
* @param string $object_type
*
* @return string
*/
private function compose_table_name( $object_type ) {
global $wpdb;
if ( ! $this->models->has( $object_type ) || ! $this->models->get( $object_type )->forced_table_name() ) {
return sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $object_type );
}
$object_model = $this->models->get( $object_type );
return sprintf( '%s%s', $wpdb->prefix, $object_model->forced_table_name() );
}
/**
* Compose the appropriate join table name for a given suffix.
*
* @param string $suffix
*
* @return string
*/
private function compose_join_table_name( $suffix ) {
global $wpdb;
return sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, $suffix );
}
/**
* Composes a table alias to ensure every table has a unique name in the query.
*
* @param string $object_name
* @param string $parent_table_name
*
* @return string
*/
private function compose_table_alias( $object_name, $parent_table_name = false ) {
if ( ! empty( $parent_table_name ) ) {
return sprintf( '%s_%s', $parent_table_name, $object_name );
}
return sprintf( 'table_%s', $object_name );
}
/**
* Search an array of arguments and return the appropriate SQL LIMIT clause from
* the values.
*
* @param array $arguments
*
* @return string
*/
private function get_limit_from_arguments( $arguments ) {
$response = '';
$limit = array_values(
array_filter(
$arguments,
function ( $item ) {
return $item['key'] === 'limit';
}
)
);
$offset = array_values(
array_filter(
$arguments,
function ( $item ) {
return $item['key'] === 'offset';
}
)
);
if ( ! empty( $limit ) ) {
$response .= sprintf( 'LIMIT %s', $limit[0]['value'] );
}
if ( ! empty( $offset ) ) {
$response .= sprintf( ' OFFSET %s', $offset[0]['value'] );
}
return $response;
}
private function get_order_from_arguments( $arguments, $table_alias ) {
$response = '';
$order = array_values(
array_filter(
$arguments,
function ( $item ) {
return $item['key'] === 'order';
}
)
);
$order_by = array_values(
array_filter(
$arguments,
function ( $item ) {
return $item['key'] === 'orderBy';
}
)
);
if ( empty( $order_by ) ) {
return null;
}
if ( empty( $order ) ) {
$order = array( array( 'value' => 'DESC' ) );
}
/**
* Allows third-parties to modify the ORDER parameter before being applied.
*
* @param string The current order direction.
* @param array The arguments for this query.
*
* @return string (either DESC or ASC)
*/
$order = apply_filters( 'gt_hermes_order_dir', $order[0]['value'], $arguments );
/**
* Allows third-parties to modify the ORDER BY parameter before being applied.
*
* @param string The current ORDER BY value.
* @param array The arguments for this query.
*
* @return string The new ORDER BY value.
*/
$order_by = apply_filters( 'gt_hermes_order_value', $order_by[0]['value'], $arguments );
if ( empty( $order_by ) ) {
return null;
}
$response = sprintf( 'ORDER BY %s.%s %s', $table_alias, $order_by, $order );
return $response;
}
private function recursively_get_model_relationships( Model $object_model ) {
$map = array();
foreach ( $object_model->relationships()->all() as $relationship ) {
if ( ! $relationship->has_access() || $relationship->is_reverse() ) {
continue;
}
$new_object_model = $this->models->get( $relationship->to() );
$children = $this->recursively_get_model_relationships( $new_object_model );
$map[ $new_object_model->type() ] = $children;
}
return $map;
}
private function recursively_order_relationship_map( $parent_relationships, $relationship_map ) {
$data = array();
foreach ( $relationship_map as $key => $value ) {
$model = $this->models->get( $key );
$searchable_fields = $model->searchable_fields();
$children = array();
if ( ! empty( $value ) ) {
$child_parent_relationships = $parent_relationships;
$child_parent_relationships[] = $key;
$children = $this->recursively_order_relationship_map( $child_parent_relationships, $value );
}
$data[ $key ] = array(
'parent_relationships' => $parent_relationships,
'searchable_fields' => $searchable_fields,
'children' => $children,
);
}
return $data;
}
private function recursively_get_ids_from_relationship_map( &$ids, $search_term, $relationship_map ) {
foreach ( $relationship_map as $object_type => $values ) {
// Nothing to search for, bail.
if ( empty( $values['searchable_fields'] ) ) {
continue;
}
global $wpdb;
$wpdb_prefix = $wpdb->prefix;
$db_namespace = $this->db_namespace;
$table_name = sprintf( '%s%s_%s', $wpdb_prefix, $db_namespace, $object_type );
$parent_relationships = array_reverse( $values['parent_relationships'] );
$id_table_alias = $this->get_proper_related_table_id_for_search( $values['parent_relationships'], $object_type );
$id_column_alias = $this->get_proper_related_column_alias_for_search( $values['parent_relationships'], $id_table_alias, $object_type );
$select_clause = sprintf( 'SELECT %s AS id FROM %s AS mt', $id_column_alias, $table_name );
$match_statements = array_map( function ( $field_name ) use ( $search_term ) {
return sprintf( 'MATCH( mt.%s ) AGAINST( "%s*" IN BOOLEAN MODE )', $field_name, $search_term );
}, $values['searchable_fields'] );
$models = $this->models;
$join_statements = array_map( function ( $related_type, $idx ) use ( $object_type, $db_namespace, $wpdb_prefix, $parent_relationships, $models ) {
$object_model = $models->get( $object_type );
$relationship = $object_model->relationships()->get( $related_type );
if ( ! $relationship || $relationship->relationship_type() === 'one_to_many' ) {
return null;
}
$joined_type = $idx === 0 ? $object_type : $parent_relationships[ $idx - 1 ];
$joined_table_alias = $idx === 0 ? 'mt' : sprintf( 'pt%s', $idx );
$joined_table_column = $idx === 0 ? 'id' : sprintf( '%s_id', $parent_relationships[ $idx - 1 ] );
$join_table_name = sprintf( '%s%s_%s_%s', $wpdb_prefix, $db_namespace, $related_type, $joined_type );
return sprintf( 'LEFT JOIN %s AS pt%s ON pt%s.%s_id = %s.%s', $join_table_name, $idx + 1, $idx + 1, $joined_type, $joined_table_alias, $joined_table_column );
}, $parent_relationships, array_keys( $parent_relationships ) );
$join_statements = array_filter( $join_statements );
$sql = sprintf( '%s %s WHERE %s', $select_clause, implode( ' ', $join_statements ), implode( ' OR ', $match_statements ) );
$results = $wpdb->get_results( $sql, ARRAY_A );
$result_ids = wp_list_pluck( $results, 'id' );
$ids[] = $result_ids;
if ( ! empty( $values['children'] ) ) {
$this->recursively_get_ids_from_relationship_map( $ids, $search_term, $values['children'] );
}
}
}
private function get_proper_related_table_id_for_search( $parent_relationships, $object_type ) {
if ( empty( $parent_relationships ) ) {
return 'id';
}
$related_type = $parent_relationships[ array_key_last( $parent_relationships ) ];
$object_model = $this->models->get( $object_type );
$relationship = $object_model->relationships()->get( $related_type );
if ( $relationship->relationship_type() === 'one_to_many' ) {
return 'mt';
}
return sprintf( 'pt%s', count( $parent_relationships ) );
}
private function get_proper_related_column_alias_for_search( $parent_relationships, $id_table_alias, $object_type ) {
if ( empty( $parent_relationships ) ) {
return 'id';
}
$related_type = $parent_relationships[ array_key_last( $parent_relationships ) ];
$object_model = $this->models->get( $object_type );
$relationship = $object_model->relationships()->get( $related_type );
if ( $relationship->relationship_type() === 'one_to_many' ) {
return sprintf( '%s.%sId', $id_table_alias, $related_type );
}
return sprintf( '%s.%s_id', $id_table_alias, $parent_relationships[0] );
}
/**
* Get IDs for a search term from child objects.
*
* @param string $search_term
*
* @return array
*/
private function get_aggregate_ids_for_search( $search_term, Model $parent_object_model ) {
$relationship_map = array();
$relationship_map[ $parent_object_model->type() ] = $this->recursively_get_model_relationships( $parent_object_model );
$relationship_map = $this->recursively_order_relationship_map( array(), $relationship_map );
$ids = array();
// $ids passed as reference, so it gets updated with values here.
$this->recursively_get_ids_from_relationship_map( $ids, $search_term, $relationship_map );
$all_ids = array();
// flatten the multi-level $ids array into a single array of values
foreach( $ids as $id_set ) {
$all_ids = array_merge( $all_ids, $id_set );
}
// get rid of dupes
$all_ids = array_unique( $all_ids );
// remove empty values
return array_filter( $all_ids );
}
/**
* Search an array of arguments and compose the appropriate WHERE clause for the values provided.
*
* @param array $where_clauses
* @param string $table_alias
* @param array $arguments
* @param Model $object_model
*
* @return void
*/
private function get_where_clauses_from_arguments( &$where_clauses, $table_alias, $arguments, $object_model ) {
foreach ( $arguments as $argument ) {
if ( $argument['key'] === 'order' || $argument['key'] === 'orderBy' || $argument['key'] === 'limit' || $argument['key'] === 'offset' ) {
continue;
}
if ( $argument['key'] === 'search' ) {
// If a search is present, we need to do some work to get an aggregate based on all the searchable fields.
$ids = $this->get_aggregate_ids_for_search( $argument['value'], $object_model );
// Set the IDs to a single array value of 0 to ensure empty results are returned. (a blank IN statement throws SQL errors).
if ( empty( $ids ) ) {
$ids = array( 0 );
}
$where_clauses[] = sprintf( '%s.%s IN (%s)', $table_alias, 'id', implode( ', ', $ids ) );
continue;
}
if ( $argument['comparator'] === 'in' ) {
$in_vals = is_array( $argument['value'] ) ? $argument['value'] : explode( '|', $argument['value'] );
foreach ( $in_vals as $key => $value ) {
$in_vals[ $key ] = sprintf( '"%s"', $value );
}
$in_string = implode( ', ', $in_vals );
$clause = sprintf( '%s.%s IN (%s)', $table_alias, $argument['key'], $in_string );
$where_clauses[] = $clause;
continue;
}
$clause = sprintf( '%s.%s %s "%s"', $table_alias, $argument['key'], $argument['comparator'], $argument['value'] );
$where_clauses[] = $clause;
}
}
/**
* Traverse the Query Object and gather all the transformation arguments into a hierarchical array which can
* be used to modify the actual data from the database.
*
* @param array $transformations
*
* @return array
*/
private function recursively_get_transformations( Data_Object_From_Array_Token $data, $transformations = array() ) {
foreach ( $data->fields() as $field ) {
if ( is_a( $field, Data_Object_From_Array_Token::class ) ) {
$this_alias = ! empty( $field->alias() ) ? $field->alias() : $field->object_type();
$transformations[ $this_alias ] = $this->recursively_get_transformations( $field, array() );
continue;
}
$arguments = $field->arguments( false );
if ( empty( $arguments ) ) {
continue;
}
$transformations[ $field->alias() ] = array(
'items' => array(),
);
foreach ( $arguments->items() as $argument ) {
if ( strpos( $argument['key'], 'transform' ) !== false ) {
$transformations[ $field->alias() ]['items'][ $argument['key'] ] = array(
'key' => $argument['key'],
'value' => $argument['value'],
'object_type' => $data->object_type(),
);
}
}
}
return $transformations;
}
/**
* Traverse all of the data rows (and the corresponding array of transformations gathered previously) and apply
* the transformations to the data.
*
* @param array $rows
* @param array $transformations
*
* @return array
*/
private function recursively_apply_transformations( $rows, $transformations ) {
foreach ( $rows as $idx => $row ) {
foreach ( $row as $key => $value ) {
if ( ! array_key_exists( $key, $transformations ) ) {
continue;
}
$local_transformations = $transformations[ $key ];
if ( is_array( $value ) ) {
$rows[ $idx ][ $key ] = $this->recursively_apply_transformations( $value, $local_transformations );
continue;
}
if ( empty( $local_transformations['items'] ) ) {
continue;
}
foreach ( $local_transformations['items'] as $transformation_name => $transformation_values ) {
$object_model = $this->models->get( $transformation_values['object_type'] );
$rows[ $idx ][ $key ] = $object_model->handle_transformation( $transformation_name, $transformation_values['value'], $value );
}
}
}
return $rows;
}
public function get_schema_values_for_query( $object ) {
$schema = $this->schema_runner->run( $object );
return array(
'schema' => $schema,
'transformations' => array(),
);
}
}
@@ -0,0 +1,244 @@
<?php
namespace Gravity_Forms\Gravity_Tools\License;
/**
* Class API_Response
*
* An abstracted Response class used to standardize the responses we send back from an API Connector. Includes
* standardized serialization and JSON methods to support saving the class to the Database.
*
* @since 2.5
*
* @package Gravity_Forms\Gravity_Tools\License
*/
abstract class API_Response implements \JsonSerializable, \Serializable {
/**
* The data for this response.
*
* @var array $data
*/
protected $data = array();
/**
* The status for this response.
*
* @var array $status
*/
protected $status = array();
/**
* The errors (if any) for this response.
*
* @var array $errors
*/
protected $errors = array();
/**
* The meta data (if any) for this response.
*
* @var array $meta
*/
protected $meta = array();
/**
* Set the status for the response.
*
* @since 1.0
*
* @param $status
*/
protected function set_status( $status ) {
$this->status = $status;
}
/**
* Add data item.
*
* @since 1.0
*
* @param $item
*/
protected function add_data_item( $item ) {
$this->data[] = $item;
}
/**
* Add an error message.
*
* @since 1.0
*
* @param $error_message
*/
protected function add_error( $error_message ) {
$this->errors[] = $error_message;
}
/**
* Add a meta item to the response.
*
* @since 1.0
*
* @param $key
* @param $value
*/
protected function add_meta_item( $key, $value ) {
$this->meta[ $key ] = $value;
}
/**
* Get the data for this response
*
* @since 1.0
*
* @return array
*/
public function get_data() {
return $this->data;
}
/**
* Get any errors on this response.
*
* @since 1.0
*
* @return array
*/
public function get_errors() {
return $this->errors;
}
/**
* Get the response status.
*
* @since 1.0
*
* @return array
*/
public function get_status() {
return $this->status;
}
/**
* Get the response meta.
*
* @since 1.0
*
* @return array
*/
public function get_meta() {
return $this->meta;
}
/**
* Determine if the response has any errors.
*
* @since 1.0
*
* @return bool
*/
public function has_errors() {
return ! empty( $this->errors );
}
/**
* Get a specific piece of the data.
*
* @since 1.0
*
* @param $name
* @param int $index
*
* @return mixed|null
*/
public function get_data_value( $name, $index = 0 ) {
if ( ! isset( $this->data[ $index ][ $name ] ) ) {
return null;
}
return $this->data[ $index ][ $name ];
}
/**
* Standardization of the class when serialized and unserialized. Useful for standardizing how it
* is stored in the Database.
*
* @since 1.0
*
* @return string
*/
public function serialize() {
return serialize( $this->__serialize() );
}
/**
* Prepares the object for serializing.
*
* @since 1.0
*
* @return array
*/
public function __serialize() {
return array(
'data' => $this->data,
'errors' => $this->errors,
'status' => $this->status,
'meta' => $this->meta,
);
}
/**
* Hydrate the Response data when unserializing.
*
* @since 1.0
*
* @param string $serialized
*/
public function unserialize( $serialized ) {
$this->__unserialize( unserialize( $serialized ) );
}
/**
* Hydrates the object when unserializing.
*
* @since 1.0
*
* @param array $data The unserialized data.
*
* @return void
*/
public function __unserialize( $data ) {
$this->data = $data['data'];
$this->errors = $data['errors'];
$this->status = $data['status'];
$this->meta = $data['meta'];
}
/**
* Process data for JSON Encoding.
*
* @since 1.0
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
$response = array();
$response['status'] = $this->status;
$response['meta'] = $this->meta;
if ( empty( $this->errors ) ) {
$response['data'] = $this->data;
}
if ( ! empty( $this->errors ) ) {
$response['errors'] = $this->errors;
}
return $response;
}
}
@@ -0,0 +1,255 @@
<?php
namespace Gravity_Forms\Gravity_Tools\License;
use GFCommon;
use Gravity_Forms\Gravity_Tools\Utils\Common;
use WP_Error;
use Gravity_Forms\Gravity_Tools\License\License_API_Response_Factory;
/**
* Class License_API_Connector
*
* Connector providing methods to communicate with the License API.
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Tools\License
*/
class License_API_Connector {
private static $plugins;
private static $license_info;
/**
* @var \Gravity_Api $strategy
*/
protected $strategy;
/**
* @var \GFCache $cache
*/
protected $cache;
/**
* @var License_API_Response_Factory $response_factory
*/
protected $response_factory;
/**
* @var Common
*/
protected $common;
/**
* @var string
*/
protected $namespace;
public function __construct( $strategy, $cache, License_API_Response_Factory $response_factory, $common, $namespace ) {
$this->strategy = $strategy;
$this->cache = $cache;
$this->response_factory = $response_factory;
$this->common = $common;
$this->namespace = $namespace;
}
/**
* Check if cache debug is enabled.
*
* @since 1.0
*
* @return bool
*/
public function is_debug() {
return defined( 'GF_CACHE_DEBUG' ) && GF_CACHE_DEBUG;
}
/**
* If the site was registered with the legacy process.
*
* @since 1.0
*
* @return bool
*/
public function is_legacy_registration() {
return $this->strategy->is_legacy_registration();
}
/**
* Clear the cache for a given key.
*
* @since 1.0
*
* @param string $key
*/
public function clear_cache_for_key( $key ) {
$this->cache->delete( 'rg_gforms_license_info_' . $key );
}
/**
* Get the license info.
*
* @since 1.0
*
* @param string $key
* @param bool $cache
*
* @return License_API_Response
*/
public function check_license( $key = false, $cache = true ) {
$license_info = false;
$key = $key ? trim( $key ) : $this->strategy->get_key();
$license_info_data = $this->cache->get( 'rg_gf_license_info_' . $key );
if ( $this->is_debug() ) {
$cache = false;
}
if ( $license_info_data && $cache ) {
$license_info = $this->common->safe_unserialize( $license_info_data, License_API_Response::class );
if ( $license_info ) {
return $license_info;
} else {
$this->clear_cache_for_key( $key );
}
}
$license_info = $this->response_factory->create(
$this->strategy->check_license( $key ),
false
);
if ( $license_info->can_be_used() ) {
$this->cache->set( 'rg_gf_license_info_' . $key, serialize( $license_info ), true, DAY_IN_SECONDS );
}
return $license_info;
}
/**
* Get the license and plugins information.
*
* @since 1.0
*
* @param bool $cache If we should use the cached data.
*
* @return array|null
*/
public function get_version_info( $cache = true ) {
if ( ! is_null( self::$plugins ) && $cache ) {
$plugins = self::$plugins;
$license_info = self::$license_info;
} else {
$plugins = $this->get_plugins( $cache );
$license_info = $this->common->get_key() ? $this->check_license( false, $cache ) : new WP_Error( License_Statuses::NO_DATA );
self::$plugins = $plugins;
self::$license_info = $license_info;
}
return array(
'is_valid_key' => ! is_wp_error( $license_info ) && $license_info->can_be_used(),
'reason' => $license_info->get_error_message(),
'version' => $this->common->rgars( $plugins, 'gravityforms/version' ),
'url' => $this->common->rgars( $plugins, 'gravityforms/url' ),
'is_error' => is_wp_error( $license_info ) || $license_info->has_errors(),
'offerings' => $plugins,
);
}
/**
* Check if the saved license key is valid.
*
* @since 1.0
*
* @return true|WP_Error
*/
public function is_valid_license() {
$license_info = $this->check_license();
return $license_info->is_valid();
}
/**
* Registers a site to the specified key, or if $new_key is blank, unlinks a key from an existing site.
* Requires that the $new_key is saved in options before calling this function
*
* @since 1.0 Implement the license enforcement process.
*
* @param string $new_key Unhashed Gravity Forms license key.
*
* @return GF_License_API_Response
*/
public function update_site_registration( $new_key, $is_md5 = false ) {
// Get new license key information.
$version_info = $this->common->get_version_info( false );
if ( $version_info['is_valid_key'] ) {
$data = $this->strategy->check_license( $new_key );
$result = $this->response_factory->create( $data );
} else {
// Invalid key, do not change site registration.
$error = new WP_Error( License_Statuses::INVALID_LICENSE_KEY, License_Statuses::get_message_for_code( License_Statuses::INVALID_LICENSE_KEY ) );
$result = $this->response_factory->create( $error );
}
return $result;
}
/**
* Purge site credentials if the license info contains certain errors.
*
* @since 1.0
*
* @return void
*/
public function maybe_purge_site_credentials() {
// Check if the license info contains the revoke site error.
$license_info = $this->check_license();
$errors = array(
'gravityapi_site_revoked',
'gravityapi_fail_authentication',
'gravityapi_site_url_changed',
);
if ( is_wp_error( $license_info ) && in_array( $license_info->get_error_code(), $errors, true ) ) {
// Purge site data to ensure we can get a fresh start.
$this->strategy->purge_site_credentials();
}
}
/**
* Retrieve a list of plugins from the API.
*
* @since 1.0
*
* @param bool $cache Whether to respect the cached data.
*
* @return mixed
*/
public function get_plugins( $cache = true ) {
$cache_key = sprintf( '%s_gforms_plugins', $this->namespace );
$plugins = $this->cache->get( $cache_key, $found_in_cache );
if ( $this->is_debug() ) {
$cache = false;
}
if ( $found_in_cache && $cache ) {
return $plugins;
}
$plugins = $this->strategy->get_plugins_info();
$this->cache->set( $cache_key, $plugins, true, DAY_IN_SECONDS );
return $plugins;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Gravity_Forms\Gravity_Tools\License;
use Gravity_Forms\Gravity_Tools\License\License_API_Response;
use Gravity_Forms\Gravity_Tools\Utils\Common;
/**
* Class License_API_Response_Factory
*
* Concrete response factory used to return a License API Response
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Tools\License
*/
class License_API_Response_Factory {
private $transient_strategy;
/**
* @var Common
*/
protected $common;
/**
* License_API_Response_Factory constructor
*
* @since 1.0
*
* @param $transient_strategy
*/
public function __construct( $transient_strategy, $common ) {
$this->transient_strategy = $transient_strategy;
$this->common = $common;
}
/**
* Create a new License API Response from the given data.
*
* @since 1.0
*
* @param mixed ...$args
*
* @return GF_License_API_Response
*/
public function create( ...$args ) {
$data = $args[0];
$validate = isset( $args[1] ) ? $args[1] : true;
return new License_API_Response( $data, $validate, $this->transient_strategy, $this->common );
}
}
@@ -0,0 +1,387 @@
<?php
namespace Gravity_Forms\Gravity_Tools\License;
use Gravity_Forms\Gravity_Tools\Data\Transient_Strategy;
use Gravity_Forms\Gravity_Tools\Utils\Common;
/**
* Class License_API_Response
*
* Concrete Response class for the GF License API.
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Forms\License
*/
class License_API_Response extends API_Response {
/**
* @var Transient_Strategy
*/
private $transient_strategy;
/**
* @var Common
*/
protected $common;
/**
* License_API_Response constructor.
*
* @since 1.0
*
* @param mixed $data The data from the API connector.
* @param bool $validate Whether to validate the data passed.
* @param Transient_Strategy $transient_strategy The Transient Strategy used to store things in transients.
*/
public function __construct( $data, $validate, Transient_Strategy $transient_strategy, $common ) {
$this->transient_strategy = $transient_strategy;
$this->common = $common;
// Data is a wp_error, parse it to get the correct code and message.
if ( is_wp_error( $data ) ) {
/**
* @var \WP_Error $data
*/
if ( $data->get_error_code() == 'rest_invalid_param' ) {
$this->set_status( License_Statuses::INVALID_LICENSE_KEY );
$this->add_error( __( 'The license is invalid.', 'gravityforms' ) );
} else {
$this->set_status( $data->get_error_code() );
$this->add_error( $data->get_error_message() );
}
if ( empty( $data->get_error_data() ) ) {
return;
}
$error_data = $data->get_error_data();
if ( $this->common->rgar( $error_data, 'license' ) ) {
$error_data = $this->common->rgar( $error_data, 'license' );
}
$this->add_data_item( $error_data );
return;
}
// Data is somehow broken; set a status for Invalid license keys and bail.
if ( ! is_array( $data ) ) {
$this->set_status( License_Statuses::INVALID_LICENSE_KEY );
$this->add_error( License_Statuses::get_message_for_code( License_Statuses::INVALID_LICENSE_KEY ) );
return;
}
// Set is_valid to true since we are bypassing validation.
if ( ! $validate ) {
$data['is_valid'] = true;
}
// Data is formatted properly, but the `is_valid` param is false. Return an invalid license key error.
if ( isset( $data['is_valid'] ) && ! $data['is_valid'] ) {
$this->set_status( License_Statuses::INVALID_LICENSE_KEY );
$this->add_error( License_Statuses::get_message_for_code( License_Statuses::INVALID_LICENSE_KEY ) );
return;
}
// Finally, the data is correct, so store it and set our status to valid.
$this->add_data_item( $data );
$this->set_status( License_Statuses::VALID_KEY );
}
/**
* Get the stored error for this site license.
*
* @since 1.0
*
* @return \WP_Error|false
*/
private function get_stored_error() {
return $this->transient_strategy->get( 'rg_gforms_registration_error' );
}
/**
* Whether this license key is valid.
*
* @since 1.0
*
* @return bool
*/
public function is_valid() {
if ( empty( $this->data ) || $this->get_status() === License_Statuses::NO_DATA ) {
return false;
}
if ( ! $this->has_errors() ) {
return (bool) $this->get_data_value( 'is_valid' );
}
return $this->get_status() !== License_Statuses::INVALID_LICENSE_KEY;
}
/**
* Get the error message for the response, either the first one by default, or at a specific index.
*
* @since 1.0
*
* @param int $index The array index to use if mulitple errors exist.
*
* @return mixed|string
*/
public function get_error_message( $index = 0 ) {
if ( ! $this->has_errors() ) {
return '';
}
return $this->errors[ $index ];
}
/**
* Get the human-readable display status for the response.
*
* @since 1.0
*
* @return string|void
*/
public function get_display_status() {
if ( $this->max_seats_exceeded() ) {
return __( 'Sites Exceeded', 'gravityforms' );
}
switch ( $this->get_status() ) {
case License_Statuses::INVALID_LICENSE_KEY:
return __( 'Invalid', 'gravityforms' );
case License_Statuses::EXPIRED_LICENSE_KEY:
return __( 'Expired', 'gravityforms' );
case License_Statuses::VALID_KEY:
default:
return __( 'Active', 'gravityforms' );
}
}
/**
* Licenses can be valid and usable, technically-invalid but still usable, or invalid and unusable.
* This will return the correct usability value for this license key.
*
* @since 1.0
*
* @return string
*/
public function get_usability() {
if ( $this->get_status() === License_Statuses::VALID_KEY || $this->get_status() === License_Statuses::NO_DATA ) {
return License_Statuses::USABILITY_VALID;
}
if ( $this->get_status() === License_Statuses::INVALID_LICENSE_KEY || $this->get_status() === License_Statuses::SITE_REVOKED ) {
return License_Statuses::USABILITY_NOT_ALLOWED;
}
return License_Statuses::USABILITY_ALLOWED;
}
//----------------------------------------
//---------- Helpers/Utils ---------------
//----------------------------------------
/**
* Whether this response has any errors stored as a transient.
*
* @since 1.0
*
* @return bool
*/
private function has_stored_error() {
return (bool) $this->get_stored_error();
}
/**
* Get a properly-formatted link to the Upgrade page for this license key.
*
* @since 1.0
*
* @return string
*/
public function get_upgrade_link() {
$key = $this->get_data_value( 'license_key_md5' );
$type = $this->get_data_value( 'product_code' );
return sprintf( 'https://www.gravityforms.com/my-account/licenses/?action=upgrade&license_key=%s&license_code=%s&utm_source=gf-admin&utm_medium=upgrade-button&utm_campaign=license-enforcement', $key, $type );
}
/**
* Get the CTA information for this license key, if applicable.
*
* @since 1.0
*
* @return mixed
*/
public function get_cta() {
if ( $this->get_status() == License_Statuses::EXPIRED_LICENSE_KEY ) {
return array(
'type' => 'button',
'label' => __( 'Manage', 'gravityforms' ),
'link' => 'https://www.gravityforms.com/my-account/licenses/?utm_source=gf-admin&utm_medium=manage-button&utm_campaign=license-enforcement',
'class' => 'cog',
);
} elseif ( $this->max_seats_exceeded() ) {
return array(
'type' => 'button',
'label' => __( 'Upgrade', 'gravityforms' ),
'link' => $this->get_upgrade_link(),
'class' => 'product',
);
} else if ( $this->has_expiration() ) {
return array(
'type' => 'text',
'content' => $this->get_data_value( 'days_to_expire' ),
);
} else {
return array(
'type' => 'blank',
);
}
}
/**
* Some statuses are invalid, but get treated as usable. This determines if they should be displayed as
* though they are valid.
*
* @since 1.0
*
* @return bool
*/
public function display_as_valid() {
if ( $this->max_seats_exceeded() ) {
return false;
}
switch ( $this->get_status() ) {
case License_Statuses::INVALID_LICENSE_KEY:
case License_Statuses::EXPIRED_LICENSE_KEY:
return false;
case License_Statuses::VALID_KEY:
default:
return true;
}
}
/**
* Whether the license key can be used.
*
* @since 1.0
*
* @return bool
*/
public function can_be_used() {
return $this->get_usability() !== License_Statuses::USABILITY_NOT_ALLOWED;
}
/**
* Determine if the contained License Key has an expiration date.
*
* @since 1.0
*
* @return bool
*/
public function has_expiration() {
return $this->get_data_value( 'date_expires' ) && ! $this->get_data_value( 'renewal_date' ) && ! $this->get_data_value( 'is_perpetual' );
}
/**
* Get the text for the renewal message.
*
* @since 1.0
*
* @return string
*/
public function renewal_text() {
if ( $this->get_status() === License_Statuses::EXPIRED_LICENSE_KEY ) {
return __( 'Expired On', 'gravityforms' );
}
$has_subscription = (bool) $this->get_data_value( 'renewal_date' );
$cancelled = (bool) $this->get_data_value( 'is_subscription_canceled' );
if ( $has_subscription && ! $cancelled ) {
return __( 'Renews On', 'gravityforms' );
}
return __( 'Expires On', 'gravityforms' );
}
/**
* Returns the license renewal or expiry date or the doesn't expire message.
*
* @since 1.0
*
* @return string|void
*/
public function renewal_date() {
if ( $this->get_data_value( 'is_perpetual' ) ) {
return __( 'Does not expire', 'gravityforms' );
}
$date = $this->get_data_value( 'renewal_date' );
if ( empty( $date ) ) {
$date = $this->get_data_value( 'date_expires' );
}
return gmdate( 'M d, Y', strtotime( $date ) );
}
/**
* Whether the license has max seats exceeded.
*
* @since 1.0
*
* @return bool
*/
public function max_seats_exceeded() {
return $this->get_status() === License_Statuses::MAX_SITES_EXCEEDED || $this->get_data_value( 'remaining_seats' ) < 0;
}
//----------------------------------------
//---------- Serialization ---------------
//----------------------------------------
/**
* Prepares the object for serializing.
*
* @since 1.0
*
* @return array
*/
public function __serialize() {
return array(
'data' => $this->data,
'errors' => $this->errors,
'status' => $this->status,
'meta' => $this->meta,
'strat' => $this->transient_strategy,
);
}
/**
* Hydrates the object when unserializing.
*
* @since 1.0
*
* @param array $data The unserialized data.
*
* @return void
*/
public function __unserialize( $data ) {
$this->data = $data['data'];
$this->errors = $data['errors'];
$this->status = $data['status'];
$this->meta = $data['meta'];
$this->transient_strategy = $data['strat'];
}
}
@@ -0,0 +1,70 @@
<?php
namespace Gravity_Forms\Gravity_Tools\License;
/**
* Class License_Statuses
*
* Helper class to provide license statuse codes and messages.
*
* @since 1.0
*
* @package Gravity_Forms\Gravity_Tools\Utils
*/
class License_Statuses {
const VALID_KEY = 'valid_key';
const SITE_UNREGISTERED = 'site_unregistered';
const INVALID_LICENSE_KEY = 'gravityapi_invalid_license';
const EXPIRED_LICENSE_KEY = 'gravityapi_expired_license';
const SITE_REVOKED = 'gravityapi_site_revoked';
const URL_CHANGED = 'gravityapi_site_url_changed';
const MAX_SITES_EXCEEDED = 'gravityapi_exceeds_number_of_sites';
const MULTISITE_NOT_ALLOWED = 'gravityapi_multisite_not_allowed';
const NO_DATA = 'rest_no_route';
const USABILITY_VALID = 'success';
const USABILITY_ALLOWED = 'warning';
const USABILITY_NOT_ALLOWED = 'error';
/**
* Get the correct Message for the given code.
*
* @since 1.0
*
* @param $code
*
* @return mixed|string|void
*/
public static function get_message_for_code( $code ) {
$general_invalid_message = sprintf(
/* translators: %1s and %2s are link tag markup */
__( 'The license key entered is incorrect; please visit the %1$sGravity Forms website%2$s to verify your license.', 'gravityforms' ),
'<a href="https://www.gravityforms.com/my-account/licenses/?utm_source=gf-admin&utm_medium=purchase-link&utm_campaign=license-enforcement" target="_blank">',
'</a>'
);
$map = array(
self::VALID_KEY => __( 'Your license key has been successfully validated.', 'gravityforms' ),
self::SITE_REVOKED => sprintf(
/* translators: %1s and %2s are link tag markup */
__( 'The license key entered has been revoked; please check its status in your %1$sGravity Forms account.%2$s', 'gravityforms' ),
'<a href="https://www.gravityforms.com/my-account/licenses/?utm_source=gf-admin&utm_medium=account-link-revoked&utm_campaign=license-enforcement" target="_blank">',
'</a>'
),
self::MAX_SITES_EXCEEDED => __( 'This license key has already been activated on its maximum number of sites; please upgrade your license.', 'gravityforms' ),
self::MULTISITE_NOT_ALLOWED => __( 'This license key does not support multisite installations. Please use a different license.', 'gravityforms' ),
self::EXPIRED_LICENSE_KEY => sprintf(
/* translators: %1s and %2s are link tag markup */
__( 'This license key has expired; please visit your %1$sGravity Forms account%2$s to manage your license.', 'gravityforms' ),
'<a href="https://www.gravityforms.com/my-account/licenses/?utm_source=gf-admin&utm_medium=account-link-expired&utm_campaign=license-enforcement" target="_blank">',
'</a>'
),
self::SITE_UNREGISTERED => $general_invalid_message,
self::INVALID_LICENSE_KEY => $general_invalid_message,
self::URL_CHANGED => $general_invalid_message,
);
return isset( $map[ $code ] ) ? $map[ $code ] : $general_invalid_message;
}
}
@@ -0,0 +1,63 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging;
class DB_Logging_Provider implements Logging_Provider {
const DEBUG = 1;
const INFO = 2;
const WARN = 3;
const ERROR = 4;
const FATAL = 5;
private $timestamp_map = array(
self::INFO => "- INFO -->",
self::WARN => "- WARN -->",
self::DEBUG => "- DEBUG -->",
self::ERROR => "- ERROR -->",
self::FATAL => "- FATAL -->",
);
protected $model;
public function __construct( $model ) {
$this->model = $model;
}
public function log_info( $line ) {
$this->log( $line, self::INFO );
}
public function log_debug( $line ) {
$this->log( $line, self::DEBUG );
}
public function log_warning( $line ) {
$this->log( $line, self::WARN );
}
public function log_error( $line ) {
$this->log( $line, self::ERROR );
}
public function log_fatal( $line ) {
$this->log( $line, self::FATAL );
}
public function log( $line, $priority ) {
return $this->model->create( $line, $priority );
}
public function write_line_to_log( $line ) {
return $this->log_debug( $line );
}
public function get_lines() {
return $this->model->all( true );
}
public function delete_log() {
return $this->model->clear();
}
}
@@ -0,0 +1,247 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging;
use Gravity_Forms\Gravity_Tools\Logging\Parsers\File_Log_Parser;
/**
* File Logging Provider
*
* A logging provider which writes directly to a log file.
*/
class File_Logging_Provider implements Logging_Provider {
const DEBUG = 1;
const INFO = 2;
const WARN = 3;
const ERROR = 4;
const FATAL = 5;
const OFF = 6;
const LOG_OPEN = 1;
const OPEN_FAILED = 2;
const LOG_CLOSED = 3;
public $log_status = self::LOG_CLOSED;
public $date_format = "Y-m-d G:i:s";
public $message_queue;
private $offset;
private $log_file;
private $priority = self::INFO;
private $file_handle;
private $timestamp_map = array(
self::INFO => "- INFO -->",
self::WARN => "- WARN -->",
self::DEBUG => "- DEBUG -->",
self::ERROR => "- ERROR -->",
self::FATAL => "- FATAL -->",
);
/**
* @var File_Log_Parser
*/
private $line_parser;
/**
* Constructor
*
* @since 1.0
*
* @param $filepath
* @param $priority
* @param $offset
*/
public function __construct( $filepath, $priority, $offset = 0, File_Log_Parser $line_parser ) {
if ( $priority == self::OFF ) {
return;
}
$this->offset = $offset;
$this->log_file = $filepath;
$this->message_queue = array();
$this->priority = $priority;
$this->line_parser = $line_parser;
if ( file_exists( $this->log_file ) ) {
if ( ! is_writable( $this->log_file ) ) {
$this->log_status = self::OPEN_FAILED;
$this->message_queue[] = __( "The file exists, but could not be opened for writing. Check that appropriate permissions have been set.", 'gravitytools' );
return;
}
}
if ( $this->file_handle = fopen( $this->log_file, "a" ) ) {
$this->log_status = self::LOG_OPEN;
$this->message_queue[] = __( "The log file was opened successfully.", 'gravitytools' );
} else {
$this->log_status = self::OPEN_FAILED;
$this->message_queue[] = __( "The file could not be opened. Check permissions.", 'gravitytools' );
}
return;
}
/**
* Close file on destruct.
*
* @since 1.0
*/
public function __destruct() {
if ( $this->file_handle ) {
fclose( $this->file_handle );
}
}
public function get_log_file_path() {
return $this->log_file;
}
/**
* Add info line.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function log_info( $line ) {
$this->log( $line, self::INFO );
}
/**
* Add debug line.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function log_debug( $line ) {
$this->log( $line, self::DEBUG );
}
/**
* Add warning line.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function log_warning( $line ) {
$this->log( $line, self::WARN );
}
/**
* Add error line.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function log_error( $line ) {
$this->log( $line, self::ERROR );
}
/**
* Add fatal line.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function log_fatal( $line ) {
$this->log( $line, self::FATAL );
}
/**
* Add a log line with a defined priority.
*
* @since 1.0
*
* @param $line
* @param $priority
*
* @return void
*/
public function log( $line, $priority ) {
if ( $this->priority <= $priority ) {
$status = $this->get_timeline( $priority );
$this->write_line_to_log( "$status $line \n" );
}
}
public function delete_log() {
return;
}
/**
* Write a line to the log.
*
* @since 1.0
*
* @param $line
*
* @return void
*/
public function write_line_to_log( $line ) {
if ( $this->log_status == self::LOG_OPEN && $this->priority != self::OFF ) {
if ( fwrite( $this->file_handle, $line ) === false ) {
$this->message_queue[] = __( "The file could not be written to. Check that appropriate permissions have been set.", 'gravitytools' );
}
}
}
public function get_lines() {
$contents = file_get_contents( $this->get_log_file_path() );
return $this->line_parser->parse_log( $contents );
}
/**
* Get the timeline tet for a given log type.
*
* @since 1.0
*
* @param $level
*
* @return string
*/
private function get_timeline( $level ) {
if ( class_exists( 'DateTime' ) ) {
$original_time = microtime( true ) + $this->offset;
$microtime = sprintf( '%06d', ( $original_time - floor( $original_time ) ) * 1000000 );
$date = new \DateTime( date( 'Y-m-d H:i:s.' . $microtime, (int) $original_time ) );
$time = $date->format( $this->date_format );
} else {
$time = gmdate( $this->date_format, time() + $this->offset );
}
return $this->get_formatted_timeline_for_type( $level, $time );
}
/**
* Get the formatted timeline text for a given type.
*
* @since 1.0
*
* @param $type
* @param $time
*
* @return string
*/
private function get_formatted_timeline_for_type( $type, $time ) {
return sprintf( '[**] %s %s', $time, $this->timestamp_map[ $type ] );
}
}
@@ -0,0 +1,38 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging;
class Log_Line {
protected $timestamp;
protected $priority;
protected $line;
protected $id;
public function __construct( $timestamp, $priority, $line, $id ) {
$this->timestamp = $timestamp;
$this->priority = $priority;
$this->line = $line;
$this->id = $id;
}
public function timestamp() {
return $this->timestamp;
}
public function priority() {
return $this->priority;
}
public function line() {
return $this->line;
}
public function id() {
return $this->id;
}
}
@@ -0,0 +1,74 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging;
/**
* Logger
*
* Provides an abstract for dealing with logging. Takes a $provider class (to handle actually writing to a log/db/etc),
* and must define its own methods for whether to log, and how to delete said log.
*/
abstract class Logger {
/**
* @var Logging_Provider
*/
protected $provider;
abstract protected function should_log();
abstract protected function delete_log();
public function __construct( Logging_Provider $provider ) {
$this->provider = $provider;
}
public function log( $message, $priority ) {
if ( ! $this->should_log( $priority ) ) {
return;
}
return $this->provider->log( $message, $priority );
}
public function log_info( $message ) {
if ( ! $this->should_log( 'info' ) ) {
return;
}
return $this->provider->log_info( $message );
}
public function log_debug( $message ) {
if ( ! $this->should_log( 'debug' ) ) {
return;
}
return $this->provider->log_debug( $message );
}
public function log_warning( $message ) {
if ( ! $this->should_log( 'warning' ) ) {
return;
}
return $this->provider->log_warning( $message );
}
public function log_error( $message ) {
if ( ! $this->should_log( 'error' ) ) {
return;
}
return $this->provider->log_error( $message );
}
public function log_fatal( $message ) {
if ( ! $this->should_log( 'fatal' ) ) {
return;
}
return $this->provider->log_fatal( $message );
}
}
@@ -0,0 +1,25 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging;
interface Logging_Provider {
function log_info( $line );
function log_debug( $line );
function log_warning( $line );
function log_error( $line );
function log_fatal( $line );
function log( $line, $priority );
function delete_log();
function write_line_to_log( $line );
function get_lines();
}
@@ -0,0 +1,27 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Logging\Parsers;
use Gravity_Forms\Gravity_Tools\Logging\Log_Line;
class File_Log_Parser {
public function parse_log( $log ) {
$lines = array_filter( explode( '[**]', $log ) );
return array_map( function( $line ) {
return $this->parse_log_line( $line );
}, $lines );
}
public function parse_log_line( $log_line ) {
$log_line = trim( $log_line );
preg_match('/(.*) - (.*) --> (.*)/', trim( $log_line ), $data );
if ( count( $data ) !== 4 ) {
return new Log_Line( null, null, null );
}
return new Log_Line( $data[1], trim( strtolower( $data[2] ) ), $data[3] );
}
}
@@ -0,0 +1,284 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Model;
use Gravity_Forms\Gravity_Tools\Utils\Common;
class Form_Model {
/**
* @var Common
*/
protected $common;
public function __construct( $common ) {
$this->common = $common;
}
/**
* Gets the entry table name, including the site's database prefix
*
* @since 1.0
*
* @global $wpdb
*
* @return string The entry table name
*/
public function get_entry_table_name() {
global $wpdb;
return $wpdb->prefix . 'gf_entry';
}
/**
* Returns the current database version.
*
* @since 1.0
*
* @return string
*/
public function get_database_version() {
static $db_version = array();
$blog_id = get_current_blog_id();
if ( empty( $db_version[ $blog_id ] ) ) {
$db_version[ $blog_id ] = get_option( 'gf_db_version' );
}
return $db_version[ $blog_id ];
}
/**
* Gets the total, active, inactive, and trashed form counts.
*
* @since 1.0
*
* @uses GFFormsModel::get_form_table_name()
*
* @return array The form counts.
*/
public function get_form_count() {
global $wpdb;
$form_table_name = $this->get_form_table_name();
if ( ! $this->common->table_exists( $form_table_name ) ) {
return array(
'total' => 0,
'active' => 0,
'inactive' => 0,
'trash' => 0,
);
}
$results = $wpdb->get_results(
"
SELECT
(SELECT count(0) FROM $form_table_name WHERE is_trash = 0) as total,
(SELECT count(0) FROM $form_table_name WHERE is_active=1 AND is_trash = 0 ) as active,
(SELECT count(0) FROM $form_table_name WHERE is_active=0 AND is_trash = 0 ) as inactive,
(SELECT count(0) FROM $form_table_name WHERE is_trash=1) as trash
"
);
return array(
'total' => intval( $results[0]->total ),
'active' => intval( $results[0]->active ),
'inactive' => intval( $results[0]->inactive ),
'trash' => intval( $results[0]->trash ),
);
}
/**
* Gets the form table name, including the site's database prefix.
*
* @since 1.0
*
* @return string The form table name.
*/
public function get_form_table_name() {
global $wpdb;
if ( version_compare( $this->get_database_version(), '2.3-dev-1', '<' ) ) {
return $wpdb->prefix . 'rg_form';
}
return $wpdb->prefix . 'gf_form';
}
/**
* Returns the entry count for all forms.
*
* @since 1.0
*
* @param string $status
*
* @return null|string
*/
public function get_entry_count_all_forms( $status = 'active' ) {
global $wpdb;
if ( version_compare( $this->get_database_version(), '2.3-dev-1', '<' ) ) {
return $this->get_lead_count_all_forms( $status );
}
$entry_table_name = $this->get_entry_table_name();
if ( ! $this->common->table_exists( $entry_table_name ) ) {
return 0;
}
$sql = $wpdb->prepare( "SELECT count(id)
FROM $entry_table_name
WHERE status=%s", $status );
return $wpdb->get_var( $sql );
}
/**
* Get entry meta counts.
*
* @since 1.0
*
* @return array
*/
public function get_entry_meta_counts() {
global $wpdb;
$detail_table_name = $this->get_lead_details_table_name();
$meta_table_name = $this->get_lead_meta_table_name();
$notes_table_name = $this->get_lead_notes_table_name();
$results = $wpdb->get_results(
"
SELECT
(SELECT count(0) FROM $detail_table_name) as details,
(SELECT count(0) FROM $meta_table_name) as meta,
(SELECT count(0) FROM $notes_table_name) as notes
"
);
return array(
'details' => intval( $results[0]->details ),
'meta' => intval( $results[0]->meta ),
'notes' => intval( $results[0]->notes ),
);
}
/**
* Get lead counts for all forms.
*
* @since 1.0
*
* @param $status
*
* @return mixed
*/
public function get_lead_count_all_forms( $status = 'active' ) {
global $wpdb;
$lead_table_name = $this->get_lead_table_name();
$result = $wpdb->get_var( "SHOW COLUMNS FROM $lead_table_name LIKE 'status'" );
if ( $result ) {
$sql = $wpdb->prepare( "SELECT count(id)
FROM $lead_table_name
WHERE status=%s", $status );
} else {
$sql = "SELECT count(id) FROM $lead_table_name";
}
return $wpdb->get_var( $sql );
}
/**
* Gets the lead (entries) table name, including the site's database prefix.
*
* @since 1.0
* @global $wpdb
*
* @return string The lead (entry) table name.
*/
public function get_lead_table_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead';
}
/**
* Gets the lead (entry) meta table name, including the site's database prefix.
*
* @since 1.0
* @global $wpdb
*
* @return string The lead (entry) meta table name.
*/
public function get_lead_meta_table_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead_meta';
}
/**
* Gets the lead (entry) notes table name, including the site's database prefix.
*
* @since 1.0
*
* @global $wpdb
*
* @return string The lead (entry) notes table name.
*/
public function get_lead_notes_table_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead_notes';
}
/**
* Gets the lead (entry) details table name, including the site's database prefix.
*
* @since 1.0
*
* @global $wpdb
*
* @return string The lead (entry) details table name.
*/
public function get_lead_details_table_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead_detail';
}
/**
* Gets the lead (entry) details long table name, including the site's database prefix.
*
* @since 1.0
*
* @global $wpdb
*
* @return string The lead (entry) details long table name.
*/
public function get_lead_details_long_table_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead_detail_long';
}
/**
* Gets the lead (entry) view table name, including the site's database prefix.
*
* @since 1.0
*
* @global $wpdb
*
* @return string The lead (entry) view table name.
*/
public function get_lead_view_name() {
global $wpdb;
return $wpdb->prefix . 'rg_lead_view';
}
}
@@ -0,0 +1,99 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Providers;
use Gravity_Forms\Gravity_Tools\Service_Container;
use Gravity_Forms\Gravity_Tools\Service_Provider;
use Gravity_Forms\Gravity_Tools\Config_Collection;
use Gravity_Forms\Gravity_Tools\Config_Data_Parser;
/**
* Class Config_Collection_Service_Provider
*
* Service provider for the Config Collection Service.
*
* @package Gravity_Forms\Gravity_Tools\Providers
*/
class Config_Collection_Service_Provider extends Service_Provider {
// Organizational services
const CONFIG_COLLECTION = 'config_collection';
const DATA_PARSER = 'data_parser';
protected $rest_namespace;
/**
* The $rest_namespace will be used in order to define the API endpoint for
* retrieving mock/config data.
*
* @since 1.0
*
* @param string $rest_namespace
*
* @return void
*/
public function __construct( $rest_namespace = 'gravityforms/v2' ) {
$this->rest_namespace = $rest_namespace;
}
/**
* Register services to the container.
*
* @since 1.0
*
* @param Service_Container $container
*/
public function register( Service_Container $container ) {
// Add to container
$container->add( self::CONFIG_COLLECTION, function () {
return new Config_Collection();
} );
$container->add( self::DATA_PARSER, function () {
return new Config_Data_Parser();
} );
}
/**
* Initiailize any actions or hooks.
*
* @since 1.0
*
* @param Service_Container $container
*
* @return void
*/
public function init( Service_Container $container ) {
// Need to pass $this to callbacks; save as variable.
$self = $this;
add_action( 'wp_enqueue_scripts', function () use ( $container ) {
$container->get( self::CONFIG_COLLECTION )->handle();
}, 9999 );
add_action( 'admin_enqueue_scripts', function () use ( $container ) {
$container->get( self::CONFIG_COLLECTION )->handle();
}, 9999 );
add_action( 'gform_preview_init', function () use ( $container ) {
$container->get( self::CONFIG_COLLECTION )->handle();
}, 0 );
}
/**
* Callback for the Config Mocks REST endpoint.
*
* @since 1.0
*
* @return array
*/
public function config_mocks_endpoint() {
define( 'GFORMS_DOING_MOCK', true );
$data = $this->container->get( self::CONFIG_COLLECTION )->handle( false );
return $data;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Providers;
use Gravity_Forms\Gravity_Tools\Service_Container;
use Gravity_Forms\Gravity_Tools\Service_Provider;
use Gravity_Forms\Gravity_Tools\Config_Collection;
use Gravity_Forms\Gravity_Tools\Config_Data_Parser;
/**
* Class Config_Service_Provider
*
* Service provider for the Config Collection Service.
*
* @package Gravity_Forms\Gravity_Tools\Providers
*/
abstract class Config_Service_Provider extends Service_Provider {
/**
* Array mapping config class names to their container ID.
*
* @since 1.0
*
* @var string[]
*/
protected $configs = array();
/**
* Register services to the container.
*
* @since 1.0
*
* @param Service_Container $container
*/
public function register( Service_Container $container ) {
// Add configs to container.
$this->register_config_items( $container );
$this->register_configs_to_collection( $container );
}
/**
* For each config defined in $configs, instantiate and add to container.
*
* @since 1.0
*
* @param Service_Container $container
*
* @return void
*/
private function register_config_items( Service_Container $container ) {
$parser = $container->get( Config_Collection_Service_Provider::DATA_PARSER );
foreach ( $this->configs as $name => $class ) {
$container->add( $name, function () use ( $class, $parser ) {
return new $class( $parser );
} );
}
}
/**
* Register each config defined in $configs to the GF_Config_Collection.
*
* @since 1.0
*
* @param Service_Container $container
*
* @return void
*/
public function register_configs_to_collection( Service_Container $container ) {
$collection = $container->get( Config_Collection_Service_Provider::CONFIG_COLLECTION );
foreach ( $this->configs as $name => $config ) {
$config_class = $container->get( $name );
$collection->add_config( $config_class );
}
}
}
@@ -0,0 +1,485 @@
<?php
use Gravity_Forms\Gravity_Tools\System_Report\System_Report_Group;
use Gravity_Forms\Gravity_Tools\System_Report\System_Report_Item;
use Gravity_Forms\Gravity_Tools\Utils\Common;
class Environment_Details_Report_Details {
protected $groups = array();
private function add( $name, $group ) {
$this->groups[ $name ] = $group;
}
public function get_environment_details() {
if ( ! function_exists( 'get_locale' ) ) {
return $this->groups;
}
$wordpress_environment_data = $this->get_wordpress_environment_data();
$active_theme_data = $this->get_active_theme_data();
$active_plugins_data = $this->get_active_plugins_data();
$web_server_data = $this->get_web_server_data();
$php_data = $this->get_php_data();
$database_server_data = $this->get_database_server_data();
$date_and_time_data = $this->get_date_and_time_data();
$translations = $this->get_translations_data();
$this->add( __( 'WordPress Environment', 'gravity' ), $wordpress_environment_data );
$this->add( __( 'Active Theme', 'gravity' ), $active_theme_data );
$this->add( __( 'Active Plugins', 'gravity' ), $active_plugins_data );
$this->add( __( 'Web Server', 'gravity' ), $web_server_data );
$this->add( __( 'PHP', 'gravity' ), $php_data );
$this->add( __( 'Database Server', 'gravity' ), $database_server_data );
$this->add( __( 'Date and Time', 'gravity' ), $date_and_time_data );
$this->add( __( 'Translations', 'gravity' ), $translations );
return $this->groups;
}
protected function get_translations_data() {
$group = new System_Report_Group();
$group->add( 'site_locale', new System_Report_Item( __( 'Site Locale', 'gravity' ), get_locale() ) );
$group->add( 'user_locale', new System_Report_Item( sprintf( esc_html__( 'User (ID: %d) Locale', 'gravity' ), get_current_user_id() ), get_user_locale() ) );
return $group;
}
protected function get_wordpress_environment_data() {
$wp_cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
$alternate_wp_cron = defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON;
$args = array(
'timeout' => 2,
'body' => 'test',
'cookies' => $_COOKIE,
'sslverify' => apply_filters( 'https_local_ssl_verify', false ),
);
$filters_to_check = array(
'pre_wp_mail' => has_filter( 'pre_wp_mail' ),
);
$registered_filters = array_keys( array_filter( $filters_to_check ) );
$items = array(
array(
'label' => esc_html__( 'Home URL', 'gravity' ),
'value' => get_home_url(),
),
array(
'label' => esc_html__( 'Site URL', 'gravity' ),
'value' => get_site_url(),
),
array(
'label' => esc_html__( 'REST API Base URL', 'gravity' ),
'value' => rest_url(),
),
array(
'label' => esc_html__( 'WordPress Version', 'gravity' ),
'value' => get_bloginfo( 'version' ),
),
array(
'label' => esc_html__( 'WordPress Multisite', 'gravity' ),
'value' => is_multisite() ? esc_html__( 'Yes', 'gravity' ) : esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'WordPress Memory Limit', 'gravity' ),
'value' => WP_MEMORY_LIMIT,
),
array(
'label' => esc_html__( 'WordPress Debug Mode', 'gravity' ),
'value' => WP_DEBUG ? esc_html__( 'Yes', 'gravity' ) : esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'WordPress Debug Log', 'gravity' ),
'value' => WP_DEBUG_LOG ? esc_html__( 'Yes', 'gravity' ) : esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'WordPress Script Debug Mode', 'gravity' ),
'value' => SCRIPT_DEBUG ? __( 'Yes', 'gravity' ) : __( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'WordPress Cron', 'gravity' ),
'value' => ! $wp_cron_disabled ? __( 'Yes', 'gravity' ) : __( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'WordPress Alternate Cron', 'gravity' ),
'value' => $alternate_wp_cron ? __( 'Yes', 'gravity' ) : __( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'Registered Filters', 'gravity' ),
'value' => empty( $registered_filters ) ? esc_html__( 'N/A', 'gravity' ) : join( ', ', $registered_filters ),
),
);
$group = new System_Report_Group();
foreach ( $items as $item ) {
$group->add( $item['label'], new System_Report_Item( $item['label'], $item['value'] ) );
}
return $group;
}
protected function get_active_theme_data() {
$themes = array();
$active_theme = wp_get_theme();
$parent_theme = $active_theme->parent();
// Add active theme data
$label = ! empty( $active_theme->get( 'ThemeURI' ) )
? '<a class="gform-link" href="' . esc_url( $active_theme->get( 'ThemeURI' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $active_theme->get( 'Name' ) ) . '</a>'
: esc_html( $active_theme->get( 'Name' ) );
if ( ! empty( $active_theme->get( 'AuthorURI' ) ) ) {
$author = '<a class="gform-link" href="' . esc_url( $active_theme->get( 'AuthorURI' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $active_theme->get( 'Author' ) ) . '</a>';
} else {
$author = preg_replace_callback( '/(<a[^>]*>)/', function ( $matches ) {
preg_match( '/class="[^"]*"/', $matches[1], $class_matches );
if ( empty( $class_matches ) ) {
return str_replace( '<a', '<a class="gform-link"', $matches[1] );
}
return preg_replace( '/class="([^"]*)"/', 'class="$1 gform-link"', $matches[1] );
}, $active_theme->get( 'Author' ) );
}
$value = wp_kses_post(
sprintf(
/* translators: 1: Theme author and URL. 2: Theme version. */
__( 'by %1$s - %2$s', 'gravity' ),
$author,
$active_theme->get( 'Version' )
)
);
$themes[] = array(
'label' => $label,
'value' => $value,
);
// Add parent theme data if it exists
if ( $parent_theme instanceof \WP_Theme ) {
$parent_label = ! empty( $parent_theme->get( 'ThemeURI' ) )
? '<a class="gform-link" href="' . esc_url( $parent_theme->get( 'ThemeURI' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $parent_theme->get( 'Name' ) ) . ' (Parent)</a>'
: esc_html( $parent_theme->get( 'Name' ) . ' (Parent)' );
$parent_author_uri = $parent_theme->get( 'AuthorURI' );
if ( ! empty( $parent_theme->get( 'AuthorURI' ) ) ) {
$parent_author = '<a class="gform-link" href="' . esc_url( $parent_theme->get( 'AuthorURI' ) ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $parent_theme->get( 'Author' ) ) . '</a>';
} else {
$parent_author = preg_replace_callback( '/(<a[^>]*>)/', function ( $matches ) {
preg_match( '/class="[^"]*"/', $matches[1], $class_matches );
if ( empty( $class_matches ) ) {
return str_replace( '<a', '<a class="gform-link"', $matches[1] );
}
return preg_replace( '/class="([^"]*)"/', 'class="$1 gform-link"', $matches[1] );
}, $parent_theme->get( 'Author' ) );
}
$parent_value = wp_kses_post(
sprintf(
/* translators: 1: Theme author and URL. 2: Theme version. */
__( 'by %1$s - %2$s', 'gravity' ),
$parent_author,
$parent_theme->get( 'Version' )
)
);
$themes[] = array(
'label' => $parent_label,
'value' => $parent_value,
);
}
$group = new System_Report_Group();
foreach ( $themes as $theme ) {
$group->add( $theme['label'], new System_Report_Item( $theme['label'], $theme['value'] ) );
}
return $group;
}
protected function get_active_plugins_data() {
$plugins = array();
foreach ( get_plugins() as $plugin_path => $plugin ) {
// If plugin is not active, skip it.
if ( ! is_plugin_active( $plugin_path ) ) {
continue;
}
$label = isset( $plugin['PluginURI'] ) && ! empty( $plugin['PluginURI'] )
? '<a class="gform-link" href="' . esc_url( $plugin['PluginURI'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $plugin['Name'] ) . '</a>'
: esc_html( $plugin['Name'] );
$author = $plugin['Author'];
if ( isset( $plugin['AuthorURI'] ) && ! empty( $plugin['AuthorURI'] ) ) {
$author = '<a class="gform-link" href="' . esc_url( $plugin['AuthorURI'] ) . '" target="_blank" rel="noopener noreferrer">' . esc_html( $plugin['Author'] ) . '</a>';
} else {
$author = preg_replace_callback( '/(<a[^>]*>)/', function ( $matches ) {
preg_match( '/class="[^"]*"/', $matches[1], $class_matches );
if ( empty( $class_matches ) ) {
return str_replace( '<a', '<a class="gform-link"', $matches[1] );
}
return preg_replace( '/class="([^"]*)"/', 'class="$1 gform-link"', $matches[1] );
}, $plugin['Author'] );
}
$value = wp_kses_post(
sprintf(
/* translators: 1: Plugin author and URL. 2: Plugin version. */
__( 'by %1$s - %2$s', 'gravity' ),
$author,
$plugin['Version']
)
);
$plugins[] = array(
'label' => $label,
'value' => $value,
);
}
$group = new System_Report_Group();
foreach ( $plugins as $plugin ) {
$group->add( $plugin['label'], new System_Report_Item( $plugin['label'], $plugin['value'] ) );
}
return $group;
}
protected function get_web_server_data() {
$items = array(
array(
'label' => esc_html__( 'Software', 'gravity' ),
'value' => esc_html( $_SERVER['SERVER_SOFTWARE'] ),
),
array(
'label' => esc_html__( 'Port', 'gravity' ),
'value' => esc_html( $_SERVER['SERVER_PORT'] ),
),
array(
'label' => esc_html__( 'Document Root', 'gravity' ),
'value' => esc_html( $_SERVER['DOCUMENT_ROOT'] ),
),
);
$group = new System_Report_Group();
foreach ( $items as $item ) {
$group->add( $item['label'], new System_Report_Item( $item['label'], $item['value'] ) );
}
return $group;
}
protected function get_php_data() {
$curl_version = null;
if ( function_exists( 'curl_version' ) ) {
$curl_version_info = curl_version();
if ( is_array( $curl_version_info ) && isset( $curl_version_info['version'] ) ) {
$curl_version = $curl_version_info['version'];
}
}
$items = array(
array(
'label' => esc_html__( 'Version', 'gravity' ),
'value' => esc_html( phpversion() ),
),
array(
'label' => esc_html__( 'Memory Limit', 'gravity' ) . ' (memory_limit)',
'value' => esc_html( ini_get( 'memory_limit' ) ),
),
array(
'label' => esc_html__( 'Maximum Execution Time', 'gravity' ) . ' (max_execution_time)',
'value' => esc_html( ini_get( 'max_execution_time' ) ),
),
array(
'label' => esc_html__( 'Maximum File Upload Size', 'gravity' ) . ' (upload_max_filesize)',
'value' => esc_html( ini_get( 'upload_max_filesize' ) ),
),
array(
'label' => esc_html__( 'Maximum File Uploads', 'gravity' ) . ' (max_file_uploads)',
'value' => esc_html( ini_get( 'max_file_uploads' ) ),
),
array(
'label' => esc_html__( 'Maximum Post Size', 'gravity' ) . ' (post_max_size)',
'value' => esc_html( ini_get( 'post_max_size' ) ),
),
array(
'label' => esc_html__( 'Maximum Input Variables', 'gravity' ) . ' (max_input_vars)',
'value' => esc_html( ini_get( 'max_input_vars' ) ),
),
array(
'label' => esc_html__( 'cURL Enabled', 'gravity' ),
'value' => function_exists( 'curl_init' )
? esc_html(
sprintf(
/* translators: %s: cURL version. */
__( 'Yes (version %s)', 'gravity' ),
$curl_version
)
)
: esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'OpenSSL', 'gravity' ),
'value' => defined( 'OPENSSL_VERSION_TEXT' ) ? OPENSSL_VERSION_TEXT . ' (' . OPENSSL_VERSION_NUMBER . ')' : __( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'Mcrypt Enabled', 'gravity' ),
'value' => function_exists( 'mcrypt_encrypt' ) ? esc_html__( 'Yes', 'gravity' ) : esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'Mbstring Enabled', 'gravity' ),
'value' => function_exists( 'mb_strlen' ) ? esc_html__( 'Yes', 'gravity' ) : esc_html__( 'No', 'gravity' ),
),
array(
'label' => esc_html__( 'Loaded Extensions', 'gravity' ),
'value' => join( ', ', get_loaded_extensions() ),
),
);
$group = new System_Report_Group();
foreach( $items as $item ) {
$group->add( $item['label'], new System_Report_Item( $item['label'], $item['value'] ) );
}
return $group;
}
protected function get_database_server_data() {
global $wpdb;
$db_version = Common::get_db_version();
$db_type = Common::get_dbms_type();
$items = array(
array(
'label' => esc_html__( 'Database Management System', 'gravity' ),
'value' => esc_html( $db_type ),
),
array(
'label' => esc_html__( 'Version', 'gravity' ),
'value' => esc_html( $db_version ),
),
array(
'label' => esc_html__( 'Database Character Set', 'gravity' ),
'value' => esc_html( ( Common::get_dbms_type() === 'SQLite' ) ? $wpdb->charset : $wpdb->get_var( 'SELECT @@character_set_database' ) ),
),
array(
'label' => esc_html__( 'Database Collation', 'gravity' ),
'value' => esc_html( ( Common::get_dbms_type() === 'SQLite' ) ? ( empty( $wpdb->collate ) ? 'N/A' : $wpdb->collate ) : $wpdb->get_var( 'SELECT @@collation_database' ) ),
),
);
$group = new System_Report_Group();
foreach( $items as $item ) {
$group->add( $item['label'], new System_Report_Item( $item['label'], $item['value'] ) );
}
return $group;
}
protected function get_date_and_time_data() {
global $wpdb;
$db_date = $wpdb->get_var( 'SELECT utc_timestamp()' );
$php_date = date( 'Y-m-d H:i:s' );
$date_option = trim( get_option( 'date_format' ) );
$time_option = trim( get_option( 'time_format' ) );
$date_format = $date_option ? $date_option : 'Y-m-d';
$time_format = $time_option ? $time_option : 'H:i';
$gmt_db_date = mysql2date( 'G', $db_date );
$local_db_date = strtotime( get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $gmt_db_date ) ) );
$formatted_local_db_date = sprintf(
/* translators: 1: date, 2: time */
__( '%1$s at %2$s', 'gravity' ),
date_i18n( $date_format, $local_db_date, true ),
date_i18n( $time_format, $local_db_date, true )
);
$gmt_php_date = mysql2date( 'G', $php_date );
$local_php_date = strtotime( get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $gmt_php_date ) ) );
$formatted_local_php_date = sprintf(
/* translators: 1: date, 2: time */
__( '%1$s at %2$s', 'gravity' ),
date_i18n( $date_format, $local_php_date, true ),
date_i18n( $time_format, $local_php_date, true )
);
$items = array(
array(
'label' => esc_html__( 'WordPress (Local) Timezone', 'gravity' ),
'value' => $this->get_timezone(),
),
array(
'label' => esc_html__( 'MySQL - Universal time (UTC)', 'gravity' ),
'value' => esc_html( $db_date ),
),
array(
'label' => esc_html__( 'MySQL - Local time', 'gravity' ),
'value' => esc_html( $formatted_local_db_date ),
),
array(
'label' => esc_html__( 'PHP - Universal time (UTC)', 'gravity' ),
'value' => esc_html( $php_date ),
),
array(
'label' => esc_html__( 'PHP - Local time', 'gravity' ),
'value' => esc_html( $formatted_local_php_date ),
),
);
$group = new System_Report_Group();
foreach( $items as $item ) {
$group->add( $item['label'], new System_Report_Item( $item['label'], $item['value'] ) );
}
return $group;
}
protected function get_timezone() {
$tzstring = get_option( 'timezone_string' );
// Remove old Etc mappings. Fallback to gmt_offset.
if ( false !== strpos( $tzstring, 'Etc/GMT' ) ) {
$tzstring = '';
}
if ( empty( $tzstring ) ) { // Create a UTC+- zone if no timezone string exists
$current_offset = get_option( 'gmt_offset' );
if ( 0 == $current_offset ) {
$tzstring = 'UTC+0';
} elseif ( $current_offset < 0 ) {
$tzstring = 'UTC' . $current_offset;
} else {
$tzstring = 'UTC+' . $current_offset;
}
}
return $tzstring;
}
}
@@ -0,0 +1,56 @@
<?php
namespace Gravity_Forms\Gravity_Tools\System_Report;
class System_Report_Group {
/**
* @var System_Report_Item[]
*/
protected $items = array();
public function all() {
return $this->items;
}
public function add( $name, System_Report_Item $item ) {
$this->items[ $name ] = $item;
}
public function delete( $name ) {
unset( $this->items[ $name ] );
}
public function get( $name ) {
if ( ! isset( $this->items[ $name ] ) ) {
return null;
}
return $this->items[ $name ];
}
public function has( $name ) {
if ( ! isset( $this->items[ $name ] ) ) {
return false;
}
return true;
}
public function as_array() {
return array_map( function( $item ) {
return $item->as_array();
}, $this->items );
}
public function as_string() {
$response = '';
foreach( $this->items as $item ) {
$response .= $item->as_string();
}
return $response;
}
}
@@ -0,0 +1,51 @@
<?php
namespace Gravity_Forms\Gravity_Tools\System_Report;
class System_Report_Item {
protected $key;
protected $value;
protected $is_sensitive;
protected $obfuscated_value = '**********';
public function __construct( $key, $value, $is_sensitive = false ) {
$this->key = $key;
$this->value = $value;
$this->is_sensitive = $is_sensitive;
}
public function key() {
return $this->escape( $this->key );
}
public function value() {
if ( $this->is_sensitive ) {
return $this->obfuscated_value;
}
return $this->escape( $this->value );
}
public function is_sensitive() {
return $this->is_sensitive;
}
public function as_array() {
return array(
'key' => $this->key(),
'value' => $this->value(),
);
}
public function as_string() {
return sprintf( "%s: %s\n", $this->key(), $this->value() );
}
private function escape( $string ) {
return strip_tags( $string, array( 'a' ) );
}
}
@@ -0,0 +1,89 @@
<?php
namespace Gravity_Forms\Gravity_Tools\System_Report;
use Environment_Details_Report_Details;
class System_Report_Repository {
/**
* @var System_Report_Group[]
*/
protected $groups = array();
private static $instance;
public function __construct( $init_empty = false ) {
if ( $init_empty ) {
return;
}
$this->setup_environment_details();
}
private function setup_environment_details() {
$environment_details = new Environment_Details_Report_Details();
$groups = $environment_details->get_environment_details();
foreach( $groups as $key => $group ) {
$this->add( $key, $group );
}
}
public static function instance( $init_empty = false ) {
if ( ! is_null( self::$instance ) ) {
return self::$instance;
}
return new self( $init_empty );
}
public function all() {
return $this->groups;
}
public function get( $name ) {
if ( ! isset( $this->groups[ $name ] ) ) {
return null;
}
return $this->groups[ $name ];
}
public function delete( $name ) {
unset( $this->groups[ $name ] );
}
public function has( $name ) {
if ( ! isset( $this->groups[ $name ] ) ) {
return false;
}
return true;
}
public function add( $name, System_Report_Group $group ) {
$this->groups[ $name ] = $group;
}
public function as_array() {
$response = array();
foreach( $this->groups as $name => $group ) {
$response[ $name ] = $group->as_array();
}
return $response;
}
public function as_string() {
$response = '';
foreach( $this->groups as $name => $group ) {
$response .= sprintf( "%s\n", $name );
$response .= $group->as_string();
$response .= "\n";
}
return $response;
}
}
@@ -0,0 +1,82 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Telemetry;
/**
* Class Telemetry_Data
*
* Base class for telemetry data.
*
* @package Gravity_Forms\Gravity_Forms\Telemetry
*/
abstract class Telemetry_Data {
/**
* @var array $data Data to be sent.
*/
public $data = array();
/**
* @var string $key Unique identifier for this data object.
*/
public $key = '';
protected $enabled_setting_name = '';
protected $data_setting_name = '';
abstract public function after_send( $response );
abstract public function record_data();
/**
* Determine if the user has allowed data collection.
*
* @since 1.0.3
*
* @return false|mixed|null
*/
public function is_data_collection_allowed() {
static $is_allowed;
if ( ! is_null( $is_allowed ) ) {
return $is_allowed;
}
$is_allowed = get_option( $this->enabled_setting_name, false );
return $is_allowed;
}
/**
* Get the current telemetry data.
*
* @since 1.0
*
* @return array
*/
public function get_existing_data() {
return get_option( $this->data_setting_name, [] );
}
/**
* Save telemetry data.
*
* @since 1.0
*
* @param Telemetry_Data $data The data to save.
*
* @return void
*/
public function save_data( $data ) {
$existing_data = $this->get_existing_data();
if ( ! $existing_data ) {
$existing_data = array();
}
$existing_data[ $this->key ] = $data;
update_option( $this->data_setting_name, $existing_data, false );
}
}
@@ -0,0 +1,70 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Telemetry;
use Gravity_Forms\Gravity_Tools\Background_Processing\Background_Process;
/**
* Telemetry_Processor Class.
*/
abstract class Telemetry_Processor extends Background_Process {
/**
* @var string
*/
const TELEMETRY_ENDPOINT = 'https://in.gravity.io/';
/**
* @var string
*/
protected $action = 'telemetry_processor';
/**
* Send data to the telemetry endpoint.
*
* @since 1.0
*
* @param array $entries The data to send.
*
* @return array|WP_Error
*/
abstract public function send_data( $entries );
/**
* Task
*
* Process a single batch of telemetry data.
*
* @param mixed $batch
* @return mixed
*/
protected function task( $batch ) {
if ( ! is_array( $batch ) ) {
$batch = array( $batch );
}
$raw_response = null;
$this->logger->log_debug( __METHOD__ . sprintf( '(): Processing a batch of %d telemetry data.', count( $batch ) ) );
$raw_response = $this->send_data( $batch );
if ( is_wp_error( $raw_response ) ) {
$this->logger->log_debug( __METHOD__ . sprintf( '(): Failed sending telemetry data. Code: %s; Message: %s.', $raw_response->get_error_code(), $raw_response->get_error_message() ) );
return false;
}
foreach ( $batch as $item ) {
/**
* @var Telemetry_Data $item
*/
if ( ! is_object( $item ) ) {
$this->logger->log_debug( __METHOD__ . sprintf( '(): Telemetry data is missing. Aborting running data_sent method on this entry.' ) );
continue;
}
$item->after_send( $raw_response );
}
return false;
}
}
@@ -0,0 +1,421 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Updates;
use Gravity_Forms\Gravity_Tools\License\License_API_Connector;
use Gravity_Forms\Gravity_Tools\Utils\Common;
if ( ! defined( 'RG_CURRENT_PAGE' ) ) {
define( 'RG_CURRENT_PAGE', '' );
}
class Auto_Updater {
protected $_version;
protected $_slug;
protected $_title;
protected $_update_icon;
protected $_full_path;
protected $_path;
protected $_url;
protected $_is_forms;
/**
* @var Common
*/
protected $common;
/**
* @var License_API_Connector
*/
protected $license_connector;
public function __construct( $slug, $version, $title, $full_path, $path, $url, $update_icon, $common, $license_connector, $is_forms = true ) {
$this->_slug = $slug;
$this->_version = $version;
$this->_title = $title;
$this->_full_path = $full_path;
$this->_path = $path;
$this->_url = $url;
$this->_update_icon = $update_icon;
$this->common = $common;
$this->license_connector = $license_connector;
$this->_is_forms = $is_forms;
}
/**
* Initialize various hooks and filters.
*
* @since 1.0
*
* @return void
*/
public function init() {
if ( is_admin() ) {
add_action( 'install_plugins_pre_plugin-information', array( $this, 'display_changelog' ), 9 );
add_action( 'gform_after_check_update', array( $this, 'flush_version_info' ) );
add_action( 'gform_updates', array( $this, 'display_updates' ) );
if ( $this->_is_forms ) {
add_filter( 'gform_updates_list', array( $this, 'get_update_info' ) );
}
if ( in_array( RG_CURRENT_PAGE, array( 'admin-ajax.php' ) ) ) {
add_action( 'wp_ajax_gf_get_changelog', array( $this, 'ajax_display_changelog' ) );
}
}
// Check for updates. The check might not run the admin context. E.g. from WP-CLI.
add_filter( 'transient_update_plugins', array( $this, 'check_update' ) );
add_filter( 'site_transient_update_plugins', array( $this, 'check_update' ) );
// ManageWP premium update filters
add_filter( 'mwp_premium_update_notification', array( $this, 'premium_update_push' ) );
add_filter( 'mwp_premium_perform_update', array( $this, 'premium_update' ) );
}
/**
* Premium update push for ManageWP.
*
* @since 1.0
*
* @filter mwp_premium_update_notification 10 1
*
* @param $premium_update
*
* @return mixed
*/
public function premium_update_push( $premium_update ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
}
$update = $this->get_version_info( $this->_slug );
if ( $this->common->rgar( $update, 'is_valid_key' ) == true && version_compare( $this->_version, $update['version'], '<' ) ) {
$plugin_data = get_plugin_data( $this->_full_path );
$plugin_data['type'] = 'plugin';
$plugin_data['slug'] = $this->_path;
$plugin_data['new_version'] = isset( $update['version'] ) ? $update['version'] : false;
$premium_update[] = $plugin_data;
}
return $premium_update;
}
/**
* Integrate with ManageWP
*
* @since 1.0
*
* @filter mwp_premium_perform_update 10 1
*
* @param $premium_update
*
* @return mixed
*/
public function premium_update( $premium_update ) {
if ( ! function_exists( 'get_plugin_data' ) ) {
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
}
$update = $this->get_version_info( $this->_slug );
if ( $this->common->rgar( $update, 'is_valid_key' ) == true && version_compare( $this->_version, $update['version'], '<' ) ) {
$plugin_data = get_plugin_data( $this->_full_path );
$plugin_data['slug'] = $this->_path;
$plugin_data['type'] = 'plugin';
$plugin_data['url'] = isset( $update['url'] ) ? $update['url'] : false; // OR provide your own callback function for managing the update
array_push( $premium_update, $plugin_data );
}
return $premium_update;
}
/**
* Flush the currently-stored version info.
*
* @since 1.0
*
* @return void
*/
public function flush_version_info() {
$this->set_version_info( $this->_slug, false );
}
/**
* Set version info in a transient.
*
* @since 1.0
*
* @param $plugin_slug
* @param $version_info
*
* @return void]
*/
private function set_version_info( $plugin_slug, $version_info ) {
if ( function_exists( 'set_site_transient' ) ) {
set_site_transient( $plugin_slug . '_version', $version_info, 60 * 60 * 12 );
} else {
set_transient( $plugin_slug . '_version', $version_info, 60 * 60 * 12 );
}
}
/**
* Check for updates.
*
* @since 1.0
*
* @filter transient_update_plugins 10 1
* @filter site_transient_update_plugins 10 1
*
* @param $option
*
* @return mixed
*/
public function check_update( $option ) {
if ( empty( $option ) ) {
return $option;
}
$key = $this->get_key();
$version_info = $this->get_version_info( $this->_slug );
if ( $this->common->rgar( $version_info, 'is_error' ) == '1' ) {
return $option;
}
if ( empty( $option->response[ $this->_path ] ) ) {
$option->response[ $this->_path ] = new \stdClass();
}
$plugin = array(
'plugin' => $this->_path,
'url' => $this->_url,
'slug' => $this->_slug,
'icons' => array(
'2x' => $this->_update_icon,
),
'package' => is_string( $version_info['url'] ) ? str_replace( '{KEY}', $key, $version_info['url'] ) : '',
'new_version' => $version_info['version'],
'id' => '0',
);
//Empty response means that the key is invalid. Do not queue for upgrade
if ( ! $this->common->rgar( $version_info, 'is_valid_key' ) || version_compare( $this->_version, $version_info['version'], '>=' ) ) {
unset( $option->response[ $this->_path ] );
$option->no_update[ $this->_path ] = (object) $plugin;
} else {
$option->response[ $this->_path ] = (object) $plugin;
}
return $option;
}
/**
* Display the changelog.
*
* @since 1.0
*
* @return void
*/
public function display_changelog() {
if ( $_REQUEST['plugin'] != $this->_slug ) {
return;
}
$change_log = $this->get_changelog();
echo $change_log;
exit;
}
/**
* Get changelog with admin-ajax.php in GFForms::maybe_display_update_notification().
*
* @since 2.4.15
*
* @return void
*/
public function ajax_display_changelog() {
check_admin_referer();
$this->display_changelog();
}
/**
* Get the changelog content.
*
* @since 1.0
*
* @return string
*/
private function get_changelog() {
$key = $this->get_key();
$body = "key={$key}";
$options = array( 'method' => 'POST', 'timeout' => 3, 'body' => $body );
$options['headers'] = array(
'Content-Type' => 'application/x-www-form-urlencoded; charset=' . get_option( 'blog_charset' ),
'Content-Length' => strlen( $body ),
'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ),
);
$raw_response = $this->common->post_to_manager( 'changelog.php', $this->get_remote_request_params( $this->_slug, $key, $this->_version ), $options );
if ( is_wp_error( $raw_response ) || 200 != $raw_response['response']['code'] ) {
$text = sprintf( esc_html__( 'Oops!! Something went wrong.%sPlease try again or %scontact us%s.', 'gravityforms' ), '<br/>', "<a href='" . esc_attr( $this->common->get_support_url() ) . "'>", '</a>' );
} else {
$text = $raw_response['body'];
if ( substr( $text, 0, 10 ) != '<!--GFM-->' ) {
$text = '';
}
}
return stripslashes( $text );
}
private function get_version_info( $offering, $use_cache = true ) {
$version_info = $this->license_connector->get_version_info( $use_cache );
$is_valid_key = $this->common->rgar( $version_info, 'is_valid_key' ) && $this->common->rgars( $version_info, "offerings/{$offering}/is_available" );
$info = array(
'is_valid_key' => $is_valid_key,
'version' => $this->common->rgars( $version_info, "offerings/{$offering}/version" ),
'url' => $this->common->rgars( $version_info, "offerings/{$offering}/url" )
);
return $info;
}
private function get_remote_request_params( $offering, $key, $version ) {
global $wpdb;
return sprintf( 'of=%s&key=%s&v=%s&wp=%s&php=%s&mysql=%s', urlencode( $offering ), urlencode( $key ), urlencode( $version ), urlencode( get_bloginfo( 'version' ) ), urlencode( phpversion() ), urlencode( $this->common->get_db_version() ) );
}
private function get_key() {
return $this->common->get_key();
}
/**
* Get the update info.
*
* @since 1.0
*
* @param $updates
*
* @return mixed
*/
public function get_update_info( $updates ) {
$force_check = rgget( 'force-check' ) == 1;
$version_info = $this->get_version_info( $this->_slug, ! $force_check );
$plugin_file = $this->_path;
$upgrade_url = wp_nonce_url( 'update.php?action=upgrade-plugin&amp;plugin=' . urlencode( $plugin_file ), 'upgrade-plugin_' . $plugin_file );
if ( ! $this->common->rgar( $version_info, 'is_valid_key' ) ) {
$version_icon = 'dashicons-no';
$version_message = sprintf(
'<p>%s</p>',
sprintf(
esc_html( '%sRegister%s your copy of Gravity Forms to receive access to automatic updates and support. Need a license key? %sPurchase one now%s.', 'gravityforms' ),
'<a href="admin.php?page=gf_settings">',
'</a>',
'<a href="https://www.gravityforms.com">',
'</a>'
)
);
} elseif ( version_compare( $this->_version, $version_info['version'], '<' ) ) {
$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . urlencode( $this->_slug ) . '&section=changelog&TB_iframe=true&width=600&height=800' );
$message_link_text = sprintf( esc_html__( 'View version %s details', 'gravityforms' ), $version_info['version'] );
$message_link = sprintf( '<a href="%s" class="thickbox" title="%s">%s</a>', esc_url( $details_url ), esc_attr( $this->_title ), $message_link_text );
$message = sprintf( esc_html__( 'There is a new version of %1$s available. %s.', 'gravityforms' ), $this->_title, $message_link );
$version_icon = 'dashicons-no';
$version_message = $message;
} else {
$version_icon = 'dashicons-yes';
$version_message = sprintf( esc_html__( 'Your version of %s is up to date.', 'gravityforms' ), $this->_title );
}
$updates[] = array(
'name' => esc_html( $this->_title ),
'is_valid_key' => $this->common->rgar( $version_info, 'is_valid_key' ),
'path' => $this->_path,
'slug' => $this->_slug,
'latest_version' => $version_info['version'],
'installed_version' => $this->_version,
'upgrade_url' => $upgrade_url,
'download_url' => $version_info['url'],
'version_icon' => $version_icon,
'version_message' => $version_message,
);
return $updates;
}
/**
* Display updates if necessary.
*
* @since 1.0
*
* @return void
*/
public function display_updates() {
?>
<div class="wrap <?php echo $this->common->get_browser_class() ?>">
<h2><?php esc_html_e( $this->_title ); ?></h2>
<?php
$force_check = rgget( 'force-check' ) == 1;
$version_info = $this->get_version_info( $this->_slug, ! $force_check );
if ( ! $this->common->rgar( $version_info, 'is_valid_key' ) ) {
?>
<div class="gf_update_expired alert_red">
<?php printf( esc_html__( '%sRegister%s your copy of %s to receive access to automatic updates and support. Need a license key? %sPurchase one now%s.', 'gravityforms' ), '<a href="admin.php?page=gf_settings">', '</a>', $this->_title, '<a href="https://www.gravityforms.com">', '</a>' ); ?>
</div>
<?php
} elseif ( version_compare( $this->_version, $version_info['version'], '<' ) ) {
if ( $this->common->rgar( $version_info, 'is_valid_key' ) ) {
$plugin_file = $this->_path;
$upgrade_url = wp_nonce_url( 'update.php?action=upgrade-plugin&amp;plugin=' . urlencode( $plugin_file ), 'upgrade-plugin_' . $plugin_file );
$details_url = self_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . urlencode( $this->_slug ) . '&section=changelog&TB_iframe=true&width=600&height=800' );
$message_link_text = sprintf( esc_html__( 'View version %s details', 'gravityforms' ), $version_info['version'] );
$message_link = sprintf( '<a href="%s" class="thickbox" title="%s">%s</a>', esc_url( $details_url ), esc_attr( $this->_title ), $message_link_text );
$message = sprintf( esc_html__( 'There is a new version of %1$s available. %s.', 'gravityforms' ), $this->_title, $message_link );
?>
<div class="gf_update_outdated alert_yellow">
<?php echo $message . ' <p>' . sprintf( esc_html__( 'You can update to the latest version automatically or download the update and install it manually. %sUpdate Automatically%s %sDownload Update%s', 'gravityforms' ), "</p><a class='button-primary' href='{$upgrade_url}'>", '</a>', "&nbsp;<a class='button' href='{$version_info['url']}'>", '</a>' ); ?>
</div>
<?php
}
} else {
?>
<div class="gf_update_current alert_green">
<?php printf( esc_html__( 'Your version of %s is up to date.', 'gravityforms' ), $this->_title ); ?>
</div>
<?php
}
?>
</div>
<?php
}
}
@@ -0,0 +1,48 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Upgrades;
use Gravity_Forms\Gravity_Tools\Logging\Logger;
class Upgrade_Routines {
/**
* @var string
*/
protected $namespace;
protected $routines = array();
public function __construct( $namespace ) {
$this->namespace = $namespace;
}
public function add( $key, $callback ) {
$this->routines[ $key ] = $callback;
}
public function remove( $key ) {
unset( $this->routines[ $key ] );
}
public function get( $key ) {
return isset( $this->routines[ $key ] ) ? $this->routines[ $key ] : null;
}
public function handle() {
foreach( $this->routines as $key => $callback ) {
$option_key = sprintf( '%s_upgrade_routine_%s', $this->namespace, $key );
$handled = get_option( $option_key, false );
if ( $handled ) {
continue;
}
call_user_func( $callback );
update_option( $option_key, true );
}
}
}
@@ -0,0 +1,96 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
class Beltloop {
public static function sort( $data, $id_key = 'id', $sort_key = 'prevId' ) {
$indexed = array();
foreach ( $data as $datum ) {
$indexed[] = array(
'sort_val' => $datum[ $sort_key ],
'data' => $datum,
);
}
$sorted = array();
$current_index = 0;
while ( count( $indexed ) ) {
$item_by_idx = self::get_matching_item_by_sort_key( $current_index, $indexed, $id_key );
$current = $item_by_idx['data'];
$sorted[] = $current;
unset( $indexed[ $item_by_idx['idx'] ] );
$current_index = $current[ $id_key ];
}
return $sorted;
}
private static function get_matching_item_by_sort_key( $index, $items, $id_key = 'id' ) {
$checked = array_filter( $items, function( $item ) use ( $index ) {
return (int) $item['sort_val'] === (int) $index;
} );
if ( ! empty( $checked ) ) {
return array(
'data' => $checked[ array_key_first( $checked ) ]['data'],
'idx' => array_key_first( $checked ),
);
}
if ( (int) $index !== 0 ) {
$heads = array_filter( $items, function( $item ) {
$v = $item['sort_val'];
return $v === null || $v === '' || (int) $v === 0;
} );
if ( ! empty( $heads ) ) {
$first = $heads[ array_key_first( $heads ) ];
return array(
'data' => $first['data'],
'idx' => array_key_first( $heads ),
);
}
}
$min_id = null;
$min_idx = null;
$min_data = null;
foreach ( $items as $idx => $item ) {
$id = isset( $item['data'][ $id_key ] ) ? (int) $item['data'][ $id_key ] : 0;
if ( $min_id === null || $id < $min_id ) {
$min_id = $id;
$min_idx = $idx;
$min_data = $item['data'];
}
}
return array(
'data' => $min_data,
'idx' => $min_idx,
);
}
public static function partial_sort( $full_list, $partial_list, $id_key = 'id' ) {
$indexed = array();
foreach ( $full_list as $item ) {
$indexed[ $item[ $id_key ] ] = $item;
}
$sorted = array();
foreach ( $partial_list as $part_item ) {
$search = $part_item[ $id_key ];
$pos = (int) array_search( $search, array_keys( $indexed ) );
$sorted[ $pos ] = $part_item;
}
ksort( $sorted );
return array_values( $sorted );
}
}
@@ -0,0 +1,185 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
use ArrayAccess;
class Bettarray implements ArrayAccess {
protected $array;
public function __construct( array $array ) {
$this->array = $array;
}
public function all() {
return $this->array;
}
#[\ReturnTypeWillChange]
public function offsetExists( $offset ) {
return isset( $this->array[ $offset ] );
}
#[\ReturnTypeWillChange]
public function offsetGet( $offset ) {
return $this->get_nested_value( $offset );
}
#[\ReturnTypeWillChange]
public function offsetSet( $offset, $value ) {
if ( is_null( $offset ) ) {
$this->array[] = $value;
return;
}
$this->update_nested_value( $offset, $value );
}
#[\ReturnTypeWillChange]
public function offsetUnset( $offset ) {
$this->delete_nested_value( $offset );
}
public function get( $key ) {
return $this->get_nested_value( $key );
}
public function get_raw( $key ) {
return $this->get_nested_value( $key, false );
}
public function set( $key, $value ) {
return $this->update_nested_value( $key, $value );
}
public function delete( $key ) {
return $this->delete_nested_value( $key );
}
public function slice( $offset, $count ) {
$data = $this->array;
$sliced = array_slice( $data, $offset, $count );
return new self( $sliced );
}
public function pluck( $search_key, $raw = false ) {
$results = array();
foreach( $this->array as $row ) {
if ( isset( $row[ $search_key ]) ) {
$results[] = $row[ $search_key ];
}
}
if ( $raw ) {
return $results;
}
return new self( $results );
}
public function filter( callable $callback ) {
$results = array();
foreach( $this->array as $row ) {
$matched = call_user_func( $callback, $row );
if ( $matched ) {
$results[] = $row;
}
}
return new self( $results );
}
public function count() {
return count( $this->array );
}
public function dd() {
var_dump( $this->array );
die();
}
public function dump() {
var_dump( $this->array );
}
public function append( $key, $value ) {
$existing = $this->get_nested_value( $key, false );
if ( ! is_array( $existing ) ) {
$existing = array();
}
$existing[] = $value;
$this->update_nested_value( $key, $existing );
}
public function amend( $key, $value ) {
$existing = $this->get_nested_value( $key, false );
if ( ! is_array( $existing ) ) {
$existing = array();
}
$existing = array_merge( $existing, $value );
$this->update_nested_value( $key, $existing );
}
private function get_nested_value( $key, $as_bettarray = true ) {
$current = $this->array;
$key_arr = explode( '.', $key );
while ( count( $key_arr ) ) {
$new_key = array_shift( $key_arr );
$current = isset( $current[ $new_key ] ) ? $current[ $new_key ] : array();
}
if ( is_array( $current ) && $as_bettarray ) {
return new self( $current );
}
return $current;
}
private function update_nested_value( $key, $value ) {
$current = &$this->array;
$key_arr = explode( '.', $key );
while ( count( $key_arr ) ) {
$new_key = array_shift( $key_arr );
if ( ! isset( $current[ $new_key ] ) ) {
$current[ $new_key ] = array();
}
$current = &$current[ $new_key ];
}
$current = $value;
}
private function delete_nested_value( $key ) {
$current = &$this->array;
$key_arr = explode( '.', $key );
while ( count( $key_arr ) > 1 ) {
$new_key = array_shift( $key_arr );
if ( ! isset( $current[ $new_key ] ) ) {
$current[ $new_key ] = array();
}
$current = &$current[ $new_key ];
}
$new_key = array_shift( $key_arr );
unset( $current[ $new_key ] );
}
}
@@ -0,0 +1,26 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
class Booliesh {
public static function get( $value, $default = false ) {
if ( is_string( $value ) ) {
$value = strtolower( $value );
}
if ( is_object( $value ) || is_array( $value ) ) {
return $default;
}
if ( $value === false || $value === 0 || $value === '0' || $value === 'no' || $value === 'off' || $value === 'false' || empty( $value ) ) {
return false;
}
if ( $value === true || $value === 1 || $value === '1' || $value === 'yes' || $value === 'on' || $value === 'true' || ! empty( $value ) ) {
return true;
}
return $default;
}
}
@@ -0,0 +1,399 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
use Gravity_Forms\Gravity_Tools\License\License_API_Connector;
use Gravity_Forms\Gravity_Tools\License\License_Statuses;
class Common {
private static $plugins;
protected $gravity_manager_url;
protected $support_url;
protected $key;
public function __construct( $gravity_manager_url, $support_url, $key ) {
$this->gravity_manager_url = $gravity_manager_url;
$this->support_url = $support_url;
$this->key = $key;
}
/**
* Get the support URL
*
* @since 1.0
*
* @return string
*/
public function get_support_url() {
return $this->support_url;
}
/**
* Post request to Gravity Manager.
*
* @since 1.0
*
* @param string $file The file.
* @param string $query The query string.
* @param array $options The options.
*
* @return array|WP_Error
*/
public function post_to_manager( $file, $query, $options ) {
if ( ! isset( $options['headers'] ) ) {
$options['headers'] = array();
}
// Forcing Referer to the unfiltered home url when sending requests to gravity manager.
$options['headers']['Referer'] = get_option( 'home' );
// Sending filtered version of URL so that gravity manager can remove duplicate URLs when filtered and unfiltered URLs are different.
$options['headers']['Filtered-Site-URL'] = get_bloginfo( 'url' );
$request_url = $this->gravity_manager_url . '/' . $file . '?' . $query;
$raw_response = wp_remote_post( $request_url, $options );
return $raw_response;
}
/**
* Get the stored license key.
*
* @since 1.0
*
* @return mixed
*/
public function get_key() {
return $this->key;
}
/**
* Returns the raw value from a SELECT version() db query.
*
* @since 1.0
*
* @return string Returns the raw value from a SELECT version() or SELECT sqlite_version() db query.
*/
public static function get_dbms_version() {
static $value;
if ( empty( $value ) ) {
global $wpdb;
$value = $wpdb->get_var( 'SELECT version();' );
if ( ( get_class( $wpdb ) === 'WP_SQLite_DB' ) || $wpdb->last_error ) {
$value = $wpdb->get_var( 'SELECT sqlite_version();' );
}
}
return $value;
}
/**
* Return current database management system.
*
* @since 1.0
*
* @return string either MySQL, MariaDB, or SQLite.
*/
public static function get_dbms_type() {
static $type;
global $wpdb;
if ( empty( $type ) ) {
$type = strpos( strtolower( self::get_dbms_version() ), 'mariadb' ) ? 'MariaDB' : 'MySQL';
if ( get_class( $wpdb ) === 'WP_SQLite_DB' ) {
$type = 'SQLite';
}
}
return $type;
}
/**
* Get the total emails sent.
*
* @since 1.0
*
* @return int
*/
public static function get_emails_sent() {
$count = get_option( 'gform_email_count' );
if ( ! $count ) {
$count = 0;
}
return $count;
}
/**
* Get the number of API calls.
*
* @since 1.0
*
* @return int
*/
public static function get_api_calls() {
$count = get_option( 'gform_api_count' );
if ( ! $count ) {
$count = 0;
}
return $count;
}
/**
* Return the version of MySQL or MariaDB currently in use.
*
* @since 1.0
*
* @return string
*/
public static function get_db_version() {
static $version;
if ( empty( $version ) ) {
$version = preg_replace( '/[^0-9.].*/', '', self::get_dbms_version() );
}
return $version;
}
/**
* Unserializes a string while suppressing errors, checks if the result is of the expected type.
*
* @since 1.0
*
* @param string $string The string to be unserialized.
* @param string $expected The expected type after unserialization.
* @param bool $default The default value to return if unserialization failed.
*
* @return false|mixed
*/
public static function safe_unserialize( $string, $expected, $default = false ) {
$data = is_string( $string ) ? @unserialize( $string ) : $string;
if ( is_a( $data, $expected ) ) {
return $data;
}
return $default;
}
/**
* Checks for the existence of a MySQL table.
*
* @since 1.0
* @access public
*
* @param string $table_name Table to check for.
*
* @uses wpdb::get_var()
*
* @return bool
*/
public function table_exists( $table_name ) {
global $wpdb;
$count = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" );
return ! empty( $count );
}
/**
* Helper function for getting values from query strings or arrays
*
* @since 1.0
*
* @param string $name The key
* @param array $array The array to search through. If null, checks query strings. Defaults to null.
*
* @return string The value. If none found, empty string.
*/
public function rgget( $name, $array = null ) {
if ( ! isset( $array ) ) {
$array = $_GET;
}
if ( ! is_array( $array ) ) {
return '';
}
if ( isset( $array[ $name ] ) ) {
return $array[ $name ];
}
return '';
}
/**
* Helper function to obtain POST values.
*
* @since 1.0
*
* @param string $name The key
* @param bool $do_stripslashes Optional. Performs stripslashes_deep. Defaults to true.
*
* @return string The value. If none found, empty string.
*/
public function rgpost( $name, $do_stripslashes = true ) {
if ( isset( $_POST[ $name ] ) ) {
return $do_stripslashes ? stripslashes_deep( $_POST[ $name ] ) : $_POST[ $name ];
}
return '';
}
/**
* Get a specific property of an array without needing to check if that property exists.
*
* Provide a default value if you want to return a specific value if the property is not set.
*
* @since 1.0
* @access public
*
* @param array $array Array from which the property's value should be retrieved.
* @param string $prop Name of the property to be retrieved.
* @param string $default Optional. Value that should be returned if the property is not set or empty. Defaults to null.
*
* @return null|string|mixed The value
*/
public function rgar( $array, $prop, $default = null ) {
if ( ! is_array( $array ) && ! ( is_object( $array ) && $array instanceof ArrayAccess ) ) {
return $default;
}
if ( isset( $array[ $prop ] ) ) {
$value = $array[ $prop ];
} else {
$value = '';
}
return empty( $value ) && $default !== null ? $default : $value;
}
/**
* Gets a specific property within a multidimensional array.
*
* @since 1.0
* @access public
*
* @param array $array The array to search in.
* @param string $name The name of the property to find.
* @param string $default Optional. Value that should be returned if the property is not set or empty. Defaults to null.
*
* @return null|string|mixed The value
*/
public function rgars( $array, $name, $default = null ) {
if ( ! is_array( $array ) && ! ( is_object( $array ) && $array instanceof ArrayAccess ) ) {
return $default;
}
$names = explode( '/', $name );
$val = $array;
foreach ( $names as $current_name ) {
$val = $this->rgar( $val, $current_name, $default );
}
return $val;
}
/**
* Determines if a value is empty.
*
* @since 1.0
* @access public
*
* @param string $name The property name to check.
* @param array $array Optional. An array to check through. Otherwise, checks for POST variables.
*
* @return bool True if empty. False otherwise.
*/
public function rgempty( $name, $array = null ) {
if ( is_array( $name ) ) {
return empty( $name );
}
if ( ! $array ) {
$array = $_POST;
}
$val = $this->rgar( $array, $name );
return empty( $val );
}
/**
* Checks if the string is empty
*
* @since 1.0
* @access public
*
* @param string $text The string to check.
*
* @return bool True if empty. False otherwise.
*/
public function rgblank( $text ) {
return empty( $text ) && ! is_array( $text ) && strval( $text ) != '0';
}
/**
* Gets a property value from an object
*
* @since 1.0
* @access public
*
* @param object $obj The object to check
* @param string $name The property name to check for
*
* @return string The property value
*/
public function rgobj( $obj, $name ) {
if ( isset( $obj->$name ) ) {
return $obj->$name;
}
return '';
}
/**
* Converts a delimiter separated string to an array.
*
* @since 1.0
* @access public
*
* @param string $sep The delimiter between values
* @param string $string The string to convert
* @param int $count The expected number of items in the resulting array
*
* @return array $ary The exploded array
*/
public function rgexplode( $sep, $string, $count ) {
$ary = explode( $sep, $string );
while ( count( $ary ) < $count ) {
$ary[] = '';
}
return $ary;
}
}
@@ -0,0 +1,708 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
/**
* Provides methods for retrieving lists of geological data.
*
* Each of the public methods allows two arguments:
*
* @param $as_json - bool - Whether the data should be returned as JSON instead of an array.
* @param $process_callback - callable - An optional callback that takes the data as a parameter and returns a modified version.
*/
class GeoData {
/**
* Provides a list of Countries organized by their 2-character country codes.
*
* @return array
*/
private static function countries_list() {
return array(
'AF' => __( 'Afghanistan', 'gravitytools' ),
'AX' => __( 'Åland Islands', 'gravitytools' ),
'AL' => __( 'Albania', 'gravitytools' ),
'DZ' => __( 'Algeria', 'gravitytools' ),
'AS' => __( 'American Samoa', 'gravitytools' ),
'AD' => __( 'Andorra', 'gravitytools' ),
'AO' => __( 'Angola', 'gravitytools' ),
'AI' => __( 'Anguilla', 'gravitytools' ),
'AQ' => __( 'Antarctica', 'gravitytools' ),
'AG' => __( 'Antigua and Barbuda', 'gravitytools' ),
'AR' => __( 'Argentina', 'gravitytools' ),
'AM' => __( 'Armenia', 'gravitytools' ),
'AW' => __( 'Aruba', 'gravitytools' ),
'AC' => __( 'Ascension Island', 'gravitytools' ),
'AU' => __( 'Australia', 'gravitytools' ),
'AT' => __( 'Austria', 'gravitytools' ),
'AZ' => __( 'Azerbaijan', 'gravitytools' ),
'BS' => __( 'Bahamas', 'gravitytools' ),
'BH' => __( 'Bahrain', 'gravitytools' ),
'BD' => __( 'Bangladesh', 'gravitytools' ),
'BB' => __( 'Barbados', 'gravitytools' ),
'BY' => __( 'Belarus', 'gravitytools' ),
'BE' => __( 'Belgium', 'gravitytools' ),
'PW' => __( 'Belau', 'gravitytools' ),
'BZ' => __( 'Belize', 'gravitytools' ),
'BJ' => __( 'Benin', 'gravitytools' ),
'BM' => __( 'Bermuda', 'gravitytools' ),
'BT' => __( 'Bhutan', 'gravitytools' ),
'BO' => __( 'Bolivia', 'gravitytools' ),
'BQ' => __( 'Bonaire, Saint Eustatius and Saba', 'gravitytools' ),
'BA' => __( 'Bosnia and Herzegovina', 'gravitytools' ),
'BW' => __( 'Botswana', 'gravitytools' ),
'BV' => __( 'Bouvet Island', 'gravitytools' ),
'BR' => __( 'Brazil', 'gravitytools' ),
'IO' => __( 'British Indian Ocean Territory', 'gravitytools' ),
'BN' => __( 'Brunei', 'gravitytools' ),
'BG' => __( 'Bulgaria', 'gravitytools' ),
'BF' => __( 'Burkina Faso', 'gravitytools' ),
'BI' => __( 'Burundi', 'gravitytools' ),
'KH' => __( 'Cambodia', 'gravitytools' ),
'CM' => __( 'Cameroon', 'gravitytools' ),
'CA' => __( 'Canada', 'gravitytools' ),
'CV' => __( 'Cape Verde', 'gravitytools' ),
'KY' => __( 'Cayman Islands', 'gravitytools' ),
'CF' => __( 'Central African Republic', 'gravitytools' ),
'TD' => __( 'Chad', 'gravitytools' ),
'CL' => __( 'Chile', 'gravitytools' ),
'CN' => __( 'China', 'gravitytools' ),
'CX' => __( 'Christmas Island', 'gravitytools' ),
'CC' => __( 'Cocos (Keeling) Islands', 'gravitytools' ),
'CO' => __( 'Colombia', 'gravitytools' ),
'KM' => __( 'Comoros', 'gravitytools' ),
'CG' => __( 'Congo (Brazzaville)', 'gravitytools' ),
'CD' => __( 'Congo (Kinshasa)', 'gravitytools' ),
'CK' => __( 'Cook Islands', 'gravitytools' ),
'CR' => __( 'Costa Rica', 'gravitytools' ),
'HR' => __( 'Croatia', 'gravitytools' ),
'CU' => __( 'Cuba', 'gravitytools' ),
'CW' => __( 'Cura&ccedil;ao', 'gravitytools' ),
'CY' => __( 'Cyprus', 'gravitytools' ),
'CZ' => __( 'Czech Republic', 'gravitytools' ),
'DK' => __( 'Denmark', 'gravitytools' ),
'DJ' => __( 'Djibouti', 'gravitytools' ),
'DM' => __( 'Dominica', 'gravitytools' ),
'DO' => __( 'Dominican Republic', 'gravitytools' ),
'EC' => __( 'Ecuador', 'gravitytools' ),
'EG' => __( 'Egypt', 'gravitytools' ),
'SV' => __( 'El Salvador', 'gravitytools' ),
'GQ' => __( 'Equatorial Guinea', 'gravitytools' ),
'ER' => __( 'Eritrea', 'gravitytools' ),
'EE' => __( 'Estonia', 'gravitytools' ),
'ET' => __( 'Ethiopia', 'gravitytools' ),
'FK' => __( 'Falkland Islands', 'gravitytools' ),
'FO' => __( 'Faroe Islands', 'gravitytools' ),
'FJ' => __( 'Fiji', 'gravitytools' ),
'FI' => __( 'Finland', 'gravitytools' ),
'FR' => __( 'France', 'gravitytools' ),
'GF' => __( 'French Guiana', 'gravitytools' ),
'PF' => __( 'French Polynesia', 'gravitytools' ),
'TF' => __( 'French Southern Territories', 'gravitytools' ),
'GA' => __( 'Gabon', 'gravitytools' ),
'GM' => __( 'Gambia', 'gravitytools' ),
'GE' => __( 'Georgia', 'gravitytools' ),
'DE' => __( 'Germany', 'gravitytools' ),
'GH' => __( 'Ghana', 'gravitytools' ),
'GI' => __( 'Gibraltar', 'gravitytools' ),
'GR' => __( 'Greece', 'gravitytools' ),
'GL' => __( 'Greenland', 'gravitytools' ),
'GD' => __( 'Grenada', 'gravitytools' ),
'GP' => __( 'Guadeloupe', 'gravitytools' ),
'GU' => __( 'Guam', 'gravitytools' ),
'GT' => __( 'Guatemala', 'gravitytools' ),
'GG' => __( 'Guernsey', 'gravitytools' ),
'GN' => __( 'Guinea', 'gravitytools' ),
'GW' => __( 'Guinea-Bissau', 'gravitytools' ),
'GY' => __( 'Guyana', 'gravitytools' ),
'HT' => __( 'Haiti', 'gravitytools' ),
'HM' => __( 'Heard Island and McDonald Islands', 'gravitytools' ),
'HN' => __( 'Honduras', 'gravitytools' ),
'HK' => __( 'Hong Kong', 'gravitytools' ),
'HU' => __( 'Hungary', 'gravitytools' ),
'IS' => __( 'Iceland', 'gravitytools' ),
'IN' => __( 'India', 'gravitytools' ),
'ID' => __( 'Indonesia', 'gravitytools' ),
'IR' => __( 'Iran', 'gravitytools' ),
'IQ' => __( 'Iraq', 'gravitytools' ),
'IE' => __( 'Ireland', 'gravitytools' ),
'IM' => __( 'Isle of Man', 'gravitytools' ),
'IL' => __( 'Israel', 'gravitytools' ),
'IT' => __( 'Italy', 'gravitytools' ),
'CI' => __( 'Ivory Coast', 'gravitytools' ),
'JM' => __( 'Jamaica', 'gravitytools' ),
'JP' => __( 'Japan', 'gravitytools' ),
'JE' => __( 'Jersey', 'gravitytools' ),
'JO' => __( 'Jordan', 'gravitytools' ),
'KZ' => __( 'Kazakhstan', 'gravitytools' ),
'KE' => __( 'Kenya', 'gravitytools' ),
'KI' => __( 'Kiribati', 'gravitytools' ),
'XK' => __( 'Kosovo', 'gravitytools' ),
'KW' => __( 'Kuwait', 'gravitytools' ),
'KG' => __( 'Kyrgyzstan', 'gravitytools' ),
'LA' => __( 'Laos', 'gravitytools' ),
'LV' => __( 'Latvia', 'gravitytools' ),
'LB' => __( 'Lebanon', 'gravitytools' ),
'LS' => __( 'Lesotho', 'gravitytools' ),
'LR' => __( 'Liberia', 'gravitytools' ),
'LY' => __( 'Libya', 'gravitytools' ),
'LI' => __( 'Liechtenstein', 'gravitytools' ),
'LT' => __( 'Lithuania', 'gravitytools' ),
'LU' => __( 'Luxembourg', 'gravitytools' ),
'MO' => __( 'Macao', 'gravitytools' ),
'MK' => __( 'North Macedonia', 'gravitytools' ),
'MG' => __( 'Madagascar', 'gravitytools' ),
'MW' => __( 'Malawi', 'gravitytools' ),
'MY' => __( 'Malaysia', 'gravitytools' ),
'MV' => __( 'Maldives', 'gravitytools' ),
'ML' => __( 'Mali', 'gravitytools' ),
'MT' => __( 'Malta', 'gravitytools' ),
'MH' => __( 'Marshall Islands', 'gravitytools' ),
'MQ' => __( 'Martinique', 'gravitytools' ),
'MR' => __( 'Mauritania', 'gravitytools' ),
'MU' => __( 'Mauritius', 'gravitytools' ),
'YT' => __( 'Mayotte', 'gravitytools' ),
'MX' => __( 'Mexico', 'gravitytools' ),
'FM' => __( 'Micronesia', 'gravitytools' ),
'MD' => __( 'Moldova', 'gravitytools' ),
'MC' => __( 'Monaco', 'gravitytools' ),
'MN' => __( 'Mongolia', 'gravitytools' ),
'ME' => __( 'Montenegro', 'gravitytools' ),
'MS' => __( 'Montserrat', 'gravitytools' ),
'MA' => __( 'Morocco', 'gravitytools' ),
'MZ' => __( 'Mozambique', 'gravitytools' ),
'MM' => __( 'Myanmar', 'gravitytools' ),
'NA' => __( 'Namibia', 'gravitytools' ),
'NR' => __( 'Nauru', 'gravitytools' ),
'NP' => __( 'Nepal', 'gravitytools' ),
'NL' => __( 'Netherlands', 'gravitytools' ),
'NC' => __( 'New Caledonia', 'gravitytools' ),
'NZ' => __( 'New Zealand', 'gravitytools' ),
'NI' => __( 'Nicaragua', 'gravitytools' ),
'NE' => __( 'Niger', 'gravitytools' ),
'NG' => __( 'Nigeria', 'gravitytools' ),
'NU' => __( 'Niue', 'gravitytools' ),
'NF' => __( 'Norfolk Island', 'gravitytools' ),
'MP' => __( 'Northern Mariana Islands', 'gravitytools' ),
'KP' => __( 'North Korea', 'gravitytools' ),
'NO' => __( 'Norway', 'gravitytools' ),
'OM' => __( 'Oman', 'gravitytools' ),
'PK' => __( 'Pakistan', 'gravitytools' ),
'PS' => __( 'Palestinian Territory', 'gravitytools' ),
'PA' => __( 'Panama', 'gravitytools' ),
'PG' => __( 'Papua New Guinea', 'gravitytools' ),
'PY' => __( 'Paraguay', 'gravitytools' ),
'PE' => __( 'Peru', 'gravitytools' ),
'PH' => __( 'Philippines', 'gravitytools' ),
'PN' => __( 'Pitcairn', 'gravitytools' ),
'PL' => __( 'Poland', 'gravitytools' ),
'PT' => __( 'Portugal', 'gravitytools' ),
'PR' => __( 'Puerto Rico', 'gravitytools' ),
'QA' => __( 'Qatar', 'gravitytools' ),
'RE' => __( 'Reunion', 'gravitytools' ),
'RO' => __( 'Romania', 'gravitytools' ),
'RU' => __( 'Russia', 'gravitytools' ),
'RW' => __( 'Rwanda', 'gravitytools' ),
'BL' => __( 'Saint Barth&eacute;lemy', 'gravitytools' ),
'SH' => __( 'Saint Helena', 'gravitytools' ),
'KN' => __( 'Saint Kitts and Nevis', 'gravitytools' ),
'LC' => __( 'Saint Lucia', 'gravitytools' ),
'MF' => __( 'Saint Martin (French part)', 'gravitytools' ),
'SX' => __( 'Saint Martin (Dutch part)', 'gravitytools' ),
'PM' => __( 'Saint Pierre and Miquelon', 'gravitytools' ),
'VC' => __( 'Saint Vincent and the Grenadines', 'gravitytools' ),
'SM' => __( 'San Marino', 'gravitytools' ),
'ST' => __( 'S&atilde;o Tom&eacute; and Pr&iacute;ncipe', 'gravitytools' ),
'SA' => __( 'Saudi Arabia', 'gravitytools' ),
'SN' => __( 'Senegal', 'gravitytools' ),
'RS' => __( 'Serbia', 'gravitytools' ),
'SC' => __( 'Seychelles', 'gravitytools' ),
'SL' => __( 'Sierra Leone', 'gravitytools' ),
'SG' => __( 'Singapore', 'gravitytools' ),
'SK' => __( 'Slovakia', 'gravitytools' ),
'SI' => __( 'Slovenia', 'gravitytools' ),
'SB' => __( 'Solomon Islands', 'gravitytools' ),
'SO' => __( 'Somalia', 'gravitytools' ),
'ZA' => __( 'South Africa', 'gravitytools' ),
'GS' => __( 'South Georgia/Sandwich Islands', 'gravitytools' ),
'KR' => __( 'South Korea', 'gravitytools' ),
'SS' => __( 'South Sudan', 'gravitytools' ),
'ES' => __( 'Spain', 'gravitytools' ),
'LK' => __( 'Sri Lanka', 'gravitytools' ),
'SD' => __( 'Sudan', 'gravitytools' ),
'SR' => __( 'Suriname', 'gravitytools' ),
'SJ' => __( 'Svalbard and Jan Mayen', 'gravitytools' ),
'SZ' => __( 'Eswatini', 'gravitytools' ),
'SE' => __( 'Sweden', 'gravitytools' ),
'CH' => __( 'Switzerland', 'gravitytools' ),
'SY' => __( 'Syria', 'gravitytools' ),
'TW' => __( 'Taiwan', 'gravitytools' ),
'TJ' => __( 'Tajikistan', 'gravitytools' ),
'TZ' => __( 'Tanzania', 'gravitytools' ),
'TH' => __( 'Thailand', 'gravitytools' ),
'TL' => __( 'Timor-Leste', 'gravitytools' ),
'TG' => __( 'Togo', 'gravitytools' ),
'TK' => __( 'Tokelau', 'gravitytools' ),
'TO' => __( 'Tonga', 'gravitytools' ),
'TT' => __( 'Trinidad and Tobago', 'gravitytools' ),
'TN' => __( 'Tunisia', 'gravitytools' ),
'TR' => __( 'Turkey', 'gravitytools' ),
'TM' => __( 'Turkmenistan', 'gravitytools' ),
'TC' => __( 'Turks and Caicos Islands', 'gravitytools' ),
'TV' => __( 'Tuvalu', 'gravitytools' ),
'UG' => __( 'Uganda', 'gravitytools' ),
'UA' => __( 'Ukraine', 'gravitytools' ),
'AE' => __( 'United Arab Emirates', 'gravitytools' ),
'GB' => __( 'United Kingdom (UK)', 'gravitytools' ),
'US' => __( 'United States (US)', 'gravitytools' ),
'UM' => __( 'United States (US) Minor Outlying Islands', 'gravitytools' ),
'UY' => __( 'Uruguay', 'gravitytools' ),
'UZ' => __( 'Uzbekistan', 'gravitytools' ),
'VU' => __( 'Vanuatu', 'gravitytools' ),
'VA' => __( 'Vatican', 'gravitytools' ),
'VE' => __( 'Venezuela', 'gravitytools' ),
'VN' => __( 'Vietnam', 'gravitytools' ),
'VG' => __( 'Virgin Islands (British)', 'gravitytools' ),
'VI' => __( 'Virgin Islands (US)', 'gravitytools' ),
'WF' => __( 'Wallis and Futuna', 'gravitytools' ),
'EH' => __( 'Western Sahara', 'gravitytools' ),
'WS' => __( 'Samoa', 'gravitytools' ),
'YE' => __( 'Yemen', 'gravitytools' ),
'ZM' => __( 'Zambia', 'gravitytools' ),
'ZW' => __( 'Zimbabwe', 'gravitytools' ),
);
}
/**
* Provides a list of US states organized by their two-character state code.
*
* @return array
*/
private static function states_list() {
return array(
'AL' => __( 'Alabama', 'gravitytools' ),
'AK' => __( 'Alaska', 'gravitytools' ),
'AZ' => __( 'Arizona', 'gravitytools' ),
'AR' => __( 'Arkansas', 'gravitytools' ),
'CA' => __( 'California', 'gravitytools' ),
'CO' => __( 'Colorado', 'gravitytools' ),
'CT' => __( 'Connecticut', 'gravitytools' ),
'DE' => __( 'Delaware', 'gravitytools' ),
'DC' => __( 'District of Columbia', 'gravitytools' ),
'FL' => __( 'Florida', 'gravitytools' ),
'GA' => __( 'Georgia', 'gravitytools' ),
'HI' => __( 'Hawaii', 'gravitytools' ),
'ID' => __( 'Idaho', 'gravitytools' ),
'IL' => __( 'Illinois', 'gravitytools' ),
'IN' => __( 'Indiana', 'gravitytools' ),
'IA' => __( 'Iowa', 'gravitytools' ),
'KS' => __( 'Kansas', 'gravitytools' ),
'KY' => __( 'Kentucky', 'gravitytools' ),
'LA' => __( 'Louisiana', 'gravitytools' ),
'ME' => __( 'Maine', 'gravitytools' ),
'MD' => __( 'Maryland', 'gravitytools' ),
'MA' => __( 'Massachusetts', 'gravitytools' ),
'MI' => __( 'Michigan', 'gravitytools' ),
'MN' => __( 'Minnesota', 'gravitytools' ),
'MS' => __( 'Mississippi', 'gravitytools' ),
'MO' => __( 'Missouri', 'gravitytools' ),
'MT' => __( 'Montana', 'gravitytools' ),
'NE' => __( 'Nebraska', 'gravitytools' ),
'NV' => __( 'Nevada', 'gravitytools' ),
'NH' => __( 'New Hampshire', 'gravitytools' ),
'NJ' => __( 'New Jersey', 'gravitytools' ),
'NM' => __( 'New Mexico', 'gravitytools' ),
'NY' => __( 'New York', 'gravitytools' ),
'NC' => __( 'North Carolina', 'gravitytools' ),
'ND' => __( 'North Dakota', 'gravitytools' ),
'OH' => __( 'Ohio', 'gravitytools' ),
'OK' => __( 'Oklahoma', 'gravitytools' ),
'OR' => __( 'Oregon', 'gravitytools' ),
'PA' => __( 'Pennsylvania', 'gravitytools' ),
'RI' => __( 'Rhode Island', 'gravitytools' ),
'SC' => __( 'South Carolina', 'gravitytools' ),
'SD' => __( 'South Dakota', 'gravitytools' ),
'TN' => __( 'Tennessee', 'gravitytools' ),
'TX' => __( 'Texas', 'gravitytools' ),
'UT' => __( 'Utah', 'gravitytools' ),
'VT' => __( 'Vermont', 'gravitytools' ),
'VA' => __( 'Virginia', 'gravitytools' ),
'WA' => __( 'Washington', 'gravitytools' ),
'WV' => __( 'West Virginia', 'gravitytools' ),
'WI' => __( 'Wisconsin', 'gravitytools' ),
'WY' => __( 'Wyoming', 'gravitytools' ),
);
}
/**
* Provides a list of Canadian provinces, organized by their two-character province code.
*
* @return array
*/
private static function provinces_list() {
return array(
'AB' => __( 'Alberta', 'gravitytools' ),
'BC' => __( 'British Columbia', 'gravitytools' ),
'MB' => __( 'Manitoba', 'gravitytools' ),
'NB' => __( 'New Brunswick', 'gravitytools' ),
'NL' => __( 'Newfoundland and Labrador', 'gravitytools' ),
'NS' => __( 'Nova Scotia', 'gravitytools' ),
'NT' => __( 'Northwest Territories', 'gravitytools' ),
'NU' => __( 'Nunavut', 'gravitytools' ),
'ON' => __( 'Ontario', 'gravitytools' ),
'PE' => __( 'Prince Edward Island', 'gravitytools' ),
'QC' => __( 'Quebec', 'gravitytools' ),
'SK' => __( 'Saskatchewan', 'gravitytools' ),
'YT' => __( 'Yukon', 'gravitytools' ),
);
}
/**
* Provides a list of phone number formatting info.
*
* @return array
*/
private static function phone_list() {
$countries = self::countries_list();
$data = array(
array( 'AF', '93', '🇦🇫' ),
array( 'AX', '358', '🇦🇽' ),
array( 'AL', '355', '🇦🇱' ),
array( 'DZ', '213', '🇩🇿' ),
array( 'AS', '1', '🇦🇸' ),
array( 'AD', '376', '🇦🇩' ),
array( 'AO', '244', '🇦🇴' ),
array( 'AI', '1', '🇦🇮' ),
array( 'AG', '1', '🇦🇬' ),
array( 'AR', '54', '🇦🇷' ),
array( 'AM', '374', '🇦🇲' ),
array( 'AW', '297', '🇦🇼' ),
array( 'AC', '247', '🇦🇨' ),
array( 'AU', '61', '🇦🇺' ),
array( 'AT', '43', '🇦🇹' ),
array( 'AZ', '994', '🇦🇿' ),
array( 'BS', '1', '🇧🇸' ),
array( 'BH', '973', '🇧🇭' ),
array( 'BD', '880', '🇧🇩' ),
array( 'BB', '1', '🇧🇧' ),
array( 'BY', '375', '🇧🇾' ),
array( 'BE', '32', '🇧🇪' ),
array( 'BZ', '501', '🇧🇿' ),
array( 'BJ', '229', '🇧🇯' ),
array( 'BM', '1', '🇧🇲' ),
array( 'BT', '975', '🇧🇹' ),
array( 'BO', '591', '🇧🇴' ),
array( 'BA', '387', '🇧🇦' ),
array( 'BW', '267', '🇧🇼' ),
array( 'BR', '55', '🇧🇷' ),
array( 'IO', '246', '🇮🇴' ),
array( 'VG', '1', '🇻🇬' ),
array( 'BN', '673', '🇧🇳' ),
array( 'BG', '359', '🇧🇬' ),
array( 'BF', '226', '🇧🇫' ),
array( 'BI', '257', '🇧🇮' ),
array( 'KH', '855', '🇰🇭' ),
array( 'CM', '237', '🇨🇲' ),
array( 'CA', '1', '🇨🇦' ),
array( 'CV', '238', '🇨🇻' ),
array( 'BQ', '599', '🇧🇶' ),
array( 'KY', '1', '🇰🇾' ),
array( 'CF', '236', '🇨🇫' ),
array( 'TD', '235', '🇹🇩' ),
array( 'CL', '56', '🇨🇱' ),
array( 'CN', '86', '🇨🇳' ),
array( 'CX', '61', '🇨🇽' ),
array( 'CC', '61', '🇨🇨' ),
array( 'CO', '57', '🇨🇴' ),
array( 'KM', '269', '🇰🇲' ),
array( 'CG', '242', '🇨🇬' ),
array( 'CD', '243', '🇨🇩' ),
array( 'CK', '682', '🇨🇰' ),
array( 'CR', '506', '🇨🇷' ),
array( 'CI', '225', '🇨🇮' ),
array( 'HR', '385', '🇭🇷' ),
array( 'CU', '53', '🇨🇺' ),
array( 'CW', '599', '🇨🇼' ),
array( 'CY', '357', '🇨🇾' ),
array( 'CZ', '420', '🇨🇿' ),
array( 'DK', '45', '🇩🇰' ),
array( 'DJ', '253', '🇩🇯' ),
array( 'DM', '1', '🇩🇲' ),
array( 'DO', '1', '🇩🇴' ),
array( 'EC', '593', '🇪🇨' ),
array( 'EG', '20', '🇪🇬' ),
array( 'SV', '503', '🇸🇻' ),
array( 'GQ', '240', '🇬🇶' ),
array( 'ER', '291', '🇪🇷' ),
array( 'EE', '372', '🇪🇪' ),
array( 'SZ', '268', '🇸🇿' ),
array( 'ET', '251', '🇪🇹' ),
array( 'FK', '500', '🇫🇰' ),
array( 'FO', '298', '🇫🇴' ),
array( 'FJ', '679', '🇫🇯' ),
array( 'FI', '358', '🇫🇮' ),
array( 'FR', '33', '🇫🇷' ),
array( 'GF', '594', '🇬🇫' ),
array( 'PF', '689', '🇵🇫' ),
array( 'GA', '241', '🇬🇦' ),
array( 'GM', '220', '🇬🇲' ),
array( 'GE', '995', '🇬🇪' ),
array( 'DE', '49', '🇩🇪' ),
array( 'GH', '233', '🇬🇭' ),
array( 'GI', '350', '🇬🇮' ),
array( 'GR', '30', '🇬🇷' ),
array( 'GL', '299', '🇬🇱' ),
array( 'GD', '1', '🇬🇩' ),
array( 'GP', '590', '🇬🇵' ),
array( 'GU', '1', '🇬🇺' ),
array( 'GT', '502', '🇬🇹' ),
array( 'GG', '44', '🇬🇬' ),
array( 'GN', '224', '🇬🇳' ),
array( 'GW', '245', '🇬🇼' ),
array( 'GY', '592', '🇬🇾' ),
array( 'HT', '509', '🇭🇹' ),
array( 'HN', '504', '🇭🇳' ),
array( 'HK', '852', '🇭🇰' ),
array( 'HU', '36', '🇭🇺' ),
array( 'IS', '354', '🇮🇸' ),
array( 'IN', '91', '🇮🇳' ),
array( 'ID', '62', '🇮🇩' ),
array( 'IR', '98', '🇮🇷' ),
array( 'IQ', '964', '🇮🇶' ),
array( 'IE', '353', '🇮🇪' ),
array( 'IM', '44', '🇮🇲' ),
array( 'IL', '972', '🇮🇱' ),
array( 'IT', '39', '🇮🇹' ),
array( 'JM', '1', '🇯🇲' ),
array( 'JP', '81', '🇯🇵' ),
array( 'JE', '44', '🇯🇪' ),
array( 'JO', '962', '🇯🇴' ),
array( 'KZ', '7', '🇰🇿' ),
array( 'KE', '254', '🇰🇪' ),
array( 'KI', '686', '🇰🇮' ),
array( 'XK', '383', '🇽🇰' ),
array( 'KW', '965', '🇰🇼' ),
array( 'KG', '996', '🇰🇬' ),
array( 'LA', '856', '🇱🇦' ),
array( 'LV', '371', '🇱🇻' ),
array( 'LB', '961', '🇱🇧' ),
array( 'LS', '266', '🇱🇸' ),
array( 'LR', '231', '🇱🇷' ),
array( 'LY', '218', '🇱🇾' ),
array( 'LI', '423', '🇱🇮' ),
array( 'LT', '370', '🇱🇹' ),
array( 'LU', '352', '🇱🇺' ),
array( 'MO', '853', '🇲🇴' ),
array( 'MG', '261', '🇲🇬' ),
array( 'MW', '265', '🇲🇼' ),
array( 'MY', '60', '🇲🇾' ),
array( 'MV', '960', '🇲🇻' ),
array( 'ML', '223', '🇲🇱' ),
array( 'MT', '356', '🇲🇹' ),
array( 'MH', '692', '🇲🇭' ),
array( 'MQ', '596', '🇲🇶' ),
array( 'MR', '222', '🇲🇷' ),
array( 'MU', '230', '🇲🇺' ),
array( 'YT', '262', '🇾🇹' ),
array( 'MX', '52', '🇲🇽' ),
array( 'FM', '691', '🇫🇲' ),
array( 'MD', '373', '🇲🇩' ),
array( 'MC', '377', '🇲🇨' ),
array( 'MN', '976', '🇲🇳' ),
array( 'ME', '382', '🇲🇪' ),
array( 'MS', '1', '🇲🇸' ),
array( 'MA', '212', '🇲🇦' ),
array( 'MZ', '258', '🇲🇿' ),
array( 'MM', '95', '🇲🇲' ),
array( 'NA', '264', '🇳🇦' ),
array( 'NR', '674', '🇳🇷' ),
array( 'NP', '977', '🇳🇵' ),
array( 'NL', '31', '🇳🇱' ),
array( 'NC', '687', '🇳🇨' ),
array( 'NZ', '64', '🇳🇿' ),
array( 'NI', '505', '🇳🇮' ),
array( 'NE', '227', '🇳🇪' ),
array( 'NG', '234', '🇳🇬' ),
array( 'NU', '683', '🇳🇺' ),
array( 'NF', '672', '🇳🇫' ),
array( 'KP', '850', '🇰🇵' ),
array( 'MK', '389', '🇲🇰' ),
array( 'MP', '1', '🇲🇵' ),
array( 'NO', '47', '🇳🇴' ),
array( 'OM', '968', '🇴🇲' ),
array( 'PK', '92', '🇵🇰' ),
array( 'PW', '680', '🇵🇼' ),
array( 'PS', '970', '🇵🇸' ),
array( 'PA', '507', '🇵🇦' ),
array( 'PG', '675', '🇵🇬' ),
array( 'PY', '595', '🇵🇾' ),
array( 'PE', '51', '🇵🇪' ),
array( 'PH', '63', '🇵🇭' ),
array( 'PL', '48', '🇵🇱' ),
array( 'PT', '351', '🇵🇹' ),
array( 'PR', '1', '🇵🇷' ),
array( 'QA', '974', '🇶🇦' ),
array( 'RE', '262', '🇷🇪' ),
array( 'RO', '40', '🇷🇴' ),
array( 'RU', '7', '🇷🇺' ),
array( 'RW', '250', '🇷🇼' ),
array( 'WS', '685', '🇼🇸' ),
array( 'SM', '378', '🇸🇲' ),
array( 'ST', '239', '🇸🇹' ),
array( 'SA', '966', '🇸🇦' ),
array( 'SN', '221', '🇸🇳' ),
array( 'RS', '381', '🇷🇸' ),
array( 'SC', '248', '🇸🇨' ),
array( 'SL', '232', '🇸🇱' ),
array( 'SG', '65', '🇸🇬' ),
array( 'SX', '1', '🇸🇽' ),
array( 'SK', '421', '🇸🇰' ),
array( 'SI', '386', '🇸🇮' ),
array( 'SB', '677', '🇸🇧' ),
array( 'SO', '252', '🇸🇴' ),
array( 'ZA', '27', '🇿🇦' ),
array( 'KR', '82', '🇰🇷' ),
array( 'SS', '211', '🇸🇸' ),
array( 'ES', '34', '🇪🇸' ),
array( 'LK', '94', '🇱🇰' ),
array( 'BL', '590', '🇧🇱' ),
array( 'SH', '290', '🇸🇭' ),
array( 'KN', '1', '🇰🇳' ),
array( 'LC', '1', '🇱🇨' ),
array( 'MF', '590', '🇲🇫' ),
array( 'PM', '508', '🇵🇲' ),
array( 'VC', '1', '🇻🇨' ),
array( 'SD', '249', '🇸🇩' ),
array( 'SR', '597', '🇸🇷' ),
array( 'SJ', '47', '🇸🇯' ),
array( 'SE', '46', '🇸🇪' ),
array( 'CH', '41', '🇨🇭' ),
array( 'SY', '963', '🇸🇾' ),
array( 'TW', '886', '🇹🇼' ),
array( 'TJ', '992', '🇹🇯' ),
array( 'TZ', '255', '🇹🇿' ),
array( 'TH', '66', '🇹🇭' ),
array( 'TL', '670', '🇹🇱' ),
array( 'TG', '228', '🇹🇬' ),
array( 'TK', '690', '🇹🇰' ),
array( 'TO', '676', '🇹🇴' ),
array( 'TT', '1', '🇹🇹' ),
array( 'TN', '216', '🇹🇳' ),
array( 'TR', '90', '🇹🇷' ),
array( 'TM', '993', '🇹🇲' ),
array( 'TC', '1', '🇹🇨' ),
array( 'TV', '688', '🇹🇻' ),
array( 'UG', '256', '🇺🇬' ),
array( 'UA', '380', '🇺🇦' ),
array( 'AE', '971', '🇦🇪' ),
array( 'GB', '44', '🇬🇧' ),
array( 'US', '1', '🇺🇸' ),
array( 'UY', '598', '🇺🇾' ),
array( 'VI', '1', '🇻🇮' ),
array( 'UZ', '998', '🇺🇿' ),
array( 'VU', '678', '🇻🇺' ),
array( 'VA', '39', '🇻🇦' ),
array( 'VE', '58', '🇻🇪' ),
array( 'VN', '84', '🇻🇳' ),
array( 'WF', '681', '🇼🇫' ),
array( 'EH', '212', '🇪🇭' ),
array( 'YE', '967', '🇾🇪' ),
array( 'ZM', '260', '🇿🇲' ),
array( 'ZW', '263', '🇿🇼' ),
);
return array_map( function ( $item ) use ( $countries ) {
return array(
'iso' => $item[0],
'name' => $countries[ $item[0] ],
'calling_code' => $item[1],
'flag' => $item[2],
);
}, $data );
}
/**
* Retrieves the given list of data by type. Helper method used to route the individual type requests
* through to the appropriate data list method.
*
* @param $type - string - The type of data to retrieve.
* @param $as_json - boolean - Whether to retrieve this data as as JSON string.
* @param $process_callback - callable - An optional callback for transforming the data before returning.
*
* @return string|array
*/
private static function get_data_by_type( $type, $as_json = false, $process_callback = null ) {
switch ( $type ) {
case 'country':
$data = self::countries_list();
break;
case 'state':
$data = self::states_list();
break;
case 'province':
$data = self::provinces_list();
break;
case 'phone':
$data = self::phone_list();
break;
default:
$data = array();
break;
}
if ( ! is_null( $process_callback ) ) {
$data = call_user_func( $process_callback, $data );
}
return $as_json ? json_encode( $data ) : $data;
}
/**
* Provides an array of US States.
*
* @param $as_json - boolean - Whether to retrieve this data as a JSON string.
* @param $process_callback - callable - An optional callback for transforming the data before returning.
*
* @return string|array
*/
public static function states( $as_json = false, $process_callback = null ) {
return self::get_data_by_type( 'state', $as_json, $process_callback );
}
/**
* Provides an array of Canadian Provinces.
*
* @param $as_json - boolean - Whether to retrieve this data as a JSON string.
* @param $process_callback - callable - An optional callback for transforming the data before returning.
*
* @return string|array
*/
public static function provinces( $as_json = false, $process_callback = null ) {
return self::get_data_by_type( 'province', $as_json, $process_callback );
}
/**
* Provides an array of Countries.
*
* @param $as_json - boolean - Whether to retrieve this data as a JSON string.
* @param $process_callback - callable - An optional callback for transforming the data before returning.
*
* @return string|array
*/
public static function countries( $as_json = false, $process_callback = null ) {
return self::get_data_by_type( 'country', $as_json, $process_callback );
}
/**
* Provides an array of phone format information.
*
* @param $as_json - boolean - Whether to retrieve this data as a JSON string.
* @param $process_callback - callable - An optional callback for transforming the data before returning.
*
* @return string|array
*/
public static function phone_info( $as_json = false, $process_callback = null ) {
return self::get_data_by_type( 'phone', $as_json, $process_callback );
}
}
@@ -0,0 +1,659 @@
<?php
namespace Gravity_Forms\Gravity_Tools\Utils;
use InvalidArgumentException;
class Moola {
protected $amount;
protected $currency_code;
protected $currency_data = array(
'ALL' => array(
'decimals' => 2,
'name' => 'Albania Lek',
'symbol' => 'Lek',
),
'AFN' => array(
'decimals' => 2,
'name' => 'Afghanistan Afghani',
'symbol' => '؋',
),
'ARS' => array(
'decimals' => 2,
'name' => 'Argentina Peso',
'symbol' => '$',
),
'AWG' => array(
'decimals' => 2,
'name' => 'Aruba Guilder',
'symbol' => 'ƒ',
),
'AUD' => array(
'decimals' => 2,
'name' => 'Australia Dollar',
'symbol' => '$',
),
'AZN' => array(
'decimals' => 2,
'name' => 'Azerbaijan Manat',
'symbol' => '₼',
),
'BSD' => array(
'decimals' => 2,
'name' => 'Bahamas Dollar',
'symbol' => '$',
),
'BBD' => array(
'decimals' => 2,
'name' => 'Barbados Dollar',
'symbol' => '$',
),
'BYN' => array(
'decimals' => 2,
'name' => 'Belarus Ruble',
'symbol' => 'Br',
),
'BZD' => array(
'decimals' => 2,
'name' => 'Belize Dollar',
'symbol' => 'BZ$',
),
'BMD' => array(
'decimals' => 2,
'name' => 'Bermuda Dollar',
'symbol' => '$',
),
'BOB' => array(
'decimals' => 2,
'name' => 'Bolivia Bolíviano',
'symbol' => '$b',
),
'BAM' => array(
'decimals' => 2,
'name' => 'Bosnia and Herzegovina Convertible Mark',
'symbol' => 'KM',
),
'BWP' => array(
'decimals' => 2,
'name' => 'Botswana Pula',
'symbol' => 'P',
),
'BGN' => array(
'decimals' => 2,
'name' => 'Bulgaria Lev',
'symbol' => 'лв',
),
'BRL' => array(
'decimals' => 2,
'name' => 'Brazil Real',
'symbol' => 'R$',
),
'BND' => array(
'decimals' => 2,
'name' => 'Brunei Darussalam Dollar',
'symbol' => '$',
),
'KHR' => array(
'decimals' => 2,
'name' => 'Cambodia Riel',
'symbol' => '៛',
),
'CAD' => array(
'decimals' => 2,
'name' => 'Canada Dollar',
'symbol' => '$',
),
'KYD' => array(
'decimals' => 2,
'name' => 'Cayman Islands Dollar',
'symbol' => '$',
),
'CLP' => array(
'decimals' => 2,
'name' => 'Chile Peso',
'symbol' => '$',
),
'CNY' => array(
'decimals' => 2,
'name' => 'China Yuan Renminbi',
'symbol' => '¥',
),
'COP' => array(
'decimals' => 2,
'name' => 'Colombia Peso',
'symbol' => '$',
),
'CRC' => array(
'decimals' => 2,
'name' => 'Costa Rica Colon',
'symbol' => '₡',
),
'HRK' => array(
'decimals' => 2,
'name' => 'Croatia Kuna',
'symbol' => 'kn',
),
'CUP' => array(
'decimals' => 2,
'name' => 'Cuba Peso',
'symbol' => '₱',
),
'CZK' => array(
'decimals' => 2,
'name' => 'Czech Republic Koruna',
'symbol' => 'Kč',
),
'DKK' => array(
'decimals' => 2,
'name' => 'Denmark Krone',
'symbol' => 'kr',
),
'DOP' => array(
'decimals' => 2,
'name' => 'Dominican Republic Peso',
'symbol' => 'RD$',
),
'XCD' => array(
'decimals' => 2,
'name' => 'East Caribbean Dollar',
'symbol' => '$',
),
'EGP' => array(
'decimals' => 2,
'name' => 'Egypt Pound',
'symbol' => '£',
),
'SVC' => array(
'decimals' => 2,
'name' => 'El Salvador Colon',
'symbol' => '$',
),
'EUR' => array(
'decimals' => 2,
'name' => 'Euro Member Countries',
'symbol' => '€',
),
'FKP' => array(
'decimals' => 2,
'name' => 'Falkland Islands (Malvinas) Pound',
'symbol' => '£',
),
'FJD' => array(
'decimals' => 2,
'name' => 'Fiji Dollar',
'symbol' => '$',
),
'GHS' => array(
'decimals' => 2,
'name' => 'Ghana Cedi',
'symbol' => '¢',
),
'GIP' => array(
'decimals' => 2,
'name' => 'Gibraltar Pound',
'symbol' => '£',
),
'GTQ' => array(
'decimals' => 2,
'name' => 'Guatemala Quetzal',
'symbol' => 'Q',
),
'GGP' => array(
'decimals' => 2,
'name' => 'Guernsey Pound',
'symbol' => '£',
),
'GYD' => array(
'decimals' => 2,
'name' => 'Guyana Dollar',
'symbol' => '$',
),
'HNL' => array(
'decimals' => 2,
'name' => 'Honduras Lempira',
'symbol' => 'L',
),
'HKD' => array(
'decimals' => 2,
'name' => 'Hong Kong Dollar',
'symbol' => '$',
),
'HUF' => array(
'decimals' => 2,
'name' => 'Hungary Forint',
'symbol' => 'Ft',
),
'ISK' => array(
'decimals' => 2,
'name' => 'Iceland Krona',
'symbol' => 'kr',
),
'INR' => array(
'decimals' => 2,
'name' => 'India Rupee',
'symbol' => '',
),
'IDR' => array(
'decimals' => 0,
'name' => 'Indonesia Rupiah',
'symbol' => 'Rp',
),
'IRR' => array(
'decimals' => 2,
'name' => 'Iran Rial',
'symbol' => '﷼',
),
'IMP' => array(
'decimals' => 2,
'name' => 'Isle of Man Pound',
'symbol' => '£',
),
'ILS' => array(
'decimals' => 2,
'name' => 'Israel Shekel',
'symbol' => '₪',
),
'JMD' => array(
'decimals' => 2,
'name' => 'Jamaica Dollar',
'symbol' => 'J$',
),
'JPY' => array(
'decimals' => 0,
'name' => 'Japan Yen',
'symbol' => '¥',
),
'JEP' => array(
'decimals' => 2,
'name' => 'Jersey Pound',
'symbol' => '£',
),
'KZT' => array(
'decimals' => 2,
'name' => 'Kazakhstan Tenge',
'symbol' => 'лв',
),
'KPW' => array(
'decimals' => 2,
'name' => 'Korea (North) Won',
'symbol' => '₩',
),
'KRW' => array(
'decimals' => 0,
'name' => 'Korea (South) Won',
'symbol' => '₩',
),
'KGS' => array(
'decimals' => 2,
'name' => 'Kyrgyzstan Som',
'symbol' => 'лв',
),
'LAK' => array(
'decimals' => 2,
'name' => 'Laos Kip',
'symbol' => '₭',
),
'LBP' => array(
'decimals' => 2,
'name' => 'Lebanon Pound',
'symbol' => '£',
),
'LRD' => array(
'decimals' => 2,
'name' => 'Liberia Dollar',
'symbol' => '$',
),
'MKD' => array(
'decimals' => 2,
'name' => 'Macedonia Denar',
'symbol' => 'ден',
),
'MYR' => array(
'decimals' => 2,
'name' => 'Malaysia Ringgit',
'symbol' => 'RM',
),
'MUR' => array(
'decimals' => 2,
'name' => 'Mauritius Rupee',
'symbol' => '₨',
),
'MXN' => array(
'decimals' => 2,
'name' => 'Mexico Peso',
'symbol' => '$',
),
'MNT' => array(
'decimals' => 2,
'name' => 'Mongolia Tughrik',
'symbol' => '₮',
),
'MZN' => array(
'decimals' => 2,
'name' => 'Mozambique Metical',
'symbol' => 'MT',
),
'NAD' => array(
'decimals' => 2,
'name' => 'Namibia Dollar',
'symbol' => '$',
),
'NPR' => array(
'decimals' => 2,
'name' => 'Nepal Rupee',
'symbol' => '₨',
),
'ANG' => array(
'decimals' => 2,
'name' => 'Netherlands Antilles Guilder',
'symbol' => 'ƒ',
),
'NZD' => array(
'decimals' => 2,
'name' => 'New Zealand Dollar',
'symbol' => '$',
),
'NIO' => array(
'decimals' => 2,
'name' => 'Nicaragua Cordoba',
'symbol' => 'C$',
),
'NGN' => array(
'decimals' => 2,
'name' => 'Nigeria Naira',
'symbol' => '₦',
),
'NOK' => array(
'decimals' => 2,
'name' => 'Norway Krone',
'symbol' => 'kr',
),
'OMR' => array(
'decimals' => 3,
'name' => 'Oman Rial',
'symbol' => '﷼',
),
'PKR' => array(
'decimals' => 2,
'name' => 'Pakistan Rupee',
'symbol' => '₨',
),
'PAB' => array(
'decimals' => 2,
'name' => 'Panama Balboa',
'symbol' => 'B/.',
),
'PYG' => array(
'decimals' => 0,
'name' => 'Paraguay Guarani',
'symbol' => 'Gs',
),
'PEN' => array(
'decimals' => 2,
'name' => 'Peru Sol',
'symbol' => 'S/.',
),
'PHP' => array(
'decimals' => 2,
'name' => 'Philippines Peso',
'symbol' => '₱',
),
'PLN' => array(
'decimals' => 2,
'name' => 'Poland Zloty',
'symbol' => 'zł',
),
'QAR' => array(
'decimals' => 2,
'name' => 'Qatar Riyal',
'symbol' => '﷼',
),
'RON' => array(
'decimals' => 2,
'name' => 'Romania Leu',
'symbol' => 'lei',
),
'RUB' => array(
'decimals' => 2,
'name' => 'Russia Ruble',
'symbol' => '₽',
),
'SHP' => array(
'decimals' => 2,
'name' => 'Saint Helena Pound',
'symbol' => '£',
),
'SAR' => array(
'decimals' => 2,
'name' => 'Saudi Arabia Riyal',
'symbol' => '﷼',
),
'RSD' => array(
'decimals' => 2,
'name' => 'Serbia Dinar',
'symbol' => 'Дин.',
),
'SCR' => array(
'decimals' => 2,
'name' => 'Seychelles Rupee',
'symbol' => '₨',
),
'SGD' => array(
'decimals' => 2,
'name' => 'Singapore Dollar',
'symbol' => '$',
),
'SBD' => array(
'decimals' => 2,
'name' => 'Solomon Islands Dollar',
'symbol' => '$',
),
'SOS' => array(
'decimals' => 2,
'name' => 'Somalia Shilling',
'symbol' => 'S',
),
'ZAR' => array(
'decimals' => 2,
'name' => 'South Africa Rand',
'symbol' => 'R',
),
'LKR' => array(
'decimals' => 2,
'name' => 'Sri Lanka Rupee',
'symbol' => '₨',
),
'SEK' => array(
'decimals' => 2,
'name' => 'Sweden Krona',
'symbol' => 'kr',
),
'CHF' => array(
'decimals' => 2,
'name' => 'Switzerland Franc',
'symbol' => 'CHF',
),
'SRD' => array(
'decimals' => 2,
'name' => 'Suriname Dollar',
'symbol' => '$',
),
'SYP' => array(
'decimals' => 2,
'name' => 'Syria Pound',
'symbol' => '£',
),
'TWD' => array(
'decimals' => 2,
'name' => 'Taiwan New Dollar',
'symbol' => 'NT$',
),
'THB' => array(
'decimals' => 2,
'name' => 'Thailand Baht',
'symbol' => '฿',
),
'TTD' => array(
'decimals' => 2,
'name' => 'Trinidad and Tobago Dollar',
'symbol' => 'TT$',
),
'TRY' => array(
'decimals' => 2,
'name' => 'Turkey Lira',
'symbol' => '',
),
'TVD' => array(
'decimals' => 2,
'name' => 'Tuvalu Dollar',
'symbol' => '$',
),
'UAH' => array(
'decimals' => 2,
'name' => 'Ukraine Hryvnia',
'symbol' => '₴',
),
'GBP' => array(
'decimals' => 2,
'name' => 'United Kingdom Pound',
'symbol' => '£',
),
'USD' => array(
'decimals' => 2,
'name' => 'United States Dollar',
'symbol' => '$',
),
'UYU' => array(
'decimals' => 2,
'name' => 'Uruguay Peso',
'symbol' => '$U',
),
'UZS' => array(
'decimals' => 2,
'name' => 'Uzbekistan Som',
'symbol' => 'лв',
),
'VEF' => array(
'decimals' => 2,
'name' => 'Venezuela Bolívar',
'symbol' => 'Bs',
),
'VND' => array(
'decimals' => 0,
'name' => 'Viet Nam Dong',
'symbol' => '₫',
),
'YER' => array(
'decimals' => 2,
'name' => 'Yemen Rial',
'symbol' => '﷼',
),
'ZWD' => array(
'decimals' => 2,
'name' => 'Zimbabwe Dollar',
'symbol' => 'Z$',
),
);
public function __construct( $amount, $currency_code, $is_raw = true ) {
if ( ! $is_raw ) {
$amount = $this->convert_display_amount_to_raw( $amount, $currency_code );
}
$this->amount = $amount;
$this->currency_code = $currency_code;
$this->get_currency_data( $currency_code );
}
public function raw_value() {
return $this->amount;
}
public function display_value( $precision = 0, $show_currency = false, $commas = false ) {
$currency_data = $this->get_currency_data( $this->currency_code );
$decimals = (int) $currency_data['decimals'];
$divider = 1;
for ( $i = 0; $i < $decimals; $i++ ) {
$divider *= 10;
}
$float_val = (float) ( $this->amount / $divider );
if ( ! $show_currency ) {
return round( $float_val, $precision );
}
$rounded = round( $float_val, $precision );
$formatted_val = $commas ? number_format( $rounded ) : $rounded;
return sprintf( '%s%s', $currency_data['symbol'], $formatted_val );
}
public function change_currency( $new_currency_code ) {
$new_data = $this->get_currency_data( $new_currency_code );
$current_data = $this->get_currency_data( $this->currency_code );
$this->currency_code = $new_currency_code;
if ( $new_data['decimals'] === $current_data['decimals'] ) {
return;
}
$modifier = 1;
$start = min( $new_data['decimals'], $current_data['decimals'] );
$end = max( $new_data['decimals'], $current_data['decimals'] );
for ( $i = $start; $i < $end; $i++ ) {
$modifier *= 10;
}
if ( $new_data['decimals'] > $current_data['decimals'] ) {
$this->amount = $this->amount * $modifier;
}
if ( $new_data['decimals'] < $current_data['decimals'] ) {
$this->amount = $this->amount / $modifier;
}
}
public function convert_display_amount_to_raw( $display_value, $currency_code ) {
$currency_data = $this->get_currency_data( $currency_code );
$sanitized = $this->sanitize_display_value( $display_value );
$modifier = 1;
for( $i = 0; $i < $currency_data['decimals']; $i++ ) {
$modifier *= 10;
}
return $sanitized * $modifier;
}
private function sanitize_display_value( $display_value ) {
$stripped = preg_replace( "/[^0-9.]/", "", $display_value );
if ( $stripped === '' ) {
$stripped = 0;
}
if ( ! is_numeric( $stripped ) ) {
throw new InvalidArgumentException( 'Invalid display value provided for sanitization.' );
}
return floatval( $stripped );
}
private function get_currency_data( $currency_code ) {
if ( ! array_key_exists( $currency_code, $this->currency_data ) ) {
throw new InvalidArgumentException( 'Invalid currency code provided.' );
}
return $this->currency_data[ $currency_code ];
}
}
@@ -0,0 +1,163 @@
<?php
namespace Gravity_Forms\Gravity_Tools;
use Gravity_Forms\Gravity_Tools\Config;
/**
* Collection to hold Config items and provide their structured data when needed.
*
* @package Gravity_Forms\Gravity_Tools
*/
class Config_Collection {
/**
* @var Config[] $configs
*/
private $configs = array();
/**
* Add a config to the collection.
*
* @param Config $config
*/
public function add_config( Config $config ) {
$this->configs[] = $config;
}
/**
* Handle outputting the config data.
*
* If $localize is true, data is actually localized via `wp_localize_script`, otherwise
* data is simply returned as an array.
*
* @since 1.0
*
* @param bool $localize Whether to localize the data, or simply return it.
*
* @return array
*/
public function handle( $localize = true ) {
$scripts = $this->get_configs_by_script();
$data_to_localize = array();
foreach ( $scripts as $script => $items ) {
$item_data = $this->localize_data_for_script( $script, $items, $localize );
$data_to_localize = array_merge( $data_to_localize, $item_data );
}
return $data_to_localize;
}
/**
* Localize the data for the given script.
*
* @since 1.0
*
* @param string $script
* @param Config[] $items
*/
private function localize_data_for_script( $script, $items, $localize = true ) {
$data = array();
foreach ( $items as $name => $configs ) {
$localized_data = $this->get_merged_data_for_object( $configs );
/**
* Allows users to filter the data localized for a given script/resource.
*
* @since 1.0
*
* @param array $localized_data The current localize data
* @param string $script The script being localized
* @param array $configs An array of $configs being applied to this script
*
* @return array
*/
$localized_data = apply_filters( 'gform_localized_script_data_' . $name, $localized_data, $script, $configs );
$data[ $name ] = $localized_data;
if ( $localize ) {
wp_localize_script( $script, $name, $localized_data );
}
}
return $data;
}
/**
* Get the merged data object for the applicable configs. Will process each config by its
* $priority property, overriding or merging values as needed.
*
* @since 1.0
*
* @param Config[] $configs
*/
private function get_merged_data_for_object( $configs ) {
// Squash warnings for PHP < 7.0 when running tests.
@usort( $configs, array( $this, 'sort_by_priority' ) );
$data = array();
foreach ( $configs as $config ) {
// Config is set to overwrite data - simply return its value without attempting to merge.
if ( $config->should_overwrite() ) {
$data = $config->get_data();
continue;
}
// Config should be merged - loop through each key and attempt to recursively merge the values.
foreach ( $config->get_data() as $key => $value ) {
$existing = isset( $data[ $key ] ) ? $data[ $key ] : null;
if ( is_null( $existing ) || ! is_array( $existing ) || ! is_array( $value ) ) {
$data[ $key ] = $value;
continue;
}
$data[ $key ] = array_merge_recursive( $existing, $value );
}
}
return $data;
}
/**
* Get the appropriate configs, organized by the script they belong to.
*
* @since 1.0
*
* @return array
*/
private function get_configs_by_script() {
$data_to_localize = array();
foreach ( $this->configs as $config ) {
if ( ( ! defined( 'GFORMS_DOING_MOCK' ) || ! GFORMS_DOING_MOCK ) && ! $config->should_enqueue() ) {
continue;
}
$data_to_localize[ $config->script_to_localize() ][ $config->name() ][] = $config;
}
return $data_to_localize;
}
/**
* usort() callback to sort the configs by their $priority.
*
* @param Config $a
* @param Config $b
*
* @return int
*/
public function sort_by_priority( Config $a, Config $b ) {
if ( $a->priority() === $b->priority() ) {
return 0;
}
return $a->priority() < $b->priority() ? - 1 : 1;
}
}
@@ -0,0 +1,75 @@
<?php
namespace Gravity_Forms\Gravity_Tools;
/**
* Parses a given data array to return either Live or Mock values, depending on the
* environment and context.
*
* @package Gravity_Forms\Gravity_Tools
*/
class Config_Data_Parser {
/**
* Parse the given $data array and get the correct values for the context.
*
* @since 1.0
*
* @param $data
*
* @return array
*/
public function parse( $data ) {
$return = array();
foreach( $data as $key => $value ) {
$return[ $key ] = $this->get_correct_value( $value );
}
return $return;
}
/**
* Loop through each array key and get the correct value. Is called recursively for
* nested arrays.
*
* @since 1.0
*
* @param mixed $value
*
* @return array|mixed
*/
private function get_correct_value( $value ) {
// Value isn't array - we've reached the final level for this branch.
if ( ! is_array( $value ) ) {
return $value;
}
// Value is an array with our defined value and default keys. Return either live or mock data.
if ( array_key_exists( 'default', $value ) && array_key_exists( 'value', $value ) ) {
return $this->is_mock() ? $value['default'] : $value['value'];
}
$data = array();
// Value is an array - recursively call this method to dig into each level and return the correct value.
foreach( $value as $key => $value ) {
$data[ $key ] = $this->get_correct_value( $value );
}
return $data;
}
/**
* Determine whether the current environmental context is a Mock context.
*
* @since 1.0
*
* @return bool
*/
private function is_mock() {
return defined( 'GFORMS_DOING_MOCK' ) && GFORMS_DOING_MOCK;
}
}
@@ -0,0 +1,190 @@
<?php
namespace Gravity_Forms\Gravity_Tools;
use Gravity_Forms\Gravity_Tools\Config_Data_Parser;
/**
* Base class for providing advanced functionality when localizing Config Data
* for usage in Javascript.
*
* @package Gravity_Forms\Gravity_Tools
*/
abstract class Config {
/**
* The Data Parser
*
* @since 1.0
*
* @var Config_Data_Parser
*/
protected $parser;
/**
* The data for this config object.
*
* @since 1.0
*
* @var array
*/
protected $data;
/**
* The object name for this config.
*
* @since 1.0
*
* @var string
*/
protected $name;
/**
* The ID of the script to localize the data to.
*
* @since 1.0
*
* @var string
*/
protected $script_to_localize;
/**
* The priority of this config - can be used to control the order in
* which configs are processed in the Collection.
*
* @since 1.0
*
* @var int
*/
protected $priority = 0;
/**
* Whether the config should enqueue it's data. Can also be handled by overriding the
* ::should_enqueue() method.
*
* @since 1.0
*
* @var bool
*/
protected $should_enqueue = true;
/**
* Whether this config should overwrite previous values in the object.
*
* If set to "true", the object will be overwritten by the values provided here.
* If set to "false", the object will have its values merged with those defined here, recursively.
*
* @since 1.0
*
* @var bool
*/
protected $overwrite = false;
/**
* Constructor
*
* @param Config_Data_Parser $parser
*/
public function __construct( Config_Data_Parser $parser ) {
$this->parser = $parser;
}
/**
* Method to handle defining the data array for this config.
*
* @since 1.0
*
* @return array
*/
abstract protected function data();
/**
* Determine if the config should enqueue its data. If should_enqueue() is a method,
* call it and return the result. If not, simply return the (boolean) value of the property.
*
* @since 1.0
*
* @return bool
*/
public function should_enqueue() {
if ( is_callable( $this->should_enqueue ) ) {
return call_user_func( $this->should_enqueue );
}
return $this->should_enqueue;
}
/**
* Get the data for the config, passing it through a filter.
*
* @since 1.0
*
* @return array
*/
public function get_data() {
if ( ( ! defined( 'GFORMS_DOING_MOCK' ) || ! GFORMS_DOING_MOCK ) && ! $this->should_enqueue() ) {
return false;
}
/**
* Allows developers to modify the raw config data being sent to the Config Parser. Useful for
* adding in custom default/mock values for a given entry in the data, as well as modifying
* things like callbacks for dynamic data before it's parsed and localized.
*
* @since 1.0
*
* @param array $data
* @param string $script_to_localize
*
* @return array
*/
$data = apply_filters( 'gform_config_data_' . $this->name(), $this->data(), $this->script_to_localize() );
return $this->parser->parse( $data );
}
/**
* Get the name of the config's object.
*
* @since 1.0
*
* @return string
*/
public function name() {
return $this->name;
}
/**
* Get the $priority for the config.
*
* @since 1.0
*
* @return int
*/
public function priority() {
return $this->priority;
}
/**
* Get the script to localize.
*
* @since 1.0
*
* @return string
*/
public function script_to_localize() {
return $this->script_to_localize;
}
/**
* Get whether the config should override previous values.
*
* @since 1.0
*
* @return bool
*/
public function should_overwrite() {
return $this->overwrite;
}
}
@@ -0,0 +1,94 @@
<?php
namespace Gravity_Forms\Gravity_Tools;
use Gravity_Forms\Gravity_Tools\Service_Provider;
/**
* Class Service_Container
*
* A simple Service Container used to collect and organize Services used by the application and its modules.
*
* @package Gravity_Forms\Gravity_Tools
*/
class Service_Container {
private $services = array();
private $providers = array();
/**
* Add a service to the container.
*
* @since 1.0
*
* @param string $name The service Name
* @param mixed $service The service to add
*/
public function add( $name, $service, $defer = false ) {
if ( empty( $name ) ) {
$name = get_class( $service );
}
if ( ! $defer && is_callable( $service ) ) {
$service = $service();
}
$this->services[ $name ] = $service;
}
/**
* Remove a service from the container.
*
* @since 1.0
*
* @param string $name The service name.
*/
public function remove( $name ) {
unset( $this->services[ $name ] );
}
/**
* Get a service from the container by name.
*
* @since 1.0
*
* @param string $name The service name.
*
* @return mixed|null
*/
public function get( $name ) {
if ( ! isset( $this->services[ $name ] ) ) {
return null;
}
if ( is_callable( $this->services[ $name ] ) ) {
$called = $this->services[ $name ]();
$this->services[ $name ] = $called;
}
return $this->services[ $name ];
}
/**
* Add a service provider to the container and register each of its services.
*
* @since 1.0
*
* @param Service_Provider $provider
*/
public function add_provider( Service_Provider $provider ) {
$provider_name = get_class( $provider );
// Only add providers a single time.
if ( isset( $this->providers[ $provider_name ] ) ) {
return;
}
$this->providers[ $provider_name ] = $provider;
$provider->set_container( $this );
$provider->register( $this );
$provider->init( $this );
}
}
@@ -0,0 +1,40 @@
<?php
namespace Gravity_Forms\Gravity_Tools;
use Gravity_Forms\Gravity_Tools\Service_Container;
/**
* Class Service_Provider
*
* An abstraction which provides a contract for defining Service Providers. Service Providers facilitate
* organizing Services into discreet modules, as opposed to having to register each service in a single location.
*
* @package Gravity_Forms\Gravity_Tools
*/
abstract class Service_Provider {
/**
* @var Service_Container $container
*/
protected $container;
public function set_container( Service_Container $container ) {
$this->container = $container;
}
/**
* Register new services to the Service Container.
*
* @param Service_Container $container
*
* @return void
*/
abstract public function register( Service_Container $container );
/**
* Noop by default - used to initialize hooks and filters for the given module.
*/
public function init( Service_Container $container ) {}
}