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,40 @@
Gravity Tools - PHP libraries to support Gravity Forms.
=======================================================
Install
-------
To install with composer:
```sh
composer require gravityforms/gravity-tools
```
Usage
-----
Basic usage example:
```php
<?php
require '/path/to/vendor/autoload.php';
$container = new Gravity_Forms\Gravity_Tools\Service_Container();
```
License
-------
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses.
@@ -0,0 +1,45 @@
#!/usr/bin/env php
<?php
function rsearch($folder, $regPattern) {
$dir = new RecursiveDirectoryIterator($folder);
$ite = new RecursiveIteratorIterator($dir);
$files = new RegexIterator($ite, $regPattern, RegexIterator::GET_MATCH);
$fileList = array();
foreach($files as $file) {
$fileList = array_merge($fileList, $file);
}
return $fileList;
}
$new_namespace = $argv[1];
// First, replace all lib file namespaces.
$vendor_files_to_mod = rsearch( './vendor/gravityforms/gravity-tools/src', '/.*php/' );
foreach( $vendor_files_to_mod as $path ) {
$contents = file_get_contents( $path );
if ( strpos( $contents, 'Gravity_Forms\Gravity_Tools' ) === false ) {
continue;
}
$replaced = str_replace( 'Gravity_Forms\Gravity_Tools', "Gravity_Forms\\$new_namespace\Gravity_Tools", $contents );
file_put_contents( $path, $replaced );
}
// Next, do the same to plugin files.
$files_to_mod = rsearch( './includes', '/.*php/' );
foreach( $files_to_mod as $path ) {
$contents = file_get_contents( $path );
if ( strpos( $contents, 'Gravity_Forms\Gravity_Tools' ) === false ) {
continue;
}
$replaced = str_replace( 'Gravity_Forms\Gravity_Tools', "Gravity_Forms\\$new_namespace\Gravity_Tools", $contents );
file_put_contents( $path, $replaced );
}
?>
@@ -0,0 +1,280 @@
<?php
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\Utils\Relationship;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection;
use tad\FunctionMocker\FunctionMocker;
require_once __DIR__ . '/vendor/autoload.php';
FunctionMocker::init();
/**
* Bootstrap for WP Unit Tests
*/
require __DIR__ . '/wp-loader.php';
$core_loader = new WpLoader();
$core_loader->init();
class FakeUserModel extends Model {
protected $type = 'user';
protected $access_cap = 'manage_options';
protected $forced_table_name = 'users';
public function fields() {
return array(
'ID' => Field_Type_Validation_Enum::INT,
'user_login' => Field_Type_Validation_Enum::STRING,
'user_nicename' => Field_Type_Validation_Enum::STRING,
'user_email' => Field_Type_Validation_Enum::EMAIL,
'user_url' => Field_Type_Validation_Enum::STRING,
'user_registered' => Field_Type_Validation_Enum::DATE,
'user_status' => Field_Type_Validation_Enum::INT,
'display_name' => Field_Type_Validation_Enum::STRING,
);
}
public function relationships() {
return new Relationship_Collection( array(
new Relationship( 'user', 'deal', 'manage_options', false, 'one_to_many' ),
) );
}
}
class FakeWebsiteModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'website';
protected $access_cap = 'manage_options';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::INT,
'type' => Field_Type_Validation_Enum::STRING,
'url' => Field_Type_Validation_Enum::STRING,
);
}
public function relationships() {
return new Relationship_Collection( array() );
}
}
class FakePhoneModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'phone';
protected $access_cap = 'manage_options';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::INT,
'type' => Field_Type_Validation_Enum::STRING,
'number' => Field_Type_Validation_Enum::INT,
'countryCode' => Field_Type_Validation_Enum::STRING,
);
}
public function relationships() {
return new Relationship_Collection( array() );
}
}
class FakeEmailModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'email';
protected $access_cap = 'manage_options';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::INT,
'address' => Field_Type_Validation_Enum::STRING,
'type' => Field_Type_Validation_Enum::STRING,
);
}
public function relationships() {
return new Relationship_Collection( array() );
}
}
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,
'firstName' => Field_Type_Validation_Enum::STRING,
'lastName' => Field_Type_Validation_Enum::STRING,
'profile_picture' => Field_Type_Validation_Enum::INT,
'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 transformations() {
return array(
'transformMakeThumb' => function ( $thumb_id, $thumb_type = 'hexagon', $thumb_size = 'medium' ) {
return sprintf( 'thumbnail_url:%s/%s/%s', $thumb_id, $thumb_type, $thumb_size );
},
);
}
public function relationships() {
return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection(
array(
new Relationship( 'contact', 'email', 'manage_options', false, 'one_to_many' ),
new Relationship( 'contact', 'phone', 'manage_options', false, 'one_to_many' ),
new Relationship( 'contact', 'website', 'manage_options', false, 'one_to_many' ),
)
);
}
}
class FakeCompanyModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'company';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::STRING,
'companyName' => Field_Type_Validation_Enum::STRING,
);
}
protected $access_cap = 'manage_options';
public function transformations() {
return array(
'transformMakeFoo' => function ( $arg, $value ) {
return sprintf( "IM%s", $arg );
},
);
}
public function relationships() {
$relationships = array(
new Relationship( 'company', 'contact', 'manage_options' ),
new Relationship( 'company', 'email', 'manage_options', false, 'one_to_many' ),
new Relationship( 'company', 'phone', 'manage_options', false, 'one_to_many' ),
new Relationship( 'company', 'website', 'manage_options', false, 'one_to_many' ),
);
return new Relationship_Collection( $relationships );
}
}
class FakeGroupModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'group';
protected $fields = array(
'label',
);
public function fields() {
return array(
'label' => Field_Type_Validation_Enum::STRING,
);
}
protected $access_cap = 'manage_options';
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' ),
)
);
}
}
class FakeDealModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'deal';
public function fields() {
return array(
'label' => Field_Type_Validation_Enum::STRING,
'value' => Field_Type_Validation_Enum::INT,
'id' => Field_Type_Validation_Enum::INT,
);
}
protected $access_cap = 'manage_options';
public function relationships() {
return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection(
array(
new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship( 'deal', 'stage', 'manage_options', true, 'one_to_many' ),
new Relationship( 'deal', 'user', 'manage_options', true, 'one_to_many' ),
)
);
}
}
class FakeStageModel extends \Gravity_Forms\Gravity_Tools\Hermes\Models\Model {
protected $type = 'stage';
public function fields() {
return array(
'id' => Field_Type_Validation_Enum::INT,
'label' => Field_Type_Validation_Enum::STRING,
);
}
protected $access_cap = 'manage_options';
public function relationships() {
return new \Gravity_Forms\Gravity_Tools\Hermes\Utils\Relationship_Collection(
array()
);
}
}
function gravitytools_tests_reset_db() {
echo "\r\n";
echo '=========================================' . "\r\n";
echo 'Cleaning up test database for next run...' . "\r\n";
global $wpdb;
$tables = array(
'contact',
'company',
'deal',
'pipeline',
'company_contact',
'deal_company',
'deal_contact',
'address',
'social',
'meta',
'email',
'phone',
'website',
'stage',
);
foreach ( $tables as $table ) {
$table_name = sprintf( '%s%s_%s', $wpdb->prefix, 'gravitycrm', $table );
$sql = sprintf( 'TRUNCATE TABLE %s', $table_name );
$wpdb->query( $sql );
}
echo 'Done cleaning test database!' . "\r\n";
echo '=========================================' . "\r\n";
echo "\r\n";
}
@@ -0,0 +1,29 @@
{
"name": "gravityforms/gravity-tools",
"description": "PHP libraries to support Gravity Forms applications.",
"type": "library",
"autoload": {
"classmap": [
"src"
]
},
"authors": [
{
"name": "Rocket Genius",
"email": "info@rocketgenius.com"
}
],
"minimum-stability": "stable",
"repositories": [
{
"url": "https://github.com/WordPress/wordpress-develop.git",
"type": "git"
}
],
"require-dev": {
"phpunit/phpunit": "^9.0",
"yoast/phpunit-polyfills": "4.0.0",
"lucatume/function-mocker": "~2.0",
"wordpress/wordpress": "dev-master"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<phpunit bootstrap="bootstrap.php">
<testsuites>
<testsuite name="Gravity Tools tests.">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>
@@ -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 ) {}
}
@@ -0,0 +1,53 @@
<?php
use PHPUnit\Framework\TestCase;
use Gravity_Forms\Gravity_Tools\Utils\GeoData;
class GeoDataTest extends TestCase {
public function testCountriesDataIsProperlyFormatted() {
$countries_data = GeoData::countries();
// Countries data should be an array
$this->assertTrue( is_array( $countries_data ) );
// Countries data should contain known keys
$this->assertTrue( array_key_exists( 'US', $countries_data ) );
$this->assertTrue( array_key_exists( 'CA', $countries_data ) );
// Countries data values should be translated
$this->assertEquals( $countries_data['AU'], 'Australia');
}
public function testCountriesDataCanBeTransformed() {
$transform_callback = function( $items ) {
foreach( $items as $key => $value ) {
$items[ $key ] = 'OVERRIDDEN';
}
return $items;
};
$countries_data = GeoData::countries( false, $transform_callback );
$this->assertEquals( $countries_data['AF'], 'OVERRIDDEN' );
}
public function testDataReturnsJson() {
$countries_data = GeoData::countries( true );
$this->assertTrue( is_string( $countries_data ) );
$this->assertFalse( is_array( $countries_data ) );
$countries_data = json_decode( $countries_data, true );
$this->assertEquals( $countries_data['AU'], 'Australia' );
}
public function testPhoneDataReturns() {
$phone_data = GeoData::phone_info();
$this->assertTrue( is_array( $phone_data ) );
$first_value = $phone_data[0];
$this->assertEquals( $first_value['iso'], 'AF' );
$this->assertEquals( $first_value['calling_code'], '93' );
}
}
@@ -0,0 +1,36 @@
<?php
use PHPUnit\Framework\TestCase;
use Gravity_Forms\Gravity_Tools\Service_Container;
use Gravity_Forms\Gravity_Tools\Service_Provider;
class ServiceContainerTest extends TestCase {
public function testServiceIsAdded() {
$container = new Service_Container();
$service = array( 'foo', 'bar' );
$service_name = 'test_service';
$container->add( $service_name, $service );
$this->assertSame( $container->get( $service_name ), $service );
}
public function testServiceIsAddedDeferred() {
global $foo;
$foo = 'before';
$container = new Service_Container();
$container->add( 'foo_service', function() {
global $foo;
$foo = 'after';
}, true );
$this->assertEquals( 'before', $foo );
$container->get( 'foo_service' );
$this->assertEquals( 'after', $foo );
}
}
@@ -0,0 +1,18 @@
{
hero {
name
metricHeight: height(format: METRIC)
mainFriends: friends {
id(format: INT)
date
secondaryFriends: friends(created_gt: 11/12/2023) {
created_at
website
tertiaryFriends: friends {
email
phone
}
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
{
contact {
first_name,
main_email: email,
phone,
phone_two: secondary_phone
friends: contact {
first_name
}
}
}
@@ -0,0 +1,18 @@
{
group {
name: label,
employees: contact {
first_name,
lname: last_name,
phone_two: secondary_phone
}
},
company: group {
name: label,
employees: contact {
first_name,
lname: last_name,
phone_two: secondary_phone
}
}
}
@@ -0,0 +1,279 @@
SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
SET NAMES utf8mb4;
DROP TABLE IF EXISTS `wp_gravitycrm_address`;
CREATE TABLE `wp_gravitycrm_address` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`lineOne` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`lineTwo` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`city` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`state` varchar(100) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
`country` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`postalCode` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`isPrimary` tinyint(1) NOT NULL DEFAULT '0',
`contactId` mediumint DEFAULT NULL,
`companyId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `lineOne` (`lineOne`),
FULLTEXT KEY `lineTwo` (`lineTwo`),
FULLTEXT KEY `city` (`city`),
FULLTEXT KEY `state` (`state`),
FULLTEXT KEY `country` (`country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_address` (`id`, `type`, `lineOne`, `lineTwo`, `city`, `state`, `country`, `postalCode`, `isPrimary`, `contactId`, `companyId`, `dateCreated`, `dateUpdated`) VALUES
(1, '', '123 Street St', '', 'City', 'NY', 'US', '55555', 0, NULL, 1, '2025-10-02 19:01:17', '2025-10-02 19:01:17'),
(2, '', '123 B Street', '', 'City', 'NY', 'US', '55555', 0, NULL, 2, '2025-10-02 19:01:36', '2025-10-02 19:01:36');
DROP TABLE IF EXISTS `wp_gravitycrm_company`;
CREATE TABLE `wp_gravitycrm_company` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`companyName` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`companyLogo` mediumint DEFAULT NULL,
`companyBanner` mediumint DEFAULT NULL,
`description` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
`source` varchar(100) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `companyName` (`companyName`),
FULLTEXT KEY `description` (`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_company` (`id`, `companyName`, `companyLogo`, `companyBanner`, `description`, `source`, `dateCreated`, `dateUpdated`) VALUES
(1, 'Company A', 0, NULL, '', NULL, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(2, 'Company B', 0, NULL, '', NULL, '2025-10-02 18:59:12', '2025-10-02 18:59:12');
DROP TABLE IF EXISTS `wp_gravitycrm_company_contact`;
CREATE TABLE `wp_gravitycrm_company_contact` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`company_id` mediumint NOT NULL,
`contact_id` mediumint NOT NULL,
`isMain` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_company_contact` (`id`, `company_id`, `contact_id`, `isMain`) VALUES
(1, 1, 1, 0),
(2, 1, 2, 0),
(3, 2, 3, 0);
DROP TABLE IF EXISTS `wp_gravitycrm_contact`;
CREATE TABLE `wp_gravitycrm_contact` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`firstName` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`lastName` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`jobTitle` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`description` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
`contactAvatar` mediumint DEFAULT NULL,
`contactBanner` mediumint DEFAULT NULL,
`source` varchar(100) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `firstName` (`firstName`),
FULLTEXT KEY `lastName` (`lastName`),
FULLTEXT KEY `jobTitle` (`jobTitle`),
FULLTEXT KEY `description` (`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_contact` (`id`, `firstName`, `lastName`, `jobTitle`, `description`, `contactAvatar`, `contactBanner`, `source`, `dateCreated`, `dateUpdated`) VALUES
(1, 'Jane', 'Smith', '', '', 0, NULL, NULL, '2025-10-02 18:59:50', '2025-10-02 18:59:50'),
(2, 'John', 'Smith', '', '', 0, NULL, NULL, '2025-10-02 19:00:12', '2025-10-02 19:00:12'),
(3, 'Rob', 'Carlin', '', '', 0, NULL, NULL, '2025-10-02 19:00:41', '2025-10-02 19:00:41');
DROP TABLE IF EXISTS `wp_gravitycrm_deal`;
CREATE TABLE `wp_gravitycrm_deal` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`label` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`source` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`value` bigint NOT NULL,
`estimatedCloseDate` bigint DEFAULT NULL,
`notes` mediumtext COLLATE utf8mb4_unicode_520_ci,
`attachments` mediumtext COLLATE utf8mb4_unicode_520_ci,
`previousNeighbor` mediumint DEFAULT '0',
`pipelineId` mediumint DEFAULT NULL,
`stageId` mediumint DEFAULT NULL,
`userId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `label` (`label`),
FULLTEXT KEY `notes` (`notes`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_deal_company`;
CREATE TABLE `wp_gravitycrm_deal_company` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`deal_id` mediumint NOT NULL,
`company_id` mediumint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_deal_contact`;
CREATE TABLE `wp_gravitycrm_deal_contact` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`deal_id` mediumint NOT NULL,
`contact_id` mediumint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_email`;
CREATE TABLE `wp_gravitycrm_email` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`address` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`isPrimary` tinyint(1) NOT NULL DEFAULT '0',
`contactId` mediumint DEFAULT NULL,
`companyId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `address` (`address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_email` (`id`, `type`, `address`, `isPrimary`, `contactId`, `companyId`, `dateCreated`, `dateUpdated`) VALUES
(1, '', 'support@companya.local', 0, NULL, 1, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(2, '', 'feedback@companya.local', 0, NULL, 1, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(3, '', 'foo@companyb.local', 0, NULL, 2, '2025-10-02 18:59:12', '2025-10-02 18:59:12'),
(4, '', 'bar@companyb.local', 0, NULL, 2, '2025-10-02 18:59:12', '2025-10-02 18:59:12'),
(5, '', 'jane@companya.local', 0, 1, NULL, '2025-10-02 18:59:50', '2025-10-02 18:59:50'),
(6, '', 'john@companya.local', 0, 2, NULL, '2025-10-02 19:00:12', '2025-10-02 19:00:12'),
(7, '', 'rob@companyb.local', 0, 3, NULL, '2025-10-02 19:00:41', '2025-10-02 19:00:41');
DROP TABLE IF EXISTS `wp_gravitycrm_file`;
CREATE TABLE `wp_gravitycrm_file` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`path` mediumtext COLLATE utf8mb4_unicode_520_ci,
`fileName` mediumtext COLLATE utf8mb4_unicode_520_ci,
`extension` tinytext COLLATE utf8mb4_unicode_520_ci,
`size` int DEFAULT NULL,
`dealId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_meta`;
CREATE TABLE `wp_gravitycrm_meta` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`object_type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`object_id` mediumint NOT NULL,
`meta_name` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`meta_value` mediumtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
PRIMARY KEY (`id`),
FULLTEXT KEY `meta_value` (`meta_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_note`;
CREATE TABLE `wp_gravitycrm_note` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`contents` mediumtext COLLATE utf8mb4_unicode_520_ci,
`userId` mediumint DEFAULT NULL,
`dealId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `contents` (`contents`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_phone`;
CREATE TABLE `wp_gravitycrm_phone` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`number` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`countryCode` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`isPrimary` tinyint(1) NOT NULL DEFAULT '0',
`contactId` mediumint DEFAULT NULL,
`companyId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `number` (`number`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_phone` (`id`, `type`, `number`, `countryCode`, `isPrimary`, `contactId`, `companyId`, `dateCreated`, `dateUpdated`) VALUES
(1, '', '14445556666', 'US', 0, NULL, 1, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(2, '', '12223334444', 'US', 0, NULL, 1, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(3, '', '18887776666', 'US', 0, NULL, 2, '2025-10-02 18:59:12', '2025-10-02 18:59:12'),
(4, '', '19998887777', 'US', 0, NULL, 2, '2025-10-02 18:59:12', '2025-10-02 18:59:12'),
(5, '', '19998887777', 'US', 0, 1, NULL, '2025-10-02 18:59:50', '2025-10-02 18:59:50'),
(6, '', '18887779797', 'US', 0, 2, NULL, '2025-10-02 19:00:12', '2025-10-02 19:00:12'),
(7, '', '17776668686', 'US', 0, 3, NULL, '2025-10-02 19:00:41', '2025-10-02 19:00:41');
DROP TABLE IF EXISTS `wp_gravitycrm_pipeline`;
CREATE TABLE `wp_gravitycrm_pipeline` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`label` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`source` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `label` (`label`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_social`;
CREATE TABLE `wp_gravitycrm_social` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`platform` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`identifier` varchar(300) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`isPrimary` tinyint(1) NOT NULL DEFAULT '0',
`contactId` mediumint DEFAULT NULL,
`companyId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_social` (`id`, `platform`, `identifier`, `isPrimary`, `contactId`, `companyId`, `dateCreated`, `dateUpdated`) VALUES
(1, 'instagram', 'companyb', 0, NULL, 2, '2025-10-02 19:01:48', '2025-10-02 19:01:48'),
(2, 'instagram', 'companya', 0, NULL, 1, '2025-10-02 19:02:02', '2025-10-02 19:02:02');
DROP TABLE IF EXISTS `wp_gravitycrm_stage`;
CREATE TABLE `wp_gravitycrm_stage` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`label` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`labelStyle` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`pipelineId` mediumint DEFAULT NULL,
`previousNeighbor` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `label` (`label`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
DROP TABLE IF EXISTS `wp_gravitycrm_website`;
CREATE TABLE `wp_gravitycrm_website` (
`id` mediumint NOT NULL AUTO_INCREMENT,
`type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`url` varchar(300) COLLATE utf8mb4_unicode_520_ci NOT NULL,
`isPrimary` tinyint(1) NOT NULL DEFAULT '0',
`contactId` mediumint DEFAULT NULL,
`companyId` mediumint DEFAULT NULL,
`dateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
`dateUpdated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`),
FULLTEXT KEY `url` (`url`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
INSERT INTO `wp_gravitycrm_website` (`id`, `type`, `url`, `isPrimary`, `contactId`, `companyId`, `dateCreated`, `dateUpdated`) VALUES
(1, '', 'https://companya.local', 0, NULL, 1, '2025-10-02 18:58:25', '2025-10-02 18:58:25'),
(2, '', 'https://companyb.local', 0, NULL, 2, '2025-10-02 18:59:12', '2025-10-02 18:59:12');
-- 2025-10-02 19:05:09 UTC
@@ -0,0 +1,95 @@
<?php
namespace emails;
use Gravity_Forms\Gravity_Tools\Emails\Email_Templatizer;
use PHPUnit\Framework\TestCase;
class EmailTemplatizerTest extends TestCase {
public function testTokenReplacement() {
$markup = '<div>{{ foo }} <span>{{ bar.0.bash }}</span></div>';
$data = array(
'foo' => 'Hey this is foo.',
'bar' => array(
array(
'bash' => 'And this is bash.',
),
),
);
$templatizer = new Email_Templatizer( $markup );
$result = $templatizer->render( $data );
$this->assertEquals( '<div>Hey this is foo. <span>And this is bash.</span></div>', $result );
}
public function testConditionals() {
$markup = '<div>
<span>Hey there</span>
{{|if stats.recipients.0 |}}
This should only be rendered if recipient 1 exists.
{{|endif|}}
{{|if stats.recipients.1 |}}
This should only be rendered if recipient 2 exists.
{{|endif|}}
</div>';
$data = array(
'stats' => array(
'recipients' => array(
array(
'foo' => 'bar',
),
),
),
);
$templatizer = new Email_Templatizer( $markup );
$result = $templatizer->render( $data );
$this->assertTrue( strpos( $result, 'recipient 1' ) !== false );
$this->assertTrue( strpos( $result, 'recipient 2' ) === false );
}
public function testLoops() {
$markup = '
{{|if stats.vendors |}}
Should not appear
{{|endif|}}
{{|for recipient in stats.recipients |}}
<div>
<h1>{{ recipient.name }}</h1>
<p>You have requested {{ frequency }} digests per week.</p>
</div>
{{|endfor|}}';
$template = new Email_Templatizer( $markup );
$data = array(
'frequency' => 10,
'stats' => array(
'recipients' => array(
array(
'name' => 'Bob',
),
array(
'name' => 'Jim',
),
),
),
);
$rendered = $template->render( $data );
$this->assertTrue( strpos( $rendered, 'Bob' ) !== false );
$this->assertTrue( strpos( $rendered, 'Jim' ) !== false );
$this->assertTrue( strpos( $rendered, 'Should not appear' ) === false );
}
}
@@ -0,0 +1,398 @@
<?php
namespace hermes;
use Gravity_Forms\Gravity_Tools\Hermes\Enum\Field_Type_Validation_Enum;
use Gravity_Forms\Gravity_Tools\Hermes\Mutation_Handler;
use Gravity_Forms\Gravity_Tools\Hermes\Query_Handler;
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\Schema_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Runners\Update_Runner;
use Gravity_Forms\Gravity_Tools\Hermes\Tokens\Query_Token;
use Gravity_Forms\Gravity_Tools\Hermes\Utils\Model_Collection;
use PHPUnit\Framework\TestCase;
use tad\FunctionMocker\FunctionMocker;
global $wpdb;
class HandlerTest extends TestCase {
protected $model_collection;
protected $contact_model;
protected $group_model;
protected $query_handler;
protected $mutation_handler;
protected $db_namespace;
protected $deal_model;
protected $stage_model;
protected $company_model;
public function setUp(): void {
$this->model_collection = new Model_Collection();
$this->contact_model = new \FakeContactModel();
$this->group_model = new \FakeGroupModel();
$this->company_model = new \FakeCompanyModel();
$this->deal_model = new \FakeDealModel();
$this->stage_model = new \FakeStageModel();
$this->model_collection->add( 'contact', $this->contact_model );
$this->model_collection->add( 'group', $this->group_model );
$this->model_collection->add( 'deal', $this->deal_model );
$this->model_collection->add( 'stage', $this->stage_model );
$this->model_collection->add( 'company', $this->company_model );
$this->db_namespace = 'gravitycrm';
$schema_runner = new Schema_Runner( $this->model_collection );
$this->query_handler = new Query_Handler( $this->db_namespace, $this->model_collection, $schema_runner );
$runners = array(
'insert' => new Insert_Runner( $this->db_namespace, $this->query_handler, $this->model_collection, new Connect_Runner( $this->db_namespace, $this->query_handler, $this->model_collection ) ),
'delete' => new Delete_Runner( $this->db_namespace, $this->query_handler, $this->model_collection ),
'connect' => new Connect_Runner( $this->db_namespace, $this->query_handler, $this->model_collection ),
'update' => new Update_Runner( $this->db_namespace, $this->query_handler, $this->model_collection ),
'disconnect' => new Disconnect_Runner( $this->db_namespace, $this->query_handler, $this->model_collection ),
);
$this->mutation_handler = new Mutation_Handler( $this->db_namespace, $this->model_collection, $this->query_handler, $runners );
}
// /**
// * @dataProvider mutationHandlerProvider
// *
// * @param $text
// * @param $expected
// *
// * @return void
// */
// public function testMutationHandler( $text, $expected ) {
//
// try {
// $data = $this->mutation_handler->handle_mutation( $text );
// } catch ( \Exception $e ) {
// $this->assertEquals( $expected, 'failure' );
//
// return;
// }
//
// $this->assertEquals( $expected, 'success' );
// }
//
// public function mutationHandlerProvider() {
// return array(
// // Valid
// array(
// '{
// insert_contact(objects: [{firstName: "Foo", lastName: "Bar"}, {firstName: "Bing", lastName: "Bash", secondary_phone: "4445554848" }]) {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }',
// 'success',
// ),
//
// // Invalid custom callback
// array(
// '{
// insert_contact(objects: [{foobar: "bar", firstName: true, lastName: "Bar"}, {firstName: "Bing", lastName: "Bash", secondary_phone: "4445554848" }]) {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }',
// 'failure',
// ),
//
// // Valid update
// array(
// '{
// update_contact(id: 1, firstName: "Foo", lastName: "Bar", secondary_phone: "4445554848") {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }',
// 'success',
// ),
//
// // Update missing ID
// array(
// '{
// update_contact( firstName: "Foo", lastName: "Bar", secondary_phone: "4445554848") {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }',
// 'failure',
// ),
//
// // Delete
// array(
// '{
// delete_contact(id: 1) {
// }
// }',
// 'success',
// ),
//
// // Delete missing ID
// array(
// '{
// delete_contact() {
// }
// }',
// 'failure',
// ),
//
// // Delete missing valid object type
// array(
// '{
// delete_invalid_object(id: 1) {
// }
// }',
// 'failure',
// ),
//
// // Connect with only one connection
// array(
// '{
// connect_group_contact(from: 1, to: 2) {
// }
// }',
// 'success',
// ),
//
// // Connect with only one connection as an array
// array(
// '{
// connect_group_contact([{from:1, to: 2}])
// }',
// 'success',
// ),
//
// // Connect with multiple connections as an array
// array(
// '{
// connect_group_contact([{from:1, to: 2}, {from:1, to:3}])
// }',
// 'success',
// ),
//
// array(
// '{
// disconnect_group_contact([{from:1, to: 2}, {from:1, to:3}])
// }',
// 'success'
// ),
// );
// }
//
// public function testInsertMutation() {
// global $wpdb;
// \gravitytools_tests_reset_db();
//
// $text = '{
// insert_contact(objects: [{firstName: "Foo", lastName: "Bar"}, {firstName: "Bing", lastName: "Bash", secondary_phone: "4445554848" }]) {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }';
//
// $this->mutation_handler->handle_mutation( $text );
// $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' );
// $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' );
//
// $check_query = sprintf( 'SELECT * FROM %s', $table_name );
// $results = $wpdb->get_results( $check_query, ARRAY_A );
//
// $this->assertEquals( 2, count( $results ) );
//
// $record = $results[1];
//
// $this->assertEquals( 'Bing', $record['firstName'] );
//
// $meta_check_query = sprintf( 'SELECT meta_value FROM %s WHERE object_id = "%s" AND meta_name = "%s"', $meta_table_name, $record['id'], 'secondary_phone' );
// $meta_results = $wpdb->get_results( $meta_check_query, ARRAY_A );
//
// $this->assertEquals( '4445554848', $meta_results[0]['meta_value'] );
// }
//
// public function testUpdateMutation() {
// global $wpdb;
// \gravitytools_tests_reset_db();
//
// $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' );
// $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' );
//
// $insert_query = sprintf( 'INSERT INTO %s (firstName, lastName) VALUES ("Test", "User" )', $table_name );
// $wpdb->query( $insert_query );
//
// $insert_meta_query = sprintf( 'INSERT INTO %s (meta_name, meta_value, object_type, object_id) VALUES ("secondary_phone", "4445554545", "contact", "1" )', $meta_table_name );
// $wpdb->query( $insert_meta_query );
//
// $text = '{
// update_contact(id: 1, firstName: "Foo", lastName: "Bar", secondary_phone: "4445554848") {
// returning {
// id,
// firstName,
// lastName,
// secondary_phone,
// }
// }
// }';
//
// $this->mutation_handler->handle_mutation( $text );
//
// $check_query = sprintf( 'SELECT * FROM %s', $table_name );
// $results = $wpdb->get_results( $check_query, ARRAY_A );
//
// $this->assertEquals( 1, count( $results ) );
//
// $check_query = sprintf( 'SELECT * FROM %s WHERE id = "%s"', $table_name, 1 );
// $results = $wpdb->get_results( $check_query, ARRAY_A );
//
// $record = $results[0];
//
// $this->assertEquals( 'Foo', $record['firstName'] );
// $this->assertEquals( 'Bar', $record['lastName'] );
//
// $meta_check_query = sprintf( 'SELECT meta_value FROM %s WHERE object_id = "%s" AND meta_name = "%s"', $meta_table_name, $record['id'], 'secondary_phone' );
// $meta_results = $wpdb->get_results( $meta_check_query, ARRAY_A );
//
// $this->assertEquals( '4445554848', $meta_results[0]['meta_value'] );
// }
//
// public function testDeleteMutation() {
// global $wpdb;
// \gravitytools_tests_reset_db();
//
// $table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' );
// $meta_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'meta' );
//
// $insert_query = sprintf( 'INSERT INTO %s (firstName, lastName) VALUES ("Test", "User" )', $table_name );
// $wpdb->query( $insert_query );
//
// $text = '{
// delete_contact(id: 1) {}
// }';
//
// $check_query = sprintf( 'SELECT * FROM %s', $table_name );
// $results = $wpdb->get_results( $check_query, ARRAY_A );
//
// $this->assertEquals( 1, count( $results ) );
//
// $this->mutation_handler->handle_mutation( $text );
//
// $check_query = sprintf( 'SELECT * FROM %s', $table_name );
// $results = $wpdb->get_results( $check_query, ARRAY_A );
//
// $this->assertEquals( 0, count( $results ) );
// }
public function testOtmConnectMutation() {
global $wpdb;
\gravitytools_tests_reset_db();
$deal_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'deal' );
$stage_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'stage' );
$insert_query = sprintf( 'INSERT INTO %s (label) VALUES ("Fun Test Deal")', $deal_table_name );
$wpdb->query( $insert_query );
$insert_query = sprintf( 'INSERT INTO %s (label) VALUES ("Test Stage")', $stage_table_name );
$wpdb->query( $insert_query );
$text = '{
connect_deal_stage( objects: [ {from: 1, to: 1} ]) {}
}';
$check_query = sprintf( 'SELECT * FROM %s WHERE stageId = "1" AND id = "1"', $deal_table_name );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 0, count( $results ) );
$this->mutation_handler->handle_mutation( $text, true );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 1, count( $results ) );
$this->assertEquals( 1, $results[0]['stageId'] );
$this->assertEquals( 1, $results[0]['id'] );
$text = '{
disconnect_deal_stage([{from:1, to: 1}]) {}
}';
$this->mutation_handler->handle_mutation( $text, true );
$check_query = sprintf( 'SELECT * FROM %s WHERE stageId = "1" AND id = "1"', $deal_table_name );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 0, count( $results ) );
}
public function testConnectMutation() {
global $wpdb;
\gravitytools_tests_reset_db();
$contact_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'contact' );
$company_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'company' );
$connect_table_name = sprintf( '%s%s_%s', $wpdb->prefix, $this->db_namespace, 'company_contact' );
$insert_query = sprintf( 'INSERT INTO %s (firstName, lastName) VALUES ("Test", "User")', $contact_table_name );
$wpdb->query( $insert_query );
$insert_query = sprintf( 'INSERT INTO %s (companyName) VALUES ("Test Company")', $company_table_name );
$wpdb->query( $insert_query );
$text = '{
connect_company_contact(objects: [ {from: 1, to: 1} ]) {}
}';
$check_query = sprintf( 'SELECT * FROM %s', $connect_table_name );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 0, count( $results ) );
$this->mutation_handler->handle_mutation( $text, true );
$check_query = sprintf( 'SELECT * FROM %s', $connect_table_name );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 1, count( $results ) );
$this->assertEquals( 1, $results[0]['company_id'] );
$this->assertEquals( 1, $results[0]['contact_id'] );
$text = '{
disconnect_company_contact([{from:1, to: 1}]) {}
}';
$this->mutation_handler->handle_mutation( $text, true );
$check_query = sprintf( 'SELECT * FROM %s', $connect_table_name );
$results = $wpdb->get_results( $check_query, ARRAY_A );
$this->assertEquals( 0, count( $results ) );
}
}

Some files were not shown because too many files have changed in this diff Show More